diff --git a/cirq-ionq/cirq_ionq/__init__.py b/cirq-ionq/cirq_ionq/__init__.py index c9799d314a7..a4d41a638e4 100644 --- a/cirq-ionq/cirq_ionq/__init__.py +++ b/cirq-ionq/cirq_ionq/__init__.py @@ -22,6 +22,7 @@ from cirq_ionq.ionq_devices import ( IonQAPIDevice, + decompose_to_device, ) from cirq_ionq.ionq_exceptions import ( diff --git a/cirq-ionq/cirq_ionq/ionq_devices.py b/cirq-ionq/cirq_ionq/ionq_devices.py index 23cbaaeef1d..52638291160 100644 --- a/cirq-ionq/cirq_ionq/ionq_devices.py +++ b/cirq-ionq/cirq_ionq/ionq_devices.py @@ -16,6 +16,23 @@ from typing import AbstractSet, Sequence, Union import cirq +from cirq import _compat + + +_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, + accept_global_phase_op=False, +) class IonQAPIDevice(cirq.Device): @@ -49,20 +66,6 @@ def __init__(self, qubits: Union[Sequence[cirq.LineQubit], int], atol=1e-8): else: self.qubits = frozenset(qubits) self.atol = atol - self.gateset = 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, - accept_global_phase_op=False, - ) def qubit_set(self) -> AbstractSet['cirq.Qid']: return self.qubits @@ -78,42 +81,71 @@ 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 self.gateset + return operation in _VALID_GATES + @_compat.deprecated( + fix='Use cirq_ionq.decompose_to_device operation instead.', + deadline='v0.15', + ) def decompose_operation(self, operation: cirq.Operation) -> cirq.OP_TREE: - if self.is_api_gate(operation): - return operation - assert cirq.has_unitary(operation), ( - f'Operation {operation} that 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 self._decompose_single_qubit(operation) - if num_qubits == 2: - return self._decompose_two_qubit(operation) - raise ValueError(f'Operation {operation} not supported by IonQ API.') - - def _decompose_single_qubit(self, operation: cirq.Operation) -> cirq.OP_TREE: - qubit = operation.qubits[0] - mat = cirq.unitary(operation) - for gate in cirq.single_qubit_matrix_to_gates(mat, self.atol): - yield gate(qubit) - - def _decompose_two_qubit(self, 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_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, - ) - cirq.merge_single_qubit_gates_into_phased_x_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, - ).all_operations() + return decompose_to_device(operation) + + +def decompose_to_device(operation: cirq.Operation, atol: float = 1e-8) -> cirq.OP_TREE: + """Decompose operation to ionq native operations. + + + Merges single qubit operations and decomposes two qubit operations + into CZ gates. + + Args: + operation: `cirq.Operation` to decompose. + atol: absolute error tolerance to use when declaring two unitary + operations equal. + + Returns: + cirq.OP_TREE containing decomposed operations. + + Raises: + ValueError: If supplied operation cannot be decomposed + 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_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, + ) + cirq.merge_single_qubit_gates_into_phased_x_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, + ).all_operations() diff --git a/cirq-ionq/cirq_ionq/ionq_devices_test.py b/cirq-ionq/cirq_ionq/ionq_devices_test.py index 389f1b6d36d..674d5937312 100644 --- a/cirq-ionq/cirq_ionq/ionq_devices_test.py +++ b/cirq-ionq/cirq_ionq/ionq_devices_test.py @@ -122,45 +122,87 @@ def test_validate_circuit_valid(): @pytest.mark.parametrize('gate', VALID_GATES) -def test_decompose_leaves_supported_alone(gate): +def test_decompose_leaves_supported_alone_deprecated(gate): qubits = cirq.LineQubit.range(gate.num_qubits()) device = ionq.IonQAPIDevice(qubits=qubits) operation = gate(*qubits) - assert device.decompose_operation(operation) == operation + with cirq.testing.assert_deprecated('decompose_to_device', deadline='v0.15'): + assert device.decompose_operation(operation) == operation + + +@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(): +def test_decompose_single_qubit_matrix_gate_deprecated(): q = cirq.LineQubit(0) device = ionq.IonQAPIDevice(qubits=[q]) for _ in range(100): gate = cirq.MatrixGate(cirq.testing.random_unitary(2)) circuit = cirq.Circuit(gate(q)) - decomposed_circuit = cirq.Circuit(*device.decompose_operation(gate(q))) + with cirq.testing.assert_deprecated('decompose_to_device', deadline='v0.15'): + decomposed_circuit = cirq.Circuit(*device.decompose_operation(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(): +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_deprecated(): q0, q1 = cirq.LineQubit.range(2) device = ionq.IonQAPIDevice(qubits=[q0, q1]) for _ in range(10): gate = cirq.MatrixGate(cirq.testing.random_unitary(4)) circuit = cirq.Circuit(gate(q0, q1)) - decomposed_circuit = cirq.Circuit(*device.decompose_operation(gate(q0, q1))) + with cirq.testing.assert_deprecated('decompose_to_device', deadline='v0.15'): + decomposed_circuit = cirq.Circuit(*device.decompose_operation(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(): +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_deprecated(): q0, q1, q2 = cirq.LineQubit.range(3) device = ionq.IonQAPIDevice(qubits=[q0, q1, q2]) op = cirq.CCZ(q0, q1, q2) with pytest.raises(ValueError, match='not supported'): - _ = device.decompose_operation(op) + with cirq.testing.assert_deprecated('decompose_to_device', deadline='v0.15'): + _ = device.decompose_operation(op) + + +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)