Repository: quantumlib/Cirq

Issue:
Wavefunction simulator should accept NoiseModel too
It can simulate mixtures and coherent noise models.


Discussion:
I think we need to have noise models declare themselves as "monte carlo unitary safe" or something like that in order for this to work properly.
I was thinking the simulator could just raise a runtime error if it encounters a gate it can't simulate, but your solution is probably better.
I think we should add `is_mixture_of_unitaries` and `is_coherent` methods/properties to NoiseModel.
We decided at #2440 to make the simulator do all the work instead of letting noise models declare whether they can be simulated.
In the specific case of the wave functio nsimulator being given a noise model, it should be understood that if you give a noise model it can't handle then you will get an error.
Is this just a matter of copying the noise preprocessing code from density matrix simulator to the sparse simulator? https://github.com/quantumlib/Cirq/blob/e46f62eb1cfec26b876411042f756f6187a52285/cirq/sim/density_matrix_simulator.py#L265

Resolving patch:
diff --git a/cirq/devices/noise_model.py b/cirq/devices/noise_model.py
--- a/cirq/devices/noise_model.py
+++ b/cirq/devices/noise_model.py
@@ -197,6 +197,12 @@ def __repr__(self) -> str:
     def _json_dict_(self) -> Dict[str, Any]:
         return protocols.obj_to_dict_helper(self, [])
 
+    def _has_unitary_(self):
+        return True
+
+    def _has_mixture_(self):
+        return True
+
 
 @value.value_equality
 class ConstantQubitNoiseModel(NoiseModel):
@@ -231,6 +237,12 @@ def noisy_moment(self, moment: 'cirq.Moment', system_qubits: Sequence['cirq.Qid'
     def _json_dict_(self):
         return protocols.obj_to_dict_helper(self, ['qubit_noise_gate'])
 
+    def _has_unitary_(self):
+        return protocols.has_unitary(self.qubit_noise_gate)
+
+    def _has_mixture_(self):
+        return protocols.has_mixture(self.qubit_noise_gate)
+
 
 class GateSubstitutionNoiseModel(NoiseModel):
     def __init__(self, substitution_func: Callable[['cirq.Operation'], 'cirq.Operation']):
diff --git a/cirq/sim/sparse_simulator.py b/cirq/sim/sparse_simulator.py
--- a/cirq/sim/sparse_simulator.py
+++ b/cirq/sim/sparse_simulator.py
@@ -29,7 +29,8 @@
 
 import numpy as np
 
-from cirq import circuits, ops, protocols, qis, study, value
+from cirq import circuits, ops, protocols, qis, study, value, devices
+from cirq.ops import flatten_to_ops
 from cirq.sim import (
     simulator,
     state_vector,
@@ -145,6 +146,7 @@ def __init__(
         self,
         *,
         dtype: Type[np.number] = np.complex64,
+        noise: 'cirq.NOISE_MODEL_LIKE' = None,
         seed: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
     ):
         """A sparse matrix simulator.
@@ -152,12 +154,17 @@ def __init__(
         Args:
             dtype: The `numpy.dtype` used by the simulation. One of
                 `numpy.complex64` or `numpy.complex128`.
+            noise: A noise model to apply while simulating.
             seed: The random seed to use for this simulator.
         """
         if np.dtype(dtype).kind != 'c':
             raise ValueError('dtype must be a complex type but was {}'.format(dtype))
         self._dtype = dtype
         self._prng = value.parse_random_state(seed)
+        noise_model = devices.NoiseModel.from_noise_model_like(noise)
+        if not protocols.has_mixture(noise_model):
+            raise ValueError('noise must be unitary or mixture but was {}'.format(noise_model))
+        self.noise = noise_model
 
     def _run(
         self, circuit: circuits.Circuit, param_resolver: study.ParamResolver, repetitions: int
@@ -170,8 +177,10 @@ def _run(
 
         # Simulate as many unitary operations as possible before having to
         # repeat work for each sample.
-        unitary_prefix, general_suffix = split_into_matching_protocol_then_general(
-            resolved_circuit, protocols.has_unitary
+        unitary_prefix, general_suffix = (
+            split_into_matching_protocol_then_general(resolved_circuit, protocols.has_unitary)
+            if protocols.has_unitary(self.noise)
+            else (resolved_circuit[0:0], resolved_circuit)
         )
         step_result = None
         for step_result in self._base_iterator(
@@ -249,8 +258,9 @@ def _base_iterator(
             log_of_measurement_results={},
         )
 
-        for moment in circuit:
-            for op in moment:
+        noisy_moments = self.noise.noisy_moments(circuit, sorted(circuit.all_qubits()))
+        for op_tree in noisy_moments:
+            for op in flatten_to_ops(op_tree):
                 if perform_measurements or not isinstance(op.gate, ops.MeasurementGate):
                     sim_state.axes = tuple(qubit_map[qubit] for qubit in op.qubits)
                     protocols.act_on(op, sim_state)


Test patch:
diff --git a/cirq/sim/sparse_simulator_test.py b/cirq/sim/sparse_simulator_test.py
--- a/cirq/sim/sparse_simulator_test.py
+++ b/cirq/sim/sparse_simulator_test.py
@@ -1227,3 +1227,37 @@ def test_final_state_vector_is_not_last_object():
     assert result.state_vector() is not initial_state
     assert not np.shares_memory(result.state_vector(), initial_state)
     np.testing.assert_equal(result.state_vector(), initial_state)
+
+
+def test_deterministic_gate_noise():
+    q = cirq.LineQubit(0)
+    circuit = cirq.Circuit(cirq.I(q), cirq.measure(q))
+
+    simulator1 = cirq.Simulator(noise=cirq.X)
+    result1 = simulator1.run(circuit, repetitions=10)
+
+    simulator2 = cirq.Simulator(noise=cirq.X)
+    result2 = simulator2.run(circuit, repetitions=10)
+
+    assert result1 == result2
+
+    simulator3 = cirq.Simulator(noise=cirq.Z)
+    result3 = simulator3.run(circuit, repetitions=10)
+
+    assert result1 != result3
+
+
+def test_nondeterministic_mixture_noise():
+    q = cirq.LineQubit(0)
+    circuit = cirq.Circuit(cirq.I(q), cirq.measure(q))
+
+    simulator = cirq.Simulator(noise=cirq.ConstantQubitNoiseModel(cirq.depolarize(0.5)))
+    result1 = simulator.run(circuit, repetitions=50)
+    result2 = simulator.run(circuit, repetitions=50)
+
+    assert result1 != result2
+
+
+def test_unsupported_noise_fails():
+    with pytest.raises(ValueError, match='noise'):
+        cirq.Simulator(noise=cirq.amplitude_damp(0.5))