Repository: Qiskit/qiskit

Issue:
Probabilities ``>1`` in the sampler (and statevector)
### Environment

- **Qiskit Terra version**: main @  5c49d1400b5ceb75662ebf79d06650ca39449579
- **Python version**:  3.10
- **Operating system**: macOS Ventura


### What is happening?

The ``Statevector.probabilities`` method sometimes contains roundoff errors, leading to probabilities larger than 1. This breaks the `Sampler` if shots are emulated, as larger than 1 probabilities are not valid inputs to NumPy's multinomial library.

### How can we reproduce the issue?

```python
from qiskit.circuit import QuantumCircuit
from qiskit.primitives import Sampler
import numpy as np

np.random.seed(1)
sampler = Sampler(options={"shots": 1024})

for _ in range(1000):
    try:
        angle = np.random.random(1)[0]
        # this angle breaks on my laptop for example:
        # angle = 0.10233442882782584
        qc = QuantumCircuit(1)
        qc.ry(angle, 0)
        qc.ry(-angle, 0)
        qc.measure_all()
        _ = sampler.run(qc).result()
    except Exception as exc:
        print("Failed on", angle)

```
The error raised is 
```
  File "/Users/jul/Qiskit/qiskit-terra/qiskit/primitives/sampler.py", line 130, in _call
    counts = rng.multinomial(shots, np.fromiter(prob_dict.values(), dtype=float))
  File "_generator.pyx", line 3916, in numpy.random._generator.Generator.multinomial
  File "_common.pyx", line 391, in numpy.random._common.check_array_constraint
  File "_common.pyx", line 377, in numpy.random._common._check_array_cons_bounded_0_1
ValueError: pvals < 0, pvals > 1 or pvals contains NaNs
```

### What should happen?

The probabilities should not exceed 1.

### Any suggestions?

Clip the probabilities.


Discussion:

Resolving patch:
diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py
--- a/qiskit/quantum_info/states/densitymatrix.py
+++ b/qiskit/quantum_info/states/densitymatrix.py
@@ -480,8 +480,13 @@ def probabilities(self, qargs=None, decimals=None):
         probs = self._subsystem_probabilities(
             np.abs(self.data.diagonal()), self._op_shape.dims_l(), qargs=qargs
         )
+
+        # to account for roundoff errors, we clip
+        probs = np.clip(probs, a_min=0, a_max=1)
+
         if decimals is not None:
             probs = probs.round(decimals=decimals)
+
         return probs
 
     def reset(self, qargs=None):
diff --git a/qiskit/quantum_info/states/quantum_state.py b/qiskit/quantum_info/states/quantum_state.py
--- a/qiskit/quantum_info/states/quantum_state.py
+++ b/qiskit/quantum_info/states/quantum_state.py
@@ -234,7 +234,9 @@ def probabilities_dict(self, qargs=None, decimals=None):
             dict: The measurement probabilities in dict (ket) form.
         """
         return self._vector_to_dict(
-            self.probabilities(qargs=qargs, decimals=decimals), self.dims(qargs), string_labels=True
+            self.probabilities(qargs=qargs, decimals=decimals),
+            self.dims(qargs),
+            string_labels=True,
         )
 
     def sample_memory(self, shots, qargs=None):
diff --git a/qiskit/quantum_info/states/statevector.py b/qiskit/quantum_info/states/statevector.py
--- a/qiskit/quantum_info/states/statevector.py
+++ b/qiskit/quantum_info/states/statevector.py
@@ -575,8 +575,13 @@ def probabilities(self, qargs=None, decimals=None):
         probs = self._subsystem_probabilities(
             np.abs(self.data) ** 2, self._op_shape.dims_l(), qargs=qargs
         )
+
+        # to account for roundoff errors, we clip
+        probs = np.clip(probs, a_min=0, a_max=1)
+
         if decimals is not None:
             probs = probs.round(decimals=decimals)
+
         return probs
 
     def reset(self, qargs=None):


Test patch:
diff --git a/test/python/quantum_info/states/test_densitymatrix.py b/test/python/quantum_info/states/test_densitymatrix.py
--- a/test/python/quantum_info/states/test_densitymatrix.py
+++ b/test/python/quantum_info/states/test_densitymatrix.py
@@ -1202,6 +1202,27 @@ def test_drawings(self):
             with self.subTest(msg=f"draw('{drawtype}')"):
                 dm.draw(drawtype)
 
+    def test_clip_probabilities(self):
+        """Test probabilities are clipped to [0, 1]."""
+        dm = DensityMatrix([[1.1, 0], [0, 0]])
+
+        self.assertEqual(list(dm.probabilities()), [1.0, 0.0])
+        # The "1" key should be exactly zero and therefore omitted.
+        self.assertEqual(dm.probabilities_dict(), {"0": 1.0})
+
+    def test_round_probabilities(self):
+        """Test probabilities are correctly rounded.
+
+        This is good to test to ensure clipping, renormalizing and rounding work together.
+        """
+        p = np.sqrt(1 / 3)
+        amplitudes = [p, p, p, 0]
+        dm = DensityMatrix(np.outer(amplitudes, amplitudes))
+        expected = [0.33, 0.33, 0.33, 0]
+
+        # Exact floating-point check because fixing the rounding should ensure this is exact.
+        self.assertEqual(list(dm.probabilities(decimals=2)), expected)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/test/python/quantum_info/states/test_statevector.py b/test/python/quantum_info/states/test_statevector.py
--- a/test/python/quantum_info/states/test_statevector.py
+++ b/test/python/quantum_info/states/test_statevector.py
@@ -1308,6 +1308,24 @@ def test_statevector_len(self):
         self.assertEqual(len(empty_vector), len(empty_sv))
         self.assertEqual(len(dummy_vector), len(sv))
 
+    def test_clip_probabilities(self):
+        """Test probabilities are clipped to [0, 1]."""
+        sv = Statevector([1.1, 0])
+
+        self.assertEqual(list(sv.probabilities()), [1.0, 0.0])
+        # The "1" key should be zero and therefore omitted.
+        self.assertEqual(sv.probabilities_dict(), {"0": 1.0})
+
+    def test_round_probabilities(self):
+        """Test probabilities are correctly rounded.
+
+        This is good to test to ensure clipping, renormalizing and rounding work together.
+        """
+        p = np.sqrt(1 / 3)
+        sv = Statevector([p, p, p, 0])
+        expected = [0.33, 0.33, 0.33, 0]
+        self.assertEqual(list(sv.probabilities(decimals=2)), expected)
+
 
 if __name__ == "__main__":
     unittest.main()