From eacf4a37702ac47f81924d59c85d5b902a796fe7 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Thu, 17 Feb 2022 03:46:26 +0530 Subject: [PATCH] Add `cirq.merge_k_qubit_unitaries` transformer to replace `cirq.MergeSingleQubitGates` optimizer (#4986) * Replace cirq.MergeSingleQubitGates optimizer with cirq.merge_single_qubit_gates transformer * Add merge_k_qubit_unitaries primitive --- cirq-core/cirq/__init__.py | 4 + .../contrib/paulistring/convert_gate_set.py | 2 +- cirq-core/cirq/devices/noise_model_test.py | 7 +- cirq-core/cirq/ion/convert_to_ion_gates.py | 6 +- cirq-core/cirq/ion/ion_decomposition.py | 4 +- .../optimizers/merge_interactions_test.py | 10 +- .../merge_interactions_to_sqrt_iswap_test.py | 10 +- .../optimizers/merge_single_qubit_gates.py | 24 +-- .../merge_single_qubit_gates_test.py | 39 ++-- cirq-core/cirq/transformers/__init__.py | 8 + .../two_qubit_to_cz.py | 4 +- .../cirq/transformers/merge_k_qubit_gates.py | 77 +++++++ .../transformers/merge_k_qubit_gates_test.py | 190 ++++++++++++++++++ .../transformers/merge_single_qubit_gates.py | 132 ++++++++++++ .../merge_single_qubit_gates_test.py | 129 ++++++++++++ .../transformers/transformer_primitives.py | 2 +- .../optimizers/optimize_for_sycamore.py | 14 +- cirq-ionq/cirq_ionq/ionq_devices.py | 2 +- dev_tools/notebooks/isolated_notebook_test.py | 1 + docs/tutorials/basics.ipynb | 11 +- 20 files changed, 602 insertions(+), 74 deletions(-) create mode 100644 cirq-core/cirq/transformers/merge_k_qubit_gates.py create mode 100644 cirq-core/cirq/transformers/merge_k_qubit_gates_test.py create mode 100644 cirq-core/cirq/transformers/merge_single_qubit_gates.py create mode 100644 cirq-core/cirq/transformers/merge_single_qubit_gates_test.py diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 68656b3f9abe..3ef989919001 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -373,10 +373,14 @@ map_moments, map_operations, map_operations_and_unroll, + merge_k_qubit_unitaries, merge_k_qubit_unitaries_to_circuit_op, merge_moments, merge_operations, merge_operations_to_circuit_op, + merge_single_qubit_gates_to_phased_x_and_z, + merge_single_qubit_gates_to_phxz, + merge_single_qubit_moments_to_phxz, prepare_two_qubit_state_using_cz, prepare_two_qubit_state_using_sqrt_iswap, single_qubit_matrix_to_gates, diff --git a/cirq-core/cirq/contrib/paulistring/convert_gate_set.py b/cirq-core/cirq/contrib/paulistring/convert_gate_set.py index d8068221ea93..d72fc91f39c2 100644 --- a/cirq-core/cirq/contrib/paulistring/convert_gate_set.py +++ b/cirq-core/cirq/contrib/paulistring/convert_gate_set.py @@ -28,7 +28,7 @@ def converted_gate_set( """ conv_circuit = circuits.Circuit(circuit) optimizers.ConvertToCzAndSingleGates().optimize_circuit(conv_circuit) - optimizers.MergeSingleQubitGates().optimize_circuit(conv_circuit) + conv_circuit = transformers.merge_k_qubit_unitaries(conv_circuit, k=1) ConvertToPauliStringPhasors( ignore_failures=True, keep_clifford=not no_clifford_gates, diff --git a/cirq-core/cirq/devices/noise_model_test.py b/cirq-core/cirq/devices/noise_model_test.py index d7c28d67cd0b..247961b0c334 100644 --- a/cirq-core/cirq/devices/noise_model_test.py +++ b/cirq-core/cirq/devices/noise_model_test.py @@ -130,7 +130,6 @@ def test_noise_composition(): a, b, c = cirq.LineQubit.range(3) noise_z = cirq.ConstantQubitNoiseModel(cirq.Z) noise_inv_s = cirq.ConstantQubitNoiseModel(cirq.S ** -1) - merge = cirq.optimizers.merge_single_qubit_gates_into_phased_x_z base_moments = [cirq.Moment([cirq.X(a)]), cirq.Moment([cirq.Y(b)]), cirq.Moment([cirq.H(c)])] circuit_z = cirq.Circuit(noise_z.noisy_moments(base_moments, [a, b, c])) circuit_s = cirq.Circuit(noise_inv_s.noisy_moments(base_moments, [a, b, c])) @@ -147,9 +146,9 @@ def test_noise_composition(): ) # All of the gates will be the same, just out of order. Merging fixes this. - merge(actual_zs) - merge(actual_sz) - merge(expected_circuit) + actual_zs = cirq.merge_single_qubit_gates_to_phased_x_and_z(actual_zs) + actual_sz = cirq.merge_single_qubit_gates_to_phased_x_and_z(actual_sz) + expected_circuit = cirq.merge_single_qubit_gates_to_phased_x_and_z(expected_circuit) assert_equivalent_op_tree(actual_zs, actual_sz) assert_equivalent_op_tree(actual_zs, expected_circuit) diff --git a/cirq-core/cirq/ion/convert_to_ion_gates.py b/cirq-core/cirq/ion/convert_to_ion_gates.py index b4e47d32ffae..c6e94994007d 100644 --- a/cirq-core/cirq/ion/convert_to_ion_gates.py +++ b/cirq-core/cirq/ion/convert_to_ion_gates.py @@ -14,7 +14,7 @@ import numpy as np -from cirq import ops, protocols, optimizers, circuits, transformers +from cirq import ops, protocols, circuits, transformers from cirq.ion import ms, two_qubit_matrix_to_ion_operations, ion_device @@ -86,6 +86,4 @@ def convert_circuit(self, circuit: circuits.Circuit) -> circuits.Circuit: for moment in circuit: for op in moment.operations: new_circuit.append(self.convert_one(op)) - optimizers.merge_single_qubit_gates_into_phased_x_z(new_circuit) - - return new_circuit + return transformers.merge_single_qubit_gates_to_phased_x_and_z(new_circuit) diff --git a/cirq-core/cirq/ion/ion_decomposition.py b/cirq-core/cirq/ion/ion_decomposition.py index 66e528a54f4f..81c88bbd919c 100644 --- a/cirq-core/cirq/ion/ion_decomposition.py +++ b/cirq-core/cirq/ion/ion_decomposition.py @@ -23,7 +23,7 @@ import numpy as np -from cirq import ops, linalg, protocols, optimizers, circuits, transformers +from cirq import ops, linalg, protocols, circuits, transformers from cirq.ion import ms if TYPE_CHECKING: @@ -52,7 +52,7 @@ def two_qubit_matrix_to_ion_operations( def _cleanup_operations(operations: List[ops.Operation]): circuit = circuits.Circuit(operations) - optimizers.merge_single_qubit_gates.merge_single_qubit_gates_into_phased_x_z(circuit) + circuit = transformers.merge_single_qubit_gates_to_phased_x_and_z(circuit) circuit = transformers.eject_phased_paulis(circuit) circuit = transformers.eject_z(circuit) circuit = circuits.Circuit(circuit.all_operations(), strategy=circuits.InsertStrategy.EARLIEST) diff --git a/cirq-core/cirq/optimizers/merge_interactions_test.py b/cirq-core/cirq/optimizers/merge_interactions_test.py index 50f5f561981d..736f7c4473c4 100644 --- a/cirq-core/cirq/optimizers/merge_interactions_test.py +++ b/cirq-core/cirq/optimizers/merge_interactions_test.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Callable, List +from typing import List import pytest import sympy @@ -26,14 +26,8 @@ def assert_optimizes(before: cirq.Circuit, expected: cirq.Circuit): opt.optimize_circuit(actual) # Ignore differences that would be caught by follow-up optimizations. - followup_optimizations: List[Callable[[cirq.Circuit], None]] = [ - cirq.merge_single_qubit_gates_into_phased_x_z, - ] - for post in followup_optimizations: - post(actual) - post(expected) - followup_transformers: List[cirq.TRANSFORMER] = [ + cirq.merge_single_qubit_gates_to_phased_x_and_z, cirq.eject_phased_paulis, cirq.eject_z, cirq.drop_negligible_operations, diff --git a/cirq-core/cirq/optimizers/merge_interactions_to_sqrt_iswap_test.py b/cirq-core/cirq/optimizers/merge_interactions_to_sqrt_iswap_test.py index e2c33c81256f..1e6316553c73 100644 --- a/cirq-core/cirq/optimizers/merge_interactions_to_sqrt_iswap_test.py +++ b/cirq-core/cirq/optimizers/merge_interactions_to_sqrt_iswap_test.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Callable, List +from typing import List import pytest @@ -37,14 +37,8 @@ def assert_optimizes(before: cirq.Circuit, expected: cirq.Circuit, **kwargs): opt.optimize_circuit(actual) # Ignore differences that would be caught by follow-up optimizations. - followup_optimizations: List[Callable[[cirq.Circuit], None]] = [ - cirq.merge_single_qubit_gates_into_phased_x_z, - ] - for post in followup_optimizations: - post(actual) - post(expected) - followup_transformers: List[cirq.TRANSFORMER] = [ + cirq.merge_single_qubit_gates_to_phased_x_and_z, cirq.eject_phased_paulis, cirq.eject_z, cirq.drop_negligible_operations, diff --git a/cirq-core/cirq/optimizers/merge_single_qubit_gates.py b/cirq-core/cirq/optimizers/merge_single_qubit_gates.py index 2e168551ec25..8e59cb801c03 100644 --- a/cirq-core/cirq/optimizers/merge_single_qubit_gates.py +++ b/cirq-core/cirq/optimizers/merge_single_qubit_gates.py @@ -18,13 +18,13 @@ import numpy as np -from cirq import ops, linalg, protocols, circuits -from cirq.transformers.analytical_decompositions import single_qubit_decompositions +from cirq import ops, linalg, protocols, circuits, _compat, transformers if TYPE_CHECKING: import cirq +@_compat.deprecated_class(deadline='v1.0', fix='Use cirq.merge_k_qubit_unitaries instead.') class MergeSingleQubitGates(circuits.PointOptimizer): """Optimizes runs of adjacent unitary 1-qubit operations.""" @@ -101,6 +101,9 @@ def optimization_at( ) +@_compat.deprecated( + deadline='v1.0', fix='Use cirq.merge_single_qubit_gates_to_phased_x_and_z instead.' +) def merge_single_qubit_gates_into_phased_x_z(circuit: circuits.Circuit, atol: float = 1e-8) -> None: """Canonicalizes runs of single-qubit rotations in a circuit. @@ -113,14 +116,12 @@ def merge_single_qubit_gates_into_phased_x_z(circuit: circuits.Circuit, atol: fl atol: Absolute tolerance to angle error. Larger values allow more negligible gates to be dropped, smaller values increase accuracy. """ - - def synth(qubit: 'cirq.Qid', matrix: np.ndarray) -> List[ops.Operation]: - out_gates = single_qubit_decompositions.single_qubit_matrix_to_phased_x_z(matrix, atol) - return [gate(qubit) for gate in out_gates] - - MergeSingleQubitGates(synthesizer=synth).optimize_circuit(circuit) + circuit._moments = [ + *transformers.merge_single_qubit_gates_to_phased_x_and_z(circuit, atol=atol) + ] +@_compat.deprecated(deadline='v1.0', fix='Use cirq.merge_single_qubit_gates_to_phxz instead.') def merge_single_qubit_gates_into_phxz( circuit: circuits.Circuit, atol: float = 1e-8, @@ -135,9 +136,4 @@ def merge_single_qubit_gates_into_phxz( atol: Absolute tolerance to angle error. Larger values allow more negligible gates to be dropped, smaller values increase accuracy. """ - - def synth(qubit: 'cirq.Qid', matrix: np.ndarray) -> List[ops.Operation]: - gate = single_qubit_decompositions.single_qubit_matrix_to_phxz(matrix, atol) - return [gate(qubit)] if gate else [] - - MergeSingleQubitGates(synthesizer=synth).optimize_circuit(circuit) + circuit._moments = [*transformers.merge_single_qubit_gates_to_phxz(circuit, atol=atol)] diff --git a/cirq-core/cirq/optimizers/merge_single_qubit_gates_test.py b/cirq-core/cirq/optimizers/merge_single_qubit_gates_test.py index d1e808527d15..2a43b1c4377c 100644 --- a/cirq-core/cirq/optimizers/merge_single_qubit_gates_test.py +++ b/cirq-core/cirq/optimizers/merge_single_qubit_gates_test.py @@ -23,10 +23,12 @@ def assert_optimizes( before: cirq.Circuit, expected: cirq.Circuit, optimizer: Optional[Callable[[cirq.Circuit], None]] = None, + deprecated_msg: str = "Use cirq.merge_k_qubit_unitaries", ): - if optimizer is None: - optimizer = cirq.MergeSingleQubitGates().optimize_circuit - optimizer(before) + with cirq.testing.assert_deprecated(deprecated_msg, deadline='v1.0'): + if optimizer is None: + optimizer = cirq.MergeSingleQubitGates().optimize_circuit + optimizer(before) # Ignore differences that would be caught by follow-up optimizations. followup_transformers = [cirq.drop_negligible_operations, cirq.drop_empty_moments] @@ -38,7 +40,8 @@ def assert_optimizes( def test_leaves_singleton(): - m = cirq.MergeSingleQubitGates() + with cirq.testing.assert_deprecated("Use cirq.merge_k_qubit_unitaries", deadline='v1.0'): + m = cirq.MergeSingleQubitGates() q = cirq.NamedQubit('q') c = cirq.Circuit([cirq.Moment([cirq.X(q)])]) @@ -48,12 +51,16 @@ def test_leaves_singleton(): def test_not_both(): - with pytest.raises(ValueError): - _ = cirq.MergeSingleQubitGates(synthesizer=lambda *args: None, rewriter=lambda *args: None) + with cirq.testing.assert_deprecated("Use cirq.merge_k_qubit_unitaries", deadline='v1.0'): + with pytest.raises(ValueError): + _ = cirq.MergeSingleQubitGates( + synthesizer=lambda *args: None, rewriter=lambda *args: None + ) def test_combines_sequence(): - m = cirq.MergeSingleQubitGates() + with cirq.testing.assert_deprecated("Use cirq.merge_k_qubit_unitaries", deadline='v1.0'): + m = cirq.MergeSingleQubitGates() q = cirq.NamedQubit('q') c = cirq.Circuit(cirq.X(q) ** 0.5, cirq.Z(q) ** 0.5, cirq.X(q) ** -0.5) @@ -83,7 +90,8 @@ def test_removes_identity_sequence(): def test_stopped_at_2qubit(): - m = cirq.MergeSingleQubitGates() + with cirq.testing.assert_deprecated("Use cirq.merge_k_qubit_unitaries", deadline='v1.0'): + m = cirq.MergeSingleQubitGates() q = cirq.NamedQubit('q') q2 = cirq.NamedQubit('q2') c = cirq.Circuit( @@ -109,7 +117,8 @@ def test_stopped_at_2qubit(): def test_ignores_2qubit_target(): - m = cirq.MergeSingleQubitGates() + with cirq.testing.assert_deprecated("Use cirq.merge_k_qubit_unitaries", deadline='v1.0'): + m = cirq.MergeSingleQubitGates() q = cirq.NamedQubit('q') q2 = cirq.NamedQubit('q2') c = cirq.Circuit( @@ -132,7 +141,8 @@ class UnsupportedDummy(cirq.SingleQubitGate): UnsupportedDummy()(q0), ) c_orig = cirq.Circuit(circuit) - cirq.MergeSingleQubitGates().optimize_circuit(circuit) + with cirq.testing.assert_deprecated("Use cirq.merge_k_qubit_unitaries", deadline='v1.0'): + cirq.MergeSingleQubitGates().optimize_circuit(circuit) assert circuit == c_orig @@ -147,9 +157,10 @@ def test_rewrite(): cirq.CZ(q0, q1), cirq.Y(q1), ) - cirq.MergeSingleQubitGates(rewriter=lambda ops: cirq.H(ops[0].qubits[0])).optimize_circuit( - circuit - ) + with cirq.testing.assert_deprecated("Use cirq.merge_k_qubit_unitaries", deadline='v1.0'): + cirq.MergeSingleQubitGates(rewriter=lambda ops: cirq.H(ops[0].qubits[0])).optimize_circuit( + circuit + ) circuit = cirq.drop_empty_moments(circuit) cirq.testing.assert_same_circuits( @@ -180,6 +191,7 @@ def test_merge_single_qubit_gates_into_phased_x_z(): (cirq.PhasedXPowGate(phase_exponent=-0.5)(a)) ** 0.5, ), optimizer=cirq.merge_single_qubit_gates_into_phased_x_z, + deprecated_msg="Use cirq.merge_single_qubit_gates_to_phased_x_and_z", ) @@ -207,4 +219,5 @@ def phxz(a, x, z): phxz(-0.5, 0.5, 0).on(a), ), optimizer=cirq.merge_single_qubit_gates_into_phxz, + deprecated_msg="Use cirq.merge_single_qubit_gates_to_phxz", ) diff --git a/cirq-core/cirq/transformers/__init__.py b/cirq-core/cirq/transformers/__init__.py index 77009de16ebf..8033c6315c29 100644 --- a/cirq-core/cirq/transformers/__init__.py +++ b/cirq-core/cirq/transformers/__init__.py @@ -60,6 +60,14 @@ dephase_measurements, ) +from cirq.transformers.merge_k_qubit_gates import merge_k_qubit_unitaries + +from cirq.transformers.merge_single_qubit_gates import ( + merge_single_qubit_gates_to_phased_x_and_z, + merge_single_qubit_gates_to_phxz, + merge_single_qubit_moments_to_phxz, +) + from cirq.transformers.synchronize_terminal_measurements import synchronize_terminal_measurements from cirq.transformers.transformer_api import ( diff --git a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py index ea2c6349c729..927724ce60a8 100644 --- a/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py +++ b/cirq-core/cirq/transformers/analytical_decompositions/two_qubit_to_cz.py @@ -23,9 +23,9 @@ from cirq import ops, linalg, protocols, circuits from cirq.transformers.analytical_decompositions import single_qubit_decompositions +from cirq.transformers.merge_single_qubit_gates import merge_single_qubit_gates_to_phased_x_and_z from cirq.transformers.eject_z import eject_z from cirq.transformers.eject_phased_paulis import eject_phased_paulis -from cirq.optimizers import merge_single_qubit_gates if TYPE_CHECKING: import cirq @@ -161,7 +161,7 @@ def _xx_yy_zz_interaction_via_full_czs( def _cleanup_operations(operations: Sequence[ops.Operation]): circuit = circuits.Circuit(operations) - merge_single_qubit_gates.merge_single_qubit_gates_into_phased_x_z(circuit) + circuit = merge_single_qubit_gates_to_phased_x_and_z(circuit) circuit = eject_phased_paulis(circuit) circuit = eject_z(circuit) circuit = circuits.Circuit(circuit.all_operations(), strategy=circuits.InsertStrategy.EARLIEST) diff --git a/cirq-core/cirq/transformers/merge_k_qubit_gates.py b/cirq-core/cirq/transformers/merge_k_qubit_gates.py new file mode 100644 index 000000000000..eb0cf247a5cd --- /dev/null +++ b/cirq-core/cirq/transformers/merge_k_qubit_gates.py @@ -0,0 +1,77 @@ +# 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. + +"""Transformer pass to merge connected components of k-qubit unitary operations.""" + +from typing import cast, Optional, Callable, TYPE_CHECKING + +from cirq import ops, protocols, circuits +from cirq.transformers import transformer_api, transformer_primitives + +if TYPE_CHECKING: + import cirq + + +@transformer_api.transformer +def merge_k_qubit_unitaries( + circuit: 'cirq.AbstractCircuit', + *, + context: Optional['cirq.TransformerContext'] = None, + k: int = 0, + rewriter: Optional[Callable[['cirq.CircuitOperation'], 'cirq.OP_TREE']] = None, +) -> 'cirq.Circuit': + """Merges connected components of unitary operations, acting on <= k qubits. + + Uses rewriter to convert a connected component of unitary operations acting on <= k-qubits + into a more desirable form. If not specified, connected components are replaced by a single + `cirq.MatrixGate` containing unitary matrix of the merged component. + + Args: + circuit: Input circuit to transform. It will not be modified. + context: `cirq.TransformerContext` storing common configurable options for transformers. + k: Connected components of unitary operations acting on <= k qubits are merged. + rewriter: Callable type that takes a `cirq.CircuitOperation`, encapsulating a connected + component of unitary operations acting on <= k qubits, and produces a `cirq.OP_TREE`. + Specifies how to merge the connected component into a more desirable form. + + Returns: + Copy of the transformed input circuit. + + Raises: + ValueError: If k <= 0 + """ + if k <= 0: + raise ValueError(f"k should be greater than or equal to 1. Found {k}.") + merged_circuit_op_tag = "_merged_k_qubit_unitaries_component" + + def map_func(op: 'cirq.Operation', _) -> 'cirq.OP_TREE': + if not (protocols.num_qubits(op) <= k and protocols.has_unitary(op)): + return op + if rewriter: + return rewriter( + cast(circuits.CircuitOperation, op.untagged) + if merged_circuit_op_tag in op.tags + else circuits.CircuitOperation(circuits.FrozenCircuit(op)) + ) + return ops.MatrixGate(protocols.unitary(op)).on(*op.qubits) + + circuit = transformer_primitives.merge_k_qubit_unitaries_to_circuit_op( + circuit, + k=k, + tags_to_ignore=context.tags_to_ignore if context else (), + merged_circuit_op_tag=merged_circuit_op_tag, + ) + return transformer_primitives.map_operations_and_unroll( + circuit, map_func, tags_to_ignore=context.tags_to_ignore if context else () + ).unfreeze(copy=False) diff --git a/cirq-core/cirq/transformers/merge_k_qubit_gates_test.py b/cirq-core/cirq/transformers/merge_k_qubit_gates_test.py new file mode 100644 index 000000000000..4a39c31bab7a --- /dev/null +++ b/cirq-core/cirq/transformers/merge_k_qubit_gates_test.py @@ -0,0 +1,190 @@ +# 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. + +# pylint: skip-file + +from typing import List + +import numpy as np +import pytest + +import cirq + + +def assert_optimizes(optimized: cirq.AbstractCircuit, expected: cirq.AbstractCircuit): + # Ignore differences that would be caught by follow-up optimizations. + followup_transformers: List[cirq.TRANSFORMER] = [ + cirq.drop_negligible_operations, + cirq.drop_empty_moments, + ] + for transform in followup_transformers: + optimized = transform(optimized) + expected = transform(expected) + + cirq.testing.assert_same_circuits(optimized, expected) + + +def test_merge_1q_unitaries(): + q, q2 = cirq.LineQubit.range(2) + # 1. Combines trivial 1q sequence. + c = cirq.Circuit(cirq.X(q) ** 0.5, cirq.Z(q) ** 0.5, cirq.X(q) ** -0.5) + c = cirq.merge_k_qubit_unitaries(c, k=1) + op_list = [*c.all_operations()] + assert len(op_list) == 1 + assert isinstance(op_list[0].gate, cirq.MatrixGate) + cirq.testing.assert_allclose_up_to_global_phase( + cirq.unitary(c), cirq.unitary(cirq.Y ** 0.5), atol=1e-7 + ) + + # 2. Gets blocked at a 2q operation. + c = cirq.Circuit([cirq.Z(q), cirq.H(q), cirq.X(q), cirq.H(q), cirq.CZ(q, q2), cirq.H(q)]) + c = cirq.drop_empty_moments(cirq.merge_k_qubit_unitaries(c, k=1)) + assert len(c) == 3 + cirq.testing.assert_allclose_up_to_global_phase(cirq.unitary(c[0]), np.eye(2), atol=1e-7) + assert isinstance(c[-1][q].gate, cirq.MatrixGate) + + +def test_respects_nocompile_tags(): + q = cirq.NamedQubit("q") + c = cirq.Circuit( + [cirq.Z(q), cirq.H(q), cirq.X(q), cirq.H(q), cirq.X(q).with_tags("nocompile"), cirq.H(q)] + ) + context = cirq.TransformerContext(tags_to_ignore=("nocompile",)) + c = cirq.drop_empty_moments(cirq.merge_k_qubit_unitaries(c, k=1, context=context)) + assert len(c) == 3 + cirq.testing.assert_allclose_up_to_global_phase(cirq.unitary(c[0]), np.eye(2), atol=1e-7) + assert c[1][q] == cirq.X(q).with_tags("nocompile") + assert isinstance(c[-1][q].gate, cirq.MatrixGate) + + +def test_ignores_2qubit_target(): + c = cirq.Circuit(cirq.CZ(*cirq.LineQubit.range(2))) + assert_optimizes(optimized=cirq.merge_k_qubit_unitaries(c, k=1), expected=c) + + +def test_ignore_unsupported_gate(): + class UnsupportedDummy(cirq.Gate): + def _num_qubits_(self) -> int: + return 1 + + c = cirq.Circuit(UnsupportedDummy()(cirq.LineQubit(0))) + assert_optimizes(optimized=cirq.merge_k_qubit_unitaries(c, k=1), expected=c) + + +def test_1q_rewrite(): + q0, q1 = cirq.LineQubit.range(2) + circuit = cirq.Circuit( + cirq.X(q0), cirq.Y(q0), cirq.X(q1), cirq.CZ(q0, q1), cirq.Y(q1), cirq.measure(q0, q1) + ) + assert_optimizes( + optimized=cirq.merge_k_qubit_unitaries( + circuit, k=1, rewriter=lambda ops: cirq.H(ops.qubits[0]) + ), + expected=cirq.Circuit( + cirq.H(q0), cirq.H(q1), cirq.CZ(q0, q1), cirq.H(q1), cirq.measure(q0, q1) + ), + ) + + +def test_merge_k_qubit_unitaries_raises(): + with pytest.raises(ValueError, match="k should be greater than or equal to 1"): + _ = cirq.merge_k_qubit_unitaries(cirq.Circuit()) + + +def test_merge_complex_circuit_preserving_moment_structure(): + q = cirq.LineQubit.range(3) + c_orig = cirq.Circuit( + cirq.Moment(cirq.H.on_each(*q)), + cirq.CNOT(q[0], q[2]), + cirq.CNOT(*q[0:2]), + cirq.H(q[0]), + cirq.CZ(*q[:2]), + cirq.X(q[0]), + cirq.Y(q[1]), + cirq.CNOT(*q[0:2]), + cirq.CNOT(*q[1:3]).with_tags("ignore"), + cirq.X(q[0]), + cirq.Moment(cirq.X(q[0]).with_tags("ignore"), cirq.Y(q[1]), cirq.Z(q[2])), + cirq.Moment(cirq.CNOT(*q[:2]), cirq.measure(q[2], key="a")), + cirq.X(q[0]).with_classical_controls("a"), + strategy=cirq.InsertStrategy.NEW, + ) + cirq.testing.assert_has_diagram( + c_orig, + ''' +0: ───H───@───@───H───@───X───────@─────────────────X───X['ignore']───@───X─── + │ │ │ │ │ ║ +1: ───H───┼───X───────@───────Y───X───@['ignore']───────Y─────────────X───╫─── + │ │ ║ +2: ───H───X───────────────────────────X─────────────────Z─────────────M───╫─── + ║ ║ +a: ═══════════════════════════════════════════════════════════════════@═══^═══ +''', + ) + component_id = 0 + + def rewriter_merge_to_circuit_op(op: 'cirq.CircuitOperation') -> 'cirq.OP_TREE': + nonlocal component_id + component_id = component_id + 1 + return op.with_tags(f'{component_id}') + + c_new = cirq.merge_k_qubit_unitaries( + c_orig, + k=2, + context=cirq.TransformerContext(tags_to_ignore=("ignore",)), + rewriter=rewriter_merge_to_circuit_op, + ) + cirq.testing.assert_has_diagram( + cirq.drop_empty_moments(c_new), + ''' + [ 0: ───H───@─── ] [ 0: ───────@───H───@───X───@───X─── ] [ 0: ───────@─── ] +0: ───[ │ ]────────[ │ │ │ ]──────────────────────X['ignore']───────────[ │ ]────────X─── + [ 2: ───H───X─── ]['1'] [ 1: ───H───X───────@───Y───X─────── ]['2'] [ 1: ───Y───X─── ]['4'] ║ + │ │ │ ║ +1: ───┼─────────────────────────#2────────────────────────────────────────────@['ignore']─────────────────────────#2────────────────────────╫─── + │ │ ║ +2: ───#2──────────────────────────────────────────────────────────────────────X─────────────[ 2: ───Z─── ]['3']───M─────────────────────────╫─── + ║ ║ +a: ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════@═════════════════════════^═══''', + ) + + component_id = 0 + + def rewriter_replace_with_decomp(op: 'cirq.CircuitOperation') -> 'cirq.OP_TREE': + nonlocal component_id + component_id = component_id + 1 + tag = f'{component_id}' + if len(op.qubits) == 1: + return [cirq.T(op.qubits[0]).with_tags(tag)] + one_layer = [op.with_tags(tag) for op in cirq.T.on_each(*op.qubits)] + two_layer = [cirq.SQRT_ISWAP(*op.qubits).with_tags(tag)] + return [one_layer, two_layer, one_layer] + + c_new = cirq.merge_k_qubit_unitaries( + c_orig, + k=2, + context=cirq.TransformerContext(tags_to_ignore=("ignore",)), + rewriter=rewriter_replace_with_decomp, + ) + cirq.testing.assert_has_diagram( + cirq.drop_empty_moments(c_new), + ''' +0: ───T['1']───iSwap['1']───T['1']───T['2']───iSwap['2']───T['2']─────────────────X['ignore']───T['4']───iSwap['4']───T['4']───X─── + │ │ │ ║ +1: ────────────┼─────────────────────T['2']───iSwap^0.5────T['2']───@['ignore']─────────────────T['4']───iSwap^0.5────T['4']───╫─── + │ │ ║ +2: ───T['1']───iSwap^0.5────T['1']──────────────────────────────────X─────────────T['3']────────M──────────────────────────────╫─── + ║ ║ +a: ═════════════════════════════════════════════════════════════════════════════════════════════@══════════════════════════════^═══''', + ) diff --git a/cirq-core/cirq/transformers/merge_single_qubit_gates.py b/cirq-core/cirq/transformers/merge_single_qubit_gates.py new file mode 100644 index 000000000000..bedb05bdddf3 --- /dev/null +++ b/cirq-core/cirq/transformers/merge_single_qubit_gates.py @@ -0,0 +1,132 @@ +# 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. + +"""Transformer passes to combine adjacent single-qubit rotations.""" + +from typing import Optional, TYPE_CHECKING + +from cirq import protocols, circuits +from cirq.transformers.analytical_decompositions import single_qubit_decompositions +from cirq.transformers import transformer_api, transformer_primitives, merge_k_qubit_gates + +if TYPE_CHECKING: + import cirq + + +@transformer_api.transformer +def merge_single_qubit_gates_to_phased_x_and_z( + circuit: 'cirq.AbstractCircuit', + *, + context: Optional['cirq.TransformerContext'] = None, + atol: float = 1e-8, +) -> 'cirq.Circuit': + """Replaces runs of single qubit rotations with `cirq.PhasedXPowGate` and `cirq.ZPowGate`. + + Specifically, any run of non-parameterized single-qubit unitaries will be replaced by an + optional PhasedX operation followed by an optional Z operation. + + Args: + circuit: Input circuit to transform. It will not be modified. + context: `cirq.TransformerContext` storing common configurable options for transformers. + atol: Absolute tolerance to angle error. Larger values allow more negligible gates to be + dropped, smaller values increase accuracy. + + Returns: + Copy of the transformed input circuit. + """ + + def rewriter(op: 'cirq.CircuitOperation') -> 'cirq.OP_TREE': + return [ + g(op.qubits[0]) + for g in single_qubit_decompositions.single_qubit_matrix_to_phased_x_z( + protocols.unitary(op), atol + ) + ] + + return merge_k_qubit_gates.merge_k_qubit_unitaries( + circuit, k=1, context=context, rewriter=rewriter + ) + + +@transformer_api.transformer +def merge_single_qubit_gates_to_phxz( + circuit: 'cirq.AbstractCircuit', + *, + context: Optional['cirq.TransformerContext'] = None, + atol: float = 1e-8, +) -> 'cirq.Circuit': + """Replaces runs of single qubit rotations with a single optional `cirq.PhasedXZGate`. + + Specifically, any run of non-parameterized single-qubit unitaries will be replaced by an + optional PhasedXZ. + + Args: + circuit: Input circuit to transform. It will not be modified. + context: `cirq.TransformerContext` storing common configurable options for transformers. + atol: Absolute tolerance to angle error. Larger values allow more negligible gates to be + dropped, smaller values increase accuracy. + + Returns: + Copy of the transformed input circuit. + """ + + def rewriter(op: 'cirq.CircuitOperation') -> 'cirq.OP_TREE': + gate = single_qubit_decompositions.single_qubit_matrix_to_phxz(protocols.unitary(op), atol) + return gate(op.qubits[0]) if gate else [] + + return merge_k_qubit_gates.merge_k_qubit_unitaries( + circuit, k=1, context=context, rewriter=rewriter + ) + + +@transformer_api.transformer +def merge_single_qubit_moments_to_phxz( + circuit: 'cirq.AbstractCircuit', + *, + context: Optional['cirq.TransformerContext'] = None, + atol: float = 1e-8, +) -> 'cirq.Circuit': + """Merges adjacent moments with only 1-qubit rotations to a single moment with PhasedXZ gates. + + Args: + circuit: Input circuit to transform. It will not be modified. + context: `cirq.TransformerContext` storing common configurable options for transformers. + atol: Absolute tolerance to angle error. Larger values allow more negligible gates to be + dropped, smaller values increase accuracy. + + Returns: + Copy of the transformed input circuit. + """ + tags_to_ignore = set(context.tags_to_ignore) if context else set() + + def can_merge_moment(m: 'cirq.Moment'): + return all( + protocols.num_qubits(op) == 1 + and protocols.has_unitary(op) + and tags_to_ignore.isdisjoint(op.tags) + for op in m + ) + + def merge_func(m1: 'cirq.Moment', m2: 'cirq.Moment') -> Optional['cirq.Moment']: + if not (can_merge_moment(m1) and can_merge_moment(m2)): + return None + ret_ops = [] + for q in m1.qubits | m2.qubits: + mat = protocols.unitary(circuits.Circuit(m.operation_at(q) or [] for m in [m1, m2])) + gate = single_qubit_decompositions.single_qubit_matrix_to_phxz(mat, atol) + if gate: + ret_ops.append(gate(q)) + return circuits.Moment(ret_ops) + + return transformer_primitives.merge_moments(circuit, merge_func).unfreeze(copy=False) diff --git a/cirq-core/cirq/transformers/merge_single_qubit_gates_test.py b/cirq-core/cirq/transformers/merge_single_qubit_gates_test.py new file mode 100644 index 000000000000..7c408731f42c --- /dev/null +++ b/cirq-core/cirq/transformers/merge_single_qubit_gates_test.py @@ -0,0 +1,129 @@ +# 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. + +from typing import List + +import cirq + + +def assert_optimizes(optimized: cirq.AbstractCircuit, expected: cirq.AbstractCircuit): + # Ignore differences that would be caught by follow-up optimizations. + followup_transformers: List[cirq.TRANSFORMER] = [ + cirq.drop_negligible_operations, + cirq.drop_empty_moments, + ] + for transform in followup_transformers: + optimized = transform(optimized) + expected = transform(expected) + + cirq.testing.assert_same_circuits(optimized, expected) + + +def test_merge_single_qubit_gates_into_phased_x_z(): + a, b = cirq.LineQubit.range(2) + c = cirq.Circuit( + cirq.X(a), + cirq.Y(b) ** 0.5, + cirq.CZ(a, b), + cirq.H(a), + cirq.Z(a), + cirq.measure(b, key="m"), + cirq.H(a).with_classical_controls("m"), + ) + assert_optimizes( + optimized=cirq.merge_single_qubit_gates_to_phased_x_and_z(c), + expected=cirq.Circuit( + cirq.PhasedXPowGate(phase_exponent=1)(a), + cirq.Y(b) ** 0.5, + cirq.CZ(a, b), + (cirq.PhasedXPowGate(phase_exponent=-0.5)(a)) ** 0.5, + cirq.measure(b, key="m"), + cirq.H(a).with_classical_controls("m"), + ), + ) + + +def test_merge_single_qubit_gates_into_phxz(): + def phxz(a, x, z): + return cirq.PhasedXZGate( + axis_phase_exponent=a, + x_exponent=x, + z_exponent=z, + ) + + a, b = cirq.LineQubit.range(2) + c = cirq.Circuit( + cirq.X(a), + cirq.Y(b) ** 0.5, + cirq.CZ(a, b), + cirq.H(a), + cirq.Z(a), + cirq.measure(b, key="m"), + cirq.H(a).with_classical_controls("m"), + ) + assert_optimizes( + optimized=cirq.merge_single_qubit_gates_to_phxz(c), + expected=cirq.Circuit( + phxz(-1, 1, 0).on(a), + phxz(0.5, 0.5, 0).on(b), + cirq.CZ(a, b), + phxz(-0.5, 0.5, 0).on(a), + cirq.measure(b, key="m"), + cirq.H(a).with_classical_controls("m"), + ), + ) + + +def test_merge_single_qubit_moments_to_phxz(): + q = cirq.LineQubit.range(3) + c_orig = cirq.Circuit( + cirq.Moment(cirq.X.on_each(*q[:2])), + cirq.Moment(cirq.T.on_each(*q[1:])), + cirq.Moment(cirq.Y.on_each(*q[:2])), + cirq.Moment(cirq.CZ(*q[:2]), cirq.Y(q[2])), + cirq.Moment(cirq.X.on_each(*q[:2])), + cirq.Moment(cirq.T.on_each(*q[1:])), + cirq.Moment(cirq.Y.on_each(*q[:2])), + cirq.Moment(cirq.Y(q[0]).with_tags("nocompile"), cirq.Z.on_each(*q[1:])), + cirq.Moment(cirq.X.on_each(q[0])), + cirq.Moment(cirq.measure(q[0], key="a")), + cirq.Moment(cirq.X(q[1]).with_classical_controls("a")), + cirq.Moment(cirq.X.on_each(q[1])), + ) + cirq.testing.assert_has_diagram( + c_orig, + ''' +0: ───X───────Y───@───X───────Y───Y['nocompile']───X───M─────────── + │ ║ +1: ───X───T───Y───@───X───T───Y───Z────────────────────╫───X───X─── + ║ ║ +2: ───────T───────Y───────T───────Z────────────────────╫───╫─────── + ║ ║ +a: ════════════════════════════════════════════════════@═══^═══════ +''', + ) + context = cirq.TransformerContext(tags_to_ignore=("nocompile",)) + c_new = cirq.merge_single_qubit_moments_to_phxz(c_orig, context=context) + cirq.testing.assert_has_diagram( + c_new, + ''' +0: ───PhXZ(a=-0.5,x=0,z=-1)──────@───PhXZ(a=-0.5,x=0,z=-1)──────Y['nocompile']───X───M─────────── + │ ║ +1: ───PhXZ(a=-0.25,x=0,z=0.75)───@───PhXZ(a=-0.25,x=0,z=0.75)───Z────────────────────╫───X───X─── + ║ ║ +2: ───PhXZ(a=0.25,x=0,z=0.25)────Y───PhXZ(a=0.25,x=0,z=0.25)────Z────────────────────╫───╫─────── + ║ ║ +a: ══════════════════════════════════════════════════════════════════════════════════@═══^═══════ +''', + ) diff --git a/cirq-core/cirq/transformers/transformer_primitives.py b/cirq-core/cirq/transformers/transformer_primitives.py index 671384771ec5..f7a2d1132366 100644 --- a/cirq-core/cirq/transformers/transformer_primitives.py +++ b/cirq-core/cirq/transformers/transformer_primitives.py @@ -359,7 +359,7 @@ def merge_k_qubit_unitaries_to_circuit_op( def can_merge(ops1: Sequence['cirq.Operation'], ops2: Sequence['cirq.Operation']) -> bool: return all( - protocols.has_unitary(op) and protocols.num_qubits(op) <= k + protocols.num_qubits(op) <= k and protocols.has_unitary(op) for op_list in [ops1, ops2] for op in op_list ) diff --git a/cirq-google/cirq_google/optimizers/optimize_for_sycamore.py b/cirq-google/cirq_google/optimizers/optimize_for_sycamore.py index 9c9449cd3211..5322e5f15f11 100644 --- a/cirq-google/cirq_google/optimizers/optimize_for_sycamore.py +++ b/cirq-google/cirq_google/optimizers/optimize_for_sycamore.py @@ -39,7 +39,6 @@ def _get_xmon_optimizers( return [ convert_to_xmon_gates.ConvertToXmonGates().optimize_circuit, cirq.MergeInteractions(tolerance=tolerance, allow_partial_czs=False).optimize_circuit, - lambda c: cirq.merge_single_qubit_gates_into_phxz(c, tolerance), ] @@ -52,17 +51,13 @@ def _get_xmon_optimizers_part_cz( return [ convert_to_xmon_gates.ConvertToXmonGates().optimize_circuit, cirq.MergeInteractions(tolerance=tolerance, allow_partial_czs=True).optimize_circuit, - lambda c: cirq.merge_single_qubit_gates_into_phxz(c, tolerance), ] def _get_sycamore_optimizers( tolerance: float, tabulation: Optional[cirq.TwoQubitGateTabulation] ) -> List[Callable[[cirq.Circuit], None]]: - return [ - ConvertToSycamoreGates(tabulation=tabulation).optimize_circuit, - lambda c: cirq.merge_single_qubit_gates_into_phxz(c, tolerance), - ] + return [ConvertToSycamoreGates(tabulation=tabulation).optimize_circuit] def _get_sqrt_iswap_optimizers( @@ -71,10 +66,7 @@ def _get_sqrt_iswap_optimizers( if tabulation is not None: # coverage: ignore raise ValueError("Gate tabulation not supported for sqrt_iswap") - return [ - ConvertToSqrtIswapGates().optimize_circuit, - lambda c: cirq.merge_single_qubit_gates_into_phxz(c, tolerance), - ] + return [ConvertToSqrtIswapGates().optimize_circuit] _OPTIMIZER_TYPES = { @@ -154,7 +146,7 @@ def optimized_for_sycamore( opts = _OPTIMIZER_TYPES[optimizer_type](tolerance=tolerance, tabulation=tabulation) for optimizer in opts: optimizer(copy) - + copy = cirq.merge_single_qubit_gates_to_phxz(copy, atol=tolerance) copy = cirq.eject_phased_paulis(copy, atol=tolerance) copy = cirq.eject_z(copy, atol=tolerance) copy = cirq.drop_negligible_operations(copy, atol=tolerance) diff --git a/cirq-ionq/cirq_ionq/ionq_devices.py b/cirq-ionq/cirq_ionq/ionq_devices.py index 3d41bfd3dcea..bf8fcf5d1db2 100644 --- a/cirq-ionq/cirq_ionq/ionq_devices.py +++ b/cirq-ionq/cirq_ionq/ionq_devices.py @@ -155,7 +155,7 @@ def _decompose_two_qubit(operation: cirq.Operation) -> cirq.OP_TREE: if type(op.gate) == cirq.CZPowGate else op, ) - cirq.merge_single_qubit_gates_into_phased_x_z(temp) + 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, diff --git a/dev_tools/notebooks/isolated_notebook_test.py b/dev_tools/notebooks/isolated_notebook_test.py index 9f2480904a5d..183a4221f9fa 100644 --- a/dev_tools/notebooks/isolated_notebook_test.py +++ b/dev_tools/notebooks/isolated_notebook_test.py @@ -54,6 +54,7 @@ 'docs/tutorials/google/xeb_calibration_example.ipynb', 'docs/named_topologies.ipynb', # New Transformers. + 'docs/tutorials/basics.ipynb', 'cirq-core/cirq/contrib/quimb/Contract-a-Grid-Circuit.ipynb', 'cirq-core/cirq/contrib/quimb/Cirq-to-Tensor-Networks.ipynb', ] diff --git a/docs/tutorials/basics.ipynb b/docs/tutorials/basics.ipynb index 5a1ab150a3d1..284c21a0dae6 100644 --- a/docs/tutorials/basics.ipynb +++ b/docs/tutorials/basics.ipynb @@ -81,7 +81,9 @@ "id": "1dOjJlgrNUuz" }, "source": [ - "To begin, please follow the instructions for [installing Cirq](../install.md)." + "To begin, please follow the instructions for [installing Cirq](../install.md).\n", + "\n", + "Note: this notebook relies on unreleased Cirq features. If you want to try these features, make sure you install cirq via `pip install cirq --pre`." ] }, { @@ -96,7 +98,7 @@ " import cirq\n", "except ImportError:\n", " print(\"installing cirq...\")\n", - " !pip install --quiet cirq\n", + " !pip install --quiet cirq --pre\n", " print(\"installed cirq.\")" ] }, @@ -688,7 +690,7 @@ "\n", "The last concept in this tutorial is the optimizer. An optimizer can take a circuit and modify it. Usually, this will entail combining or modifying operations to make it more efficient and shorter, though an optimizer can, in theory, do any sort of circuit manipulation.\n", "\n", - "For example, the `MergeSingleQubitGates` optimizer will take consecutive single-qubit operations and merge them into a single `PhasedXZ` operation." + "For example, the `cirq.merge_single_qubit_gates_to_phxz` optimizer will take consecutive single-qubit operations and merge them into a single `PhasedXZ` operation." ] }, { @@ -712,10 +714,9 @@ ], "source": [ "q=cirq.GridQubit(1, 1)\n", - "optimizer=cirq.MergeSingleQubitGates()\n", "c=cirq.Circuit(cirq.X(q) ** 0.25, cirq.Y(q) ** 0.25, cirq.Z(q) ** 0.25)\n", "print(c)\n", - "optimizer.optimize_circuit(c)\n", + "c = cirq.merge_single_qubit_gates_to_phxz(c)\n", "print(c)" ] },