diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index ae2bf4f457f..9bbb84cb7e8 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -357,6 +357,7 @@ from cirq.transformers import ( align_left, align_right, + ApproximateTwoQubitTargetGateset, CompilationTargetGateset, CZTargetGateset, compute_cphase_exponents_for_fsim_decomposition, diff --git a/cirq-core/cirq/protocols/json_test_data/spec.py b/cirq-core/cirq/protocols/json_test_data/spec.py index f23a193d5ac..ff4e2480f68 100644 --- a/cirq-core/cirq/protocols/json_test_data/spec.py +++ b/cirq-core/cirq/protocols/json_test_data/spec.py @@ -25,6 +25,7 @@ resolver_cache=_class_resolver_dictionary(), not_yet_serializable=[ 'Alignment', + 'ApproximateTwoQubitTargetGateset', 'AxisAngleDecomposition', 'CircuitDag', 'CircuitDiagramInfo', diff --git a/cirq-core/cirq/transformers/__init__.py b/cirq-core/cirq/transformers/__init__.py index 1f0431f6c6c..cbff83a31d2 100644 --- a/cirq-core/cirq/transformers/__init__.py +++ b/cirq-core/cirq/transformers/__init__.py @@ -43,6 +43,7 @@ ) from cirq.transformers.target_gatesets import ( + ApproximateTwoQubitTargetGateset, CompilationTargetGateset, CZTargetGateset, SqrtIswapTargetGateset, diff --git a/cirq-core/cirq/transformers/target_gatesets/__init__.py b/cirq-core/cirq/transformers/target_gatesets/__init__.py index 222e58ef46d..564c5005378 100644 --- a/cirq-core/cirq/transformers/target_gatesets/__init__.py +++ b/cirq-core/cirq/transformers/target_gatesets/__init__.py @@ -14,6 +14,10 @@ """Gatesets which can act as compilation targets in Cirq.""" +from cirq.transformers.target_gatesets.approximate_two_qubit_gateset import ( + ApproximateTwoQubitTargetGateset, +) + from cirq.transformers.target_gatesets.compilation_target_gateset import ( CompilationTargetGateset, TwoQubitCompilationTargetGateset, diff --git a/cirq-core/cirq/transformers/target_gatesets/approximate_two_qubit_gateset.py b/cirq-core/cirq/transformers/target_gatesets/approximate_two_qubit_gateset.py new file mode 100644 index 00000000000..f2b26ebaf76 --- /dev/null +++ b/cirq-core/cirq/transformers/target_gatesets/approximate_two_qubit_gateset.py @@ -0,0 +1,124 @@ +# 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 approximately compiling under a given two qubit gate.""" + +from typing import cast, TYPE_CHECKING + +from cirq import ops, protocols +from cirq.qis import measures +from cirq.transformers.analytical_decompositions import single_qubit_decompositions +from cirq.transformers.heuristic_decompositions import two_qubit_gate_tabulation +from cirq.transformers.target_gatesets import compilation_target_gateset + +if TYPE_CHECKING: + import cirq + + +class ApproximateTwoQubitTargetGateset(compilation_target_gateset.TwoQubitCompilationTargetGateset): + """Target gateset giving approximate compilations using provided base gate.""" + + def __init__( + self, + base_gate: 'cirq.Gate', + max_infidelity: float = 0.01, + *, + sample_scaling: int = 50, + allow_missed_points: bool = True, + random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None, + ) -> None: + + """Initializes ApproximateTwoQubitTargetGateset + + + This gateset builds a `cirq.GateTabulation` (kak decomposition) + around the provided base_gate to do fidelity limited decompositions. + Note that gates with symbols are not supported and will not be + decomposed by this transformer. + + Args: + base_gate: `cirq.Gate` to use as two qubit entangler + max_infidelity: Maximum acceptable infidelity per + two qubit operation. Note that gate merging may + decrease the number of two qubit operations. + sample_scaling: Relative number of random gate products to use in the + tabulation. The total number of random local unitaries scales as + ~ $max_infidelity^{-3/2} * sample_scaling$. Must be positive. + allow_missed_points: If True, the tabulation is allowed to conclude + even if not all points in the Weyl chamber are expected to be + compilable using 2 or 3 base gates. Otherwise an error is raised + in this case. + random_state: Random state or random state seed. + + Raises: + ValueError: if base_gate is not a two qubit gate. + """ + if base_gate.num_qubits() != 2: + raise ValueError( + "base_gate requires a two qubit gate. Given" + f" {str(base_gate)} which is {base_gate.num_qubits()} qubits." + ) + + super().__init__( + base_gate, + ops.MeasurementGate, + ops.AnyUnitaryGateFamily(1), + name=f'Approximate{str(base_gate)}Gateset.', + ) + self._base_gate = base_gate + self._tabulation = two_qubit_gate_tabulation.two_qubit_gate_product_tabulation( + protocols.unitary(base_gate), + max_infidelity, + sample_scaling=sample_scaling, + allow_missed_points=allow_missed_points, + random_state=random_state, + ) + + @property + def base_gate(self) -> 'cirq.Gate': + """Get the base_gate from initialization.""" + return self._base_gate + + @property + def tabulation(self) -> two_qubit_gate_tabulation.TwoQubitGateTabulation: + """Get the GateTabulation object associated with base_gate.""" + return self._tabulation + + def _decompose_two_qubit_operation(self, op: 'cirq.Operation', _) -> 'cirq.OP_TREE': + if not protocols.has_unitary(op): + return NotImplemented + + if protocols.has_kraus(op): + e_fid = measures.entanglement_fidelity(cast(protocols.SupportsKraus, op)) + if e_fid > 1.0 - self._tabulation.max_expected_infidelity: + return [] # we are close enough to identity. + + q0, q1 = op.qubits + decomp = self._tabulation.compile_two_qubit_gate(protocols.unitary(op)) + ret = [] + for i in range(len(decomp.local_unitaries) - 1): + mats = decomp.local_unitaries[i] + for mat, q in zip(mats, [q0, q1]): + phxz_gate = single_qubit_decompositions.single_qubit_matrix_to_phxz(mat) + if phxz_gate is not None: + ret.append(phxz_gate(q)) + ret.append(self._base_gate(q0, q1)) + + mats = decomp.local_unitaries[-1] + for mat, q in zip(mats, [q0, q1]): + phxz_gate = single_qubit_decompositions.single_qubit_matrix_to_phxz(mat) + if phxz_gate is not None: + ret.append(phxz_gate(q)) + + return ret diff --git a/cirq-core/cirq/transformers/target_gatesets/approximate_two_qubit_gateset_test.py b/cirq-core/cirq/transformers/target_gatesets/approximate_two_qubit_gateset_test.py new file mode 100644 index 00000000000..c6cdcce2fdf --- /dev/null +++ b/cirq-core/cirq/transformers/target_gatesets/approximate_two_qubit_gateset_test.py @@ -0,0 +1,171 @@ +# 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 pytest +import cirq +import sympy +import numpy as np + + +def test_instantiate(): + gset = cirq.ApproximateTwoQubitTargetGateset(cirq.CZ) + assert gset.base_gate == cirq.CZ + assert cirq.CZ in gset + assert cirq.H in gset + assert np.all(gset.tabulation.base_gate == cirq.unitary(cirq.CZ)) + + a, b = cirq.LineQubit.range(2) + c = cirq.Circuit(cirq.CNOT(a, b)) + c = cirq.optimize_for_target_gateset(c, gateset=gset) + assert ( + len([1 for op in c.all_operations() if len(op.qubits) == 2]) == 1 + ), 'It should take 1 CZ gates to decompose a CX gate' + + +def test_bad_instantiate(): + with pytest.raises(ValueError, match="1"): + _ = cirq.ApproximateTwoQubitTargetGateset(cirq.H) + + +def test_correctness(): + a, b = cirq.LineQubit.range(2) + circuit = cirq.Circuit( + cirq.H(a), + cirq.H(b), + cirq.SWAP(a, b) ** 0.5, + cirq.Y(a) ** 0.456, + cirq.Y(b) ** 0.123, + cirq.CNOT(a, b), + cirq.X(a) ** 0.123, + cirq.Y(b) ** 0.9, + cirq.CNOT(b, a), + ) + c_new = cirq.optimize_for_target_gateset( + circuit, gateset=cirq.ApproximateTwoQubitTargetGateset(cirq.CZ, random_state=123) + ) + print(circuit.final_state_vector()) + assert len(c_new) == 7 # only need 3 CZs. + assert cirq.fidelity(c_new.final_state_vector(), circuit.final_state_vector()) > 0.995 + + +def test_optimizes_same_gate(): + a, b = cirq.LineQubit.range(2) + c = cirq.Circuit(cirq.ISWAP(a, b)) + c2 = cirq.optimize_for_target_gateset( + c, gateset=cirq.ApproximateTwoQubitTargetGateset(cirq.ISWAP) + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent(c, c2, atol=1e-6) + + c = cirq.Circuit(cirq.CX(a, b) ** 0.5) + c2 = cirq.optimize_for_target_gateset( + c, gateset=cirq.ApproximateTwoQubitTargetGateset(cirq.CX ** 0.5) + ) + cirq.testing.assert_circuits_with_terminal_measurements_are_equivalent(c, c2, atol=1e-6) + + +def test_optimizes_tagged_gate(): + a, b = cirq.LineQubit.range(2) + c = cirq.Circuit((cirq.CZ ** 0.5)(a, b).with_tags('mytag')) + c = cirq.optimize_for_target_gateset( + c, gateset=cirq.ApproximateTwoQubitTargetGateset(cirq.CZ, random_state=123) + ) + assert ( + len([1 for op in c.all_operations() if len(op.qubits) == 2]) == 2 + ), 'It should take 2 CZ gates to decompose a CZ**0.5 gate' + + +def test_symbols_not_supported(): + a, b = cirq.LineQubit.range(2) + c = cirq.Circuit((cirq.CZ ** sympy.Symbol('oops'))(a, b)) + c = cirq.optimize_for_target_gateset( + c, gateset=cirq.ApproximateTwoQubitTargetGateset(cirq.CZ, random_state=123) + ) + assert len([1 for op in c.all_operations() if len(op.qubits) == 2]) == 1 + + +def test_avoids_decompose_when_matrix_available(): + class OtherXX(cirq.testing.TwoQubitGate): + # coverage: ignore + def _has_unitary_(self) -> bool: + return True + + def _unitary_(self) -> np.ndarray: + m = np.array([[0, 1], [1, 0]]) + return np.kron(m, m) + + def _decompose_(self, qubits): + assert False + + class OtherOtherXX(cirq.testing.TwoQubitGate): + # coverage: ignore + def _has_unitary_(self) -> bool: + return True + + def _unitary_(self) -> np.ndarray: + m = np.array([[0, 1], [1, 0]]) + return np.kron(m, m) + + def _decompose_(self, qubits): + assert False + + a, b = cirq.LineQubit.range(2) + c = cirq.Circuit(OtherXX()(a, b), OtherOtherXX()(a, b)) + c = cirq.optimize_for_target_gateset(c, gateset=cirq.ApproximateTwoQubitTargetGateset(cirq.CZ)) + assert len(c) == 0 + + +def test_composite_gates_without_matrix(): + class CompositeDummy(cirq.SingleQubitGate): + def _decompose_(self, qubits): + yield cirq.X(qubits[0]) + yield cirq.Y(qubits[0]) ** 0.5 + + class CompositeDummy2(cirq.testing.TwoQubitGate): + def _decompose_(self, qubits): + yield cirq.CZ(qubits[0], qubits[1]) + yield CompositeDummy()(qubits[1]) + + q0, q1 = cirq.LineQubit.range(2) + circuit = cirq.Circuit( + CompositeDummy()(q0), + CompositeDummy2()(q0, q1), + ) + expected = cirq.Circuit( + cirq.X(q0), + cirq.Y(q0) ** 0.5, + cirq.CZ(q0, q1), + cirq.X(q1), + cirq.Y(q1) ** 0.5, + ) + c_new = cirq.optimize_for_target_gateset( + circuit, gateset=cirq.ApproximateTwoQubitTargetGateset(cirq.CZ, random_state=123) + ) + + assert len(c_new) == 3 + assert cirq.fidelity(c_new.final_state_vector(), expected.final_state_vector()) > 0.995 + + +def test_unsupported_gate(): + class UnsupportedDummy(cirq.testing.TwoQubitGate): + pass + + q0, q1 = cirq.LineQubit.range(2) + circuit = cirq.Circuit(UnsupportedDummy()(q0, q1)) + assert circuit == cirq.optimize_for_target_gateset( + circuit, gateset=cirq.ApproximateTwoQubitTargetGateset(cirq.CZ) + ) + with pytest.raises(ValueError, match='Unable to convert'): + _ = cirq.optimize_for_target_gateset( + circuit, gateset=cirq.ApproximateTwoQubitTargetGateset(cirq.CZ), ignore_failures=False + )