Repository: quantumlib/Cirq

Issue:
Add `_phase_by_` magic method to `ControlledGate`
Comes after https://github.com/quantumlib/Cirq/issues/947

The logic is as follows: if the qubit index is 0 (the control), the operation is returned unchanged. If it is larger then we delegate to phasing the sub gate with `cirq.phase_by` and a default result of NotImplemented. If it's NotImplemented, we return NotImplemented. Otherwise we return a controlled gate with the phased sub gate.


Discussion:

Resolving patch:
diff --git a/cirq/ops/controlled_gate.py b/cirq/ops/controlled_gate.py
--- a/cirq/ops/controlled_gate.py
+++ b/cirq/ops/controlled_gate.py
@@ -29,12 +29,6 @@ def __init__(self, sub_gate: raw_types.Gate) -> None:
 
         Args:
             sub_gate: The gate to add a control qubit to.
-            default_extensions: The extensions method that should be used when
-                determining if the controlled gate supports certain gate
-                features. For example, if this extensions instance is able to
-                cast sub_gate to a ExtrapolatableEffect then the controlled gate
-                can also be cast to a ExtrapolatableEffect. When this value is
-                None, an empty extensions instance is used instead.
         """
         self.sub_gate = sub_gate
 
@@ -102,6 +96,15 @@ def __pow__(self, exponent: Any) -> 'ControlledGate':
             return NotImplemented
         return ControlledGate(new_sub_gate)
 
+    def _phase_by_(self, phase_turns: float, qubit_index: int):
+        if qubit_index == 0:
+            return self
+        phased_gate = protocols.phase_by(
+            self.sub_gate, phase_turns, qubit_index-1, None)
+        if phased_gate is None:
+            return NotImplemented
+        return ControlledGate(phased_gate)
+
     def _is_parameterized_(self):
         return protocols.is_parameterized(self.sub_gate)
 


Test patch:
diff --git a/cirq/ops/controlled_gate_test.py b/cirq/ops/controlled_gate_test.py
--- a/cirq/ops/controlled_gate_test.py
+++ b/cirq/ops/controlled_gate_test.py
@@ -176,6 +176,27 @@ def test_reversible():
     assert (cirq.inverse(cirq.ControlledGate(cirq.S)) ==
             cirq.ControlledGate(cirq.S**-1))
 
+class UnphasableGate(cirq.SingleQubitGate):
+    pass
+
+def test_phase_by():
+    assert (cirq.phase_by(
+                cirq.ControlledGate(UnphasableGate), 0.25, 1, default=None) ==
+            None)
+    sub_gate = cirq.google.ExpWGate(axis_half_turns = 0.5)
+    phased_sub_gate = cirq.phase_by(sub_gate, 0.25, 0)
+    assert phased_sub_gate != sub_gate
+    cg = cirq.ControlledGate(sub_gate)
+    assert cirq.phase_by(cg, 0.25, 0) == cg
+    assert cirq.phase_by(cg, 0.25, 1) != cg
+    assert cirq.phase_by(cg, 0.25, 1) == cirq.ControlledGate(phased_sub_gate)
+    # Test that the qubit_index arg gets decremented at each subgate step.
+    ccg = cirq.ControlledGate(cg)
+    assert cirq.phase_by(ccg, 0.25, 0) == ccg
+    assert cirq.phase_by(ccg, 0.25, 1) == ccg
+    assert cirq.phase_by(ccg, 0.25, 2) != ccg
+    assert (cirq.phase_by(ccg, 0.25, 2) ==
+            cirq.ControlledGate(cirq.ControlledGate(phased_sub_gate)))
 
 def test_parameterizable():
     a = cirq.Symbol('a')