From b589dc784cec1e6447ae16d05cf72dc1a06464c2 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Thu, 17 Feb 2022 10:06:18 -0800 Subject: [PATCH] Add convert_to_target_gateset and CZ, SqrtIswap target gatesets --- cirq-core/cirq/__init__.py | 6 + .../cirq/protocols/json_test_data/spec.py | 3 + cirq-core/cirq/transformers/__init__.py | 12 ++ .../transformers/convert_to_target_gateset.py | 123 ++++++++++++++ .../convert_to_target_gateset_test.py | 24 +++ .../transformers/target_gatesets/__init__.py | 24 +++ .../compilation_target_gateset.py | 151 ++++++++++++++++++ .../compilation_target_gateset_test.py | 65 ++++++++ .../target_gatesets/cz_gateset.py | 56 +++++++ .../target_gatesets/cz_gateset_test.py | 57 +++++++ .../target_gatesets/sqrt_iswap_gateset.py | 76 +++++++++ .../sqrt_iswap_gateset_test.py | 62 +++++++ 12 files changed, 659 insertions(+) create mode 100644 cirq-core/cirq/transformers/convert_to_target_gateset.py create mode 100644 cirq-core/cirq/transformers/convert_to_target_gateset_test.py create mode 100644 cirq-core/cirq/transformers/target_gatesets/__init__.py create mode 100644 cirq-core/cirq/transformers/target_gatesets/compilation_target_gateset.py create mode 100644 cirq-core/cirq/transformers/target_gatesets/compilation_target_gateset_test.py create mode 100644 cirq-core/cirq/transformers/target_gatesets/cz_gateset.py create mode 100644 cirq-core/cirq/transformers/target_gatesets/cz_gateset_test.py create mode 100644 cirq-core/cirq/transformers/target_gatesets/sqrt_iswap_gateset.py create mode 100644 cirq-core/cirq/transformers/target_gatesets/sqrt_iswap_gateset_test.py diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index b656f1c48b8..078462dc2ea 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -355,11 +355,15 @@ from cirq.transformers import ( align_left, align_right, + CompilationTargetGateset, + CZTargetGateset, compute_cphase_exponents_for_fsim_decomposition, + convert_to_target_gateset, decompose_clifford_tableau_to_operations, decompose_cphase_into_two_fsim, decompose_multi_controlled_x, decompose_multi_controlled_rotation, + decompose_operations_to_target_gateset, decompose_two_qubit_interaction_into_four_fsim_gates, defer_measurements, dephase_measurements, @@ -382,6 +386,7 @@ merge_single_qubit_moments_to_phxz, prepare_two_qubit_state_using_cz, prepare_two_qubit_state_using_sqrt_iswap, + SqrtIswapTargetGateset, single_qubit_matrix_to_gates, single_qubit_matrix_to_pauli_rotations, single_qubit_matrix_to_phased_x_z, @@ -398,6 +403,7 @@ two_qubit_matrix_to_operations, two_qubit_matrix_to_sqrt_iswap_operations, two_qubit_gate_product_tabulation, + TwoQubitAnalyticalCompilationTarget, TwoQubitGateTabulation, TwoQubitGateTabulationResult, toggle_tags, diff --git a/cirq-core/cirq/protocols/json_test_data/spec.py b/cirq-core/cirq/protocols/json_test_data/spec.py index 9df089bc156..ffc3bad231d 100644 --- a/cirq-core/cirq/protocols/json_test_data/spec.py +++ b/cirq-core/cirq/protocols/json_test_data/spec.py @@ -32,6 +32,8 @@ 'CircuitSampleJob', 'CliffordSimulatorStepResult', 'CliffordTrialResult', + 'CompilationTargetGateset', + 'CZTargetGateset', 'DensityMatrixSimulator', 'DensityMatrixSimulatorState', 'DensityMatrixStepResult', @@ -72,6 +74,7 @@ 'TwoQubitDiagonalGate', 'TwoQubitGateTabulationResult', 'UnitSweep', + 'SqrtIswapTargetGateset', 'StateVectorSimulatorState', 'StateVectorTrialResult', 'ZerosSampler', diff --git a/cirq-core/cirq/transformers/__init__.py b/cirq-core/cirq/transformers/__init__.py index 8033c6315c2..ee3e6ce8d3d 100644 --- a/cirq-core/cirq/transformers/__init__.py +++ b/cirq-core/cirq/transformers/__init__.py @@ -41,6 +41,13 @@ two_qubit_gate_product_tabulation, ) +from cirq.transformers.target_gatesets import ( + CompilationTargetGateset, + CZTargetGateset, + SqrtIswapTargetGateset, + TwoQubitAnalyticalCompilationTarget, +) + from cirq.transformers.align import align_left, align_right from cirq.transformers.stratify import stratified_circuit @@ -49,6 +56,11 @@ from cirq.transformers.eject_phased_paulis import eject_phased_paulis +from cirq.transformers.convert_to_target_gateset import ( + convert_to_target_gateset, + decompose_operations_to_target_gateset, +) + from cirq.transformers.drop_empty_moments import drop_empty_moments from cirq.transformers.drop_negligible_operations import drop_negligible_operations diff --git a/cirq-core/cirq/transformers/convert_to_target_gateset.py b/cirq-core/cirq/transformers/convert_to_target_gateset.py new file mode 100644 index 00000000000..46ee2fdebc3 --- /dev/null +++ b/cirq-core/cirq/transformers/convert_to_target_gateset.py @@ -0,0 +1,123 @@ +# 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 Optional, Callable, TYPE_CHECKING + +from cirq import protocols +from cirq.transformers.target_gatesets import cz_gateset +from cirq.transformers import transformer_api, transformer_primitives +from cirq.protocols.decompose_protocol import DecomposeResult + +if TYPE_CHECKING: + import cirq + + +def _create_on_stuck_raise_error(gateset: 'cirq.Gateset'): + def _value_error_describing_bad_operation(op: 'cirq.Operation') -> ValueError: + return ValueError(f"Unable to convert {op} to target gateset {gateset!r}") + + return _value_error_describing_bad_operation + + +@transformer_api.transformer +def decompose_operations_to_target_gateset( + circuit: 'cirq.AbstractCircuit', + *, + context: Optional['cirq.TransformerContext'] = None, + gateset: 'cirq.Gateset' = cz_gateset.CZTargetGateset(), + decomposer: Callable[ + ['cirq.Operation', int], DecomposeResult + ] = cz_gateset.CZTargetGateset().decompose_to_target_gateset, + ignore_failures=True, +) -> 'cirq.Circuit': + """Decomposes every operation to `gateset` using `cirq.decompose` and `decomposer`. + + This transformer attempts to decompose every operation `op` in the given circuit to `gateset` + using `cirq.decompose` protocol with `decomposer` used as an intercepting decomposer. This + ensures that `op` is recursively decomposed using implicitly defined known decompositions + (eg: in `_decompose_` magic method on the gaet class) till either `decomposer` knows how to + decompose the given operation or the given operation belongs to `gateset`. + + Args: + circuit: Input circuit to transform. It will not be modified. + context: `cirq.TransformerContext` storing common configurable options for transformers. + gateset: Target gateset, which the decomposed operations should belong to. + decomposer: A callable type which accepts an (operation, moment_index) and returns + - An equivalent `cirq.OP_TREE` implementing `op` using gates from `gateset`. + - `None` or `NotImplemented` if does not know how to decompose a given `op`. + ignore_failures: If set, operations that fail to convert are left unchanged. If not set, + conversion failures raise a TypeError. + + Returns: + An equivalent circuit containing gates accepted by `gateset`. + + Raises: + TypeError: If any input operation fails to convert and `ignore_failures` is False. + """ + + def map_func(op: 'cirq.Operation', moment_index: int): + return protocols.decompose( + op, + intercepting_decomposer=lambda o: decomposer(o, moment_index), + keep=gateset._validate_operation, + on_stuck_raise=(None if ignore_failures else _create_on_stuck_raise_error(gateset)), + ) + + return transformer_primitives.map_operations_and_unroll( + circuit, map_func, tags_to_ignore=context.tags_to_ignore if context else () + ).unfreeze(copy=False) + + +@transformer_api.transformer +def convert_to_target_gateset( + circuit: 'cirq.AbstractCircuit', + *, + context: Optional['cirq.TransformerContext'] = None, + gateset: 'cirq.CompilationTargetGateset' = cz_gateset.CZTargetGateset(), + ignore_failures: bool = True, +) -> 'cirq.Circuit': + """Transforms the given circuit into an equivalent circuit using gates accepted by `gateset`. + + 1. Run all `gateset.preprocess_transformers` + 2. Convert operations using built-in cirq decompose + `gateset.decompose_to_target_gateset`. + 3. Run all `gateset.postprocess_transformers` + + Args: + circuit: Input circuit to transform. It will not be modified. + context: `cirq.TransformerContext` storing common configurable options for transformers. + gateset: Target gateset, which should be an instance of `cirq.CompilationTargetGateset`. + ignore_failures: If set, operations that fail to convert are left unchanged. If not set, + conversion failures raise a TypeError. + + Returns: + An equivalent circuit containing gates accepted by `gateset`. + + Raises: + TypeError: If any input operation fails to convert and `ignore_failures` is False. + """ + for transformer in gateset.preprocess_transformers: + circuit = transformer(circuit, context=context) + + circuit = decompose_operations_to_target_gateset( + circuit, + context=context, + gateset=gateset, + decomposer=gateset.decompose_to_target_gateset, + ignore_failures=ignore_failures, + ) + + for transformer in gateset.postprocess_transformers: + circuit = transformer(circuit, context=context) + + return circuit.unfreeze(copy=False) diff --git a/cirq-core/cirq/transformers/convert_to_target_gateset_test.py b/cirq-core/cirq/transformers/convert_to_target_gateset_test.py new file mode 100644 index 00000000000..6256942df74 --- /dev/null +++ b/cirq-core/cirq/transformers/convert_to_target_gateset_test.py @@ -0,0 +1,24 @@ +# 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. + +import cirq +import pytest + + +def test_decompose_operations_raises_on_stuck(): + c = cirq.Circuit(cirq.X(cirq.NamedQubit("q"))) + with pytest.raises(ValueError, match="Unable to convert"): + _ = cirq.decompose_operations_to_target_gateset( + c, gateset=cirq.Gateset(cirq.Y), decomposer=lambda *_: None, ignore_failures=False + ) diff --git a/cirq-core/cirq/transformers/target_gatesets/__init__.py b/cirq-core/cirq/transformers/target_gatesets/__init__.py new file mode 100644 index 00000000000..c8a5ff15953 --- /dev/null +++ b/cirq-core/cirq/transformers/target_gatesets/__init__.py @@ -0,0 +1,24 @@ +# 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. + +"""Gatesets which can act as compilation targets in Cirq.""" + +from cirq.transformers.target_gatesets.compilation_target_gateset import ( + CompilationTargetGateset, + TwoQubitAnalyticalCompilationTarget, +) + +from cirq.transformers.target_gatesets.cz_gateset import CZTargetGateset + +from cirq.transformers.target_gatesets.sqrt_iswap_gateset import SqrtIswapTargetGateset diff --git a/cirq-core/cirq/transformers/target_gatesets/compilation_target_gateset.py b/cirq-core/cirq/transformers/target_gatesets/compilation_target_gateset.py new file mode 100644 index 00000000000..461db9e6ecd --- /dev/null +++ b/cirq-core/cirq/transformers/target_gatesets/compilation_target_gateset.py @@ -0,0 +1,151 @@ +# 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. + +"""Base class for creating custom target gatesets which can be used for compilation.""" + +from typing import Optional, List, Hashable, TYPE_CHECKING +import abc + +from cirq import circuits, ops, protocols, _import +from cirq.protocols.decompose_protocol import DecomposeResult +from cirq.transformers import ( + merge_k_qubit_gates, + merge_single_qubit_gates, +) + +drop_empty_moments = _import.LazyLoader('drop_empty_moments', globals(), 'cirq.transformers') +drop_negligible = _import.LazyLoader('drop_negligible_operations', globals(), 'cirq.transformers') +expand_composite = _import.LazyLoader('expand_composite', globals(), 'cirq.transformers') + +if TYPE_CHECKING: + import numpy as np + import cirq + + +def _create_transformer_with_kwargs(func: 'cirq.TRANSFORMER', **kwargs) -> 'cirq.TRANSFORMER': + """Hack to capture additional keyword arguments to transformers while preserving mypy type.""" + + def transformer( + circuit: 'cirq.AbstractCircuit', *, context: Optional['cirq.TransformerContext'] = None + ) -> 'cirq.AbstractCircuit': + return func(circuit, context=context, **kwargs) # type: ignore + + return transformer + + +class CompilationTargetGateset(ops.Gateset, metaclass=abc.ABCMeta): + """Abstract base class to create gatesets that can be used as targets for compilation. + + An instance of this type can be passed to transformers like `cirq.convert_to_target_gateset`, + which can transform any given circuit to contain gates accepted by this gateset. + """ + + @property + @abc.abstractmethod + def num_qubits(self) -> int: + """Maximum number of qubits on which a gate from this gateset can act upon.""" + + @abc.abstractmethod + def decompose_to_target_gateset(self, op: 'cirq.Operation', moment_idx: int) -> DecomposeResult: + """Method to rewrite the given operation using gates from this gateset. + + Args: + op: `cirq.Operation` to be rewritten using gates from this gateset. + moment_idx: Moment index where the given operation `op` occurs in a circuit. + + Returns: + - An equivalent `cirq.OP_TREE` implementing `op` using gates from this gateset. + - `None` or `NotImplemented` if does not know how to decompose `op`. + """ + + @property + def _intermediate_result_tag(self) -> Hashable: + """A tag used to identify intermediate compilation results.""" + return "_default_merged_k_qubit_unitaries" + + @property + def preprocess_transformers(self) -> List['cirq.TRANSFORMER']: + """List of transformers which should be run before decomposing individual operations.""" + return [ + _create_transformer_with_kwargs( + expand_composite.expand_composite, + no_decomp=lambda op: protocols.num_qubits(op) <= self.num_qubits, + ), + _create_transformer_with_kwargs( + merge_k_qubit_gates.merge_k_qubit_unitaries, + k=self.num_qubits, + rewriter=lambda op: op.with_tags(self._intermediate_result_tag), + ), + ] + + @property + def postprocess_transformers(self) -> List['cirq.TRANSFORMER']: + """List of transformers which should be run after decomposing individual operations.""" + return [ + merge_single_qubit_gates.merge_single_qubit_moments_to_phxz, + drop_negligible.drop_negligible_operations, + drop_empty_moments.drop_empty_moments, + ] + + +class TwoQubitAnalyticalCompilationTarget(CompilationTargetGateset): + """Abstract base class to create two-qubit target gateset with known analytical decompositions. + + The class is useful to create 2-qubit target gatesets where an analytical method, like KAK + Decomposition, can be used to decompose any arbitrary 2q unitary matrix into a sequence of + gates from this gateset. + + Derived classes should simply implement `_decompose_two_qubit_matrix_to_operations` method. + + Note: The implementation assumes that any single qubit rotation is accepted by this gateset. + """ + + @property + def num_qubits(self) -> int: + return 2 + + def decompose_to_target_gateset(self, op: 'cirq.Operation', moment_idx: int) -> DecomposeResult: + if self._intermediate_result_tag not in op.tags: + return NotImplemented + if protocols.num_qubits(op) == 1: + return op + op_untagged = op.untagged + assert protocols.num_qubits(op) == 2 + assert isinstance(op_untagged, circuits.CircuitOperation) + switch_to_new = any(op not in self for op in op_untagged.circuit.all_operations()) + old_2q_gate_count = len( + [*op_untagged.circuit.findall_operations(lambda o: len(o.qubits) == 2)] + ) + new_optree = self._decompose_two_qubit_matrix_to_operations( + op.qubits[0], op.qubits[1], protocols.unitary(op) + ) + new_2q_gate_count = len([o for o in ops.flatten_to_ops(new_optree) if len(o.qubits) == 2]) + switch_to_new |= new_2q_gate_count < old_2q_gate_count + return new_optree if switch_to_new else [*op_untagged.circuit] + + @abc.abstractmethod + def _decompose_two_qubit_matrix_to_operations( + self, q0: 'cirq.Qid', q1: 'cirq.Qid', mat: 'np.ndarray' + ) -> 'cirq.OP_TREE': + """Decomposes the given 2-qubit unitary matrix into a sequence of gates from this gateset. + + Args: + q0: The first qubit being operated on. + q1: The other qubit being operated on. + mat: Unitary matrix of two qubit operation to apply to the given pair of qubits. + + Returns: + A `cirq.OP_TREE` implementing `cirq.MatrixGate(mat).on(q0, q1)` using gates from this + gateset. + """ diff --git a/cirq-core/cirq/transformers/target_gatesets/compilation_target_gateset_test.py b/cirq-core/cirq/transformers/target_gatesets/compilation_target_gateset_test.py new file mode 100644 index 00000000000..0b8e8a8424d --- /dev/null +++ b/cirq-core/cirq/transformers/target_gatesets/compilation_target_gateset_test.py @@ -0,0 +1,65 @@ +# 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. + +import cirq +import numpy as np + + +class DummyTargetGateset(cirq.TwoQubitAnalyticalCompilationTarget): + def __init__(self): + super().__init__(cirq.AnyUnitaryGateFamily(1), cirq.CNOT) + + def _decompose_two_qubit_matrix_to_operations( + self, q0: cirq.Qid, q1: cirq.Qid, mat: np.ndarray + ) -> cirq.OP_TREE: + return [cirq.X.on_each(q0, q1), cirq.CNOT(q0, q1), cirq.Y.on_each(q0, q1)] + + +def test_two_qubit_analytical_compilation(): + q = cirq.LineQubit.range(2) + c_orig = cirq.Circuit( + cirq.Moment(cirq.Z(q[1])), + cirq.Moment(cirq.X(q[0])), + cirq.Moment(cirq.CZ(*q).with_tags("no_compile")), + cirq.Moment(cirq.Z.on_each(*q)), + cirq.Moment(cirq.X(q[0])), + cirq.Moment(cirq.CZ(*q)), + cirq.Moment(cirq.Z.on_each(*q)), + cirq.Moment(cirq.X(q[0])), + ) + cirq.testing.assert_has_diagram( + c_orig, + ''' +0: ───────X───@['no_compile']───Z───X───@───Z───X─── + │ │ +1: ───Z───────@─────────────────Z───────@───Z─────── +''', + ) + gateset = DummyTargetGateset() + assert gateset.decompose_to_target_gateset(c_orig[2][q[0]], 2) is NotImplemented + merged_op = c_orig[1][q[0]].with_tags(gateset._intermediate_result_tag) + assert gateset.decompose_to_target_gateset(merged_op, 1) is merged_op + c_new = cirq.convert_to_target_gateset( + c_orig, + gateset=DummyTargetGateset(), + context=cirq.TransformerContext(tags_to_ignore=("no_compile",)), + ) + cirq.testing.assert_has_diagram( + c_new, + ''' +0: ───PhXZ(a=-1,x=1,z=0)──────@['no_compile']───X───@───Y─── + │ │ +1: ───PhXZ(a=-0.5,x=0,z=-1)───@─────────────────X───X───Y─── +''', + ) diff --git a/cirq-core/cirq/transformers/target_gatesets/cz_gateset.py b/cirq-core/cirq/transformers/target_gatesets/cz_gateset.py new file mode 100644 index 00000000000..ba0b4cbb770 --- /dev/null +++ b/cirq-core/cirq/transformers/target_gatesets/cz_gateset.py @@ -0,0 +1,56 @@ +# 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. + +"""CZ + single rotations target gateset.""" + +from typing import TYPE_CHECKING + +from cirq import ops +from cirq.transformers.analytical_decompositions import two_qubit_to_cz +from cirq.transformers.target_gatesets import compilation_target_gateset + +if TYPE_CHECKING: + import numpy as np + import cirq + + +class CZTargetGateset(compilation_target_gateset.TwoQubitAnalyticalCompilationTarget): + """Target gateset containing CZ + single qubit rotations + Measurement gates.""" + + def __init__(self, *, atol: float = 1e-8, allow_partial_czs: bool = False) -> None: + """Initializes CZTargetGateset + + Args: + atol: A limit on the amount of absolute error introduced by the decomposition. + allow_partial_czs: If set, all powers of the form `cirq.CZ**t`, and not just + `cirq.CZ`, are part of this gateset. + """ + super().__init__( + ops.CZPowGate if allow_partial_czs else ops.CZ, + ops.MeasurementGate, + ops.AnyUnitaryGateFamily(1), + name='CZPowTargetGateset' if allow_partial_czs else 'CZTargetGateset', + ) + self.atol = atol + self.allow_partial_czs = allow_partial_czs + + def _decompose_two_qubit_matrix_to_operations( + self, q0: 'cirq.Qid', q1: 'cirq.Qid', mat: 'np.ndarray' + ) -> 'cirq.OP_TREE': + return two_qubit_to_cz.two_qubit_matrix_to_operations( + q0, q1, mat, allow_partial_czs=self.allow_partial_czs, atol=self.atol + ) + + +CIRQ_DEFAULT_TARGET_GATESET = CZTargetGateset(allow_partial_czs=True) diff --git a/cirq-core/cirq/transformers/target_gatesets/cz_gateset_test.py b/cirq-core/cirq/transformers/target_gatesets/cz_gateset_test.py new file mode 100644 index 00000000000..8c88025f9a6 --- /dev/null +++ b/cirq-core/cirq/transformers/target_gatesets/cz_gateset_test.py @@ -0,0 +1,57 @@ +# 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. + +import cirq + + +def all_gates_of_type(m: cirq.Moment, g: cirq.Gateset): + for op in m: + if op not in g: + return False + return True + + +def test_convert_to_sqrt_iswap(): + q = cirq.LineQubit.range(5) + op = lambda q0, q1: cirq.H(q1).controlled_by(q0) + c_orig = cirq.Circuit( + cirq.Moment(cirq.X(q[2])), + cirq.Moment(op(q[0], q[1]), op(q[2], q[3])), + cirq.Moment(op(q[2], q[1]), op(q[4], q[3])), + cirq.Moment(op(q[1], q[2]), op(q[3], q[4])), + cirq.Moment(op(q[3], q[2]), op(q[1], q[0])), + ) + + c_new = cirq.convert_to_target_gateset(c_orig, gateset=cirq.CZTargetGateset()) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent(c_orig, c_new, atol=1e-6) + print(c_new) + assert all( + ( + all_gates_of_type(m, cirq.Gateset(cirq.AnyUnitaryGateFamily(1))) + or all_gates_of_type(m, cirq.Gateset(cirq.CZ)) + ) + for m in c_new + ) + + c_new = cirq.convert_to_target_gateset( + c_orig, gateset=cirq.CZTargetGateset(allow_partial_czs=True), ignore_failures=False + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent(c_orig, c_new, atol=1e-6) + assert all( + ( + all_gates_of_type(m, cirq.Gateset(cirq.AnyUnitaryGateFamily(1))) + or all_gates_of_type(m, cirq.Gateset(cirq.CZPowGate)) + ) + for m in c_new + ) diff --git a/cirq-core/cirq/transformers/target_gatesets/sqrt_iswap_gateset.py b/cirq-core/cirq/transformers/target_gatesets/sqrt_iswap_gateset.py new file mode 100644 index 00000000000..4b5bde6a7dc --- /dev/null +++ b/cirq-core/cirq/transformers/target_gatesets/sqrt_iswap_gateset.py @@ -0,0 +1,76 @@ +# 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. + +"""√iSWAP + single qubit rotations target gateset.""" + +from typing import TYPE_CHECKING, Optional + +from cirq import ops +from cirq.transformers.analytical_decompositions import two_qubit_to_sqrt_iswap +from cirq.transformers.target_gatesets import compilation_target_gateset + +if TYPE_CHECKING: + import numpy as np + import cirq + + +class SqrtIswapTargetGateset(compilation_target_gateset.TwoQubitAnalyticalCompilationTarget): + """Target gateset containing √iSWAP + single qubit rotations + Measurement gates.""" + + def __init__( + self, + *, + atol: float = 1e-8, + required_sqrt_iswap_count: Optional[int] = None, + use_sqrt_iswap_inv: bool = False, + ): + """Initializes SqrtIswapTargetGateset + + Args: + atol: A limit on the amount of absolute error introduced by the decomposition. + required_sqrt_iswap_count: When specified, the `decompose_to_target_gateset` will + decompose each operation into exactly this many sqrt-iSWAP gates even if fewer is + possible (maximum 3). A ValueError will be raised if this number is 2 or lower and + synthesis of the operation requires more. + use_sqrt_iswap_inv: If True, `cirq.SQRT_ISWAP_INV` is used as part of the gateset + instead of `cirq.SQRT_ISWAP`. + + Raises: + ValueError: If `required_sqrt_iswap_count` is specified and is not 0, 1, 2, or 3. + """ + if required_sqrt_iswap_count is not None and not 0 <= required_sqrt_iswap_count <= 3: + raise ValueError('the argument `required_sqrt_iswap_count` must be 0, 1, 2, or 3.') + super().__init__( + ops.SQRT_ISWAP_INV if use_sqrt_iswap_inv else ops.SQRT_ISWAP, + ops.MeasurementGate, + ops.AnyUnitaryGateFamily(1), + name='SqrtIswapInvTargetGateset' if use_sqrt_iswap_inv else 'SqrtIswapTargetGateset', + ) + self.atol = atol + self.required_sqrt_iswap_count = required_sqrt_iswap_count + self.use_sqrt_iswap_inv = use_sqrt_iswap_inv + + def _decompose_two_qubit_matrix_to_operations( + self, q0: 'cirq.Qid', q1: 'cirq.Qid', mat: 'np.ndarray' + ) -> 'cirq.OP_TREE': + return two_qubit_to_sqrt_iswap.two_qubit_matrix_to_sqrt_iswap_operations( + q0, + q1, + mat, + required_sqrt_iswap_count=self.required_sqrt_iswap_count, + use_sqrt_iswap_inv=self.use_sqrt_iswap_inv, + atol=self.atol, + check_preconditions=False, + clean_operations=True, + ) diff --git a/cirq-core/cirq/transformers/target_gatesets/sqrt_iswap_gateset_test.py b/cirq-core/cirq/transformers/target_gatesets/sqrt_iswap_gateset_test.py new file mode 100644 index 00000000000..37b6fdbfb5b --- /dev/null +++ b/cirq-core/cirq/transformers/target_gatesets/sqrt_iswap_gateset_test.py @@ -0,0 +1,62 @@ +# 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. + +import cirq +import pytest + + +def all_gates_of_type(m: cirq.Moment, g: cirq.Gateset): + for op in m: + if op not in g: + return False + return True + + +def test_convert_to_sqrt_iswap(): + q = cirq.LineQubit.range(5) + op = lambda q0, q1: cirq.H(q1).controlled_by(q0) + c_orig = cirq.Circuit( + cirq.Moment(cirq.X(q[2])), + cirq.Moment(op(q[0], q[1]), op(q[2], q[3])), + cirq.Moment(op(q[2], q[1]), op(q[4], q[3])), + cirq.Moment(op(q[1], q[2]), op(q[3], q[4])), + cirq.Moment(op(q[3], q[2]), op(q[1], q[0])), + ) + + c_new = cirq.convert_to_target_gateset(c_orig, gateset=cirq.SqrtIswapTargetGateset()) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent(c_orig, c_new, atol=1e-6) + assert all( + ( + all_gates_of_type(m, cirq.Gateset(cirq.AnyUnitaryGateFamily(1))) + or all_gates_of_type(m, cirq.Gateset(cirq.SQRT_ISWAP)) + ) + for m in c_new + ) + + c_new = cirq.convert_to_target_gateset( + c_orig, gateset=cirq.SqrtIswapTargetGateset(use_sqrt_iswap_inv=True), ignore_failures=False + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent(c_orig, c_new, atol=1e-6) + assert all( + ( + all_gates_of_type(m, cirq.Gateset(cirq.AnyUnitaryGateFamily(1))) + or all_gates_of_type(m, cirq.Gateset(cirq.SQRT_ISWAP_INV)) + ) + for m in c_new + ) + + +def test_sqrt_iswap_gateset_raises(): + with pytest.raises(ValueError, match="`required_sqrt_iswap_count` must be 0, 1, 2, or 3"): + _ = cirq.SqrtIswapTargetGateset(required_sqrt_iswap_count=4)