Repository: quantumlib/Cirq

Issue:
Add PauliSum-level expectation from density matrix without validation
If a user is confident their density matrix is valid, it would be good if they could calculate the expectation value without the additional overhead of an eigenvalue decomposition or numerical error causing this computation to error out (especially when the density matrix represents a nominally pure state). @peterse thoughts?


Discussion:
We have many linalg methods with `check_preconditions=True` on them. You could add such a thing to the method.

I'm a bit surprised that validating the density matrix involves taking an eigendecomposition. Shouldn't checking that it is Hermitian with unit trace be sufficient?
The eigendecomp is to check positive-definiteness. I'll go ahead and add something along those lines to both expectation methods in a way that makes sense. 
Ah I see, the eigenvalues (-1, +2) or (-0.5, 0.5, 0.5) would pass the Hermitian-and-unit-trace test.

If you pick a random vector and multiply it by (D-I)^N where D is the density matrix and N is very large (use repeated squaring), the vector will diverge to infinite size iff there's any negative eigenvalues (note that there can't be any positive eigenvalues larger than 1 if there are no negative eigenvalues given the unit trace constraint). But unfortunately this is worse complexity than eigendecomposition.

https://scicomp.stackexchange.com/questions/12979/testing-if-a-matrix-is-positive-semi-definite suggests we should use the Cholesky decomposition https://en.wikipedia.org/wiki/Cholesky_decomposition https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.cholesky.html
Performance certainly checks out, especially since the cholseky timing is the _worst case_ for input validation:
```
>>> timeit.timeit('np.linalg.cholesky(b)', setup='import numpy as np; a = np.random.random((128,128)).astype(np.complex6
4); b = (a @ a.T); b = b + b.T; b / np.trace(b)', number=1000)
1.3936694000149146
>>> timeit.timeit('np.linalg.eigvalsh(b)', setup='import numpy as np; a = np.random.random((128,128)).astype(np.complex6
4); b = (a @ a.T); b = b + b.T; b / np.trace(b)', number=1000)
6.356422999990173
>>> timeit.timeit('np.linalg.eigvals(b)', setup='import numpy as np; a = np.random.random((128,128)).astype(np.complex64
); b = (a @ a.T); b = b + b.T; b / np.trace(b)', number=1000)
54.523648499976844
```

Resolving patch:
diff --git a/cirq/ops/linear_combinations.py b/cirq/ops/linear_combinations.py
--- a/cirq/ops/linear_combinations.py
+++ b/cirq/ops/linear_combinations.py
@@ -342,8 +342,11 @@ def copy(self) -> 'PauliSum':
         factory = type(self)
         return factory(self._linear_dict.copy())
 
-    def expectation_from_wavefunction(self, state: np.ndarray,
-                                      qubit_map: Mapping[raw_types.Qid, int]
+    def expectation_from_wavefunction(self,
+                                      state: np.ndarray,
+                                      qubit_map: Mapping[raw_types.Qid, int],
+                                      *,
+                                      check_preconditions: bool = True
                                      ) -> float:
         """Evaluate the expectation of this PauliSum given a wavefunction.
 
@@ -352,7 +355,9 @@ def expectation_from_wavefunction(self, state: np.ndarray,
         Args:
             state: An array representing a valid wavefunction.
             qubit_map: A map from all qubits used in this PauliSum to the
-            indices of the qubits that `state` is defined over.
+                indices of the qubits that `state` is defined over.
+            check_preconditions: Whether to check that `state` represents a
+                valid wavefunction.
 
         Returns:
             The expectation value of the input state.
@@ -376,17 +381,21 @@ def expectation_from_wavefunction(self, state: np.ndarray,
             raise ValueError("Input array does not represent a wavefunction "
                              "with shape `(2 ** n,)` or `(2, ..., 2)`.")
 
-        # HACK: avoid circular import
-        from cirq.sim.wave_function import validate_normalized_state
-        validate_normalized_state(state=state,
-                                  qid_shape=(2,) * num_qubits,
-                                  dtype=state.dtype)
+        if check_preconditions:
+            # HACK: avoid circular import
+            from cirq.sim.wave_function import validate_normalized_state
+            validate_normalized_state(state=state,
+                                      qid_shape=(2,) * num_qubits,
+                                      dtype=state.dtype)
         return sum(
             p._expectation_from_wavefunction_no_validation(state, qubit_map)
             for p in self)
 
-    def expectation_from_density_matrix(self, state: np.ndarray,
-                                        qubit_map: Mapping[raw_types.Qid, int]
+    def expectation_from_density_matrix(self,
+                                        state: np.ndarray,
+                                        qubit_map: Mapping[raw_types.Qid, int],
+                                        *,
+                                        check_preconditions: bool = True
                                        ) -> float:
         """Evaluate the expectation of this PauliSum given a density matrix.
 
@@ -395,7 +404,9 @@ def expectation_from_density_matrix(self, state: np.ndarray,
         Args:
             state: An array representing a valid  density matrix.
             qubit_map: A map from all qubits used in this PauliSum to the
-            indices of the qubits that `state` is defined over.
+                indices of the qubits that `state` is defined over.
+            check_preconditions: Whether to check that `state` represents a
+                valid density matrix.
 
         Returns:
             The expectation value of the input state.
@@ -420,12 +431,14 @@ def expectation_from_density_matrix(self, state: np.ndarray,
             raise ValueError("Input array does not represent a density matrix "
                              "with shape `(2 ** n, 2 ** n)` or `(2, ..., 2)`.")
 
-        # HACK: avoid circular import
-        from cirq.sim.density_matrix_utils import to_valid_density_matrix
-        # 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)
+        if check_preconditions:
+            # HACK: avoid circular import
+            from cirq.sim.density_matrix_utils import to_valid_density_matrix
+            # 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)
         return sum(
             p._expectation_from_density_matrix_no_validation(state, qubit_map)
             for p in self)
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
@@ -303,7 +303,10 @@ def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs'):
     def expectation_from_wavefunction(self,
                                       state: np.ndarray,
                                       qubit_map: Mapping[raw_types.Qid, int],
-                                      atol: float = 1e-7) -> float:
+                                      *,
+                                      atol: float = 1e-7,
+                                      check_preconditions: bool = True
+                                     ) -> float:
         r"""Evaluate the expectation of this PauliString given a wavefunction.
 
         Compute the expectation value of this PauliString with respect to a
@@ -326,7 +329,10 @@ def expectation_from_wavefunction(self,
         Args:
             state: An array representing a valid wavefunction.
             qubit_map: A map from all qubits used in this PauliString to the
-            indices of the qubits that `state` is defined over.
+                indices of the qubits that `state` is defined over.
+            atol: Absolute numerical tolerance.
+            check_preconditions: Whether to check that `state` represents a
+                valid wavefunction.
 
         Returns:
             The expectation value of the input state.
@@ -352,12 +358,13 @@ def expectation_from_wavefunction(self,
                              "with shape `(2 ** n,)` or `(2, ..., 2)`.")
 
         _validate_qubit_mapping(qubit_map, self.qubits, num_qubits)
-        # HACK: avoid circular import
-        from cirq.sim.wave_function import validate_normalized_state
-        validate_normalized_state(state=state,
-                                  qid_shape=(2,) * num_qubits,
-                                  dtype=state.dtype,
-                                  atol=atol)
+        if check_preconditions:
+            # HACK: avoid circular import
+            from cirq.sim.wave_function import validate_normalized_state
+            validate_normalized_state(state=state,
+                                      qid_shape=(2,) * num_qubits,
+                                      dtype=state.dtype,
+                                      atol=atol)
         return self._expectation_from_wavefunction_no_validation(
             state, qubit_map)
 
@@ -395,7 +402,10 @@ def _expectation_from_wavefunction_no_validation(
     def expectation_from_density_matrix(self,
                                         state: np.ndarray,
                                         qubit_map: Mapping[raw_types.Qid, int],
-                                        atol: float = 1e-7) -> float:
+                                        *,
+                                        atol: float = 1e-7,
+                                        check_preconditions: bool = True
+                                       ) -> float:
         r"""Evaluate the expectation of this PauliString given a density matrix.
 
         Compute the expectation value of this PauliString with respect to an
@@ -418,7 +428,10 @@ def expectation_from_density_matrix(self,
         Args:
             state: An array representing a valid  density matrix.
             qubit_map: A map from all qubits used in this PauliString to the
-            indices of the qubits that `state` is defined over.
+                indices of the qubits that `state` is defined over.
+            atol: Absolute numerical tolerance.
+            check_preconditions: Whether to check that `state` represents a
+                valid density matrix.
 
         Returns:
             The expectation value of the input state.
@@ -445,13 +458,15 @@ def expectation_from_density_matrix(self,
                              "with shape `(2 ** n, 2 ** n)` or `(2, ..., 2)`.")
 
         _validate_qubit_mapping(qubit_map, self.qubits, num_qubits)
-        # HACK: avoid circular import
-        from cirq.sim.density_matrix_utils import to_valid_density_matrix
-        # 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,
-                                    atol=atol)
+        if check_preconditions:
+            # HACK: avoid circular import
+            from cirq.sim.density_matrix_utils import to_valid_density_matrix
+            # 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,
+                                        atol=atol)
         return self._expectation_from_density_matrix_no_validation(
             state, qubit_map)
 


Test patch:
diff --git a/cirq/ops/linear_combinations_test.py b/cirq/ops/linear_combinations_test.py
--- a/cirq/ops/linear_combinations_test.py
+++ b/cirq/ops/linear_combinations_test.py
@@ -1014,6 +1014,20 @@ def test_expectation_from_wavefunction_invalid_input():
         psum.expectation_from_wavefunction(wf.reshape((4, 4, 1)), q_map_2)
 
 
+def test_expectation_from_wavefunction_check_preconditions():
+    q0, q1, q2, q3 = cirq.LineQubit.range(4)
+    psum = cirq.X(q0) + 2 * cirq.Y(q1) + 3 * cirq.Z(q3)
+    q_map = {q0: 0, q1: 1, q2: 2, q3: 3}
+
+    with pytest.raises(ValueError, match='normalized'):
+        psum.expectation_from_wavefunction(np.arange(16, dtype=np.complex64),
+                                           q_map)
+
+    _ = psum.expectation_from_wavefunction(np.arange(16, dtype=np.complex64),
+                                           q_map,
+                                           check_preconditions=False)
+
+
 def test_expectation_from_wavefunction_basis_states():
     q = cirq.LineQubit.range(2)
     psum = cirq.X(q[0]) + 2 * cirq.Y(q[0]) + 3 * cirq.Z(q[0])
@@ -1131,6 +1145,22 @@ def test_expectation_from_density_matrix_invalid_input():
         psum.expectation_from_density_matrix(rho.reshape((-1)), q_map)
 
 
+def test_expectation_from_density_matrix_check_preconditions():
+    q0, q1, _, q3 = cirq.LineQubit.range(4)
+    psum = cirq.X(q0) + 2 * cirq.Y(q1) + 3 * cirq.Z(q3)
+    q_map = {q0: 0, q1: 1, q3: 2}
+    not_psd = np.zeros((8, 8), dtype=np.complex64)
+    not_psd[0, 0] = 1.1
+    not_psd[1, 1] = -0.1
+
+    with pytest.raises(ValueError, match='semidefinite'):
+        psum.expectation_from_density_matrix(not_psd, q_map)
+
+    _ = psum.expectation_from_density_matrix(not_psd,
+                                             q_map,
+                                             check_preconditions=False)
+
+
 def test_expectation_from_density_matrix_basis_states():
     q = cirq.LineQubit.range(2)
     psum = cirq.X(q[0]) + 2 * cirq.Y(q[0]) + 3 * cirq.Z(q[0])
diff --git a/cirq/ops/pauli_string_test.py b/cirq/ops/pauli_string_test.py
--- a/cirq/ops/pauli_string_test.py
+++ b/cirq/ops/pauli_string_test.py
@@ -814,6 +814,20 @@ def test_expectation_from_wavefunction_invalid_input():
         ps.expectation_from_wavefunction(wf.reshape((4, 4, 1)), q_map_2)
 
 
+def test_expectation_from_wavefunction_check_preconditions():
+    q0, q1, q2, q3 = _make_qubits(4)
+    ps = cirq.PauliString({q0: cirq.X, q1: cirq.Y})
+    q_map = {q0: 0, q1: 1, q2: 2, q3: 3}
+
+    with pytest.raises(ValueError, match='normalized'):
+        ps.expectation_from_wavefunction(np.arange(16, dtype=np.complex64),
+                                         q_map)
+
+    _ = ps.expectation_from_wavefunction(np.arange(16, dtype=np.complex64),
+                                         q_map,
+                                         check_preconditions=False)
+
+
 def test_expectation_from_wavefunction_basis_states():
     q0 = cirq.LineQubit(0)
     x0 = cirq.PauliString({q0: cirq.X})
@@ -1047,6 +1061,24 @@ def test_expectation_from_density_matrix_invalid_input():
     _ = ps.expectation_from_density_matrix(rho_or_wf, q_map)
 
 
+def test_expectation_from_density_matrix_check_preconditions():
+    q0, q1 = _make_qubits(2)
+    ps = cirq.PauliString({q0: cirq.X, q1: cirq.Y})
+    q_map = {q0: 0, q1: 1}
+
+    with pytest.raises(ValueError, match='semidefinite'):
+        ps.expectation_from_density_matrix(
+            np.array(
+                [[1.1, 0, 0, 0], [0, -.1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
+                dtype=np.complex64), q_map)
+
+    _ = ps.expectation_from_density_matrix(np.array(
+        [[1.1, 0, 0, 0], [0, -.1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
+        dtype=np.complex64),
+                                           q_map,
+                                           check_preconditions=False)
+
+
 def test_expectation_from_density_matrix_basis_states():
     q0 = cirq.LineQubit(0)
     x0_pauli_map = {q0: cirq.X}