diff --git a/cirq-google/cirq_google/__init__.py b/cirq-google/cirq_google/__init__.py index 7ecf824a81e..c253ac20c1c 100644 --- a/cirq-google/cirq_google/__init__.py +++ b/cirq-google/cirq_google/__init__.py @@ -109,6 +109,7 @@ from cirq_google.transformers import ( known_2q_op_to_sycamore_operations, two_qubit_matrix_to_sycamore_operations, + SycamoreTargetGateset, ) from cirq_google.serialization import ( diff --git a/cirq-google/cirq_google/json_test_data/spec.py b/cirq-google/cirq_google/json_test_data/spec.py index afe944f8d28..8fa96036aaf 100644 --- a/cirq-google/cirq_google/json_test_data/spec.py +++ b/cirq-google/cirq_google/json_test_data/spec.py @@ -22,6 +22,7 @@ 'WITHOUT_CHI_FLOQUET_PHASED_FSIM_CHARACTERIZATION', 'XmonDevice', 'XMON', + 'SycamoreTargetGateset', ], should_not_be_serialized=[ 'AnnealSequenceSearchStrategy', diff --git a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py index 07ccb156b01..b6e076adc3c 100644 --- a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py +++ b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py @@ -21,6 +21,10 @@ from cirq_google.transformers.analytical_decompositions import two_qubit_to_sycamore +@cirq._compat.deprecated_class( + deadline='v1.0', + fix='Use cirq.optimize_for_target_gateset and cirq_google.SycamoreTargetGateset instead.', +) class ConvertToSycamoreGates(cirq.PointOptimizer): """Attempts to convert non-native gates into SycamoreGates. diff --git a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py index af9ed9518f1..f8458b0f99c 100644 --- a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py +++ b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py @@ -27,9 +27,11 @@ def test_convert_to_sycamore_gates_swap_zz(): ) compiled_circuit1 = circuit1.copy() - cgoc.ConvertToSycamoreGates()(compiled_circuit1) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates()(compiled_circuit1) compiled_circuit2 = circuit2.copy() - cgoc.ConvertToSycamoreGates()(compiled_circuit2) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates()(compiled_circuit2) cirq.testing.assert_same_circuits(compiled_circuit1, compiled_circuit2) assert ( @@ -45,7 +47,8 @@ def test_convert_to_sycamore_gates_fsim(): q0, q1 = cirq.LineQubit.range(2) circuit = cirq.Circuit(cirq.FSimGate(theta=np.pi / 2, phi=np.pi / 6)(q0, q1)) compiled_circuit = circuit.copy() - cgoc.ConvertToSycamoreGates()(compiled_circuit) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates()(compiled_circuit) cirq.testing.assert_same_circuits(circuit, compiled_circuit) @@ -56,7 +59,8 @@ def test_single_qubit_gate(): gate = cirq.MatrixGate(mat, qid_shape=(2,)) circuit = cirq.Circuit(gate(q)) converted_circuit = circuit.copy() - cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) ops = list(converted_circuit.all_operations()) assert len(ops) == 1 assert isinstance(ops[0].gate, cirq.PhasedXZGate) @@ -70,7 +74,8 @@ def test_single_qubit_gate_phased_xz(): gate = cirq.PhasedXZGate(axis_phase_exponent=0.2, x_exponent=0.3, z_exponent=0.4) circuit = cirq.Circuit(gate(q)) converted_circuit = circuit.copy() - cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) ops = list(converted_circuit.all_operations()) assert len(ops) == 1 assert ops[0].gate == gate @@ -80,10 +85,12 @@ def test_circuit_operation_inspection(): q0, q1 = cirq.LineQubit.range(2) gate = cirq.PhasedXZGate(axis_phase_exponent=0.2, x_exponent=0.3, z_exponent=0.4) cop = cirq.CircuitOperation(cirq.FrozenCircuit(gate(q0))) - assert cgoc.ConvertToSycamoreGates()._is_native_sycamore_op(cop) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + assert cgoc.ConvertToSycamoreGates()._is_native_sycamore_op(cop) cop2 = cirq.CircuitOperation(cirq.FrozenCircuit(cirq.SWAP(q0, q1))) - assert not cgoc.ConvertToSycamoreGates()._is_native_sycamore_op(cop2) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + assert not cgoc.ConvertToSycamoreGates()._is_native_sycamore_op(cop2) def test_circuit_operation_conversion(): @@ -91,13 +98,15 @@ def test_circuit_operation_conversion(): subcircuit = cirq.FrozenCircuit(cirq.X(q0), cirq.SWAP(q0, q1)) circuit = cirq.Circuit(cirq.CircuitOperation(subcircuit)) converted_circuit = circuit.copy() - cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) # Verify that the CircuitOperation was preserved. ops = list(converted_circuit.all_operations()) assert isinstance(ops[0], cirq.CircuitOperation) # Verify that the contents of the CircuitOperation were optimized. reconverted_subcircuit = ops[0].circuit.unfreeze().copy() - cgoc.ConvertToSycamoreGates().optimize_circuit(reconverted_subcircuit) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates().optimize_circuit(reconverted_subcircuit) assert ops[0].circuit == reconverted_subcircuit cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( circuit, converted_circuit, atol=1e-8 @@ -111,7 +120,10 @@ class UnknownGate(cirq.testing.TwoQubitGate): q0, q1 = cirq.LineQubit.range(2) circuit = cirq.Circuit(UnknownGate()(q0, q1)) with pytest.raises(TypeError, match='gate with a known unitary'): - cgoc.ConvertToSycamoreGates().optimize_circuit(circuit) + with cirq.testing.assert_deprecated( + "Use cirq.optimize_for_target_gateset", deadline='v1.0' + ): + cgoc.ConvertToSycamoreGates().optimize_circuit(circuit) def test_nested_unsupported_gate(): @@ -123,7 +135,10 @@ class UnknownGate(cirq.testing.TwoQubitGate): subcircuit = cirq.FrozenCircuit(UnknownGate()(q0, q1)) circuit = cirq.Circuit(cirq.CircuitOperation(subcircuit)) with pytest.raises(TypeError, match='gate with a known unitary'): - cgoc.ConvertToSycamoreGates().optimize_circuit(circuit) + with cirq.testing.assert_deprecated( + "Use cirq.optimize_for_target_gateset", deadline='v1.0' + ): + cgoc.ConvertToSycamoreGates().optimize_circuit(circuit) def test_unsupported_phased_iswap(): @@ -133,7 +148,8 @@ def test_unsupported_phased_iswap(): q1 = cirq.LineQubit(1) circuit = cirq.Circuit(cirq.PhasedISwapPowGate(exponent=0.5, phase_exponent=0.33)(q0, q1)) converted_circuit = circuit.copy() - cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( circuit, converted_circuit, atol=1e-8 ) @@ -155,7 +171,8 @@ def with_qubits(self, *new_qubits): q0 = cirq.LineQubit(0) circuit = cirq.Circuit(UnknownOperation([q0])) converted_circuit = circuit.copy() - cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates().optimize_circuit(converted_circuit) assert circuit == converted_circuit @@ -168,7 +185,10 @@ class ThreeQubitGate(cirq.testing.ThreeQubitGate): q2 = cirq.LineQubit(2) circuit = cirq.Circuit(ThreeQubitGate()(q0, q1, q2)) with pytest.raises(TypeError): - cgoc.ConvertToSycamoreGates().optimize_circuit(circuit) + with cirq.testing.assert_deprecated( + "Use cirq.optimize_for_target_gateset", deadline='v1.0' + ): + cgoc.ConvertToSycamoreGates().optimize_circuit(circuit) def random_single_qubit_unitary(): @@ -199,7 +219,10 @@ def test_zztheta_qaoa_like(): ] ) syc_circuit = cirq_circuit.copy() - cgoc.ConvertToSycamoreGates().optimize_circuit(syc_circuit) + with cirq.testing.assert_deprecated( + "Use cirq.optimize_for_target_gateset", deadline='v1.0' + ): + cgoc.ConvertToSycamoreGates().optimize_circuit(syc_circuit) cirq.testing.assert_allclose_up_to_global_phase( cirq.unitary(cirq_circuit), cirq.unitary(syc_circuit), atol=1e-7 @@ -214,7 +237,8 @@ def test_zztheta_zzpow_unsorted_qubits(): cirq.ZZPowGate(exponent=exponent, global_shift=-0.5).on(qubits[0], qubits[1]), ) actual_circuit = expected_circuit.copy() - cgoc.ConvertToSycamoreGates().optimize_circuit(actual_circuit) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + cgoc.ConvertToSycamoreGates().optimize_circuit(actual_circuit) cirq.testing.assert_allclose_up_to_global_phase( cirq.unitary(expected_circuit), cirq.unitary(actual_circuit), atol=1e-7 @@ -231,7 +255,10 @@ def test_swap_zztheta(): ) expected_unitary = cirq.unitary(expected_circuit) actual_circuit = expected_circuit.copy() - cgoc.ConvertToSycamoreGates().optimize_circuit(actual_circuit) + with cirq.testing.assert_deprecated( + "Use cirq.optimize_for_target_gateset", deadline='v1.0' + ): + cgoc.ConvertToSycamoreGates().optimize_circuit(actual_circuit) actual_unitary = cirq.unitary(actual_circuit) cirq.testing.assert_allclose_up_to_global_phase(actual_unitary, expected_unitary, atol=1e-7) @@ -239,7 +266,8 @@ def test_swap_zztheta(): def test_known_two_q_operations_to_sycamore_operations_cnot(): a, b = cirq.LineQubit.range(2) op = cirq.CNOT(a, b) - decomposed = cirq.Circuit(cgoc.ConvertToSycamoreGates().convert(op)) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + decomposed = cirq.Circuit(cgoc.ConvertToSycamoreGates().convert(op)) # Should be equivalent. cirq.testing.assert_allclose_up_to_global_phase( @@ -271,7 +299,8 @@ def test_known_two_q_operations_to_sycamore_operations_cnot(): def test_convert_to_sycamore_equivalent_unitaries(gate): qubits = [cirq.NamedQubit('a'), cirq.NamedQubit('b')] operation = gate.on(qubits[0], qubits[1]) - converted = cgoc.ConvertToSycamoreGates().convert(operation) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + converted = cgoc.ConvertToSycamoreGates().convert(operation) u1 = cirq.unitary(cirq.Circuit(converted)) u2 = cirq.unitary(operation) cirq.testing.assert_allclose_up_to_global_phase(u1, u2, atol=1e-8) @@ -284,7 +313,8 @@ def test_convert_to_sycamore_tabulation(): ) qubits = [cirq.NamedQubit('a'), cirq.NamedQubit('b')] operation = cirq.MatrixGate(cirq.unitary(cirq.CX), qid_shape=(2, 2)).on(qubits[0], qubits[1]) - converted = cgoc.ConvertToSycamoreGates(sycamore_tabulation).convert(operation) + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + converted = cgoc.ConvertToSycamoreGates(sycamore_tabulation).convert(operation) u1 = cirq.unitary(cirq.Circuit(converted)) u2 = cirq.unitary(operation) overlap = abs(np.trace(u1.conj().T @ u2)) @@ -295,7 +325,10 @@ def test_sycamore_invalid_tabulation(): # An object other than a tabulation. sycamore_tabulation = {} with pytest.raises(ValueError): - cgoc.ConvertToSycamoreGates(sycamore_tabulation) + with cirq.testing.assert_deprecated( + "Use cirq.optimize_for_target_gateset", deadline='v1.0' + ): + cgoc.ConvertToSycamoreGates(sycamore_tabulation) q = cirq.GridQubit.rect(1, 3) @@ -315,4 +348,7 @@ def test_sycamore_invalid_tabulation(): ) def test_supported_operation(op, is_valid): c = cirq.Circuit(op) - assert (cirq_google.ConvertToSycamoreGates().optimization_at(c, 0, op) is not None) == is_valid + with cirq.testing.assert_deprecated("Use cirq.optimize_for_target_gateset", deadline='v1.0'): + assert ( + cirq_google.ConvertToSycamoreGates().optimization_at(c, 0, op) is not None + ) == is_valid diff --git a/cirq-google/cirq_google/optimizers/optimize_for_sycamore.py b/cirq-google/cirq_google/optimizers/optimize_for_sycamore.py index ca8753f75da..41fd81cf5ba 100644 --- a/cirq-google/cirq_google/optimizers/optimize_for_sycamore.py +++ b/cirq-google/cirq_google/optimizers/optimize_for_sycamore.py @@ -21,8 +21,8 @@ from cirq_google import ops as cg_ops from cirq_google.optimizers import ( convert_to_xmon_gates, - ConvertToSycamoreGates, ) +from cirq_google.transformers.target_gatesets import sycamore_gateset if TYPE_CHECKING: import cirq_google @@ -51,20 +51,16 @@ def _get_xmon_optimizers_part_cz( ] -def _get_sycamore_optimizers( - tolerance: float, tabulation: Optional[cirq.TwoQubitGateTabulation] -) -> List[Callable[[cirq.Circuit], None]]: - return [ConvertToSycamoreGates(tabulation=tabulation).optimize_circuit] - - _OPTIMIZER_TYPES = { 'xmon': _get_xmon_optimizers, 'xmon_partial_cz': _get_xmon_optimizers_part_cz, - 'sycamore': _get_sycamore_optimizers, } _TARGET_GATESETS = { 'sqrt_iswap': lambda atol, _: cirq.SqrtIswapTargetGateset(atol=atol), + 'sycamore': lambda atol, tabulation: sycamore_gateset.SycamoreTargetGateset( + atol=atol, tabulation=tabulation + ), 'xmon': lambda atol, _: cirq.CZTargetGateset(atol=atol), 'xmon_partial_cz': lambda atol, _: cirq.CZTargetGateset(atol=atol, allow_partial_czs=True), } diff --git a/cirq-google/cirq_google/transformers/__init__.py b/cirq-google/cirq_google/transformers/__init__.py index 8561e45f8ed..6b758655388 100644 --- a/cirq-google/cirq_google/transformers/__init__.py +++ b/cirq-google/cirq_google/transformers/__init__.py @@ -16,3 +16,5 @@ known_2q_op_to_sycamore_operations, two_qubit_matrix_to_sycamore_operations, ) + +from cirq_google.transformers.target_gatesets import SycamoreTargetGateset diff --git a/cirq-google/cirq_google/transformers/target_gatesets/__init__.py b/cirq-google/cirq_google/transformers/target_gatesets/__init__.py new file mode 100644 index 00000000000..98de96915fb --- /dev/null +++ b/cirq-google/cirq_google/transformers/target_gatesets/__init__.py @@ -0,0 +1,17 @@ +# 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. + +"""`cirq.CompilationTargetGateset` implementations for cirq_google gatesets and devices.""" + +from cirq_google.transformers.target_gatesets.sycamore_gateset import SycamoreTargetGateset diff --git a/cirq-google/cirq_google/transformers/target_gatesets/sycamore_gateset.py b/cirq-google/cirq_google/transformers/target_gatesets/sycamore_gateset.py new file mode 100644 index 00000000000..eb2fb8b5a94 --- /dev/null +++ b/cirq-google/cirq_google/transformers/target_gatesets/sycamore_gateset.py @@ -0,0 +1,149 @@ +# 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 Sycamore + 1-q rotations + measurement gates.""" + +import itertools +from typing import List, Optional, Sequence + +import cirq +from cirq.protocols.decompose_protocol import DecomposeResult +from cirq.transformers.target_gatesets.compilation_target_gateset import ( + _create_transformer_with_kwargs, +) +from cirq_google import ops +from cirq_google.transformers.analytical_decompositions import two_qubit_to_sycamore + + +@cirq.transformer +def merge_swap_rzz_and_2q_unitaries( + circuit: 'cirq.AbstractCircuit', + *, + context: Optional['cirq.TransformerContext'] = None, + merged_swap_rzz_tag: str = "_merged_swap_rzz", + merged_2q_component_tag: str = "_merged_2q_unitaries", +) -> 'cirq.Circuit': + """Merges 2-qubit connected components and adjacent `cirq.SWAP` and `cirq.ZZPowGate` gates. + + Does the following two transformations, in that order: + 1. Merges adjacent occurrences of `cirq.SWAP` and `cirq.ZZPowGate` into a + `cirq.CircuitOperation` tagged with `merged_swap_rzz_tag`. + 2. Merges connected components of 1 and 2 qubit unitaries in the circuit into a + `cirq.CircuitOperation` tagged with `merged_2q_component_tag`, ignoring the newly + introduced tagged circuit operations added in Step-1. + + Args: + circuit: Input circuit to transform. It will not be modified. + context: `cirq.TransformerContext` storing common configurable options for transformers. + merged_swap_rzz_tag: Tag to apply on newly introduced circuit operations wrapping adjacent + `cirq.SWAP` and `cirq.ZZPowGate`s. + merged_2q_component_tag: Tag to apply on newly introduced circuit operations wrapping + connected components of 1 and 2 qubit unitaries. + + Returns: + Copy of the transformed input circuit. + + Raises: + ValueError: If merged_2q_component_tag and merged_swap_rzz_tag are equal. + """ + if merged_2q_component_tag == merged_swap_rzz_tag: + raise ValueError("merged_swap_rzz_tag and merged_2q_component_tag should be different.") + + def merge_func_swap_rzz( + ops1: Sequence['cirq.Operation'], ops2: Sequence['cirq.Operation'] + ) -> bool: + if not (len(ops1) == 1 and len(ops2) == 1): + return False + for op1, op2 in itertools.permutations([ops1[0], ops2[0]]): + if op1.gate == cirq.SWAP and isinstance(op2.gate, cirq.ZZPowGate): + return True + return False + + tags_to_ignore = context.tags_to_ignore if context else () + circuit = cirq.merge_operations_to_circuit_op( + circuit, + merge_func_swap_rzz, + tags_to_ignore=tags_to_ignore, + merged_circuit_op_tag=merged_swap_rzz_tag, + ) + + return cirq.merge_k_qubit_unitaries_to_circuit_op( + circuit, + k=2, + tags_to_ignore=tags_to_ignore + (merged_swap_rzz_tag,), + merged_circuit_op_tag=merged_2q_component_tag, + ).unfreeze(copy=False) + + +class SycamoreTargetGateset(cirq.TwoQubitCompilationTargetGateset): + """Target gateset containing Sycamore + single qubit rotations + Measurement gates.""" + + def __init__( + self, *, atol: float = 1e-8, tabulation: Optional[cirq.TwoQubitGateTabulation] = None + ) -> None: + """Inits `cirq_google.SycamoreTargetGateset`. + + Args: + atol: A limit on the amount of absolute error introduced by the decomposition. + tabulation: If set, a tabulation for the Sycamore gate is used for decomposing Matrix + gates. If unset, an analytic calculation is used for Matrix gates. In both cases, + known decompositions for gates take priority over analytical / tabulation methods. + To get `cirq.TwoQubitGateTabulation`, call `cirq.two_qubit_gate_product_tabulation` + with a base gate (in this case, `cirq_google.SYC`) and a maximum infidelity. + """ + super().__init__( + ops.SYC, + cirq.MeasurementGate, + cirq.PhasedXZGate, + cirq.PhasedXPowGate, + cirq.XPowGate, + cirq.YPowGate, + cirq.ZPowGate, + name='SycamoreTargetGateset', + ) + self.atol = atol + self.tabulation = tabulation + + @property + def preprocess_transformers(self) -> List[cirq.TRANSFORMER]: + return [ + _create_transformer_with_kwargs( + cirq.expand_composite, + no_decomp=lambda op: cirq.num_qubits(op) <= self.num_qubits, + ), + merge_swap_rzz_and_2q_unitaries, + ] + + def _decompose_two_qubit_operation(self, op: cirq.Operation, _) -> DecomposeResult: + if not cirq.has_unitary(op): + return NotImplemented + + known_decomp = two_qubit_to_sycamore.known_2q_op_to_sycamore_operations(op) + if known_decomp is not None: + return known_decomp + if self.tabulation is not None: + return two_qubit_to_sycamore._decompose_arbitrary_into_syc_tabulation( + op, self.tabulation + ) + return two_qubit_to_sycamore.two_qubit_matrix_to_sycamore_operations( + op.qubits[0], op.qubits[1], cirq.unitary(op) + ) + + def __repr__(self) -> str: + return ( + f'cirq_google.SycamoreTargetGateset(' + f'atol={self.atol}, ' + f'tabulation={self.tabulation}, ' + f')' + ) diff --git a/cirq-google/cirq_google/transformers/target_gatesets/sycamore_gateset_test.py b/cirq-google/cirq_google/transformers/target_gatesets/sycamore_gateset_test.py new file mode 100644 index 00000000000..4e57b7f24ea --- /dev/null +++ b/cirq-google/cirq_google/transformers/target_gatesets/sycamore_gateset_test.py @@ -0,0 +1,344 @@ +# 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. + +"""Tests for SycamoreTargetGateset.""" + +import pytest +import numpy as np + +import cirq +import cirq_google +from cirq_google.transformers.target_gatesets import sycamore_gateset + +# pylint: disable=line-too-long + + +def test_merge_swap_rzz_and_2q_unitaries(): + q = cirq.LineQubit.range(3) + c_orig = cirq.Circuit( + cirq.SWAP(*q[:2]), + cirq.ZZ(*q[:2]) ** 0.5, + cirq.ZZ(*q[:2]) ** 0.25, + cirq.SWAP(*q[:2]), + cirq.SWAP(q[0], q[2]).with_tags("ignore"), + cirq.ZZ(q[0], q[2]) ** 0.75, + cirq.Moment(cirq.H.on_each(*q)), + cirq.CNOT(q[0], q[2]), + cirq.CircuitOperation( + cirq.FrozenCircuit( + cirq.CNOT(*q[0:2]), + cirq.H(q[0]), + cirq.CZ(*q[:2]), + ) + ), + cirq.CNOT(*q[1:3]), + cirq.X(q[0]), + cirq.ZZ(*q[:2]) ** 0.15, + cirq.SWAP(*q[:2]), + cirq.Moment(cirq.X(q[0]).with_tags("ignore"), cirq.Y(q[1])), + cirq.CNOT(*q[:2]), + strategy=cirq.InsertStrategy.NEW, + ) + cirq.testing.assert_has_diagram( + c_orig, + ''' + [ 0: ───@───H───@─── ] +0: ───×───ZZ───────ZZ────────×───×['ignore']───ZZ────────H───@───[ │ │ ]───────X───ZZ────────×───X['ignore']───@─── + │ │ │ │ │ │ │ [ 1: ───X───────@─── ] │ │ │ + │ │ │ │ │ │ │ │ │ │ │ +1: ───×───ZZ^0.5───ZZ^0.25───×───┼─────────────┼─────────H───┼───#2───────────────────────@───────ZZ^0.15───×───Y─────────────X─── + │ │ │ │ +2: ──────────────────────────────×─────────────ZZ^0.75───H───X────────────────────────────X─────────────────────────────────────── +''', + ) + + c_new = sycamore_gateset.merge_swap_rzz_and_2q_unitaries( + c_orig, + context=cirq.TransformerContext(tags_to_ignore=("ignore",)), + merged_swap_rzz_tag='swap_rzz', + merged_2q_component_tag='2q_component', + ) + cirq.testing.assert_has_diagram( + c_new, + ''' + [ [ 0: ───@───H───@─── ] ] + [ 0: ───×───ZZ─────── ] [ 0: ───ZZ────────×─── ] [ 0: ───ZZ────────H───@─── ] [ 0: ───────[ │ │ ]───X─── ] [ 0: ───ZZ────────×─── ] [ 0: ───────@─── ] +0: ───[ │ │ ]───────────────────[ │ │ ]───────────────────×['ignore']───[ │ │ ]───────────────────────────[ [ 1: ───X───────@─── ] ]───────────────────────────[ │ │ ]───────────────────X['ignore']───[ │ ]─────────────────── + [ 1: ───×───ZZ^0.5─── ]['swap_rzz'] [ 1: ───ZZ^0.25───×─── ]['swap_rzz'] │ [ 2: ───ZZ^0.75───H───X─── ]['2q_component'] [ │ ] [ 1: ───ZZ^0.15───×─── ]['swap_rzz'] [ 1: ───Y───X─── ]['2q_component'] + │ │ │ │ [ 1: ───H───#2─────────────────────────── ]['2q_component'] │ │ + │ │ │ │ │ │ │ +1: ───#2────────────────────────────────────────#2─────────────────────────────────────────┼─────────────┼──────────────────────────────────────────────────────#2────────────────────────────────────────────────────────────@───────#2───────────────────────────────────────────────────────#2─────────────────────────────────── + │ │ │ +2: ────────────────────────────────────────────────────────────────────────────────────────×─────────────#2───────────────────────────────────────────────────────────────────────────────────────────────────────────────────X───────────────────────────────────────────────────────────────────────────────────────────────────── +''', + ) + + +# pylint: enable=line-too-long + + +def test_merge_swap_rzz_and_2q_unitaries_raises_if_tags_sames(): + with pytest.raises(ValueError, match="should be different"): + sycamore_gateset.merge_swap_rzz_and_2q_unitaries( + cirq.Circuit(), + merged_swap_rzz_tag='merged_component', + merged_2q_component_tag='merged_component', + ) + + +def test_sycamore_gateset_compiles_swap_zz(): + qubits = cirq.LineQubit.range(3) + + gamma = np.random.randn() + circuit1 = cirq.Circuit( + cirq.SWAP(qubits[0], qubits[1]), + cirq.Z(qubits[2]), + cirq.ZZ(qubits[0], qubits[1]) ** gamma, + strategy=cirq.InsertStrategy.NEW, + ) + circuit2 = cirq.Circuit( + cirq.ZZ(qubits[0], qubits[1]) ** gamma, + cirq.Z(qubits[2]), + cirq.SWAP(qubits[0], qubits[1]), + strategy=cirq.InsertStrategy.NEW, + ) + gateset = cirq_google.SycamoreTargetGateset() + compiled_circuit1 = cirq.optimize_for_target_gateset(circuit1, gateset=gateset) + compiled_circuit2 = cirq.optimize_for_target_gateset(circuit2, gateset=gateset) + cirq.testing.assert_same_circuits(compiled_circuit1, compiled_circuit2) + assert ( + len(list(compiled_circuit1.findall_operations_with_gate_type(cirq_google.SycamoreGate))) + == 3 + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( + circuit1, compiled_circuit1, atol=1e-7 + ) + + +def test_convert_to_sycamore_gates_fsim(): + q0, q1 = cirq.LineQubit.range(2) + circuit = cirq.Circuit(cirq.FSimGate(theta=np.pi / 2, phi=np.pi / 6)(q0, q1)) + compiled_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + cirq.testing.assert_same_circuits(circuit, compiled_circuit) + + +def test_single_qubit_gate(): + q = cirq.LineQubit(0) + mat = cirq.testing.random_unitary(2) + gate = cirq.MatrixGate(mat, qid_shape=(2,)) + circuit = cirq.Circuit(gate(q)) + compiled_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + ops = list(compiled_circuit.all_operations()) + assert len(ops) == 1 + assert isinstance(ops[0].gate, cirq.PhasedXZGate) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( + circuit, compiled_circuit, atol=1e-8 + ) + + +def test_single_qubit_gate_phased_xz(): + q = cirq.LineQubit(0) + gate = cirq.PhasedXZGate(axis_phase_exponent=0.2, x_exponent=0.3, z_exponent=0.4) + circuit = cirq.Circuit(gate(q)) + compiled_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + ops = list(compiled_circuit.all_operations()) + assert len(ops) == 1 + assert ops[0].gate == gate + + +def test_unsupported_gate(): + class UnknownGate(cirq.testing.TwoQubitGate): + pass + + q0, q1 = cirq.LineQubit.range(2) + circuit = cirq.Circuit(UnknownGate()(q0, q1)) + with pytest.raises(ValueError, match='Unable to convert'): + cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset(), ignore_failures=False + ) + + +def test_nested_unsupported_gate(): + class UnknownGate(cirq.testing.TwoQubitGate): + pass + + q0 = cirq.LineQubit(0) + q1 = cirq.LineQubit(1) + subcircuit = cirq.FrozenCircuit(UnknownGate()(q0, q1)) + circuit = cirq.Circuit(cirq.CircuitOperation(subcircuit)) + with pytest.raises(ValueError, match='Unable to convert'): + cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset(), ignore_failures=False + ) + + +def test_unsupported_gate_ignoring_failures(): + class UnknownOperation(cirq.Operation): + def __init__(self, qubits): + self._qubits = qubits + + @property + def qubits(self): + return self._qubits + + def with_qubits(self, *new_qubits): + # coverage: ignore + return UnknownOperation(self._qubits) + + q0 = cirq.LineQubit(0) + circuit = cirq.Circuit(UnknownOperation([q0])) + converted_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + assert circuit == converted_circuit + + +def test_zztheta_qaoa_like(): + qubits = cirq.LineQubit.range(4) + for exponent in np.linspace(-1, 1, 10): + circuit = cirq.Circuit( + [ + cirq.H.on_each(qubits), + cirq.ZZPowGate(exponent=exponent)(qubits[0], qubits[1]), + cirq.ZZPowGate(exponent=exponent)(qubits[2], qubits[3]), + cirq.rx(0.123).on_each(qubits), + ] + ) + converted_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( + circuit, converted_circuit, atol=1e-8 + ) + + +def test_zztheta_zzpow_unsorted_qubits(): + qubits = cirq.LineQubit(1), cirq.LineQubit(0) + exponent = 0.06366197723675814 + circuit = cirq.Circuit( + cirq.ZZPowGate(exponent=exponent, global_shift=-0.5).on(qubits[0], qubits[1]), + ) + converted_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( + circuit, converted_circuit, atol=1e-8 + ) + + +def test_swap_zztheta(): + qubits = cirq.LineQubit.range(2) + a, b = qubits + for theta in np.linspace(0, 2 * np.pi, 10): + circuit = cirq.Circuit( + cirq.SWAP(a, b), cirq.ZZPowGate(exponent=2 * theta / np.pi, global_shift=-0.5).on(a, b) + ) + converted_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( + circuit, converted_circuit, atol=1e-8 + ) + + +def test_known_two_q_operations_to_sycamore_operations_cnot(): + a, b = cirq.LineQubit.range(2) + circuit = cirq.Circuit(cirq.CNOT(a, b)) + converted_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + + # Should be equivalent. + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( + circuit, converted_circuit, atol=1e-8 + ) + + # Should have decomposed into two Sycamores. + multi_qubit_ops = [e for e in converted_circuit.all_operations() if len(e.qubits) > 1] + assert len(multi_qubit_ops) == 2 + assert all(isinstance(e.gate, cirq_google.SycamoreGate) for e in multi_qubit_ops) + + +@pytest.mark.parametrize( + 'gate', + [ + cirq.MatrixGate(cirq.unitary(cirq.CX), qid_shape=(2, 2)), + cirq.ISWAP, + cirq.SWAP, + cirq.CNOT, + cirq.CZ, + cirq.PhasedISwapPowGate(exponent=1.0), + cirq.PhasedISwapPowGate(exponent=1.0, phase_exponent=0.33), + cirq.PhasedISwapPowGate(exponent=0.66, phase_exponent=0.25), + *[cirq.givens(theta) for theta in np.linspace(0, 2 * np.pi, 30)], + *[cirq.ZZPowGate(exponent=2 * phi / np.pi) for phi in np.linspace(0, 2 * np.pi, 30)], + *[cirq.CZPowGate(exponent=phi / np.pi) for phi in np.linspace(0, 2 * np.pi, 30)], + ], +) +def test_convert_to_sycamore_equivalent_unitaries(gate): + circuit = cirq.Circuit(gate.on(*cirq.LineQubit.range(2))) + converted_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( + circuit, converted_circuit, atol=1e-8 + ) + + +def test_convert_to_sycamore_tabulation(): + # A tabulation for the sycamore gate with an infidelity of .1. + sycamore_tabulation = cirq.two_qubit_gate_product_tabulation( + cirq.unitary(cirq_google.SYC), 0.1, random_state=cirq.value.parse_random_state(11) + ) + circuit = cirq.Circuit(cirq.MatrixGate(cirq.unitary(cirq.CX)).on(*cirq.LineQubit.range(2))) + converted_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset(tabulation=sycamore_tabulation) + ) + u1 = cirq.unitary(circuit) + u2 = cirq.unitary(converted_circuit) + overlap = abs(np.trace(u1.conj().T @ u2)) + assert np.isclose(overlap, 4.0, 0.1) + + +q = cirq.GridQubit.rect(1, 3) +matrix_gate = cirq.MatrixGate(cirq.testing.random_unitary(2)) + + +@pytest.mark.parametrize( + 'op', + [ + cirq.CircuitOperation(cirq.FrozenCircuit(matrix_gate(q[0]))), + matrix_gate(q[0]), + matrix_gate(q[0]).with_tags('test_tags'), + matrix_gate(q[0]).controlled_by(q[1]), + matrix_gate(q[0]).controlled_by(q[1]).with_tags('test_tags'), + matrix_gate(q[0]).with_tags('test_tags').controlled_by(q[1]), + ], +) +def test_supported_operation(op): + circuit = cirq.Circuit(op) + converted_circuit = cirq.optimize_for_target_gateset( + circuit, gateset=cirq_google.SycamoreTargetGateset() + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent( + circuit, converted_circuit, atol=1e-8 + ) + multi_qubit_ops = [e for e in converted_circuit.all_operations() if len(e.qubits) > 1] + assert all(isinstance(e.gate, cirq_google.SycamoreGate) for e in multi_qubit_ops)