Repository: quantumlib/Cirq

Issue:
2 Qubit ControlledOperations should be supported in convert_to_sycamore_gates 
## Description of the Issue
We check `isinstance(op, GateOperation)` while verifying that the given operation can be decomposed using analytical / tabulation methods. This leads to the problem that we don't recognize two qubit controlled operations as known gates and hence don't support them. 

https://github.com/quantumlib/Cirq/blob/12530f1531e5da663a6b172b1967763e08819912/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py#L111

For example:

```python
num_qubits = 3
qubits = cirq.GridQubit.rect(1, num_qubits, 4, 4)
circuit = cirq.Circuit(cirq.QuantumFourierTransformGate(num_qubits)(*qubits))
cirq.Circuit(cirq.google.ConvertToSycamoreGates().convert(circuit), device = cirq.google.Sycamore)
```

The above code fails with the following error:
```python
TypeError: Don't know how to work with cirq.S(cirq.GridQubit(4, 4)).controlled_by(cirq.GridQubit(4, 5)). It isn't a native xmon operation, a 1 or 2 qubit gate with a known unitary, or composite.
```

The reason it fails is because `ControlledOperations` don't satisfy the `isinstance(gate, GateOperation)` constraint. 

```python
op = cirq.S(cirq.GridQubit(4, 4)).controlled_by(cirq.GridQubit(4, 5))
print(type(op), isinstance(op, cirq.GateOperation))
```

gives

```bash
<class 'cirq.ops.controlled_operation.ControlledOperation'> False
```


However,  the gate is actually just a two qubit controlled gate and should be decomposable into the native gateset via `known_two_q_operations_to_sycamore_operations`. 

```python
op = cirq.S(cirq.GridQubit(4, 4)).controlled_by(cirq.GridQubit(4, 5))
print([*known_two_q_operations_to_sycamore_operations(op.qubits[0], op.qubits[1], op, None)])
```

gives the following decomposition

```python
[cirq.PhasedXZGate(axis_phase_exponent=-1.1102230246251565e-16, x_exponent=0.7239292897100449, z_exponent=0.08333333333333348).on(cirq.GridQubit(4, 5)), cirq.PhasedXZGate(axis_phase_exponent=0.5416666666666665, x_exponent=0.0, z_exponent=0.916666666666667).on(cirq.GridQubit(4, 4)), cirq_google.SYC.on(cirq.GridQubit(4, 5), cirq.GridQubit(4, 4)), cirq.Rx(rads=0.5922772837339743).on(cirq.GridQubit(4, 4)), cirq_google.SYC.on(cirq.GridQubit(4, 5), cirq.GridQubit(4, 4)), cirq.PhasedXZGate(axis_phase_exponent=-0.08333333333333315, x_exponent=0.7239292897100451, z_exponent=-0.916666666666667).on(cirq.GridQubit(4, 5)), cirq.PhasedXZGate(axis_phase_exponent=0.22741638234956674, x_exponent=2.220446049250313e-16, z_exponent=0.25).on(cirq.GridQubit(4, 4)), cirq.Rz(rads=0.7853981633974483).on(cirq.GridQubit(4, 5)), cirq.Rz(rads=0.7853981633974483).on(cirq.GridQubit(4, 4))]
```

## Proposed Solution
https://github.com/quantumlib/Cirq/blob/12530f1531e5da663a6b172b1967763e08819912/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py#L111

should be modified to also recognize two qubit `ControlledOperations`.


**Cirq version**
0.11.0.dev

Part of #3242


Discussion:
I'm assuming the `GateOperation` check is supposed to filter out Operations that don't have gates.

We have had these discussions about `Operation` hierarchy and how the type checks are used as proxies for whether there is an underlying Gate or not. I think it's useful to have a `has_gate` protocol that actually tries to get the underlying gate and performs not-None, etc checks. This would be more pythonic than type checking IMO.
`op.gate is not None` seems about as simple as `cirq.has_gate(op)`, and I think that's basically all the `has_gate` method would do anyway. Certainly the `isinstance(op, cirq.GateOperation)` checks are too restrictive in this case.
The protocol will also be doing a `getattr` check for `gate` first. `op.gate is not None` assumes that `op` is some `Operation` type which might not be the case for all instances of these checks in Cirq.
Upd: Similarly, `_is_native_xmon_op` checks for `isinstance(op, cirq.GateOperation)` and hence does not recognize gates like `CS(0, 1)` to be native gates which leads to much worse (decomposed) target circuit lengths. xref #4172 

Resolving patch:
diff --git a/cirq-google/cirq_google/devices/xmon_device.py b/cirq-google/cirq_google/devices/xmon_device.py
--- a/cirq-google/cirq_google/devices/xmon_device.py
+++ b/cirq-google/cirq_google/devices/xmon_device.py
@@ -98,7 +98,7 @@ def validate_gate(self, gate: cirq.Gate):
             raise ValueError(f'Unsupported gate type: {gate!r}')
 
     def validate_operation(self, operation: cirq.Operation):
-        if not isinstance(operation, cirq.GateOperation):
+        if operation.gate is None:
             raise ValueError(f'Unsupported operation: {operation!r}')
 
         self.validate_gate(operation.gate)
diff --git a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py
--- a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py
+++ b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates.py
@@ -113,7 +113,7 @@ def _convert_one(self, op: cirq.Operation) -> cirq.OP_TREE:
         """
         if len(op.qubits) == 1:
             return _phased_x_z_ops(cirq.unitary(op, None), op.qubits[0])
-        elif len(op.qubits) == 2 and isinstance(op, cirq.GateOperation):
+        elif len(op.qubits) == 2:
             return known_two_q_operations_to_sycamore_operations(
                 op.qubits[0], op.qubits[1], op, self.tabulation
             )
@@ -139,7 +139,7 @@ def on_stuck_raise(bad):
     def optimization_at(
         self, circuit: cirq.Circuit, index: int, op: cirq.Operation
     ) -> Optional[cirq.PointOptimizationSummary]:
-        if not isinstance(op, cirq.GateOperation):
+        if op.gate is None:
             return None
 
         gate = op.gate
@@ -151,7 +151,7 @@ def optimization_at(
             next_index = circuit.next_moment_operating_on(op.qubits, index + 1)
             if next_index is not None:
                 ops_in_front = list({circuit.operation_at(q, next_index) for q in op.qubits})
-                if len(ops_in_front) == 1 and isinstance(ops_in_front[0], cirq.GateOperation):
+                if len(ops_in_front) == 1 and ops_in_front[0] is not None:
                     gate2 = ops_in_front[0].gate
             else:
                 next_index = 0
diff --git a/cirq-google/cirq_google/optimizers/convert_to_xmon_gates.py b/cirq-google/cirq_google/optimizers/convert_to_xmon_gates.py
--- a/cirq-google/cirq_google/optimizers/convert_to_xmon_gates.py
+++ b/cirq-google/cirq_google/optimizers/convert_to_xmon_gates.py
@@ -65,7 +65,7 @@ def _is_native_xmon_op(self, op: cirq.Operation) -> bool:
         """
         from cirq_google.devices import XmonDevice
 
-        return isinstance(op, cirq.GateOperation) and XmonDevice.is_supported_gate(op.gate)
+        return op.gate is not None and XmonDevice.is_supported_gate(op.gate)
 
     def convert(self, op: cirq.Operation) -> List[cirq.Operation]:
         def on_stuck_raise(bad):
@@ -86,6 +86,9 @@ def on_stuck_raise(bad):
     def optimization_at(
         self, circuit: cirq.Circuit, index: int, op: cirq.Operation
     ) -> Optional[cirq.PointOptimizationSummary]:
+        if op.gate is None:
+            return None
+
         converted = self.convert(op)
         if len(converted) == 1 and converted[0] is op:
             return None


Test patch:
diff --git a/cirq-google/cirq_google/devices/xmon_device_test.py b/cirq-google/cirq_google/devices/xmon_device_test.py
--- a/cirq-google/cirq_google/devices/xmon_device_test.py
+++ b/cirq-google/cirq_google/devices/xmon_device_test.py
@@ -153,20 +153,39 @@ def test_validate_operation_existing_qubits():
         d.validate_operation(cirq.CZ(cirq.GridQubit(1, 0), cirq.GridQubit(1, 1)))
 
 
-def test_validate_operation_supported_gate():
+class MyGate(cirq.Gate):
+    def num_qubits(self):
+        return 1
+
+
+q = cirq.GridQubit.rect(1, 3)
+matrix_gate = cirq.MatrixGate(cirq.testing.random_unitary(2))
+
+
+@pytest.mark.parametrize(
+    'op,is_valid',
+    [
+        (cirq.Z(cirq.GridQubit(0, 0)), True),
+        (cirq.Z(cirq.GridQubit(0, 0)).with_tags('test_tag'), True),
+        (
+            cirq.Z(cirq.GridQubit(0, 0)).with_tags('test_tag').controlled_by(cirq.GridQubit(0, 1)),
+            True,
+        ),
+        (
+            cirq.Z(cirq.GridQubit(0, 0)).controlled_by(cirq.GridQubit(0, 1)).with_tags('test_tag'),
+            True,
+        ),
+        (NotImplementedOperation(), False),
+        (MyGate()(cirq.GridQubit(0, 0)), False),
+    ],
+)
+def test_validate_operation_supported_gate(op, is_valid):
     d = square_device(3, 3)
-
-    class MyGate(cirq.Gate):
-        def num_qubits(self):
-            return 1
-
-    d.validate_operation(cirq.GateOperation(cirq.Z, [cirq.GridQubit(0, 0)]))
-
-    assert MyGate().num_qubits() == 1
-    with pytest.raises(ValueError):
-        d.validate_operation(cirq.GateOperation(MyGate(), [cirq.GridQubit(0, 0)]))
-    with pytest.raises(ValueError):
-        d.validate_operation(NotImplementedOperation())
+    if is_valid:
+        d.validate_operation(op)
+    else:
+        with pytest.raises(ValueError):
+            d.validate_operation(op)
 
 
 def test_validate_circuit_repeat_measurement_keys():
diff --git a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py
--- a/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py
+++ b/cirq-google/cirq_google/optimizers/convert_to_sycamore_gates_test.py
@@ -278,3 +278,23 @@ def test_sycamore_invalid_tabulation():
     sycamore_tabulation = {}
     with pytest.raises(ValueError):
         cgoc.ConvertToSycamoreGates(sycamore_tabulation)
+
+
+q = cirq.GridQubit.rect(1, 3)
+matrix_gate = cirq.MatrixGate(cirq.testing.random_unitary(2))
+
+
+@pytest.mark.parametrize(
+    'op, is_valid',
+    [
+        (cirq.CircuitOperation(cirq.FrozenCircuit(matrix_gate(q[0]))), False),
+        (matrix_gate(q[0]), True),
+        (matrix_gate(q[0]).with_tags('test_tags'), True),
+        (matrix_gate(q[0]).controlled_by(q[1]), True),
+        (matrix_gate(q[0]).controlled_by(q[1]).with_tags('test_tags'), True),
+        (matrix_gate(q[0]).with_tags('test_tags').controlled_by(q[1]), True),
+    ],
+)
+def test_supported_operation(op, is_valid):
+    c = cirq.Circuit(op)
+    assert (cirq_google.ConvertToSycamoreGates().optimization_at(c, 0, op) is not None) == is_valid
diff --git a/cirq-google/cirq_google/optimizers/convert_to_xmon_gates_test.py b/cirq-google/cirq_google/optimizers/convert_to_xmon_gates_test.py
--- a/cirq-google/cirq_google/optimizers/convert_to_xmon_gates_test.py
+++ b/cirq-google/cirq_google/optimizers/convert_to_xmon_gates_test.py
@@ -45,8 +45,27 @@ def test_avoids_infinite_cycle_when_matrix_available():
     cirq.protocols.decompose(c)
 
 
+q = cirq.GridQubit.rect(1, 3)
+matrix_gate = cirq.MatrixGate(cirq.testing.random_unitary(2))
+
+
 def test_bad_operation():
-    qubits = cirq.GridQubit.rect(1, 3)
-    c = cirq.Circuit(NonNativeGate().on(qubits[0]))
+    c = cirq.Circuit(NonNativeGate().on(q[0]))
     with pytest.raises(TypeError):
         cirq_google.ConvertToXmonGates().optimize_circuit(c)
+
+
+@pytest.mark.parametrize(
+    'op, is_valid',
+    [
+        (cirq.CircuitOperation(cirq.FrozenCircuit(matrix_gate(q[0]))), False),
+        (matrix_gate(q[0]), True),
+        (matrix_gate(q[0]).with_tags('test_tags'), True),
+        (matrix_gate(q[0]).controlled_by(q[1]), True),
+        (matrix_gate(q[0]).controlled_by(q[1]).with_tags('test_tags'), True),
+        (matrix_gate(q[0]).with_tags('test_tags').controlled_by(q[1]), True),
+    ],
+)
+def test_supported_operation(op, is_valid):
+    c = cirq.Circuit(op)
+    assert (cirq_google.ConvertToXmonGates().optimization_at(c, 0, op) is not None) == is_valid