Repository: quantumlib/Cirq

Issue:
`cirq.Simulator()` drops global phase after measurement?
**Description of the issue**
When using measurement outcomes with classical feedback, we often write circuits which correct a local / global phase conditioned on the measurement outcome. However, it seems to be that `cirq.Simulator()` drops global phase after measurement, which leads to inconsistencies. 

**How to reproduce the issue**
For example, consider the following circuit which performs the inverse of an `And` / `Toffoli` gate using measurement based uncomputation. 

```python
>>> c1, c2, t = cirq.LineQubit.range(3)
>>> circuit = cirq.Circuit(
    cirq.H(t), cirq.measure(t, key="t"), 
    cirq.CZ(c1, c2).with_classical_controls("t"),
    cirq.reset(t),
)
>>> print(circuit)
              ┌──┐
0: ────────────@─────
               ║
1: ────────────@─────
               ║
2: ───H───M────╫R────
          ║    ║
t: ═══════@════^═════
              └──┘
```
The circuit above should take an initial state `|111>`  to final state `|110>`, because:

- Step-1: H(t) leads to equal superposition of `|110> - |111>`.
- Step-2: Measuring the target will collapse the superposition to `-|111>` with prob 0.5
- Step-3: If the measurement result was `1`, we add a phase correction by applying `CZ(c1, c2)` that fixes the `-|111>` state to `|111>` state.
- Step-4: Reset(t) changes `|111>` to `|110>`.

However, simulating the simulator drops the -1 global phase in step-2 and therefore the classically conditioned phase correction leads to final output being `-|111>` instead of `|111>`, which is wrong. 

The step-by-step simulation output is given as follows:
```python
>>> simulator = cirq.Simulator()
>>> for step in simulator.simulate_moment_steps(circuit, initial_state=(1, 1, 1), qubit_order=(c1, c2, t)):
           print(step.dirac_notation())
0.71|110⟩ - 0.71|111⟩ # Step-1: H(t) results in an equal superposition of |110> - |111>
|111⟩ # Measurement collapses the state to `-|111>`, but Global phase of -1 is ignored.    
-1|110⟩ # Final output is -|110> instead of |110>, which is wrong. 
```

**Cirq version**
1.0.0


cc @95-martin-orion @daxfohl @mpharrigan 



Discussion:
Does running with `split_untangled_qubits=False` fix it? My hunch is global phase gets lost in the factoring and merging of states.
> Does running with `split_untangled_qubits=False` fix it? My hunch is global phase gets lost in the factoring and merging of states.

`Simulator(split_untangled_states=False)` resolves it for me.
I think it's here: https://github.com/quantumlib/Cirq/blob/79d725068594685a564549e24106ee1c63a26529/cirq-core/cirq/sim/simulation_product_state.py#L125

If we are measuring all the qubits in the set, then it'll factor them all out and leave a global phase that it throws away.

Can you try adding a `and op_args.qubits` to that condition, to make sure we aren't factoring out the last qubit? I think that might fix it.
Sorry just realized I meant "and len(op_args.qubits) > 1"
And I take it back. Looks like the factor method itself does not necessarily preserve global phase: https://github.com/quantumlib/cirq/blob/e362cb6fe2120a3a74a8feefc2c6e5957a219c13/cirq-core/cirq/linalg/transformations.py#L597
(FWIW I'm not going to have time to work on this. Probably there are better ways to factor state vectors and density matrices. All the stuff I added to linalg in https://github.com/quantumlib/Cirq/pull/4100#issuecomment-865282552 should probably be reviewed by someone with a better linalg background).
Unless there is some extra context I'm missing that's not described here, I don't think there is a bug here.

Measurement-based uncomputation uses measurement result to correct relative phases, not the global phase. The latter is unobservable. Therefore, -1|110⟩ is in fact a correct result for the circuit above. The phase becomes important when it is relative, so to confirm this we can apply the circuit to a state such as |000⟩+|111⟩ and verifying that we obtain |000⟩+|110⟩. You can prepare the GHZ state using a Hadamard and two CNOTs prepended before the circuit

```console
In [22]: pre = cirq.Circuit(cirq.H(c1), cirq.CNOT(c1, c2), cirq.CNOT(c1, t))

In [23]: simulator.simulate(pre + circuit)
Out[23]: 
measurements: t=1

qubits: (cirq.LineQubit(0), cirq.LineQubit(1))
output vector: 0.707|00⟩ + 0.707|11⟩

qubits: (cirq.LineQubit(2),)
output vector: |0⟩

phase:
output vector: |⟩

In [24]: pre + circuit
Out[24]: 
                          ┌──┐
0: ───H───@───@────────────@─────
          │   │            ║
1: ───────X───┼────────────@─────
              │            ║
2: ───────────X───H───M────╫R────
                      ║    ║
t: ═══════════════════@════^═════
                          └──┘

```

and we see that the relative phase is correct.

Generally, phase corrections should be tested on superpositions, not on computational basis states, because in the latter case the corrections modify the unobservable global phase while in the latter they fix the relative phases.
Isn't it true that most of the time the simulator will track global phase? or at least not randomly randomize it. I understand that it's not _wrong_ in terms of observables; but for a software tool it would be nice to be able to test with basis states and to build confidence that it will also work with superposition, see https://algassert.com/post/1903 for example
In a sense, global phase doesn't really exist, so one could argue that our simulator doesn't need to track it at all. Density matrix simulators don't. That our present wavefunction simulator does is an implementation detail which may change in future. Moreover, not making it a requirement enables optimizations such as the one that has apparently already been done for example [here](https://github.com/quantumlib/cirq/blob/e362cb6fe2120a3a74a8feefc2c6e5957a219c13/cirq-core/cirq/linalg/transformations.py#L597) (link from Dax's [comment](https://github.com/quantumlib/Cirq/issues/5834#issuecomment-1223367204) above).

It's not clear to me that testing with computational basis is fundamentally easier than testing with superpositions (my superposition might be someone else's computational basis). Perhaps we just need to add more constants to serve as a convenient supply of initial states for testing?

Resolving patch:
diff --git a/cirq-core/cirq/linalg/transformations.py b/cirq-core/cirq/linalg/transformations.py
--- a/cirq-core/cirq/linalg/transformations.py
+++ b/cirq-core/cirq/linalg/transformations.py
@@ -589,12 +589,12 @@ def factor_state_vector(
     slices1 = (slice(None),) * n_axes + pivot[n_axes:]
     slices2 = pivot[:n_axes] + (slice(None),) * (t1.ndim - n_axes)
     extracted = t1[slices1]
-    extracted = extracted / np.sum(abs(extracted) ** 2) ** 0.5
+    extracted = extracted / np.linalg.norm(extracted)
     remainder = t1[slices2]
-    remainder = remainder / np.sum(abs(remainder) ** 2) ** 0.5
+    remainder = remainder / (np.linalg.norm(remainder) * t1[pivot] / abs(t1[pivot]))
     if validate:
         t2 = state_vector_kronecker_product(extracted, remainder)
-        if not predicates.allclose_up_to_global_phase(t2, t1, atol=atol):
+        if not np.allclose(t2, t1, atol=atol):
             if not np.isclose(np.linalg.norm(t1), 1):
                 raise ValueError('Input state must be normalized.')
             raise EntangledStateError('The tensor cannot be factored by the requested axes')
diff --git a/cirq-core/cirq/sim/simulation_product_state.py b/cirq-core/cirq/sim/simulation_product_state.py
--- a/cirq-core/cirq/sim/simulation_product_state.py
+++ b/cirq-core/cirq/sim/simulation_product_state.py
@@ -122,7 +122,7 @@ def _act_on_fallback_(
             gate_opt, (ops.ResetChannel, ops.MeasurementGate)
         ):
             for q in qubits:
-                if op_args.allows_factoring:
+                if op_args.allows_factoring and len(op_args.qubits) > 1:
                     q_args, op_args = op_args.factor((q,), validate=False)
                     self._sim_states[q] = q_args
 


Test patch:
diff --git a/cirq-core/cirq/linalg/transformations_test.py b/cirq-core/cirq/linalg/transformations_test.py
--- a/cirq-core/cirq/linalg/transformations_test.py
+++ b/cirq-core/cirq/linalg/transformations_test.py
@@ -613,3 +613,22 @@ def test_default_tolerance():
     # Here, we do NOT specify the default tolerance. It is merely to check that the default value
     # is reasonable.
     cirq.sub_state_vector(final_state_vector, [0])
+
+
+@pytest.mark.parametrize('state_1', [0, 1])
+@pytest.mark.parametrize('state_2', [0, 1])
+def test_factor_state_vector(state_1: int, state_2: int):
+    # Kron two state vectors and apply a phase. Factoring should produce the expected results.
+    n = 12
+    for i in range(n):
+        phase = np.exp(2 * np.pi * 1j * i / n)
+        a = cirq.to_valid_state_vector(state_1, 1)
+        b = cirq.to_valid_state_vector(state_2, 1)
+        c = cirq.linalg.transformations.state_vector_kronecker_product(a, b) * phase
+        a1, b1 = cirq.linalg.transformations.factor_state_vector(c, [0], validate=True)
+        c1 = cirq.linalg.transformations.state_vector_kronecker_product(a1, b1)
+        assert np.allclose(c, c1)
+
+        # All phase goes into a1, and b1 is just the dephased state vector
+        assert np.allclose(a1, a * phase)
+        assert np.allclose(b1, b)
diff --git a/cirq-core/cirq/sim/sparse_simulator_test.py b/cirq-core/cirq/sim/sparse_simulator_test.py
--- a/cirq-core/cirq/sim/sparse_simulator_test.py
+++ b/cirq-core/cirq/sim/sparse_simulator_test.py
@@ -1434,3 +1434,19 @@ def test_unseparated_states_str():
 qubits: (cirq.LineQubit(0), cirq.LineQubit(1))
 output vector: 0.707j|00⟩ + 0.707j|10⟩"""
     )
+
+
+@pytest.mark.parametrize('split', [True, False])
+def test_measurement_preserves_phase(split: bool):
+    c1, c2, t = cirq.LineQubit.range(3)
+    circuit = cirq.Circuit(
+        cirq.H(t),
+        cirq.measure(t, key='t'),
+        cirq.CZ(c1, c2).with_classical_controls('t'),
+        cirq.reset(t),
+    )
+    simulator = cirq.Simulator(split_untangled_states=split)
+    # Run enough times that both options of |110> - |111> are likely measured.
+    for _ in range(20):
+        result = simulator.simulate(circuit, initial_state=(1, 1, 1), qubit_order=(c1, c2, t))
+        assert result.dirac_notation() == '|110⟩'