Repository: quantumlib/Cirq

Issue:
n-qubit state tomography broken
n-qubit state tomography returns incorrect density matrices on some inputs, notably on asymmetric circuits with qubits re-ordered, e.g.: when you pass `(LineQubit(0), LineQubit(1))` and simple circuit with identity and the X gate you get:

```
0: ───I───

1: ───X───

Actual rho:
[[ 0.-0.j   -0.+0.j    0.-0.j    0.-0.01j]
 [-0.-0.j    1.+0.j   -0.-0.01j -0.+0.01j]
 [ 0.+0.j   -0.+0.01j -0.+0.j   -0.-0.j  ]
 [ 0.+0.01j -0.-0.01j -0.+0.j    0.-0.j  ]]

Expected rho:
[[0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 1.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j]]
```

which is close enough, but when you pass the qubits in reverse order `(LineQubit(1), LineQubit(0))` you get

```
0: ───I───

1: ───X───

Actual rho:
[[ 0.  +0.j   -0.08-0.08j  0.08+0.09j -0.  -0.j  ]
 [-0.08+0.08j  0.33+0.j   -0.  -0.01j  0.08+0.08j]
 [ 0.08-0.09j -0.  +0.01j  0.67-0.j   -0.09-0.08j]
 [-0.  +0.j    0.08-0.08j -0.09+0.08j -0.  +0.j  ]]

Expected rho:
[[0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 1.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j 0.+0.j]]
```

Incidentally, the other implementation of state tomography (see #2737) does not exhibit the same issue.


Discussion:

Resolving patch:
diff --git a/cirq/experiments/n_qubit_tomography.py b/cirq/experiments/n_qubit_tomography.py
--- a/cirq/experiments/n_qubit_tomography.py
+++ b/cirq/experiments/n_qubit_tomography.py
@@ -80,11 +80,17 @@ def __init__(self,
 
         self.rot_circuit = circuits.Circuit(operations)
         self.rot_sweep = study.Product(*sweeps)
-        self.mat = self._make_state_tomography_matrix()
+        self.mat = self._make_state_tomography_matrix(qubits)
 
-    def _make_state_tomography_matrix(self) -> np.ndarray:
+    def _make_state_tomography_matrix(
+            self,
+            qubits: Sequence['cirq.Qid'],
+    ) -> np.ndarray:
         """Gets the matrix used for solving the linear system of the tomography.
 
+        Args:
+            qubits: Qubits to do the tomography on.
+
         Returns:
             A matrix of dimension ((number of rotations)**n * 2**n, 4**n)
             where each column corresponds to the coefficient of a term in the
@@ -96,8 +102,8 @@ def _make_state_tomography_matrix(self) -> np.ndarray:
 
         # Unitary matrices of each rotation circuit.
         unitaries = np.array([
-            protocols.unitary(
-                protocols.resolve_parameters(self.rot_circuit, rots))
+            protocols.resolve_parameters(self.rot_circuit,
+                                         rots).unitary(qubit_order=qubits)
             for rots in self.rot_sweep
         ])
         mat = np.einsum('jkm,jkn->jkmn', unitaries, unitaries.conj())


Test patch:
diff --git a/cirq/experiments/n_qubit_tomography_test.py b/cirq/experiments/n_qubit_tomography_test.py
--- a/cirq/experiments/n_qubit_tomography_test.py
+++ b/cirq/experiments/n_qubit_tomography_test.py
@@ -11,11 +11,15 @@
 # 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.
+from typing import Sequence
 
 import numpy as np
+import pytest
 
 import cirq
 
+Q0, Q1, Q2, Q3 = cirq.LineQubit.range(4)
+
 
 def test_state_tomography_diagonal():
     n = 2
@@ -63,3 +67,64 @@ def test_make_experiment_no_rots():
          cirq.LineQubit(1),
          cirq.LineQubit(2)])
     assert len(exp.rot_sweep) > 0
+
+
+def compute_density_matrix(circuit: cirq.Circuit,
+                           qubits: Sequence[cirq.Qid]) -> np.ndarray:
+    """Computes density matrix prepared by circuit based on its unitary."""
+    u = circuit.unitary(qubit_order=qubits)
+    phi = u[:, 0]
+    rho = np.outer(phi, np.conjugate(phi))
+    return rho
+
+
+@pytest.mark.parametrize('circuit, qubits', (
+    (cirq.Circuit(cirq.X(Q0)**0.25), (Q0,)),
+    (cirq.Circuit(cirq.CNOT(Q0, Q1)**0.25), (Q0, Q1)),
+    (cirq.Circuit(cirq.CNOT(Q0, Q1)**0.25), (Q1, Q0)),
+    (cirq.Circuit(cirq.TOFFOLI(Q0, Q1, Q2)), (Q1, Q0, Q2)),
+    (cirq.Circuit(
+        cirq.H(Q0),
+        cirq.H(Q1),
+        cirq.CNOT(Q0, Q2),
+        cirq.CNOT(Q1, Q3),
+        cirq.X(Q0),
+        cirq.X(Q1),
+        cirq.CNOT(Q1, Q0),
+    ), (Q1, Q0, Q2, Q3)),
+))
+def test_density_matrix_from_state_tomography_is_correct(circuit, qubits):
+    sim = cirq.Simulator()
+    tomography_result = cirq.experiments.state_tomography(sim,
+                                                          qubits,
+                                                          circuit,
+                                                          repetitions=5000)
+    actual_rho = tomography_result.data
+    expected_rho = compute_density_matrix(circuit, qubits)
+    error_rho = actual_rho - expected_rho
+    assert np.linalg.norm(error_rho) < 0.05
+    assert np.max(np.abs(error_rho)) < 0.05
+
+
+@pytest.mark.parametrize('circuit', (
+    cirq.Circuit(cirq.CNOT(Q0, Q1)**0.3),
+    cirq.Circuit(cirq.H(Q0), cirq.CNOT(Q0, Q1)),
+    cirq.Circuit(cirq.X(Q0)**0.25, cirq.ISWAP(Q0, Q1)),
+))
+def test_agrees_with_two_qubit_state_tomography(circuit):
+    qubits = (Q0, Q1)
+    sim = cirq.Simulator()
+    tomography_result = cirq.experiments.state_tomography(sim,
+                                                          qubits,
+                                                          circuit,
+                                                          repetitions=5000)
+    actual_rho = tomography_result.data
+
+    two_qubit_tomography_result = cirq.experiments.two_qubit_state_tomography(
+        sim, qubits[0], qubits[1], circuit, repetitions=5000)
+    expected_rho = two_qubit_tomography_result.data
+
+    error_rho = actual_rho - expected_rho
+
+    assert np.linalg.norm(error_rho) < 0.05
+    assert np.max(np.abs(error_rho)) < 0.05