Repository: quantumlib/Cirq

Issue:
Complete coverage of aqt_device.py
**Description of the issue**

#5696 moves some uncovered code from ion_device.py (deleted) to aqt_device.py causing a CI coverage failure on the *new* lines.  I have added a few `# coverage: ignore` comments to unblock the PR.

TODO: Fix this by adding tests that will complete the code coverage of aqt_device.py.



Discussion:

Resolving patch:
diff --git a/cirq-aqt/cirq_aqt/aqt_device.py b/cirq-aqt/cirq_aqt/aqt_device.py
--- a/cirq-aqt/cirq_aqt/aqt_device.py
+++ b/cirq-aqt/cirq_aqt/aqt_device.py
@@ -30,7 +30,7 @@
 import numpy as np
 
 import cirq
-from cirq.protocols.decompose_protocol import DecomposeResult
+from cirq_aqt import aqt_target_gateset
 
 gate_dict = {'X': cirq.X, 'Y': cirq.Y, 'Z': cirq.Z, 'MS': cirq.XX, 'R': cirq.PhasedXPowGate}
 
@@ -222,55 +222,6 @@ def simulate_samples(self, repetitions: int) -> cirq.Result:
         return result
 
 
-class AQTTargetGateset(cirq.TwoQubitCompilationTargetGateset):
-    """Target gateset accepting XXPowGate + X/Y/Z/PhX single qubit rotations + measurement gates.
-
-    By default, `cirq_aqt.AQTTargetGateset` will accept and compile unknown
-    gates to the following universal target gateset:
-
-    - `cirq.XXPowGate`: The two qubit entangling gate.
-    - `cirq.XPowGate`, `cirq.YPowGate`, `cirq.ZPowGate`,
-      `cirq.PhasedXPowGate`: Single qubit rotations.
-    - `cirq.MeasurementGate`: Measurements.
-    """
-
-    def __init__(self):
-        super().__init__(
-            cirq.XXPowGate,
-            cirq.MeasurementGate,
-            cirq.XPowGate,
-            cirq.YPowGate,
-            cirq.ZPowGate,
-            cirq.PhasedXPowGate,
-            unroll_circuit_op=False,
-        )
-
-    # TODO(#5698) Complete code coverage and remove coverage-ignore labels below
-
-    def _decompose_single_qubit_operation(self, op: 'cirq.Operation', _: int) -> DecomposeResult:
-        if isinstance(op.gate, cirq.HPowGate) and op.gate.exponent == 1:
-            # coverage: ignore
-            return [cirq.rx(np.pi).on(op.qubits[0]), cirq.ry(-1 * np.pi / 2).on(op.qubits[0])]
-        if cirq.has_unitary(op):
-            # coverage: ignore
-            gates = cirq.single_qubit_matrix_to_phased_x_z(cirq.unitary(op))
-            return [g.on(op.qubits[0]) for g in gates]
-        return NotImplemented
-
-    def _decompose_two_qubit_operation(self, op: 'cirq.Operation', _) -> DecomposeResult:
-        if cirq.has_unitary(op):
-            # coverage: ignore
-            return cirq.two_qubit_matrix_to_ion_operations(
-                op.qubits[0], op.qubits[1], cirq.unitary(op)
-            )
-        return NotImplemented
-
-    @property
-    def postprocess_transformers(self) -> List['cirq.TRANSFORMER']:
-        """List of transformers which should be run after decomposing individual operations."""
-        return [cirq.drop_negligible_operations, cirq.drop_empty_moments]
-
-
 @cirq.value_equality
 class AQTDevice(cirq.Device):
     """Ion trap device with qubits having all-to-all connectivity and placed on a line."""
@@ -303,7 +254,7 @@ def __init__(
                 f"{set(type(qubit) for qubit in qubits)}"
             )
         self.qubits = frozenset(qubits)
-        self.gateset = AQTTargetGateset()
+        self.gateset = aqt_target_gateset.AQTTargetGateset()
 
         graph = nx.Graph()
         graph.add_edges_from([(a, b) for a in qubits for b in qubits if a != b], directed=False)
diff --git a/cirq-aqt/cirq_aqt/aqt_target_gateset.py b/cirq-aqt/cirq_aqt/aqt_target_gateset.py
new file mode 100644
--- /dev/null
+++ b/cirq-aqt/cirq_aqt/aqt_target_gateset.py
@@ -0,0 +1,74 @@
+# 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 for ion trap device with mutually linked qubits placed on a line.
+"""
+
+from typing import List
+
+import numpy as np
+
+import cirq
+from cirq.protocols.decompose_protocol import DecomposeResult
+
+
+class AQTTargetGateset(cirq.TwoQubitCompilationTargetGateset):
+    """Target gateset accepting XXPowGate + X/Y/Z/PhX single qubit rotations + measurement gates.
+
+    By default, `cirq_aqt.AQTTargetGateset` will accept and compile unknown
+    gates to the following universal target gateset:
+
+    - `cirq.XXPowGate`: The two qubit entangling gate.
+    - `cirq.XPowGate`, `cirq.YPowGate`, `cirq.ZPowGate`,
+      `cirq.PhasedXPowGate`: Single qubit rotations.
+    - `cirq.MeasurementGate`: Measurements.
+    """
+
+    def __init__(self):
+        super().__init__(
+            cirq.XXPowGate,
+            cirq.MeasurementGate,
+            cirq.XPowGate,
+            cirq.YPowGate,
+            cirq.ZPowGate,
+            cirq.PhasedXPowGate,
+            unroll_circuit_op=False,
+        )
+
+    def _decompose_single_qubit_operation(self, op: 'cirq.Operation', _: int) -> DecomposeResult:
+        # unwrap tagged and circuit operations to get the actual operation
+        opu = op.untagged
+        opu = (
+            next(opu.circuit.all_operations()).untagged
+            if isinstance(opu, cirq.CircuitOperation) and len(opu.circuit) == 1
+            else opu
+        )
+        if isinstance(opu.gate, cirq.HPowGate) and opu.gate.exponent == 1:
+            return [cirq.rx(np.pi).on(opu.qubits[0]), cirq.ry(-1 * np.pi / 2).on(opu.qubits[0])]
+        if cirq.has_unitary(opu):
+            gates = cirq.single_qubit_matrix_to_phased_x_z(cirq.unitary(opu))
+            return [g.on(opu.qubits[0]) for g in gates]
+        return NotImplemented
+
+    def _decompose_two_qubit_operation(self, op: 'cirq.Operation', _) -> DecomposeResult:
+        if cirq.has_unitary(op):
+            return cirq.two_qubit_matrix_to_ion_operations(
+                op.qubits[0], op.qubits[1], cirq.unitary(op)
+            )
+        return NotImplemented
+
+    @property
+    def postprocess_transformers(self) -> List['cirq.TRANSFORMER']:
+        """List of transformers which should be run after decomposing individual operations."""
+        return [cirq.drop_negligible_operations, cirq.drop_empty_moments]


Test patch:
diff --git a/cirq-aqt/cirq_aqt/aqt_target_gateset_test.py b/cirq-aqt/cirq_aqt/aqt_target_gateset_test.py
new file mode 100644
--- /dev/null
+++ b/cirq-aqt/cirq_aqt/aqt_target_gateset_test.py
@@ -0,0 +1,90 @@
+# 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 AQTTargetGateset."""
+
+import pytest
+import sympy
+
+import cirq
+from cirq_aqt import aqt_target_gateset
+
+
+Q, Q2, Q3, Q4 = cirq.LineQubit.range(4)
+
+
+@pytest.mark.parametrize(
+    "op,expected",
+    [
+        (cirq.H(Q), False),
+        (cirq.HPowGate(exponent=0.5)(Q), False),
+        (cirq.XX(Q, Q2), True),
+        (cirq.measure(Q), True),
+        (cirq.XPowGate(exponent=0.5)(Q), True),
+        (cirq.YPowGate(exponent=0.25)(Q), True),
+        (cirq.ZPowGate(exponent=0.125)(Q), True),
+        (cirq.PhasedXPowGate(exponent=0.25, phase_exponent=0.125)(Q), True),
+        (cirq.CZPowGate(exponent=0.5)(Q, Q2), False),
+        (cirq.CZ(Q, Q2), False),
+        (cirq.CNOT(Q, Q2), False),
+        (cirq.SWAP(Q, Q2), False),
+        (cirq.ISWAP(Q, Q2), False),
+        (cirq.CCNOT(Q, Q2, Q3), False),
+        (cirq.CCZ(Q, Q2, Q3), False),
+        (cirq.ParallelGate(cirq.X, num_copies=3)(Q, Q2, Q3), False),
+        (cirq.ParallelGate(cirq.Y, num_copies=3)(Q, Q2, Q3), False),
+        (cirq.ParallelGate(cirq.Z, num_copies=3)(Q, Q2, Q3), False),
+        (cirq.X(Q).controlled_by(Q2, Q3), False),
+        (cirq.Z(Q).controlled_by(Q2, Q3), False),
+        (cirq.ZPowGate(exponent=0.5)(Q).controlled_by(Q2, Q3), False),
+    ],
+)
+def test_gateset(op: cirq.Operation, expected: bool):
+    gs = aqt_target_gateset.AQTTargetGateset()
+    assert gs.validate(op) == expected
+    assert gs.validate(cirq.Circuit(op)) == expected
+
+
+def test_decompose_single_qubit_operation():
+    gs = aqt_target_gateset.AQTTargetGateset()
+    tgoph = gs.decompose_to_target_gateset(cirq.H(Q), 0)
+    assert len(tgoph) == 2
+    assert isinstance(tgoph[0].gate, cirq.Rx)
+    assert isinstance(tgoph[1].gate, cirq.Ry)
+    tcoph = cirq.CircuitOperation(cirq.FrozenCircuit(cirq.H(Q))).with_tags('tagged')
+    tgtcoph = gs.decompose_to_target_gateset(tcoph, 0)
+    assert len(tgtcoph) == 2
+    assert isinstance(tgtcoph[0].gate, cirq.Rx)
+    assert isinstance(tgtcoph[1].gate, cirq.Ry)
+    tgopz = gs.decompose_to_target_gateset(cirq.Z(Q), 0)
+    assert len(tgopz) == 1
+    assert isinstance(tgopz[0].gate, cirq.ZPowGate)
+    theta = sympy.Symbol('theta')
+    assert gs.decompose_to_target_gateset(cirq.H(Q) ** theta, 0) is NotImplemented
+    return
+
+
+def test_decompose_two_qubit_operation():
+    gs = aqt_target_gateset.AQTTargetGateset()
+    tgopsqrtxx = gs.decompose_to_target_gateset(cirq.XX(Q, Q2) ** 0.5, 0)
+    assert len(tgopsqrtxx) == 1
+    assert isinstance(tgopsqrtxx[0].gate, cirq.XXPowGate)
+    theta = sympy.Symbol('theta')
+    assert gs.decompose_to_target_gateset(cirq.XX(Q, Q2) ** theta, 0) is NotImplemented
+    return
+
+
+def test_postprocess_transformers():
+    gs = aqt_target_gateset.AQTTargetGateset()
+    assert len(gs.postprocess_transformers) == 2