Repository: Qiskit/qiskit

Issue:
Unnecessary zero-probability states returned by Sampler, with different behaviour to Aer Sampler
### Environment

- **Qiskit Terra version**: 0.23.0.dev0+ad07847
- **Python version**: 3.10.4
- **Operating system**: macOS Monterey 12.6 M1 2020


### What is happening?

* ``Sampler``  returns a dictionary with every possible state, even if some states have a probability 0 of being sampled. As the number of qubits is increased, the size of this dictionary will scale exponentially. 
* The behaviour is different to the qiskit_aer ``Sampler``, which does not return states with probability 0. Importantly, since the number of qubits is not saved, this results in a faulty conversion to binary strings in qiskit_aer using the ``binary_probabilities()`` method. This is probably because the length of the binary strings is calculated based on the number of returned states instead of the number of qubits, which in Aer is a subset of all possible states.

### How can we reproduce the issue?

```python
from qiskit_aer.primitives import Sampler as aer_sampler
from qiskit.primitives import Sampler as terra_sampler

qc = QuantumCircuit(4)
qc.h(0)
qc.measure_all()

terra_job = terra_sampler().run([qc], [], shots=10)
terra_quasis = terra_job.result().quasi_dists[0]
terra_bin_probs = terra_quasis.binary_probabilities()
print('TERRA RESULTS:')
print('\n', terra_quasis)
print('\n', terra_bin_probs)

aer_job = aer_sampler(backend_options={"method": "statevector"}).run([qc], [], shots=100)
aer_quasis = aer_job.result().quasi_dists[0]
aer_bin_probs = aer_quasis.binary_probabilities()
print('\nAER RESULTS:')
print('\n', aer_quasis)
print('\n', aer_bin_probs)
```

TERRA RESULTS:

 {0: 0.5, 1: 0.5, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0, 10: 0.0, 11: 0.0, 12: 0.0, 13: 0.0, 14: 0.0, 15: 0.0}

 {'0000': 0.5, '0001': 0.5, '0010': 0.0, '0011': 0.0, '0100': 0.0, '0101': 0.0, '0110': 0.0, '0111': 0.0, '1000': 0.0, '1001': 0.0, '1010': 0.0, '1011': 0.0, '1100': 0.0, '1101': 0.0, '1110': 0.0, '1111': 0.0}

AER RESULTS:

 {1: 0.54, 0: 0.46}

 {'1': 0.54, '0': 0.46}

### What should happen?

We should see only the non-zero probability states in Terra, and a correct conversion to binary in Aer (i.e. matches the number of qubits):

TERRA RESULTS:

 {0: 0.5, 1: 0.5}

 {'0000': 0.5, '0001': 0.5}

AER RESULTS:

 {1: 0.54, 0: 0.46}

 {'0001': 0.54, '0000': 0.46}

### Any suggestions?

* Since the ``"statevector"`` method in Aer doesn't return zero probability states, the same steps could be applied to the ``Statevector`` in Terra (which is called by the ``Sampler``). 
* The number of qubits could be saved in the metadata of the ``SamplerResult``, such that Aer can properly do the conversion to binary probabilities.


Discussion:
Thank you for your report.

Aer passes bitstrings as integer to QuasiDistribution. So, the result looses the information of number of qubits.
I made a simple PoC to fix the issue for `shots is not None` https://github.com/Qiskit/qiskit-aer/pull/1668.
@ikkoham can elaborate this patch.

It might be good to remove results with zero probability for the reference implementation of Sampler though we don't guarantee the behavior. It may be a good first issue.
Couldn't the `QuasiDistribution` just contain the number of bits? It already contains the number of shots so it's already strictly tied to a quantum circuit result 😄 because this exponential scaling would be nice to avoid if e.g. someone is iterating over the outcomes.
Yes, it might be good to remove bits with 0 prob at `QuasiDistribution` side. It can be a good first issue too.
Also Terra's Sampler should use `probabilities_dict` method instead of `probabilities`.
https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/quantum_info/states/quantum_state.py#L216
This would be nice to fix.

Resolving patch:
diff --git a/qiskit/primitives/sampler.py b/qiskit/primitives/sampler.py
--- a/qiskit/primitives/sampler.py
+++ b/qiskit/primitives/sampler.py
@@ -118,16 +118,20 @@ def _call(
             )
             qargs_list.append(self._qargs_list[i])
         probabilities = [
-            Statevector(bound_circuit_to_instruction(circ)).probabilities(qargs=qargs)
+            Statevector(bound_circuit_to_instruction(circ)).probabilities_dict(
+                qargs=qargs, decimals=16
+            )
             for circ, qargs in zip(bound_circuits, qargs_list)
         ]
         if shots is not None:
-            probabilities = [
-                rng.multinomial(shots, probability) / shots for probability in probabilities
-            ]
+            for i, prob_dict in enumerate(probabilities):
+                counts = rng.multinomial(shots, np.fromiter(prob_dict.values(), dtype=float))
+                probabilities[i] = {
+                    key: count / shots for key, count in zip(prob_dict.keys(), counts) if count > 0
+                }
             for metadatum in metadata:
                 metadatum["shots"] = shots
-        quasis = [QuasiDistribution(dict(enumerate(p)), shots=shots) for p in probabilities]
+        quasis = [QuasiDistribution(p, shots=shots) for p in probabilities]
 
         return SamplerResult(quasis, metadata)
 


Test patch:
diff --git a/test/python/algorithms/test_grover.py b/test/python/algorithms/test_grover.py
--- a/test/python/algorithms/test_grover.py
+++ b/test/python/algorithms/test_grover.py
@@ -257,10 +257,11 @@ def test_circuit_result(self, use_sampler):
         if use_sampler:
             for i, dist in enumerate(result.circuit_results):
                 keys, values = zip(*sorted(dist.items()))
-                self.assertTupleEqual(keys, ("00", "01", "10", "11"))
                 if i in (0, 3):
-                    np.testing.assert_allclose(values, [0, 0, 0, 1], atol=0.2)
+                    self.assertTupleEqual(keys, ("11",))
+                    np.testing.assert_allclose(values, [1], atol=0.2)
                 else:
+                    self.assertTupleEqual(keys, ("00", "01", "10", "11"))
                     np.testing.assert_allclose(values, [0.25, 0.25, 0.25, 0.25], atol=0.2)
         else:
             expected_results = [
diff --git a/test/python/primitives/test_sampler.py b/test/python/primitives/test_sampler.py
--- a/test/python/primitives/test_sampler.py
+++ b/test/python/primitives/test_sampler.py
@@ -133,8 +133,8 @@ def test_sampler_example(self):
         self.assertIsInstance(result, SamplerResult)
         self.assertEqual(len(result.quasi_dists), 1)
         keys, values = zip(*sorted(result.quasi_dists[0].items()))
-        self.assertTupleEqual(keys, tuple(range(4)))
-        np.testing.assert_allclose(values, [0.5, 0, 0, 0.5])
+        self.assertTupleEqual(keys, (0, 3))
+        np.testing.assert_allclose(values, [0.5, 0.5])
         self.assertEqual(len(result.metadata), 1)
 
         # executes three Bell circuits
@@ -146,8 +146,8 @@ def test_sampler_example(self):
         self.assertEqual(len(result.metadata), 3)
         for dist in result.quasi_dists:
             keys, values = zip(*sorted(dist.items()))
-            self.assertTupleEqual(keys, tuple(range(4)))
-            np.testing.assert_allclose(values, [0.5, 0, 0, 0.5])
+            self.assertTupleEqual(keys, (0, 3))
+            np.testing.assert_allclose(values, [0.5, 0.5])
 
         with self.assertWarns(DeprecationWarning):
             sampler = Sampler([bell])
@@ -157,8 +157,8 @@ def test_sampler_example(self):
         self.assertEqual(len(result.metadata), 3)
         for dist in result.quasi_dists:
             keys, values = zip(*sorted(dist.items()))
-            self.assertTupleEqual(keys, tuple(range(4)))
-            np.testing.assert_allclose(values, [0.5, 0, 0, 0.5])
+            self.assertTupleEqual(keys, (0, 3))
+            np.testing.assert_allclose(values, [0.5, 0.5])
 
         # parametrized circuit
         pqc = RealAmplitudes(num_qubits=2, reps=2)
@@ -224,23 +224,23 @@ def test_sampler_param_order(self):
 
         # qc({x: 0, y: 0})
         keys, values = zip(*sorted(result.quasi_dists[0].items()))
-        self.assertTupleEqual(keys, tuple(range(8)))
-        np.testing.assert_allclose(values, [0, 0, 0, 0, 1, 0, 0, 0])
+        self.assertTupleEqual(keys, (4,))
+        np.testing.assert_allclose(values, [1])
 
         # qc({x: 0, y: 0})
         keys, values = zip(*sorted(result.quasi_dists[1].items()))
-        self.assertTupleEqual(keys, tuple(range(8)))
-        np.testing.assert_allclose(values, [0, 0, 0, 0, 1, 0, 0, 0])
+        self.assertTupleEqual(keys, (4,))
+        np.testing.assert_allclose(values, [1])
 
         # qc({x: pi/2, y: 0})
         keys, values = zip(*sorted(result.quasi_dists[2].items()))
-        self.assertTupleEqual(keys, tuple(range(8)))
-        np.testing.assert_allclose(values, [0, 0, 0, 0, 0.5, 0.5, 0, 0])
+        self.assertTupleEqual(keys, (4, 5))
+        np.testing.assert_allclose(values, [0.5, 0.5])
 
         # qc({x: 0, y: pi/2})
         keys, values = zip(*sorted(result.quasi_dists[3].items()))
-        self.assertTupleEqual(keys, tuple(range(8)))
-        np.testing.assert_allclose(values, [0, 0, 0, 0, 0.5, 0, 0.5, 0])
+        self.assertTupleEqual(keys, (4, 6))
+        np.testing.assert_allclose(values, [0.5, 0.5])
 
     def test_sampler_reverse_meas_order(self):
         """test for sampler with reverse measurement order"""
@@ -263,23 +263,23 @@ def test_sampler_reverse_meas_order(self):
 
         # qc({x: 0, y: 0})
         keys, values = zip(*sorted(result.quasi_dists[0].items()))
-        self.assertTupleEqual(keys, tuple(range(8)))
-        np.testing.assert_allclose(values, [0, 1, 0, 0, 0, 0, 0, 0])
+        self.assertTupleEqual(keys, (1,))
+        np.testing.assert_allclose(values, [1])
 
         # qc({x: 0, y: 0})
         keys, values = zip(*sorted(result.quasi_dists[1].items()))
-        self.assertTupleEqual(keys, tuple(range(8)))
-        np.testing.assert_allclose(values, [0, 1, 0, 0, 0, 0, 0, 0])
+        self.assertTupleEqual(keys, (1,))
+        np.testing.assert_allclose(values, [1])
 
         # qc({x: pi/2, y: 0})
         keys, values = zip(*sorted(result.quasi_dists[2].items()))
-        self.assertTupleEqual(keys, tuple(range(8)))
-        np.testing.assert_allclose(values, [0, 0.5, 0, 0, 0, 0.5, 0, 0])
+        self.assertTupleEqual(keys, (1, 5))
+        np.testing.assert_allclose(values, [0.5, 0.5])
 
         # qc({x: 0, y: pi/2})
         keys, values = zip(*sorted(result.quasi_dists[3].items()))
-        self.assertTupleEqual(keys, tuple(range(8)))
-        np.testing.assert_allclose(values, [0, 0.5, 0, 0.5, 0, 0, 0, 0])
+        self.assertTupleEqual(keys, (1, 3))
+        np.testing.assert_allclose(values, [0.5, 0.5])
 
     def test_1qubit(self):
         """test for 1-qubit cases"""
@@ -295,13 +295,10 @@ def test_1qubit(self):
         self.assertIsInstance(result, SamplerResult)
         self.assertEqual(len(result.quasi_dists), 2)
 
-        keys, values = zip(*sorted(result.quasi_dists[0].items()))
-        self.assertTupleEqual(keys, tuple(range(2)))
-        np.testing.assert_allclose(values, [1, 0])
-
-        keys, values = zip(*sorted(result.quasi_dists[1].items()))
-        self.assertTupleEqual(keys, tuple(range(2)))
-        np.testing.assert_allclose(values, [0, 1])
+        for i in range(2):
+            keys, values = zip(*sorted(result.quasi_dists[i].items()))
+            self.assertTupleEqual(keys, (i,))
+            np.testing.assert_allclose(values, [1])
 
     def test_2qubit(self):
         """test for 2-qubit cases"""
@@ -326,21 +323,10 @@ def test_2qubit(self):
         self.assertIsInstance(result, SamplerResult)
         self.assertEqual(len(result.quasi_dists), 4)
 
-        keys, values = zip(*sorted(result.quasi_dists[0].items()))
-        self.assertTupleEqual(keys, tuple(range(4)))
-        np.testing.assert_allclose(values, [1, 0, 0, 0])
-
-        keys, values = zip(*sorted(result.quasi_dists[1].items()))
-        self.assertTupleEqual(keys, tuple(range(4)))
-        np.testing.assert_allclose(values, [0, 1, 0, 0])
-
-        keys, values = zip(*sorted(result.quasi_dists[2].items()))
-        self.assertTupleEqual(keys, tuple(range(4)))
-        np.testing.assert_allclose(values, [0, 0, 1, 0])
-
-        keys, values = zip(*sorted(result.quasi_dists[3].items()))
-        self.assertTupleEqual(keys, tuple(range(4)))
-        np.testing.assert_allclose(values, [0, 0, 0, 1])
+        for i in range(4):
+            keys, values = zip(*sorted(result.quasi_dists[i].items()))
+            self.assertTupleEqual(keys, (i,))
+            np.testing.assert_allclose(values, [1])
 
     def test_errors(self):
         """Test for errors"""
@@ -530,13 +516,10 @@ def test_run_1qubit(self):
         self.assertIsInstance(result, SamplerResult)
         self.assertEqual(len(result.quasi_dists), 2)
 
-        keys, values = zip(*sorted(result.quasi_dists[0].items()))
-        self.assertTupleEqual(keys, tuple(range(2)))
-        np.testing.assert_allclose(values, [1, 0])
-
-        keys, values = zip(*sorted(result.quasi_dists[1].items()))
-        self.assertTupleEqual(keys, tuple(range(2)))
-        np.testing.assert_allclose(values, [0, 1])
+        for i in range(2):
+            keys, values = zip(*sorted(result.quasi_dists[i].items()))
+            self.assertTupleEqual(keys, (i,))
+            np.testing.assert_allclose(values, [1])
 
     def test_run_2qubit(self):
         """test for 2-qubit cases"""
@@ -557,21 +540,10 @@ def test_run_2qubit(self):
         self.assertIsInstance(result, SamplerResult)
         self.assertEqual(len(result.quasi_dists), 4)
 
-        keys, values = zip(*sorted(result.quasi_dists[0].items()))
-        self.assertTupleEqual(keys, tuple(range(4)))
-        np.testing.assert_allclose(values, [1, 0, 0, 0])
-
-        keys, values = zip(*sorted(result.quasi_dists[1].items()))
-        self.assertTupleEqual(keys, tuple(range(4)))
-        np.testing.assert_allclose(values, [0, 1, 0, 0])
-
-        keys, values = zip(*sorted(result.quasi_dists[2].items()))
-        self.assertTupleEqual(keys, tuple(range(4)))
-        np.testing.assert_allclose(values, [0, 0, 1, 0])
-
-        keys, values = zip(*sorted(result.quasi_dists[3].items()))
-        self.assertTupleEqual(keys, tuple(range(4)))
-        np.testing.assert_allclose(values, [0, 0, 0, 1])
+        for i in range(4):
+            keys, values = zip(*sorted(result.quasi_dists[i].items()))
+            self.assertTupleEqual(keys, (i,))
+            np.testing.assert_allclose(values, [1])
 
     def test_run_single_circuit(self):
         """Test for single circuit case."""
@@ -719,6 +691,19 @@ def test_run_with_shots_option_none(self):
         ).result()
         self.assertDictAlmostEqual(result_42.quasi_dists, result_15.quasi_dists)
 
+    def test_run_shots_result_size(self):
+        """test with shots option to validate the result size"""
+        n = 10
+        shots = 100
+        qc = QuantumCircuit(n)
+        qc.h(range(n))
+        qc.measure_all()
+        sampler = Sampler()
+        result = sampler.run(qc, [], shots=shots, seed=42).result()
+        self.assertEqual(len(result.quasi_dists), 1)
+        self.assertLessEqual(len(result.quasi_dists[0]), shots)
+        self.assertAlmostEqual(sum(result.quasi_dists[0].values()), 1.0)
+
     def test_primitive_job_status_done(self):
         """test primitive job's status"""
         bell = self._circuit[1]