Repository: quantumlib/Cirq

Issue:
Gate inconsistencies between versions when using controlled interface
It seems there is a (unintentional?) change to what the controlled interface does before and after the `8179aa2d678bcce480c0ce14bd2852068657abf6` commit in certain cases, possibly related to how the global phase is handled.

Running the following circuit (and inspecting the states at each moment):
```
q0 = cirq.GridQubit(0, 0)
q1 = cirq.GridQubit(0, 1)
q2 = cirq.GridQubit(0, 2)
circuit = cirq.Circuit()
circuit.append(cirq.X(q0))
circuit.append(cirq.rx(1).controlled()(q0,q1))
circuit.append(cirq.rx(1).controlled()(q1,q2))
simulator = cirq.Simulator()
for i, step in enumerate(simulator.simulate_moment_steps(circuit))
    temp = np.around(step.state_vector(), 3)
    print('state at step %d: %s %s' % (i, temp[temp != 0], temp.nonzero()))
```

gives the following output before the `8179aa2d678bcce480c0ce14bd2852068657abf6` commit
```
state at step 0: [1.+0.j] (array([4]),)
state at step 1: [0.878+0.j    0.   -0.479j] (array([4, 6]),)
state at step 2: [ 0.878+0.j     0.   -0.421j -0.23 +0.j   ] (array([4, 6, 7]),)
```
and this output after:
```
state at step 0: [1.+0.j] (array([4]),)
state at step 1: [0.878+0.j    0.   -0.479j] (array([4, 6]),)
state at step 2: [ 0.77-0.421j  0.  -0.421j -0.23+0.j   ] (array([4, 6, 7]),)
```
The output I would expect to see is the former one, as it matches other implementations of the same circuit.


Discussion:
@dstrain115 could you give an opinion about this when you get a chance? This is an issue for one of our EAP projects and it seems you merged the PR that @stasja claims introduced the problem.

But if this is really an issue with a global phase then maybe it isn't really an error, per se. I know that, at least previously, we did not make any promises that the Cirq simulator would do predictable things with the global phase.
PR in question: https://github.com/quantumlib/Cirq/pull/2597
@dstrain115 is out for a bit.  I can take a look.

Thanks for the detailed report!
What is happening here is that the current code constructs a CXPowGate, but uses the global phase of the gate.  Since Rx gates have global phase, that phase ends up in the "identity" part of the gate.  That is not at all what users expect, they expect that the gate is a CRx gate.  I think the solution here is if the global shift is not 0, use the old way to construct a controlled not.  

Alternatively we could add CR* Gates. This is one of the "features" on the roadmap that needs to be decided, so I think for now the "don't do it when the shift doesn't work" is the right path

@smitsanghavi or @Strilanc any comments since you were involved in the bug leading up to this change.


Agreed, ideally we would construct a CR* gate as Dave said. But until then, if this CXPowGate construction is proving confusing, I don't mind changing it to only happen when there is no global shift.
The bug is that the code is not checking the global phase (self._global_phase_shift) before performing the substitution.

Resolving patch:
diff --git a/cirq/ops/common_gates.py b/cirq/ops/common_gates.py
--- a/cirq/ops/common_gates.py
+++ b/cirq/ops/common_gates.py
@@ -134,13 +134,16 @@ def controlled(self,
         This behavior only occurs when the last control qubit is a default-type
         control qubit. A default-type control qubit is one with shape of 2 (not
         a generic qudit) and where the control is satisfied by the qubit being
-        ON, as opposed to OFF.
+        ON, as opposed to OFF. Note also that this only transforms into a
+        CXPowGate (or controlled version of that gate) if the global shift on
+        the XPowGate is 0, otherwise it produces a normal ControlledGate.
 
         (Note that a CXPowGate is, by definition, a controlled-XPowGate.)
         """
         result = super().controlled(num_controls, control_values,
                                     control_qid_shape)
-        if (isinstance(result, controlled_gate.ControlledGate) and
+        if (self._global_shift == 0 and
+                isinstance(result, controlled_gate.ControlledGate) and
                 result.control_values[-1] == (1,) and
                 result.control_qid_shape[-1] == 2):
             return cirq.CXPowGate(exponent=self._exponent,
@@ -452,13 +455,16 @@ def controlled(self,
         This behavior only occurs when the last control qubit is a default-type
         control qubit. A default-type control qubit is one with shape of 2 (not
         a generic qudit) and where the control is satisfied by the qubit being
-        ON, as opposed to OFF.
+        ON, as opposed to OFF. Note also that this only transforms into a
+        CZPowGate (or controlled version of that gate) if the global shift of
+        the ZPowGate is 0, otherwise it produces a normal ControlledGate.
 
         (Note that a CZPowGate is, by definition, a controlled-ZPowGate.)
         """
         result = super().controlled(num_controls, control_values,
                                     control_qid_shape)
-        if (isinstance(result, controlled_gate.ControlledGate) and
+        if (self._global_shift == 0 and
+                isinstance(result, controlled_gate.ControlledGate) and
                 result.control_values[-1] == (1,) and
                 result.control_qid_shape[-1] == 2):
             return cirq.CZPowGate(exponent=self._exponent,
@@ -803,13 +809,16 @@ def controlled(self,
         This behavior only occurs when the last control qubit is a default-type
         control qubit. A default-type control qubit is one with shape of 2 (not
         a generic qudit) and where the control is satisfied by the qubit being
-        ON, as opposed to OFF.
+        ON, as opposed to OFF. Note also that this only transforms into a
+        CCZPowGate (or controlled version of that gate) if the global shift of
+        the CZPowGate is 0, otherwise it produces a normal ControlledGate.
 
         (Note that a CCZPowGate is, by definition, a controlled-CZPowGate.)
         """
         result = super().controlled(num_controls, control_values,
                                     control_qid_shape)
-        if (isinstance(result, controlled_gate.ControlledGate) and
+        if (self._global_shift == 0 and
+                isinstance(result, controlled_gate.ControlledGate) and
                 result.control_values[-1] == (1,) and
                 result.control_qid_shape[-1] == 2):
             return cirq.CCZPowGate(exponent=self._exponent,
@@ -969,13 +978,16 @@ def controlled(self,
         This behavior only occurs when the last control qubit is a default-type
         control qubit. A default-type control qubit is one with shape of 2 (not
         a generic qudit) and where the control is satisfied by the qubit being
-        ON, as opposed to OFF.
+        ON, as opposed to OFF. Note also that this only transforms into a
+        CCXPowGate (or controlled version of that gate) if the global shift of
+        the CXPowGate is 0, otherwise it produces a normal ControlledGate.
 
         (Note that a CCXPowGate is, by definition, a controlled-CXPowGate.)
         """
         result = super().controlled(num_controls, control_values,
                                     control_qid_shape)
-        if (isinstance(result, controlled_gate.ControlledGate) and
+        if (self._global_shift == 0 and
+                isinstance(result, controlled_gate.ControlledGate) and
                 result.control_values[-1] == (1,) and
                 result.control_qid_shape[-1] == 2):
             return cirq.CCXPowGate(exponent=self._exponent,


Test patch:
diff --git a/cirq/ops/common_gates_test.py b/cirq/ops/common_gates_test.py
--- a/cirq/ops/common_gates_test.py
+++ b/cirq/ops/common_gates_test.py
@@ -183,6 +183,35 @@ def test_specialized_control(input_gate, specialized_output):
                 input_gate, num_controls=3, control_qid_shape=(3, 2, 4))
 
 
+@pytest.mark.parametrize(
+    'gate, specialized_type',
+    [(cirq.ZPowGate(global_shift=-0.5, exponent=0.5), cirq.CZPowGate),
+     (cirq.CZPowGate(global_shift=-0.5, exponent=0.5), cirq.CCZPowGate),
+     (cirq.XPowGate(global_shift=-0.5, exponent=0.5), cirq.CXPowGate),
+     (cirq.CXPowGate(global_shift=-0.5, exponent=0.5), cirq.CCXPowGate)])
+def test_no_specialized_control_for_global_shift_non_zero(
+        gate, specialized_type):
+    assert not isinstance(gate.controlled(), specialized_type)
+
+
+@pytest.mark.parametrize(
+    'gate, matrix',
+    [(cirq.ZPowGate(global_shift=-0.5, exponent=1), np.diag([1, 1, -1j, 1j])),
+     (cirq.CZPowGate(global_shift=-0.5,
+                     exponent=1), np.diag([1, 1, 1, 1, -1j, -1j, -1j, 1j])),
+     (cirq.XPowGate(global_shift=-0.5, exponent=1),
+      np.block([[np.eye(2), np.zeros(
+          (2, 2))], [np.zeros(
+              (2, 2)), np.array([[0, -1j], [-1j, 0]])]])),
+     (cirq.CXPowGate(global_shift=-0.5, exponent=1),
+      np.block([[np.diag([1, 1, 1, 1, -1j, -1j]),
+                 np.zeros((6, 2))],
+                [np.zeros(
+                    (2, 6)), np.array([[0, -1j], [-1j, 0]])]]))])
+def test_global_phase_controlled_gate(gate, matrix):
+    np.testing.assert_equal(cirq.unitary(gate.controlled()), matrix)
+
+
 def test_rot_gates_eq():
     eq = cirq.testing.EqualsTester()
     gates = [