Repository: quantumlib/Cirq

Issue:
to_valid_density_matrix is too picky
The density matrix result of `sim.simulate()` regularly fails the assertions in `to_valid_density_matrix`. I had to weaken the tolerances for my code (which worked just fine under the previous implementation that just didn't check)

cc #1901 


Discussion:

Resolving patch:
diff --git a/cirq/ops/pauli_string.py b/cirq/ops/pauli_string.py
--- a/cirq/ops/pauli_string.py
+++ b/cirq/ops/pauli_string.py
@@ -252,9 +252,10 @@ def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs'):
         return protocols.apply_unitaries([self[q].on(q) for q in self.qubits],
                                          self.qubits, args)
 
-    def expectation_from_wavefunction(self, state: np.ndarray,
-                                      qubit_map: Mapping[raw_types.Qid, int]
-                                     ) -> float:
+    def expectation_from_wavefunction(self,
+                                      state: np.ndarray,
+                                      qubit_map: Mapping[raw_types.Qid, int],
+                                      atol: float = 1e-7) -> float:
         r"""Evaluate the expectation of this PauliString given a wavefunction.
 
         Compute the expectation value of this PauliString with respect to a
@@ -307,7 +308,8 @@ def expectation_from_wavefunction(self, state: np.ndarray,
         from cirq.sim.wave_function import validate_normalized_state
         validate_normalized_state(state=state,
                                   qid_shape=(2,) * num_qubits,
-                                  dtype=state.dtype)
+                                  dtype=state.dtype,
+                                  atol=atol)
         return self._expectation_from_wavefunction_no_validation(
             state, qubit_map)
 
@@ -342,9 +344,10 @@ def _expectation_from_wavefunction_no_validation(
         return self.coefficient * (np.tensordot(
             state.conj(), ket, axes=len(ket.shape)).item())
 
-    def expectation_from_density_matrix(self, state: np.ndarray,
-                                        qubit_map: Mapping[raw_types.Qid, int]
-                                       ) -> float:
+    def expectation_from_density_matrix(self,
+                                        state: np.ndarray,
+                                        qubit_map: Mapping[raw_types.Qid, int],
+                                        atol: float = 1e-7) -> float:
         r"""Evaluate the expectation of this PauliString given a density matrix.
 
         Compute the expectation value of this PauliString with respect to an
@@ -399,7 +402,8 @@ def expectation_from_density_matrix(self, state: np.ndarray,
         # Do not enforce reshaping if the state all axes are dimension 2.
         _ = to_valid_density_matrix(density_matrix_rep=state.reshape(dim, dim),
                                     num_qubits=num_qubits,
-                                    dtype=state.dtype)
+                                    dtype=state.dtype,
+                                    atol=atol)
         return self._expectation_from_density_matrix_no_validation(
             state, qubit_map)
 
diff --git a/cirq/sim/density_matrix_utils.py b/cirq/sim/density_matrix_utils.py
--- a/cirq/sim/density_matrix_utils.py
+++ b/cirq/sim/density_matrix_utils.py
@@ -27,7 +27,8 @@ def to_valid_density_matrix(
         num_qubits: Optional[int] = None,
         *,  # Force keyword arguments
         qid_shape: Optional[Tuple[int, ...]] = None,
-        dtype: Type[np.number] = np.complex64) -> np.ndarray:
+        dtype: Type[np.number] = np.complex64,
+        atol: float = 1e-7) -> np.ndarray:
     """Verifies the density_matrix_rep is valid and converts it to ndarray form.
 
     This method is used to support passing a matrix, a vector (wave function),
@@ -45,6 +46,7 @@ def to_valid_density_matrix(
         dtype: The numpy dtype of the density matrix, will be used when creating
             the state for a computational basis state (int), or validated
             against if density_matrix_rep is a numpy array.
+        atol: Numerical tolerance for verifying density matrix properties.
 
     Returns:
         A numpy matrix corresponding to the density matrix on the given number
@@ -61,9 +63,10 @@ def to_valid_density_matrix(
                 'Density matrix was not square and of size 2 ** num_qubit, '
                 'instead was {}'.format(density_matrix_rep.shape))
         if not np.allclose(density_matrix_rep,
-                           np.transpose(np.conj(density_matrix_rep))):
+                           np.transpose(np.conj(density_matrix_rep)),
+                           atol=atol):
             raise ValueError('The density matrix is not hermitian.')
-        if not np.isclose(np.trace(density_matrix_rep), 1.0):
+        if not np.isclose(np.trace(density_matrix_rep), 1.0, atol=atol):
             raise ValueError(
                 'Density matrix did not have trace 1 but instead {}'.format(
                     np.trace(density_matrix_rep)))
@@ -71,7 +74,7 @@ def to_valid_density_matrix(
             raise ValueError(
                 'Density matrix had dtype {} but expected {}'.format(
                     density_matrix_rep.dtype, dtype))
-        if not np.all(np.linalg.eigvalsh(density_matrix_rep) > -1e-8):
+        if not np.all(np.linalg.eigvalsh(density_matrix_rep) > -atol):
             raise ValueError('The density matrix is not positive semidefinite.')
         return density_matrix_rep
 
diff --git a/cirq/sim/wave_function.py b/cirq/sim/wave_function.py
--- a/cirq/sim/wave_function.py
+++ b/cirq/sim/wave_function.py
@@ -314,7 +314,8 @@ def to_valid_state_vector(
         num_qubits: int,
         *,  # Force keyword arguments
         qid_shape: Optional[Tuple[int, ...]] = None,
-        dtype: Type[np.number] = np.complex64) -> np.ndarray:
+        dtype: Type[np.number] = np.complex64,
+        atol: float = 1e-7) -> np.ndarray:
     """Verifies the state_rep is valid and converts it to ndarray form.
 
     This method is used to support passing in an integer representing a
@@ -334,6 +335,8 @@ def to_valid_state_vector(
         dtype: The numpy dtype of the state, will be used when creating the
             state for a computational basis state, or validated against if
             state_rep is a numpy array.
+        atol: Numerical tolerance for verifying that the norm of the state
+            is close to 1.
 
     Returns:
         A numpy ndarray corresponding to the state on the given number of
@@ -367,7 +370,10 @@ def to_valid_state_vector(
                                    index=state_rep)
     else:
         raise TypeError('initial_state was not of type int or ndarray')
-    validate_normalized_state(state, qid_shape=qid_shape, dtype=dtype)
+    validate_normalized_state(state,
+                              qid_shape=qid_shape,
+                              dtype=dtype,
+                              atol=atol)
     return state
 
 
@@ -375,7 +381,8 @@ def validate_normalized_state(
         state: np.ndarray,
         *,  # Force keyword arguments
         qid_shape: Tuple[int, ...],
-        dtype: Type[np.number] = np.complex64) -> None:
+        dtype: Type[np.number] = np.complex64,
+        atol: float = 1e-7) -> None:
     """Validates that the given state is a valid wave function."""
     if state.size != np.prod(qid_shape, dtype=int):
         raise ValueError(
@@ -386,7 +393,7 @@ def validate_normalized_state(
             'State has invalid dtype. Expected {} but was {}'.format(
                 dtype, state.dtype))
     norm = np.sum(np.abs(state) ** 2)
-    if not np.isclose(norm, 1):
+    if not np.isclose(norm, 1, atol=atol):
         raise ValueError('State is not normalized instead had norm %s' % norm)
 
 


Test patch:
diff --git a/cirq/sim/density_matrix_utils_test.py b/cirq/sim/density_matrix_utils_test.py
--- a/cirq/sim/density_matrix_utils_test.py
+++ b/cirq/sim/density_matrix_utils_test.py
@@ -528,3 +528,14 @@ def test_measure_state_empty_density_matrix():
     bits, out_matrix = cirq.measure_density_matrix(matrix, [])
     assert [] == bits
     np.testing.assert_almost_equal(matrix, out_matrix)
+
+
+@pytest.mark.parametrize('seed', [17, 35, 48])
+def test_to_valid_density_matrix_on_simulator_output(seed):
+    circuit = cirq.testing.random_circuit(qubits=5,
+                                          n_moments=20,
+                                          op_density=0.9,
+                                          random_state=seed)
+    simulator = cirq.DensityMatrixSimulator()
+    result = simulator.simulate(circuit)
+    _ = cirq.to_valid_density_matrix(result.final_density_matrix, num_qubits=5)
diff --git a/cirq/testing/random_circuit.py b/cirq/testing/random_circuit.py
--- a/cirq/testing/random_circuit.py
+++ b/cirq/testing/random_circuit.py
@@ -44,9 +44,11 @@ def random_circuit(
     """Generates a random circuit.
 
     Args:
-        qubits: the qubits that the circuit acts on. Because the qubits on
-            which an operation acts are chosen randomly, not all given qubits
-            may be acted upon.
+        qubits: If a sequence of qubits, then these are the qubits that
+            the circuit should act on. Because the qubits on which an
+            operation acts are chosen randomly, not all given qubits
+            may be acted upon. If an int, then this number of qubits will
+            be automatically generated.
         n_moments: the number of moments in the generated circuit.
         op_density: the expected proportion of qubits that are acted on in any
             moment.