Skip to content

Commit

Permalink
Added Deneb-style transpiling support (requires iqm-client 18.0) (#133)
Browse files Browse the repository at this point in the history
* Added basic support for MOVE operations

* File cleanup

* Validate move gate with correct arguments

* Added move gate validation

* initial implementation of transpiler tests

* Improved testing and operation validation to check against what is calibrated rather than assuming all operation in the gateset areallowed

* wip add transpiler

* Improved test coverage

* Version bumb for iqm-client

* Version bumb iqm-client

* Version bump iqm-client

* Added qubit subset option for routing.

* Exposed transpile method to users directly

* Updated documentation

* Added compiler option support to the IQM Sampler

* Update to iqm-client==18.0

* Fix formatting

* Fix automatic Merge resolver

* Added reviewer feedback

---------

Co-authored-by: Arianne Meijer <arianne.meijer@meetiqm.com>
  • Loading branch information
Aerylia and Arianne Meijer committed Aug 29, 2024
1 parent 572edf4 commit 8c447f8
Show file tree
Hide file tree
Showing 18 changed files with 848 additions and 86 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/iqm-finland/cirq-on-iqm/pull/133>`_
* Added IQMMoveGate class for Deneb architectures. `#133 <https://github.com/iqm-finland/cirq-on-iqm/pull/133>`_
* Updated IQMDevice class to support devices with resonators. `#133 <https://github.com/iqm-finland/cirq-on-iqm/pull/133>`_
* Support for :class:`CircuitCompilationOptions` from ``iqm-client`` when submitting a circuit to an IQM device.
* Require iqm-client >= 18.0. `#133 <https://github.com/iqm-finland/cirq-on-iqm/pull/133>`_

Version 14.2
============

Expand Down
3 changes: 3 additions & 0 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions src/iqm/cirq_iqm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
120 changes: 112 additions & 8 deletions src/iqm/cirq_iqm/devices/iqm_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
)
Expand All @@ -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

Expand All @@ -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):
Expand All @@ -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
Loading

0 comments on commit 8c447f8

Please sign in to comment.