Repository: qiboteam/qibo

Issue:
`DepolarizingChannel` gives wrong probabilities when used with state vectors
**Describe the bug**

The `DepolarizingChannel` with `lam=1` on a single qubit should give 0.5 probability for both 0 and 1 but gives 0.6 - 0.4.
The problem does not appear when using density matrices.

**To Reproduce**
```py
from qibo import Circuit, gates

nqubits = 1
qc = Circuit(nqubits)
lam = 1

qc.add(gates.DepolarizingChannel([0,], lam))
qc.add(gates.M(0))

# Execute circuit
result = qc.execute(nshots=10000)
counts = dict(result.frequencies(binary=True))
print(counts)
```

**Expected behavior**
Gives `{'0': 6316, '1': 3684}`. 

When using `qc = Circuit(nqubits, density_matrix=True)` it gives `{'0': 5035, '1': 4965}` which is more right.

Comments:

Patch:
diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py
index b540fa487..b0447bd56 100644
--- a/src/qibo/backends/numpy.py
+++ b/src/qibo/backends/numpy.py
@@ -256,9 +256,11 @@ class NumpyBackend(Backend):
         return np.reshape(state, 2 * (2**nqubits,))
 
     def apply_channel(self, channel, state, nqubits):
-        for coeff, gate in zip(channel.coefficients, channel.gates):
-            if self.np.random.random() < coeff:
-                state = self.apply_gate(gate, state, nqubits)
+        probabilities = channel.coefficients + (1 - np.sum(channel.coefficients),)
+        index = self.sample_shots(probabilities, 1)[0]
+        if index != len(channel.gates):
+            gate = channel.gates[index]
+            state = self.apply_gate(gate, state, nqubits)
         return state
 
     def apply_channel_density_matrix(self, channel, state, nqubits):


Test patch:
diff --git a/tests/test_measurements_probabilistic.py b/tests/test_measurements_probabilistic.py
index f15fefc93..2d520516a 100644
--- a/tests/test_measurements_probabilistic.py
+++ b/tests/test_measurements_probabilistic.py
@@ -83,14 +83,15 @@ def test_measurements_with_probabilistic_noise(backend):
 
     backend.set_seed(123)
     target_samples = []
+    channel_gates = [gates.Y, gates.Z]
+    probs = [0.2, 0.4, 0.4]
     for _ in range(20):
         noiseless_c = models.Circuit(5)
         noiseless_c.add((gates.RX(i, t) for i, t in enumerate(thetas)))
         for i in range(5):
-            if backend.np.random.random() < 0.2:
-                noiseless_c.add(gates.Y(i))
-            if backend.np.random.random() < 0.4:
-                noiseless_c.add(gates.Z(i))
+            index = backend.sample_shots(probs, 1)[0]
+            if index != len(channel_gates):
+                noiseless_c.add(channel_gates[index](i))
         noiseless_c.add(gates.M(*range(5)))
         result = backend.execute_circuit(noiseless_c, nshots=1)
         target_samples.append(backend.to_numpy(result.samples()))
diff --git a/tests/test_models_circuit_features.py b/tests/test_models_circuit_features.py
index dc3d5fb1a..9d22f98ce 100644
--- a/tests/test_models_circuit_features.py
+++ b/tests/test_models_circuit_features.py
@@ -277,16 +277,15 @@ def test_repeated_execute_pauli_noise_channel(backend):
 
     backend.set_seed(1234)
     target_state = []
+    channel_gates = [gates.X, gates.Y, gates.Z]
+    probs = [0.1, 0.2, 0.3, 0.4]
     for _ in range(20):
         noiseless_c = Circuit(4)
         noiseless_c.add((gates.RY(i, t) for i, t in enumerate(thetas)))
         for i in range(4):
-            if backend.np.random.random() < 0.1:
-                noiseless_c.add(gates.X(i))
-            if backend.np.random.random() < 0.2:
-                noiseless_c.add(gates.Y(i))
-            if backend.np.random.random() < 0.3:
-                noiseless_c.add(gates.Z(i))
+            index = backend.sample_shots(probs, 1)[0]
+            if index != len(channel_gates):
+                noiseless_c.add(channel_gates[index](i))
         result = backend.execute_circuit(noiseless_c)
         target_state.append(result.state(numpy=True))
     final_state = [backend.to_numpy(x) for x in final_state]
@@ -304,14 +303,15 @@ def test_repeated_execute_with_pauli_noise(backend):
 
     backend.set_seed(1234)
     target_state = []
+    channel_gates = [gates.X, gates.Z]
+    probs = [0.2, 0.1, 0.7]
     for _ in range(20):
         noiseless_c = Circuit(4)
         for i, t in enumerate(thetas):
             noiseless_c.add(gates.RY(i, theta=t))
-            if backend.np.random.random() < 0.2:
-                noiseless_c.add(gates.X(i))
-            if backend.np.random.random() < 0.1:
-                noiseless_c.add(gates.Z(i))
+            index = backend.sample_shots(probs, 1)[0]
+            if index != len(channel_gates):
+                noiseless_c.add(channel_gates[index](i))
         result = backend.execute_circuit(noiseless_c)
         target_state.append(result.state(numpy=True))
     target_state = np.stack(target_state)
@@ -336,21 +336,20 @@ def test_repeated_execute_probs_and_freqs(backend, nqubits):
     # Tensorflow seems to yield different results with same seed
     if backend.__class__.__name__ == "TensorflowBackend":
         if nqubits == 1:
-            test_probabilities = [0.171875, 0.828125]
-            test_frequencies = Counter({1: 848, 0: 176})
+            test_probabilities = [0.17578125, 0.82421875]
+            test_frequencies = Counter({1: 844, 0: 180})
         else:
-            test_probabilities = [0.04101562, 0.12695312, 0.140625, 0.69140625]
-            test_frequencies = Counter({11: 708, 10: 144, 1: 130, 0: 42})
+            test_probabilities = [0.04003906, 0.15039062, 0.15136719, 0.65820312]
+            test_frequencies = Counter({11: 674, 10: 155, 1: 154, 0: 41})
     else:
         if nqubits == 1:
-            test_probabilities = [0.20117188, 0.79882812]
-            test_frequencies = Counter({"1": 818, "0": 206})
+            test_probabilities = [0.22851562, 0.77148438]
+            test_frequencies = Counter({"1": 790, "0": 234})
         else:
-            test_probabilities = [0.0390625, 0.16113281, 0.17382812, 0.62597656]
-            test_frequencies = Counter({"11": 641, "10": 178, "01": 165, "00": 40})
+            test_probabilities = [0.05078125, 0.18066406, 0.16503906, 0.60351562]
+            test_frequencies = Counter({"11": 618, "10": 169, "01": 185, "00": 52})
 
     test_probabilities = backend.cast(test_probabilities, dtype=float)
-
     print(result.probabilities())
     backend.assert_allclose(
         backend.calculate_norm(result.probabilities() - test_probabilities)
diff --git a/tests/test_models_circuit_noise.py b/tests/test_models_circuit_noise.py
index 1fe77462d..d4fdb96f1 100644
--- a/tests/test_models_circuit_noise.py
+++ b/tests/test_models_circuit_noise.py
@@ -2,8 +2,9 @@
 import numpy as np
 import pytest
 
-import qibo
 from qibo import Circuit, gates
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info import random_clifford, random_statevector, random_unitary
 
 
 def test_pauli_noise_channel(backend):
@@ -198,3 +199,38 @@ def test_circuit_add_sampling(backend):
     target_samples = np.stack(target_samples)
 
     backend.assert_allclose(samples, target_samples[:, 0])
+
+
+@pytest.mark.parametrize("nqubits", [2, 4, 6])
+def test_probabilities_repeated_execution(backend, nqubits):
+    probabilities = list(np.random.rand(nqubits + 1)) + [1.0]
+    probabilities /= np.sum(probabilities)
+
+    unitaries = [random_unitary(2**1, backend=backend) for _ in range(nqubits)]
+    unitaries += [random_unitary(2**nqubits, backend=backend)]
+
+    qubits_list = [(q,) for q in range(nqubits)]
+    qubits_list += [tuple(q for q in range(nqubits))]
+
+    circuit = random_clifford(nqubits, return_circuit=True, backend=backend)
+    circuit.add(gates.UnitaryChannel(qubits_list, list(zip(probabilities, unitaries))))
+    circuit.add(gates.M(*range(nqubits)))
+
+    circuit_density_matrix = circuit.copy(deep=True)
+    circuit_density_matrix.density_matrix = True
+
+    statevector = random_statevector(2**nqubits, backend=backend)
+
+    result = backend.execute_circuit_repeated(
+        circuit, initial_state=statevector, nshots=int(1e4)
+    )
+    result = result.probabilities()
+
+    result_density_matrix = backend.execute_circuit(
+        circuit_density_matrix,
+        initial_state=np.outer(statevector, np.conj(statevector)),
+        nshots=int(1e4),
+    )
+    result_density_matrix = result_density_matrix.probabilities()
+
+    backend.assert_allclose(result, result_density_matrix, rtol=2e-2, atol=5e-3)