Skip to content

Commit

Permalink
Add IonQTargetGateset and use it for circuit compilations. (quantumli…
Browse files Browse the repository at this point in the history
…b#5479)

Fixes quantumlib#5129 quantumlib#4901 quantumlib#4153
Replaces quantumlib#5127

* Adds support for compiling > 2q operations to ionq's target gateset
* Does not do the "merge 2q operations to a connected component" optimization, to preserve existing behavior. We can add another `preprocess_transformer` if we want to do the optimization. 
* Deprecates `cirq_ionq.decompose_to_device`  in favour of using `cirq.optimize_for_target_gateset(circuit, gateset=cirq_ionq.IonQTargetGateset)` for compiling circuits. 

cc @Cynocracy @dabacon
  • Loading branch information
tanujkhattar authored Jun 13, 2022
1 parent e2f32e2 commit 3360198
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 124 deletions.
2 changes: 2 additions & 0 deletions cirq-ionq/cirq_ionq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

from cirq_ionq.ionq_devices import IonQAPIDevice, decompose_to_device

from cirq_ionq.ionq_gateset import IonQTargetGateset

from cirq_ionq.ionq_exceptions import (
IonQException,
IonQNotFoundException,
Expand Down
66 changes: 13 additions & 53 deletions cirq-ionq/cirq_ionq/ionq_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,11 @@
# limitations under the License.
"""Devices for IonQ hardware."""

from typing import Sequence, Union
from typing import Sequence
from typing import Union

import cirq


_VALID_GATES = cirq.Gateset(
cirq.H,
cirq.CNOT,
cirq.SWAP,
cirq.XPowGate,
cirq.YPowGate,
cirq.ZPowGate,
cirq.XXPowGate,
cirq.YYPowGate,
cirq.ZZPowGate,
cirq.MeasurementGate,
unroll_circuit_op=False,
)
from cirq_ionq import ionq_gateset


class IonQAPIDevice(cirq.Device):
Expand Down Expand Up @@ -64,6 +51,7 @@ def __init__(self, qubits: Union[Sequence[cirq.LineQubit], int], atol=1e-8):
else:
self.qubits = frozenset(qubits)
self.atol = atol
self.gateset = ionq_gateset.IonQTargetGateset()
self._metadata = cirq.DeviceMetadata(
self.qubits, [(a, b) for a in self.qubits for b in self.qubits if a != b]
)
Expand All @@ -83,9 +71,15 @@ def validate_operation(self, operation: cirq.Operation):
raise ValueError(f'Operation with qubits not on the device. Qubits: {operation.qubits}')

def is_api_gate(self, operation: cirq.Operation) -> bool:
return operation in _VALID_GATES
return operation in self.gateset


@cirq._compat.deprecated(
deadline='v0.16',
fix='Use cirq.optimize_for_target_gateset(circuit, '
'gateset=cirq_ionq.IonQTargetGateset(atol)) '
'instead.',
)
def decompose_to_device(operation: cirq.Operation, atol: float = 1e-8) -> cirq.OP_TREE:
"""Decompose operation to ionq native operations.
Expand All @@ -106,40 +100,6 @@ def decompose_to_device(operation: cirq.Operation, atol: float = 1e-8) -> cirq.O
for the ionq device.
"""
if operation in _VALID_GATES:
return operation
assert cirq.has_unitary(operation), (
f'Operation {operation} is not available on the IonQ API nor does it have a '
'unitary matrix to use to decompose it to the API.'
)
num_qubits = len(operation.qubits)
if num_qubits == 1:
return _decompose_single_qubit(operation, atol)
if num_qubits == 2:
return _decompose_two_qubit(operation)
raise ValueError(f'Operation {operation} not supported by IonQ API.')


def _decompose_single_qubit(operation: cirq.Operation, atol: float) -> cirq.OP_TREE:
qubit = operation.qubits[0]
mat = cirq.unitary(operation)
for gate in cirq.single_qubit_matrix_to_gates(mat, atol):
yield gate(qubit)


def _decompose_two_qubit(operation: cirq.Operation) -> cirq.OP_TREE:
"""Decomposes a two qubit unitary operation into ZPOW, XPOW, and CNOT."""
mat = cirq.unitary(operation)
q0, q1 = operation.qubits
naive = cirq.two_qubit_matrix_to_cz_operations(q0, q1, mat, allow_partial_czs=False)
temp = cirq.map_operations_and_unroll(
cirq.Circuit(naive),
lambda op, _: [cirq.H(op.qubits[1]), cirq.CNOT(*op.qubits), cirq.H(op.qubits[1])]
if type(op.gate) == cirq.CZPowGate
else op,
)
temp = cirq.merge_single_qubit_gates_to_phased_x_and_z(temp)
# A final pass breaks up PhasedXPow into Rz, Rx.
yield cirq.map_operations_and_unroll(
temp, lambda op, _: cirq.decompose_once(op) if type(op.gate) == cirq.PhasedXPowGate else op
return cirq.optimize_for_target_gateset(
cirq.Circuit(operation), gateset=ionq_gateset.IonQTargetGateset(), ignore_failures=False
).all_operations()
78 changes: 7 additions & 71 deletions cirq-ionq/cirq_ionq/ionq_devices_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

import cirq
import cirq_ionq as ionq


VALID_GATES = (
cirq.X,
cirq.Y,
cirq.Z,
cirq.X**0.5,
cirq.Y**0.5,
cirq.Z**0.5,
cirq.rx(0.1),
cirq.ry(0.1),
cirq.rz(0.1),
cirq.H,
cirq.HPowGate(exponent=1, global_shift=-0.5),
cirq.T,
cirq.S,
cirq.CNOT,
cirq.CXPowGate(exponent=1, global_shift=-0.5),
cirq.XX,
cirq.YY,
cirq.ZZ,
cirq.XX**0.5,
cirq.YY**0.5,
cirq.ZZ**0.5,
cirq.SWAP,
cirq.SwapPowGate(exponent=1, global_shift=-0.5),
cirq.MeasurementGate(num_qubits=1, key='a'),
cirq.MeasurementGate(num_qubits=2, key='b'),
cirq.MeasurementGate(num_qubits=10, key='c'),
)
import pytest
from cirq_ionq.ionq_gateset_test import VALID_GATES


@pytest.mark.parametrize('gate', VALID_GATES)
Expand Down Expand Up @@ -120,42 +90,8 @@ def test_validate_circuit_valid():
device.validate_circuit(circuit)


@pytest.mark.parametrize('gate', VALID_GATES)
def test_decompose_leaves_supported_alone(gate):
qubits = cirq.LineQubit.range(gate.num_qubits())
operation = gate(*qubits)
assert ionq.decompose_to_device(operation) == operation


VALID_DECOMPOSED_GATES = cirq.Gateset(cirq.XPowGate, cirq.ZPowGate, cirq.CNOT)


def test_decompose_single_qubit_matrix_gate():
q = cirq.LineQubit(0)
for _ in range(100):
gate = cirq.MatrixGate(cirq.testing.random_unitary(2))
circuit = cirq.Circuit(gate(q))
decomposed_circuit = cirq.Circuit(*ionq.decompose_to_device(gate(q)))
cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent(
circuit, decomposed_circuit, atol=1e-8
)
assert VALID_DECOMPOSED_GATES.validate(decomposed_circuit)


def test_decompose_two_qubit_matrix_gate():
q0, q1 = cirq.LineQubit.range(2)
for _ in range(10):
gate = cirq.MatrixGate(cirq.testing.random_unitary(4))
circuit = cirq.Circuit(gate(q0, q1))
decomposed_circuit = cirq.Circuit(*ionq.decompose_to_device(gate(q0, q1)))
cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent(
circuit, decomposed_circuit, atol=1e-8
)
assert VALID_DECOMPOSED_GATES.validate(decomposed_circuit)


def test_decompose_unsupported_gate():
q0, q1, q2 = cirq.LineQubit.range(3)
op = cirq.CCZ(q0, q1, q2)
with pytest.raises(ValueError, match='not supported'):
_ = ionq.decompose_to_device(op)
def test_decompose_operation_deprecated():
with cirq.testing.assert_deprecated(
'Use cirq.optimize_for_target_gateset', deadline='v0.16', count=2
):
_ = ionq.decompose_to_device(cirq.CZ(*cirq.LineQubit.range(2)))
109 changes: 109 additions & 0 deletions cirq-ionq/cirq_ionq/ionq_gateset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Copyright 2022 The Cirq 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
#
# https://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.

"""Target gateset used for compiling circuits to IonQ device."""
from typing import Any
from typing import Dict
from typing import List

import cirq
from cirq.transformers.target_gatesets.compilation_target_gateset import (
_create_transformer_with_kwargs,
)


class IonQTargetGateset(cirq.TwoQubitCompilationTargetGateset):
"""Target gateset for compiling circuits to IonQ devices.
The gate families accepted by this gateset are:
Type gate families:
* Single-Qubit Gates: `cirq.XPowGate`, `cirq.YPowGate`, `cirq.ZPowGate`.
* Two-Qubit Gates: `cirq.XXPowGate`, `cirq.YYPowGate`, `cirq.ZZPowGate`.
* Measurement Gate: `cirq.MeasurementGate`.
Instance gate families:
* Single-Qubit Gates: `cirq.H`.
* Two-Qubit Gates: `cirq.CNOT`, `cirq.SWAP`.
"""

def __init__(self, *, atol: float = 1e-8):
"""Initializes CZTargetGateset
Args:
atol: A limit on the amount of absolute error introduced by the decomposition.
"""
super().__init__(
cirq.H,
cirq.CNOT,
cirq.SWAP,
cirq.XPowGate,
cirq.YPowGate,
cirq.ZPowGate,
cirq.XXPowGate,
cirq.YYPowGate,
cirq.ZZPowGate,
cirq.MeasurementGate,
unroll_circuit_op=False,
)
self.atol = atol

def _decompose_single_qubit_operation(self, op: cirq.Operation, _) -> cirq.OP_TREE:
qubit = op.qubits[0]
mat = cirq.unitary(op)
for gate in cirq.single_qubit_matrix_to_gates(mat, self.atol):
yield gate(qubit)

def _decompose_two_qubit_operation(self, op: cirq.Operation, _) -> cirq.OP_TREE:
if not cirq.has_unitary(op):
return NotImplemented
mat = cirq.unitary(op)
q0, q1 = op.qubits
naive = cirq.two_qubit_matrix_to_cz_operations(q0, q1, mat, allow_partial_czs=False)
temp = cirq.map_operations_and_unroll(
cirq.Circuit(naive),
lambda op, _: [cirq.H(op.qubits[1]), cirq.CNOT(*op.qubits), cirq.H(op.qubits[1])]
if op.gate == cirq.CZ
else op,
)
return cirq.merge_k_qubit_unitaries(
temp, k=1, rewriter=lambda op: self._decompose_single_qubit_operation(op, -1)
).all_operations()

@property
def preprocess_transformers(self) -> List['cirq.TRANSFORMER']:
"""List of transformers which should be run before decomposing individual operations."""
return [
_create_transformer_with_kwargs(
cirq.expand_composite, no_decomp=lambda op: cirq.num_qubits(op) <= self.num_qubits
)
]

@property
def postprocess_transformers(self) -> List['cirq.TRANSFORMER']:
"""List of transformers which should be run after decomposing individual operations."""
return [cirq.drop_negligible_operations, cirq.drop_empty_moments]

def __repr__(self) -> str:
return f'cirq_ionq.IonQTargetGateset(atol={self.atol})'

def _value_equality_values_(self) -> Any:
return self.atol

def _json_dict_(self) -> Dict[str, Any]:
return cirq.obj_to_dict_helper(self, ['atol'])

@classmethod
def _from_json_dict_(cls, atol, **kwargs):
return cls(atol=atol)
Loading

0 comments on commit 3360198

Please sign in to comment.