diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e59789b..41fdb35e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,15 @@ Changelog ========= +Version 14.3 +============ + +* Improved operation validation to check if it is calibrated according to the metadata rather than assuming. `#133 `_ +* Added IQMMoveGate class for Deneb architectures. `#133 `_ +* Updated IQMDevice class to support devices with resonators. `#133 `_ +* Support for :class:`CircuitCompilationOptions` from ``iqm-client`` when submitting a circuit to an IQM device. +* Require iqm-client >= 18.0. `#133 `_ + Version 14.2 ============ diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 4982f6c9..e1a6436b 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -205,6 +205,9 @@ If you have gates involving more than two qubits you need to decompose them befo Since routing may add some SWAP gates to the circuit, you will need to decompose the circuit again after the routing, unless SWAP is a native gate for the target device. +To ensure that the transpiler is restricted to a specific subset of qubits, you can provide a list of qubits in the ``qubit_subset`` argument such that ancillary qubits will not be added during routing. This is particularly useful when running Quantum Volume benchmarks. + +Additionally, if the target device supports MOVE gates (e.g. IQM Deneb), a final MOVE gate insertion step is performed. Under the hood, this uses the :meth:`transpile_insert_moves`method of the iqm_client library. This method is exposed through :meth:`transpile_insert_moves_into_circuit` which can also be used by advanced users to transpile circuits that have already some MOVE gates in them, or to remove existing MOVE gates from a circuit so the circuit can be reused on a device that does not support them. Optimization ------------ diff --git a/pyproject.toml b/pyproject.toml index e6f1b78d..86ac0dff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "numpy", "cirq-core[contrib] ~= 1.2", "ply", # Required by cirq.contrib.qasm_import - "iqm-client >= 17.8, < 18.0" + "iqm-client >= 18.0, < 19.0" ] [project.urls] diff --git a/src/iqm/cirq_iqm/__init__.py b/src/iqm/cirq_iqm/__init__.py index 520b8cbd..981603bb 100644 --- a/src/iqm/cirq_iqm/__init__.py +++ b/src/iqm/cirq_iqm/__init__.py @@ -25,3 +25,6 @@ __version__ = 'unknown' finally: del version, PackageNotFoundError +# pylint: disable=wrong-import-position +from .iqm_gates import * +from .transpiler import transpile_insert_moves_into_circuit diff --git a/src/iqm/cirq_iqm/devices/iqm_device.py b/src/iqm/cirq_iqm/devices/iqm_device.py index a7b7d4cc..88887ebd 100644 --- a/src/iqm/cirq_iqm/devices/iqm_device.py +++ b/src/iqm/cirq_iqm/devices/iqm_device.py @@ -21,12 +21,17 @@ from __future__ import annotations import collections.abc as ca +from itertools import zip_longest from math import pi as PI -from typing import Optional, cast +from typing import Optional, Sequence, cast import uuid import cirq from cirq import InsertStrategy, MeasurementGate, devices, ops, protocols +from cirq.contrib.routing.router import nx + +from iqm.cirq_iqm.iqm_gates import IQMMoveGate +from iqm.cirq_iqm.transpiler import transpile_insert_moves_into_circuit from .iqm_device_metadata import IQMDeviceMetadata @@ -60,6 +65,8 @@ class IQMDevice(devices.Device): def __init__(self, metadata: IQMDeviceMetadata): self._metadata = metadata self.qubits = tuple(sorted(self._metadata.qubit_set)) + self.resonators = tuple(sorted(self._metadata.resonator_set)) + self.supported_operations = self._metadata.operations @property def metadata(self) -> IQMDeviceMetadata: @@ -77,17 +84,37 @@ def get_qubit(self, index: int) -> cirq.Qid: def check_qubit_connectivity(self, operation: cirq.Operation) -> None: """Raises a ValueError if operation acts on qubits that are not connected.""" - if len(operation.qubits) >= 2 and not isinstance(operation.gate, ops.MeasurementGate): - if operation.qubits not in self._metadata.nx_graph.edges: - raise ValueError(f'Unsupported qubit connectivity required for {operation!r}') + if len(operation.qubits) >= 2 and not self.has_valid_operation_targets(operation): + raise ValueError(f'Unsupported qubit connectivity required for {operation!r}') def is_native_operation(self, op: cirq.Operation) -> bool: """Predicate, True iff the given operation is considered native for the architecture.""" - return ( + check = ( isinstance(op, (ops.GateOperation, ops.TaggedOperation)) and (op.gate is not None) and (op.gate in self._metadata.gateset) ) + if check and isinstance(op.gate, ops.CZPowGate): + return op.gate.exponent == 1 + return check + + def has_valid_operation_targets(self, op: cirq.Operation) -> bool: + """Predicate, True iff the given operation is native and its targets are valid.""" + matched_support = [ + (g, qbs) + for g, qbs in self.supported_operations.items() + if op.gate is not None and op.gate in cirq.GateFamily(g) + ] + if len(matched_support) > 0: + gf, valid_targets = matched_support[0] + valid_qubits = set(q for qb in valid_targets for q in qb) + if gf == cirq.MeasurementGate: # Measurements can be done on any available qubits + return all(q in valid_qubits for q in op.qubits) + if issubclass(gf, cirq.InterchangeableQubitsGate): + target_qubits = set(op.qubits) + return any(set(t) == target_qubits for t in valid_targets) + return any(all(q1 == q2 for q1, q2 in zip_longest(op.qubits, t)) for t in valid_targets) + return False def operation_decomposer(self, op: cirq.Operation) -> Optional[list[cirq.Operation]]: """Decomposes operations into the native operation set. @@ -177,6 +204,7 @@ def route_circuit( circuit: cirq.Circuit, *, initial_mapper: Optional[cirq.AbstractInitialMapper] = None, + qubit_subset: Optional[Sequence[cirq.Qid]] = None, ) -> tuple[cirq.Circuit, dict[cirq.Qid, cirq.Qid], dict[cirq.Qid, cirq.Qid]]: """Routes the given circuit to the device connectivity and qubit names. @@ -214,8 +242,26 @@ def route_circuit( for q in measurement_qubits: modified_circuit.append(cirq.I(q).with_tags(i_tag)) + if self.metadata.resonator_set: + move_routing = True + graph = nx.Graph() + for edge in self.metadata.nx_graph.edges: + q, r = edge if edge[1] in self.resonators else edge[::-1] + if r not in self.resonators: + graph.add_edge(*edge) + else: + for n in self.metadata.nx_graph.neighbors(r): + if n != q and not graph.has_edge(q, n) and not graph.has_edge(n, q): + graph.add_edge(q, n) + else: + graph = self._metadata.nx_graph + move_routing = False + + if qubit_subset is not None: + graph = graph.subgraph(qubit_subset) + # Route the modified circuit. - router = cirq.RouteCQC(self._metadata.nx_graph) + router = cirq.RouteCQC(graph) routed_circuit, initial_mapping, final_mapping = router.route_circuit( modified_circuit, initial_mapper=initial_mapper ) @@ -233,6 +279,11 @@ def route_circuit( # Remove additional identity gates. identity_gates = routed_circuit.findall_operations(lambda op: i_tag in op.tags) routed_circuit.batch_remove(identity_gates) + if move_routing: + # Decompose the SWAP gates to the native gate set. + routed_circuit = self.decompose_circuit(routed_circuit) + # Insert IQMMoveGates into the circuit. + routed_circuit = transpile_insert_moves_into_circuit(routed_circuit, self) return routed_circuit, initial_mapping, final_mapping @@ -256,6 +307,7 @@ def validate_circuit(self, circuit: cirq.AbstractCircuit) -> None: super().validate_circuit(circuit) _verify_unique_measurement_keys(circuit.all_operations()) _validate_for_routing(circuit) + self.validate_moves(circuit) def validate_operation(self, operation: cirq.Operation) -> None: if not isinstance(operation.untagged, cirq.GateOperation): @@ -265,10 +317,62 @@ def validate_operation(self, operation: cirq.Operation) -> None: raise ValueError(f'Unsupported gate type: {operation.gate!r}') for qubit in operation.qubits: - if qubit not in self.qubits: + if qubit not in self.qubits and qubit not in self.resonators: raise ValueError(f'Qubit not on device: {qubit!r}') - self.check_qubit_connectivity(operation) + if not self.has_valid_operation_targets(operation): + raise ValueError(f'Unsupported operation between qubits: {operation!r}') + + def validate_move(self, operation: cirq.Operation) -> None: + """Validates whether the IQMMoveGate is between qubit and resonator registers. + + Args: + operation (cirq.Operation): Operation to check + + Raises: + ValueError: In case the the first argument of the IQMMoveGate is not a qubit, + or if the second argument is not a resonator on this device. + + Returns: + None when the IQMMoveGate is used correctly. + """ + if isinstance(operation.gate, IQMMoveGate): + if operation.qubits[0] not in self.qubits: + raise ValueError( + f'IQMMoveGate is only supported with a qubit register as the first argument, \ + but got {operation.qubits[0]!r}' + ) + if operation.qubits[1] not in self.resonators: + raise ValueError( + f'IQMMoveGate is only supported with a resonator register as the second argument, \ + but got {operation.qubits[1]!r}' + ) + + def validate_moves(self, circuit: cirq.AbstractCircuit) -> None: + """Validates whether the IQMMoveGates are correctly applied in the circuit. + + Args: + circuit (cirq.AbstractCircuit): The circuit to validate. + + Raises: + ValueError: If the IQMMoveGate is applied incorrectly. + Returns: + None if the IQMMoveGates are applied correctly. + """ + moves: dict[cirq.Qid, list[cirq.Qid]] = {r: [] for r in self.resonators} + for moment in circuit: + for operation in moment.operations: + if isinstance(operation.gate, IQMMoveGate): + self.validate_move(operation) + moves[operation.qubits[1]].append(operation.qubits[0]) + for res, qubits in moves.items(): + while len(qubits) > 1: + q1, q2, *rest = qubits + if q1 != q2: + raise ValueError(f'IQMMoveGate({q2!r}, {res!r}) is applied between two logical qubit states.') + qubits = rest + if len(qubits) != 0: + raise ValueError(f'Circuit ends with a qubit state in the resonator {res!r}.') def __eq__(self, other): return self.__class__ == other.__class__ and self._metadata == other._metadata diff --git a/src/iqm/cirq_iqm/devices/iqm_device_metadata.py b/src/iqm/cirq_iqm/devices/iqm_device_metadata.py index 984dfbc7..8e12fec9 100644 --- a/src/iqm/cirq_iqm/devices/iqm_device_metadata.py +++ b/src/iqm/cirq_iqm/devices/iqm_device_metadata.py @@ -15,26 +15,15 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Optional, Union +from typing import FrozenSet, Optional import cirq -from cirq import NamedQubit, Qid, devices, ops +from cirq import Gate, NamedQid, devices, ops from cirq.contrib.routing.router import nx +from iqm.cirq_iqm.iqm_operation_mapping import _IQM_CIRQ_OP_MAP from iqm.iqm_client import QuantumArchitectureSpecification -# Mapping from IQM operation names to cirq operations -_IQM_CIRQ_OP_MAP: dict[str, tuple[Union[type[cirq.Gate], cirq.Gate, cirq.GateFamily], ...]] = { - # XPow and YPow kept for convenience, Cirq does not know how to decompose them into PhasedX - # so we would have to add those rules... - 'prx': (cirq.ops.PhasedXPowGate, cirq.ops.XPowGate, cirq.ops.YPowGate), - 'phased_rx': (cirq.ops.PhasedXPowGate, cirq.ops.XPowGate, cirq.ops.YPowGate), - 'cz': (cirq.ops.CZ,), - 'measurement': (cirq.ops.MeasurementGate,), - 'measure': (cirq.ops.MeasurementGate,), - 'barrier': (), -} - @cirq.value.value_equality class IQMDeviceMetadata(devices.DeviceMetadata): @@ -49,11 +38,16 @@ class IQMDeviceMetadata(devices.DeviceMetadata): QUBIT_NAME_PREFIX: str = 'QB' """prefix for qubit names, to be followed by their numerical index""" + RESONATOR_DIMENSION: int = 2 + """Dimension abstraction for the resonator Qids""" + def __init__( self, - qubits: Iterable[Qid], - connectivity: Iterable[Iterable[Qid]], + qubits: Iterable[NamedQid], + connectivity: Iterable[Iterable[NamedQid]], + operations: Optional[dict[type[cirq.Gate], list[tuple[cirq.NamedQid, ...]]]] = None, gateset: Optional[cirq.Gateset] = None, + resonators: Iterable[NamedQid] = (), ): """Construct an IQMDeviceMetadata object.""" nx_graph = nx.Graph() @@ -61,33 +55,112 @@ def __init__( edge_qubits = list(edge) nx_graph.add_edge(edge_qubits[0], edge_qubits[1]) super().__init__(qubits, nx_graph) + self._qubit_set: FrozenSet[NamedQid] = frozenset(qubits) + self._resonator_set: FrozenSet[NamedQid] = frozenset(resonators) if gateset is None: - # default gateset for IQM devices - self._gateset = cirq.Gateset( - ops.PhasedXPowGate, ops.XPowGate, ops.YPowGate, ops.MeasurementGate, ops.CZPowGate() - ) - else: - self._gateset = gateset + if operations is None: + # default gateset for IQM devices + gateset = cirq.Gateset( + ops.PhasedXPowGate, ops.XPowGate, ops.YPowGate, ops.MeasurementGate, ops.CZPowGate + ) + sqg_list: list[type[Gate]] = [ops.PhasedXPowGate, ops.XPowGate, ops.YPowGate, ops.MeasurementGate] + operations = {} + operations[ops.CZPowGate] = list(tuple(edge) for edge in connectivity) + operations.update({gate: [(qb,) for qb in qubits] for gate in sqg_list}) + else: + gateset = cirq.Gateset(*operations.keys()) + self._gateset = gateset + + if operations is None: + raise ValueError('Operations must be provided if a gateset is provided, it cannot be reconstructed.') + self.operations = operations + + @property + def resonator_set(self) -> FrozenSet[NamedQid]: + """Returns the set of resonators on the device. + + Returns: + Frozenset of resonators on device. + """ + return self._resonator_set @classmethod def from_architecture(cls, architecture: QuantumArchitectureSpecification) -> IQMDeviceMetadata: """Returns device metadata object created based on architecture specification""" - qubits = tuple(NamedQubit(qb) for qb in architecture.qubits) - connectivity = tuple(tuple(NamedQubit(qb) for qb in edge) for edge in architecture.qubit_connectivity) - gateset = cirq.Gateset(*(cirq_op for iqm_op in architecture.operations for cirq_op in _IQM_CIRQ_OP_MAP[iqm_op])) - return cls(qubits, connectivity, gateset) + qubits = tuple(NamedQid(qb, dimension=2) for qb in architecture.qubits if qb.startswith(cls.QUBIT_NAME_PREFIX)) + resonators = tuple( + NamedQid(qb, dimension=cls.RESONATOR_DIMENSION) + for qb in architecture.qubits + if not qb.startswith(cls.QUBIT_NAME_PREFIX) + ) + connectivity = tuple( + tuple( + ( + NamedQid(qb, dimension=2) + if qb.startswith(cls.QUBIT_NAME_PREFIX) + else NamedQid(qb, dimension=cls.RESONATOR_DIMENSION) + ) + for qb in edge + ) + for edge in architecture.qubit_connectivity + ) + operations: dict[type[cirq.Gate], list[tuple[NamedQid, ...]]] = { + cirq_op: [ + tuple( + ( + NamedQid(qb, dimension=2) + if qb.startswith(cls.QUBIT_NAME_PREFIX) + else NamedQid(qb, dimension=cls.RESONATOR_DIMENSION) + ) + for qb in args + ) + for args in qubits + ] + for iqm_op, qubits in architecture.operations.items() + for cirq_op in _IQM_CIRQ_OP_MAP[iqm_op] + } + return cls(qubits, connectivity, operations=operations, resonators=resonators) + + def to_architecture(self) -> QuantumArchitectureSpecification: + """Returns the architecture specification object created based on device metadata.""" + qubits = tuple(qb.name for qb in self._qubit_set) + resonators = tuple(qb.name for qb in self.resonator_set) + connectivity = tuple(tuple(qb.name for qb in edge) for edge in self.nx_graph.edges()) + operations: dict[str, list[tuple[str, ...]]] = { + iqm_op: [tuple(qb.name for qb in args) for args in qubits] + for cirq_op, qubits in self.operations.items() + for iqm_op, cirq_ops in _IQM_CIRQ_OP_MAP.items() + if cirq_op in cirq_ops + } + return QuantumArchitectureSpecification( + name='From Cirq object', qubits=resonators + qubits, qubit_connectivity=connectivity, operations=operations + ) @classmethod def from_qubit_indices( - cls, qubit_count: int, connectivity_indices: tuple[set[int], ...], gateset: Optional[cirq.Gateset] = None + cls, + qubit_count: int, + connectivity_indices: tuple[set[int], ...], + gateset: Optional[tuple[type[cirq.Gate]]] = None, ) -> IQMDeviceMetadata: """Returns device metadata object created based on connectivity specified using qubit indices only.""" - qubits = tuple(NamedQubit.range(1, qubit_count + 1, prefix=cls.QUBIT_NAME_PREFIX)) + qubits = tuple(NamedQid.range(1, qubit_count + 1, prefix=cls.QUBIT_NAME_PREFIX, dimension=2)) connectivity = tuple( - tuple(NamedQubit(f'{cls.QUBIT_NAME_PREFIX}{qb}') for qb in edge) for edge in connectivity_indices + tuple(NamedQid(f'{cls.QUBIT_NAME_PREFIX}{qb}', dimension=2) for qb in edge) for edge in connectivity_indices ) - return cls(qubits, connectivity, gateset) + if gateset: + sqg_list: list[type[Gate]] = [ + g for g in gateset if g in [ops.PhasedXPowGate, ops.XPowGate, ops.YPowGate, ops.MeasurementGate] + ] + operations: dict[type[cirq.Gate], list[tuple[cirq.NamedQid, ...]]] = {} + if ops.CZPowGate in gateset: + operations[ops.CZPowGate] = list(tuple(edge) for edge in connectivity) + if ops.ISwapPowGate in gateset: + operations[ops.ISwapPowGate] = list(tuple(edge) for edge in connectivity) + operations.update({gate: [(qb,) for qb in qubits] for gate in sqg_list}) + return cls(qubits, connectivity, operations=operations, gateset=cirq.Gateset(*gateset)) + return cls(qubits, connectivity) @property def gateset(self) -> cirq.Gateset: diff --git a/src/iqm/cirq_iqm/devices/valkmusa.py b/src/iqm/cirq_iqm/devices/valkmusa.py index 424e4011..5a32f006 100644 --- a/src/iqm/cirq_iqm/devices/valkmusa.py +++ b/src/iqm/cirq_iqm/devices/valkmusa.py @@ -17,10 +17,9 @@ from math import pi as PI from typing import Optional -import cirq from cirq import ops -from .iqm_device import IQMDevice, IQMDeviceMetadata +from iqm.cirq_iqm.devices import IQMDevice, IQMDeviceMetadata PI_2 = PI / 2 @@ -48,7 +47,7 @@ class Valkmusa(IQMDevice): def __init__(self): qubit_count = 2 connectivity = ({1, 2},) - gateset = cirq.Gateset( + gateset = ( ops.PhasedXPowGate, # XPow and YPow kept for convenience, Cirq does not know how to decompose them into PhasedX # so we would have to add those rules... diff --git a/src/iqm/cirq_iqm/iqm_gates.py b/src/iqm/cirq_iqm/iqm_gates.py new file mode 100644 index 00000000..8d57f7ba --- /dev/null +++ b/src/iqm/cirq_iqm/iqm_gates.py @@ -0,0 +1,57 @@ +# Copyright 2020–2022 Cirq 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. +""" +Implementations for IQM specific quantum gates +""" +from typing import List, Tuple + +from cirq import CircuitDiagramInfo, CircuitDiagramInfoArgs, EigenGate +import numpy as np + + +class IQMMoveGate(EigenGate): + r"""The MOVE operation is a unitary population exchange operation between a qubit and a resonator. + Its effect is only defined in the invariant subspace :math:`S = \text{span}\{|00\rangle, |01\rangle, |10\rangle\}`, + where it swaps the populations of the states :math:`|01\rangle` and :math:`|10\rangle`. + Its effect on the orthogonal subspace is undefined. + + MOVE has the following presentation in the subspace :math:`S`: + + .. math:: \text{MOVE}_S = |00\rangle \langle 00| + a |10\rangle \langle 01| + a^{-1} |01\rangle \langle 10|, + + where :math:`a` is an undefined complex phase that is canceled when the MOVE gate is applied a second time. + + To ensure that the state of the qubit and resonator has no overlap with :math:`|11\rangle`, it is + 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]``. + Additionally, the matrix representation of the gate set to be a SWAP gate, even though this is not what physically + happens. + """ + + def _num_qubits_(self) -> int: + return 2 + + def _eigen_components(self) -> List[Tuple[float, np.ndarray]]: + return [ + (0, np.array([[1, 0, 0, 0], [0, 0.5, 0.5, 0], [0, 0.5, 0.5, 0], [0, 0, 0, 1]])), + (1, np.array([[0, 0, 0, 0], [0, 0.5, -0.5, 0], [0, -0.5, 0.5, 0], [0, 0, 0, 0]])), + ] + + def _circuit_diagram_info_(self, args: CircuitDiagramInfoArgs) -> CircuitDiagramInfo: + return CircuitDiagramInfo(wire_symbols=('MOVE(QB)', 'MOVE(Res)'), exponent=self._diagram_exponent(args)) + + def __str__(self) -> str: + return 'MOVE' diff --git a/src/iqm/cirq_iqm/iqm_operation_mapping.py b/src/iqm/cirq_iqm/iqm_operation_mapping.py index 65dbecdb..0970a4a8 100644 --- a/src/iqm/cirq_iqm/iqm_operation_mapping.py +++ b/src/iqm/cirq_iqm/iqm_operation_mapping.py @@ -13,10 +13,51 @@ # limitations under the License. """Logic for mapping Cirq Operations to the IQM transfer format. """ -from cirq.ops import CZPowGate, MeasurementGate, Operation, PhasedXPowGate, XPowGate, YPowGate +from cirq import NamedQid +from cirq.ops import CZPowGate, Gate, MeasurementGate, Operation, PhasedXPowGate, XPowGate, YPowGate +from iqm.cirq_iqm.iqm_gates import IQMMoveGate from iqm.iqm_client import Instruction +# Mapping from IQM operation names to cirq operations +_IQM_CIRQ_OP_MAP: dict[str, tuple[type[Gate], ...]] = { + # XPow and YPow kept for convenience, Cirq does not know how to decompose them into PhasedX + # so we would have to add those rules... + 'prx': (PhasedXPowGate, XPowGate, YPowGate), + 'phased_rx': (PhasedXPowGate, XPowGate, YPowGate), + 'cz': (CZPowGate,), + 'move': (IQMMoveGate,), + 'measurement': (MeasurementGate,), + 'measure': (MeasurementGate,), + 'barrier': tuple(), +} + + +def instruction_to_operation(instr: Instruction) -> Operation: + """Convert an IQM instruction to a Cirq Operation. + + Args: + instr: the IQM instruction + + Returns: + Operation: the converted operation + + Raises: + OperationNotSupportedError When the circuit contains an unsupported operation. + + """ + if instr.name not in _IQM_CIRQ_OP_MAP: + raise OperationNotSupportedError(f'Operation {instr.name} not supported.') + + gate_type = _IQM_CIRQ_OP_MAP[instr.name][0] + args_map = {'angle_t': 'exponent', 'phase_t': 'phase_exponent'} + args = {args_map[k]: v * 2 for k, v in instr.args.items() if k in args_map} + if 'key' in instr.args.keys(): + args['key'] = instr.args['key'] + args['num_qubits'] = len(instr.qubits) + qubits = [NamedQid(qubit, dimension=2) for qubit in instr.qubits] + return gate_type(**args)(*qubits) + class OperationNotSupportedError(RuntimeError): """Raised when a given operation is not supported by the IQM server.""" @@ -39,7 +80,7 @@ def map_operation(operation: Operation) -> Instruction: """ phased_rx_name = 'prx' - qubits = [str(qubit) for qubit in operation.qubits] + qubits = [qubit.name if isinstance(qubit, NamedQid) else str(qubit) for qubit in operation.qubits] if isinstance(operation.gate, PhasedXPowGate): return Instruction( name=phased_rx_name, @@ -78,4 +119,11 @@ def map_operation(operation: Operation) -> Instruction: f'CZPowGate exponent was {operation.gate.exponent}, but only 1 is natively supported.' ) + if isinstance(operation.gate, IQMMoveGate): + return Instruction( + name='move', + qubits=tuple(qubits), + args={}, + ) + raise OperationNotSupportedError(f'{type(operation.gate)} not natively supported.') diff --git a/src/iqm/cirq_iqm/iqm_sampler.py b/src/iqm/cirq_iqm/iqm_sampler.py index 9387a6a5..d93c7fd4 100644 --- a/src/iqm/cirq_iqm/iqm_sampler.py +++ b/src/iqm/cirq_iqm/iqm_sampler.py @@ -28,21 +28,8 @@ import numpy as np from iqm.cirq_iqm.devices.iqm_device import IQMDevice, IQMDeviceMetadata -from iqm.cirq_iqm.iqm_operation_mapping import map_operation -from iqm.iqm_client import Circuit, HeraldingMode, IQMClient, JobAbortionError, RunRequest - - -def serialize_circuit(circuit: cirq.Circuit) -> Circuit: - """Serializes a quantum circuit into the IQM data transfer format. - - Args: - circuit: quantum circuit to serialize - - Returns: - data transfer object representing the circuit - """ - instructions = tuple(map(map_operation, circuit.all_operations())) - return Circuit(name='Serialized from Cirq', instructions=instructions) +from iqm.cirq_iqm.serialize import serialize_circuit +from iqm.iqm_client import Circuit, CircuitCompilationOptions, IQMClient, JobAbortionError, RunRequest class IQMSampler(cirq.work.Sampler): @@ -56,10 +43,7 @@ class IQMSampler(cirq.work.Sampler): ID of the calibration set to use. If ``None``, use the latest one. run_sweep_timeout: timeout to poll sweep results in seconds. - max_circuit_duration_over_t2: Circuits are disqualified on the server if they are longer than - this ratio of the T2 time of the qubits. If set to 0.0, no circuits are disqualified. - If set to None the server default value is used. - heralding_mode: Heralding mode to use during execution. + compiler_options: The compilation options to use for the circuits as defined by IQM Client. Keyword Args: auth_server_url (str): URL of user authentication server, if required by the IQM Cortex server. @@ -77,8 +61,7 @@ def __init__( *, calibration_set_id: Optional[UUID] = None, run_sweep_timeout: Optional[int] = None, - max_circuit_duration_over_t2: Optional[float] = None, - heralding_mode: HeraldingMode = HeraldingMode.NONE, + compiler_options: Optional[CircuitCompilationOptions] = None, **user_auth_args, # contains keyword args auth_server_url, username and password ): self._client = IQMClient(url, client_signature=f'cirq-iqm {version("cirq-iqm")}', **user_auth_args) @@ -89,8 +72,7 @@ def __init__( self._device = device self._calibration_set_id = calibration_set_id self._run_sweep_timeout = run_sweep_timeout - self._max_circuit_duration_over_t2 = max_circuit_duration_over_t2 - self._heralding_mode = heralding_mode + self._compiler_options = compiler_options if compiler_options is not None else CircuitCompilationOptions() @property def device(self) -> IQMDevice: @@ -167,8 +149,7 @@ def create_run_request( serialized_circuits, calibration_set_id=self._calibration_set_id, shots=repetitions, - max_circuit_duration_over_t2=self._max_circuit_duration_over_t2, - heralding_mode=self._heralding_mode, + options=self._compiler_options, ) def _send_circuits( diff --git a/src/iqm/cirq_iqm/serialize.py b/src/iqm/cirq_iqm/serialize.py new file mode 100644 index 00000000..f380fbfe --- /dev/null +++ b/src/iqm/cirq_iqm/serialize.py @@ -0,0 +1,51 @@ +# Copyright 2020–2022 Cirq 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 serializing and deserializing quantum circuits between Cirq and IQM Circuit formats. +""" +from cirq import Circuit + +from iqm import iqm_client +from iqm.cirq_iqm.iqm_operation_mapping import instruction_to_operation, map_operation + + +def serialize_circuit(circuit: Circuit) -> iqm_client.Circuit: + """Serializes a quantum circuit into the IQM data transfer format. + + Args: + circuit: quantum circuit to serialize + + Returns: + data transfer object representing the circuit + """ + instructions = tuple(map(map_operation, circuit.all_operations())) + return iqm_client.Circuit(name='Serialized from Cirq', instructions=instructions) + + +def deserialize_circuit(circuit: iqm_client.Circuit) -> Circuit: + """Deserializes a quantum circuit from the IQM data transfer format to a Cirq Circuit. + + Args: + circuit: data transfer object representing the circuit + + Returns: + quantum circuit + """ + return Circuit( + map( + instruction_to_operation, + circuit.instructions, + ) + ) diff --git a/src/iqm/cirq_iqm/transpiler.py b/src/iqm/cirq_iqm/transpiler.py new file mode 100644 index 00000000..25df6dfb --- /dev/null +++ b/src/iqm/cirq_iqm/transpiler.py @@ -0,0 +1,53 @@ +# Copyright 2020–2022 Cirq 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 IQM specific transpilation needs.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from cirq import Circuit + +from iqm.cirq_iqm.serialize import deserialize_circuit, serialize_circuit +from iqm.iqm_client import ExistingMoveHandlingOptions, transpile_insert_moves + +if TYPE_CHECKING: + from iqm.cirq_iqm.devices import IQMDevice + + +def transpile_insert_moves_into_circuit( + cirq_circuit: Circuit, + device: IQMDevice, + existing_moves: Optional[ExistingMoveHandlingOptions] = None, + qubit_mapping: Optional[dict[str, str]] = None, +) -> Circuit: + """Transpile the circuit to insert MOVE gates where needed. + + Args: + cirq_circuit: Circuit to transpile. + device: Device to transpile for. + existing_moves: How to handle existing MOVE gates, obtained from the IQM client library. + qubit_mapping: Mapping from qubit names in the circuit to the device. + + Returns: + Transpiled circuit. + """ + iqm_client_circuit = serialize_circuit(cirq_circuit) + new_iqm_client_circuit = transpile_insert_moves( + iqm_client_circuit, + device.metadata.to_architecture(), + existing_moves=existing_moves, + qubit_mapping=qubit_mapping, + ) + new_cirq_circuit = deserialize_circuit(new_iqm_client_circuit) + return new_cirq_circuit diff --git a/tests/conftest.py b/tests/conftest.py index 44d6a761..9bc35b6c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,13 @@ # Copyright 2020–2021 Cirq on IQM developers # -# Licensed under the Apache License, Version 2.0 (the "License"); +# 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, +# 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. @@ -20,6 +20,9 @@ import pytest +from iqm.cirq_iqm import IQMDevice, IQMDeviceMetadata +from iqm.iqm_client import QuantumArchitectureSpecification + existing_run = UUID('3c3fcda3-e860-46bf-92a4-bcc59fa76ce9') missing_run = UUID('059e4186-50a3-4e6c-ba1f-37fe6afbdfc2') @@ -27,3 +30,117 @@ @pytest.fixture() def base_url(): return 'https://example.com' + + +@pytest.fixture() +def fake_spec_with_resonator(): + ndonis_architecture_specification = { + 'name': 'Ndonis', + 'operations': { + 'cz': [ + ['QB1', 'COMP_R'], + ['QB2', 'COMP_R'], + ['QB3', 'COMP_R'], + ['QB4', 'COMP_R'], + ['QB5', 'COMP_R'], + ['QB6', 'COMP_R'], + ], + 'prx': [['QB1'], ['QB2'], ['QB3'], ['QB4'], ['QB5'], ['QB6']], + 'move': [ + ['QB1', 'COMP_R'], + ['QB2', 'COMP_R'], + ['QB3', 'COMP_R'], + ['QB4', 'COMP_R'], + ['QB5', 'COMP_R'], + ], + 'barrier': [], + 'measure': [['QB1'], ['QB2'], ['QB3'], ['QB4'], ['QB5'], ['QB6']], + }, + 'qubits': ['COMP_R', 'QB1', 'QB2', 'QB3', 'QB4', 'QB5', 'QB6'], + 'qubit_connectivity': [ + ['QB1', 'COMP_R'], + ['QB2', 'COMP_R'], + ['QB3', 'COMP_R'], + ['QB4', 'COMP_R'], + ['QB5', 'COMP_R'], + ['QB6', 'COMP_R'], + ], + } + return QuantumArchitectureSpecification(**ndonis_architecture_specification) + + +@pytest.fixture +def adonis_architecture_shuffled_names(): + return QuantumArchitectureSpecification( + name='Adonis', + operations={ + 'prx': [['QB2'], ['QB3'], ['QB1'], ['QB5'], ['QB4']], + 'cz': [['QB1', 'QB3'], ['QB2', 'QB3'], ['QB4', 'QB3'], ['QB5', 'QB3']], + 'measure': [['QB2'], ['QB3'], ['QB1'], ['QB5'], ['QB4']], + 'barrier': [], + }, + qubits=['QB2', 'QB3', 'QB1', 'QB5', 'QB4'], + qubit_connectivity=[['QB1', 'QB3'], ['QB2', 'QB3'], ['QB4', 'QB3'], ['QB5', 'QB3']], + ) + + +@pytest.fixture() +def device_without_resonator(adonis_architecture_shuffled_names): + """Returns device object created based on architecture specification""" + return IQMDevice(IQMDeviceMetadata.from_architecture(adonis_architecture_shuffled_names)) + + +@pytest.fixture() +def device_with_resonator(fake_spec_with_resonator): + """Returns device object created based on architecture specification""" + return IQMDevice(IQMDeviceMetadata.from_architecture(fake_spec_with_resonator)) + + +@pytest.fixture +def device_with_multiple_resonators(): + """Some fictional 5 qubit device with multiple resonators.""" + multiple_resonators_specification = { + 'name': 'MultiResonators', + 'operations': { + 'cz': [ + ['QB1', 'COMP_R0'], + ['QB2', 'COMP_R0'], + ['QB2', 'COMP_R1'], + ['QB3', 'COMP_R1'], + ['QB3', 'COMP_R2'], + ['QB4', 'COMP_R2'], + ['QB4', 'COMP_R3'], + ['QB5', 'COMP_R3'], + ['QB5', 'COMP_R4'], + ['QB1', 'COMP_R4'], + ['QB1', 'QB3'], + ], + 'prx': [['QB2'], ['QB3'], ['QB1'], ['QB5'], ['QB4']], + 'move': [ + ['QB1', 'COMP_R0'], + ['QB2', 'COMP_R1'], + ['QB3', 'COMP_R2'], + ['QB4', 'COMP_R3'], + ['QB5', 'COMP_R4'], + ], + 'barrier': [], + 'measure': [['QB2'], ['QB3'], ['QB1'], ['QB5'], ['QB4']], + }, + 'qubits': ['COMP_R0', 'COMP_R1', 'COMP_R2', 'COMP_R3', 'COMP_R4', 'QB1', 'QB2', 'QB3', 'QB4', 'QB5'], + 'qubit_connectivity': [ + ['QB1', 'COMP_R0'], + ['QB2', 'COMP_R0'], + ['QB2', 'COMP_R1'], + ['QB3', 'COMP_R1'], + ['QB3', 'COMP_R2'], + ['QB4', 'COMP_R2'], + ['QB4', 'COMP_R3'], + ['QB5', 'COMP_R3'], + ['QB5', 'COMP_R4'], + ['QB1', 'COMP_R4'], + ['QB1', 'QB3'], + ], + } + return IQMDevice( + IQMDeviceMetadata.from_architecture(QuantumArchitectureSpecification(**multiple_resonators_specification)) + ) diff --git a/tests/test_adonis.py b/tests/test_adonis.py index 94223440..fe367109 100644 --- a/tests/test_adonis.py +++ b/tests/test_adonis.py @@ -153,10 +153,10 @@ def test_qubits_not_connected(self, adonis, gate): q0, q1 = adonis.qubits[:2] - with pytest.raises(ValueError, match='Unsupported qubit connectivity'): + with pytest.raises(ValueError, match='Unsupported operation between qubits'): adonis.validate_operation(gate(q0, q1)) - with pytest.raises(ValueError, match='Unsupported qubit connectivity'): + with pytest.raises(ValueError, match='Unsupported operation between qubits'): adonis.validate_operation(gate(q1, q0)) diff --git a/tests/test_apollo.py b/tests/test_apollo.py index dfff0c40..6f66498d 100644 --- a/tests/test_apollo.py +++ b/tests/test_apollo.py @@ -153,10 +153,10 @@ def test_qubits_not_connected(self, apollo, gate): q0, _, q2 = apollo.qubits[:3] - with pytest.raises(ValueError, match='Unsupported qubit connectivity'): + with pytest.raises(ValueError, match='Unsupported operation between qubits'): apollo.validate_operation(gate(q0, q2)) - with pytest.raises(ValueError, match='Unsupported qubit connectivity'): + with pytest.raises(ValueError, match='Unsupported operation between qubits'): apollo.validate_operation(gate(q2, q0)) @@ -395,10 +395,16 @@ def test_valid_routed_circuit(self, apollo, qubits): cirq.CZ(qubits[0], qubits[4]), cirq.CZ(qubits[0], qubits[5]), ) + print('routing started') + print(circuit) routed_circuit, _, _ = apollo.route_circuit(circuit) + print('decomposing') + print(routed_circuit) valid_circuit = apollo.decompose_circuit(routed_circuit) - + print('validating') + print(valid_circuit) apollo.validate_circuit(valid_circuit) + print('done') @pytest.mark.parametrize( 'qid', diff --git a/tests/test_iqm_device.py b/tests/test_iqm_device.py index 22013b97..c0bc1de1 100644 --- a/tests/test_iqm_device.py +++ b/tests/test_iqm_device.py @@ -11,7 +11,11 @@ # 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. -from iqm.cirq_iqm import Adonis, Apollo, Valkmusa +from cirq import Circuit, HardCodedInitialMapper, NamedQid, NamedQubit, ops +from cirq.testing import assert_circuits_have_same_unitary_given_final_permutation, assert_has_diagram, random_circuit +import pytest + +from iqm.cirq_iqm import Adonis, Apollo, IQMDevice, IQMMoveGate, Valkmusa def test_equality_method(): @@ -27,3 +31,153 @@ def test_equality_method(): assert valkmusa != adonis_1 assert apollo_1 == apollo_2 assert adonis_2 != adonis_3 + + +def test_device_without_resonator(device_without_resonator): + assert_qubit_indexing( + device_without_resonator, + set(zip(range(1, len(device_without_resonator.qubits) + 1), device_without_resonator.qubits)), + ) + assert len(device_without_resonator.resonators) == 0 + + +def test_device_with_resonator(device_with_resonator): + print(device_with_resonator.qubits) + # Tests for device with resonator should pass too. + assert_qubit_indexing( + device_with_resonator, set(zip(range(1, len(device_with_resonator.qubits) + 1), device_with_resonator.qubits)) + ) + assert len(device_with_resonator.resonators) == 1 + assert device_with_resonator.resonators[0] == NamedQid( + "COMP_R", device_with_resonator._metadata.RESONATOR_DIMENSION + ) + + +def assert_qubit_indexing(backend: IQMDevice, correct_idx_name_associations): + assert all(backend.get_qubit(idx) == name for idx, name in correct_idx_name_associations) + assert all(backend.get_qubit_index(name) == idx for idx, name in correct_idx_name_associations) + # Below assertions are done in Qiskit but do not make sense for Cirq. + # assert backend.index_to_qubit_name(7) is None + # assert backend.qubit_name_to_index("Alice") is None + + +@pytest.mark.parametrize("device", ["device_without_resonator", "device_with_resonator", Apollo(), Adonis()]) +def test_transpilation(device: IQMDevice, request): + if isinstance(device, str): + device = request.getfixturevalue(device) + print(device) + size = 5 + circuit = random_circuit(device.qubits[:size], size, 1, random_state=1337) + print(circuit) + print("decomposing") + decomposed_circuit = device.decompose_circuit(circuit) + naive_map = {q: q for q in device.qubits} + print("routing") + routed_circuit, initial_map, final_map = device.route_circuit( + decomposed_circuit, initial_mapper=HardCodedInitialMapper(naive_map) + ) + assert initial_map == naive_map + print("decomposing") + decomposed_routed_circuit = device.decompose_circuit(routed_circuit) + print("validating") + device.validate_circuit(decomposed_routed_circuit) + print("checking equivalence") + qubit_map = {q1: q2 for q1, q2 in final_map.items() if q1 in decomposed_routed_circuit.all_qubits()} + assert_circuits_have_same_unitary_given_final_permutation( + decomposed_circuit, circuit, qubit_map={q: q for q in device.qubits[:size]} + ) + print("Circuit qubits:", circuit.all_qubits()) + print("Routed circuit qubits:", routed_circuit.all_qubits()) + for q in routed_circuit.all_qubits(): + if q not in circuit.all_qubits(): + circuit.append(ops.IdentityGate(1)(q)) + print("Circuit qubits after adding routing qubits:", circuit.all_qubits()) + assert_circuits_have_same_unitary_given_final_permutation(routed_circuit, circuit, qubit_map=qubit_map) + assert_circuits_have_same_unitary_given_final_permutation(decomposed_routed_circuit, circuit, qubit_map=qubit_map) + + +@pytest.mark.parametrize( + "device", + ["device_without_resonator", "device_with_resonator", Apollo(), Adonis(), "device_with_multiple_resonators"], +) +def test_qubit_connectivity(device: IQMDevice, request): + if isinstance(device, str): + device = request.getfixturevalue(device) + for edge in [(q1, q2) for q1 in device.qubits for q2 in device.qubits if q1 != q2]: + gate = ops.CZPowGate()(*edge) + if ( + edge in device.supported_operations[ops.CZPowGate] + or tuple(reversed(edge)) in device.supported_operations[ops.CZPowGate] + ): + device.check_qubit_connectivity(gate) # This should not raise an error + else: + with pytest.raises(ValueError): + device.check_qubit_connectivity(gate) + with pytest.raises(ValueError): + device.check_qubit_connectivity(ops.SwapPowGate()(*edge)) + + +def test_validate_moves(device_with_resonator): + # Test valid MOVE sandwich + circuit = Circuit( + IQMMoveGate()(device_with_resonator.qubits[0], device_with_resonator.resonators[0]), + IQMMoveGate()(device_with_resonator.qubits[0], device_with_resonator.resonators[0]), + ) + assert device_with_resonator.validate_moves(circuit) is None + # Test invalid MOVE sandwich + circuit = Circuit( + IQMMoveGate()(device_with_resonator.qubits[0], device_with_resonator.resonators[0]), + IQMMoveGate()(device_with_resonator.qubits[1], device_with_resonator.resonators[0]), + ) + with pytest.raises(ValueError): + device_with_resonator.validate_moves(circuit) + + circuit = Circuit( + IQMMoveGate()(device_with_resonator.qubits[0], device_with_resonator.resonators[0]), + IQMMoveGate()(device_with_resonator.qubits[0], device_with_resonator.qubits[1]), + ) + with pytest.raises(ValueError): + device_with_resonator.validate_moves(circuit) + + circuit = Circuit( + IQMMoveGate()(device_with_resonator.resonators[0], device_with_resonator.qubits[1]), + IQMMoveGate()(device_with_resonator.resonators[0], device_with_resonator.qubits[1]), + ) + with pytest.raises(ValueError): + device_with_resonator.validate_moves(circuit) + + # Test valid MOVE without sandwich + circuit = Circuit( + IQMMoveGate()(device_with_resonator.qubits[0], device_with_resonator.resonators[0]), + ) + with pytest.raises(ValueError): + device_with_resonator.validate_moves(circuit) + + # Test odd valid MOVEs (incomplete sandwich) + circuit = Circuit( + IQMMoveGate()(device_with_resonator.qubits[0], device_with_resonator.resonators[0]), + IQMMoveGate()(device_with_resonator.qubits[0], device_with_resonator.resonators[0]), + IQMMoveGate()(device_with_resonator.qubits[0], device_with_resonator.resonators[0]), + ) + with pytest.raises(ValueError): + device_with_resonator.validate_moves(circuit) + # Test no moves + circuit = Circuit() + assert device_with_resonator.validate_moves(circuit) is None + assert ( + device_with_resonator.validate_move( + ops.CZPowGate()(device_with_resonator.qubits[0], device_with_resonator.resonators[0]) + ) + is None + ) + + +def test_move_gate_drawing(): + """Check that the MOVE gate can be drawn.""" + res = NamedQid("Resonator", dimension=2) + qubit = NamedQubit("Qubit") + gate = IQMMoveGate()(qubit, res) + c = Circuit(gate) + assert_has_diagram(c, "Qubit: ─────────────MOVE(QB)────\n │\nResonator (d=2): ───MOVE(Res)───") + + assert str(gate) == "MOVE(Qubit, Resonator (d=2))" diff --git a/tests/test_iqm_operation_mapping.py b/tests/test_iqm_operation_mapping.py index 46766f3b..1e8c935f 100644 --- a/tests/test_iqm_operation_mapping.py +++ b/tests/test_iqm_operation_mapping.py @@ -13,9 +13,11 @@ # limitations under the License. import cirq from cirq import CZPowGate, GateOperation, MeasurementGate, PhasedXPowGate, XPowGate, YPowGate, ZPowGate +from mockito import mock import pytest -from iqm.cirq_iqm.iqm_operation_mapping import OperationNotSupportedError, map_operation +from iqm.cirq_iqm.iqm_gates import IQMMoveGate +from iqm.cirq_iqm.iqm_operation_mapping import OperationNotSupportedError, instruction_to_operation, map_operation from iqm.iqm_client import Instruction @@ -81,3 +83,34 @@ def test_raises_error_for_non_trivial_invert_mask(qubit_1, qubit_2): operation = GateOperation(MeasurementGate(2, 'measurement key', invert_mask=(True, False)), [qubit_1, qubit_2]) with pytest.raises(OperationNotSupportedError): map_operation(operation) + + +def test_instruction_to_operation(): + instruction = Instruction(name='prx', qubits=('QB1',), args={'angle_t': 0.5, 'phase_t': 0.25}) + operation = instruction_to_operation(instruction) + assert isinstance(operation.gate, PhasedXPowGate) + assert operation.qubits == (cirq.NamedQubit('QB1'),) + assert operation.gate.exponent == 1.0 + assert operation.gate.phase_exponent == 0.5 + + instruction = Instruction(name='cz', qubits=('QB1', 'QB2'), args={}) + operation = instruction_to_operation(instruction) + assert isinstance(operation.gate, CZPowGate) + assert operation.qubits == (cirq.NamedQubit('QB1'), cirq.NamedQubit('QB2')) + assert operation.gate.exponent == 1.0 + assert operation.gate.global_shift == 0.0 + + instruction = Instruction(name='measurement', qubits=('QB1',), args={'key': 'test key'}) + operation = instruction_to_operation(instruction) + assert isinstance(operation.gate, MeasurementGate) + assert operation.qubits == (cirq.NamedQubit('QB1'),) + assert operation.gate.key == 'test key' + + instruction = mock({'name': 'unsupported', 'qubits': ('QB1',), 'args': {}}, spec=Instruction) + with pytest.raises(OperationNotSupportedError): + operation = instruction_to_operation(instruction) + + instruction = Instruction(name='move', qubits=('QB1', 'COMP_R'), args={}) + operation = instruction_to_operation(instruction) + assert isinstance(operation.gate, IQMMoveGate) + assert operation.qubits == (cirq.NamedQubit('QB1'), cirq.NamedQid('COMP_R', dimension=2)) diff --git a/tests/test_iqm_sampler.py b/tests/test_iqm_sampler.py index 6e9fb24d..3d90f533 100644 --- a/tests/test_iqm_sampler.py +++ b/tests/test_iqm_sampler.py @@ -23,9 +23,11 @@ from iqm.cirq_iqm import Adonis import iqm.cirq_iqm as module_under_test +from iqm.cirq_iqm.iqm_gates import IQMMoveGate from iqm.cirq_iqm.iqm_sampler import IQMResult, IQMSampler, ResultMetadata, serialize_circuit from iqm.iqm_client import ( Circuit, + CircuitCompilationOptions, HeraldingMode, Instruction, IQMClient, @@ -80,8 +82,7 @@ def create_run_request_default_kwargs() -> dict: return { 'calibration_set_id': None, 'shots': 1, - 'max_circuit_duration_over_t2': None, - 'heralding_mode': HeraldingMode.NONE, + 'options': CircuitCompilationOptions(), } @@ -149,7 +150,10 @@ def test_run_sweep_has_duration_check_enabled_by_default( client = mock(IQMClient) sampler = IQMSampler(base_url, Adonis()) run_result = RunResult(status=Status.READY, measurements=[{'some stuff': [[0], [1]]}], metadata=iqm_metadata) - kwargs = create_run_request_default_kwargs | {'max_circuit_duration_over_t2': None} + assert sampler._compiler_options.max_circuit_duration_over_t2 is None + kwargs = create_run_request_default_kwargs | { + 'options': CircuitCompilationOptions(max_circuit_duration_over_t2=None) + } when(client).create_run_request(ANY, **kwargs).thenReturn(run_request) when(client).submit_run_request(run_request).thenReturn(job_id) when(client).wait_for_results(job_id).thenReturn(run_result) @@ -167,9 +171,14 @@ def test_run_sweep_executes_circuit_with_duration_check_disabled( ): # pylint: disable=too-many-arguments client = mock(IQMClient) - sampler = IQMSampler(base_url, Adonis(), max_circuit_duration_over_t2=0.0) + sampler = IQMSampler( + base_url, Adonis(), compiler_options=CircuitCompilationOptions(max_circuit_duration_over_t2=0.0) + ) run_result = RunResult(status=Status.READY, measurements=[{'some stuff': [[0], [1]]}], metadata=iqm_metadata) - kwargs = create_run_request_default_kwargs | {'max_circuit_duration_over_t2': 0.0} + assert sampler._compiler_options.max_circuit_duration_over_t2 == 0.0 + kwargs = create_run_request_default_kwargs | { + 'options': CircuitCompilationOptions(max_circuit_duration_over_t2=0.0) + } when(client).create_run_request(ANY, **kwargs).thenReturn(run_request) when(client).submit_run_request(run_request).thenReturn(job_id) when(client).wait_for_results(job_id).thenReturn(run_result) @@ -209,7 +218,8 @@ def test_run_sweep_has_heralding_mode_none_by_default( client = mock(IQMClient) sampler = IQMSampler(base_url, Adonis()) run_result = RunResult(status=Status.READY, measurements=[{'some stuff': [[0], [1]]}], metadata=iqm_metadata) - kwargs = create_run_request_default_kwargs | {'heralding_mode': HeraldingMode.NONE} + kwargs = create_run_request_default_kwargs + assert sampler._compiler_options.heralding_mode == HeraldingMode.NONE when(client).create_run_request(ANY, **kwargs).thenReturn(run_request) when(client).submit_run_request(run_request).thenReturn(job_id) when(client).wait_for_results(job_id).thenReturn(run_result) @@ -227,9 +237,14 @@ def test_run_sweep_executes_circuit_with_heralding_mode_zeros( ): # pylint: disable=too-many-arguments client = mock(IQMClient) - sampler = IQMSampler(base_url, Adonis(), heralding_mode=HeraldingMode.ZEROS) + sampler = IQMSampler( + base_url, Adonis(), compiler_options=CircuitCompilationOptions(heralding_mode=HeraldingMode.ZEROS) + ) run_result = RunResult(status=Status.READY, measurements=[{'some stuff': [[0], [1]]}], metadata=iqm_metadata) - kwargs = create_run_request_default_kwargs | {'heralding_mode': HeraldingMode.ZEROS} + kwargs = create_run_request_default_kwargs | { + 'options': CircuitCompilationOptions(heralding_mode=HeraldingMode.ZEROS) + } + assert sampler._compiler_options.heralding_mode == HeraldingMode.ZEROS when(client).create_run_request(ANY, **kwargs).thenReturn(run_request) when(client).submit_run_request(run_request).thenReturn(job_id) when(client).wait_for_results(job_id).thenReturn(run_result) @@ -318,6 +333,62 @@ def test_run_iqm_batch_raises_with_non_physical_names(adonis_sampler, circuit_no adonis_sampler.run_iqm_batch([circuit_non_physical]) +@pytest.mark.usefixtures('unstub') +def test_run(adonis_sampler, iqm_metadata, create_run_request_default_kwargs, job_id): + client = mock(IQMClient) + repetitions = 123 + run_result = RunResult( + status=Status.READY, measurements=[{'some stuff': [[0]]}, {'some stuff': [[1]]}], metadata=iqm_metadata + ) + kwargs = create_run_request_default_kwargs | {'shots': repetitions} + when(client).create_run_request(ANY, **kwargs).thenReturn(run_request) + when(client).submit_run_request(run_request).thenReturn(job_id) + when(client).wait_for_results(job_id).thenReturn(run_result) + + qubit_1 = cirq.NamedQubit('QB1') + qubit_2 = cirq.NamedQubit('QB2') + circuit1 = cirq.Circuit(cirq.X(qubit_1), cirq.measure(qubit_1, qubit_2, key='result')) + + adonis_sampler._client = client + result = adonis_sampler.run(circuit1, repetitions=repetitions) + + assert isinstance(result, IQMResult) + assert isinstance(result.metadata, ResultMetadata) + np.testing.assert_array_equal(result.measurements['some stuff'], np.array([[0]])) + + +@pytest.mark.usefixtures('unstub') +def test_run_ndonis(device_with_resonator, base_url, iqm_metadata, create_run_request_default_kwargs, job_id): + sampler = IQMSampler(base_url, device=device_with_resonator) + client = mock(IQMClient) + repetitions = 123 + run_result = RunResult( + status=Status.READY, measurements=[{'some stuff': [[0]]}, {'some stuff': [[1]]}], metadata=iqm_metadata + ) + kwargs = create_run_request_default_kwargs | {'shots': repetitions} + when(client).create_run_request(ANY, **kwargs).thenReturn(run_request) + when(client).submit_run_request(run_request).thenReturn(job_id) + when(client).wait_for_results(job_id).thenReturn(run_result) + + qubit_1, qubit_2 = device_with_resonator.qubits[:2] + resonator = device_with_resonator.resonators[0] + circuit = cirq.Circuit() + circuit.append(device_with_resonator.decompose_operation(cirq.H(qubit_1))) + circuit.append(IQMMoveGate().on(qubit_1, resonator)) + circuit.append(device_with_resonator.decompose_operation(cirq.H(qubit_2))) + circuit.append(cirq.CZ(resonator, qubit_2)) + circuit.append(IQMMoveGate().on(qubit_1, resonator)) + circuit.append(device_with_resonator.decompose_operation(cirq.H(qubit_2))) + circuit.append(cirq.MeasurementGate(2, key='result').on(qubit_1, qubit_2)) + + sampler._client = client + result = sampler.run(circuit, repetitions=repetitions) + + assert isinstance(result, IQMResult) + assert isinstance(result.metadata, ResultMetadata) + np.testing.assert_array_equal(result.measurements['some stuff'], np.array([[0]])) + + @pytest.mark.usefixtures('unstub') def test_run_iqm_batch(adonis_sampler, iqm_metadata, create_run_request_default_kwargs, job_id, run_request): client = mock(IQMClient)