Repository: Qiskit/qiskit

Issue:
Inconsistent ordering of marginal counts
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: 0.24.1
- **Python version**:
- **Operating system**:

### What is the current behavior?
`marginal_counts` ordering does not obey a standard convention when returning its output.  For example, consider the counts:

```
counts = {'10011':1, '01100':1}
```
using `indices = [0,1,2,3,4]` give the identity mapping, suggesting that the 0th index in the indices list maps the input counts bit to the least significant bit in the new marginal counts, the next least significant is set by the 1st index value etc.  However, this does not hold true in general.  Namely `indices = [0,1,2,4,3]` does not follow the prescribed convention and returns:

```
{'10011': 1, '01100': 1}
```

whereas one would expect the following:

```
{'01011': 1, '10100': 1}
```
i.e. the output bits are in the order q3q4q2q1q0

I believe it is due to the line: https://github.com/Qiskit/qiskit-terra/blob/40ffd2b07c03e220a174b2bfd0bf8c23aa021c5f/qiskit/result/utils.py#L111

that implicitly reorders any permutation of indices.  This is mentioned no where in the docs, and is confusing  



### Steps to reproduce the problem



### What is the expected behavior?
I would expect the order of indices given to have a one to one mapping to those in the output.
### Suggested solutions





Discussion:
@nonhermitian `marginal_counts` has, from inception (0.11.0), treated the `indices` as a set, even though it's entered as a list. In addition to the fact they get sorted, there is also this line,
https://github.com/Qiskit/qiskit-terra/blob/3cf63baa3582d6cd5bcbeb976659dde3236f9007/qiskit/result/utils.py#L70-L72
which is what's being triggered in your case.
I think the docs for `indices` could be clearer about that, but the intent for `indices` is not to be entered as an ordered mapping but as a set of which `indices` to use to produce the `marginal_counts`.
Yeah, I am not disputing that is the way it has been. More that it doesn't make much sense to do an implicit conversion like this, where the ordering specified by the user is ignored. However, It is easy enough to write ones own marginal counts function so I am also fine if this gets close as expected behavior. 
Yes the fact that `indices` is a list is confusing, because this function just marginalizes. It does not reorder any bits. So in reality this should have been a set from the beginning, not a list.

The other thing I noticed was that the docstring is confusing a bit. It says:

> Marginalize counts from an experiment **over some indices of interest.**
    Args:
        indices (list(int) or None): The bit positions of interest
            **to marginalize over.** If ``None`` (default), do not marginalize at all.

The bold parts to me indicate that the indices you give are the ones ignored (tossed out), but in reality those are the indices that are kept. This wording should be fixed too.
HI, Can I pick this issue? if yes please assign it to me, I would love to work on.
Hi @AshwinKul28 sure go for it! Assigning to you 😄 
@AshwinKul28 @javabster Can i take this issue over?
@AshwinKul28 are you still working on this? If not please let us know so I can assign to @mattwright99 😄 
Hi @mattwright99 it looks like @AshwinKul28 is MIA so I've reassigned to you, let us know if you have any questions 😄 
@javabster Sounds good, thanks! 

@nonhermitian I just wanted to double check before I start working on this issue but it seems that the fix here is essentially clearifying some documentation and setting the expected type to be a `set` instead of a `list`. Is that right?
That is the fix that @ajavadia suggested yes.
@nonhermitian I found a new bug when calling `marginal_counts` with the default parameter `indices=None`:

```
>>> from qiskit import QuantumCircuit, Aer, assemble
>>> from  qiskit.result import marginal_counts
>>> qc = QuantumCircuit(2)
>>> qc.h(0)
>>> qc.x(1)
>>> qc.cnot(0,1)
>>> qc.measure_all()
>>>
>>> qobj = assemble(qc)
>>> sim = Aer.get_backend('aer_simulator')
>>> res = sim.run(qobj).result()
>>>
>>> marg_res = marginal_counts(res)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "c:\path\to\workspace\qiskit-terra\qiskit\result\utils.py", line 58, in marginal_counts
    experiment_result.header.memory_slots = len(indices)
TypeError: object of type 'NoneType' has no len()
```

This won't be a difficult fix so can I just include it in the PR for this ticket?

I think the best way to fix this would be to just return the `result` input at the very start of the function if `indices=None` becasue this funciton would have no effect. Thoughts?

I arrived here after spending an embarrassing amount of time trying to figure out why my code using `marginal_counts` wasn't working as expected. Like @nonhermitian, I had assumed that it would respect the ordering I passed in `indices`, especially since the [documentation](https://qiskit.org/documentation/stubs/qiskit.result.marginal_counts.html) still indicates that it expects a list, not a set, for that argument.

Regarding the [notion](https://github.com/Qiskit/qiskit-terra/issues/6230#issuecomment-819827199) that the current behavior is acceptable since one can write one's own marginal_counts function if one wants a different functionality, maybe it's worth elaborating a little bit more on how I arrived here. I was overhauling and expanding on some code written by someone else, and as part of that effort, I discovered that they'd effectively written their own marginal_counts function. I said to myself "this is silly -- Qiskit provides this function, so I'll simplify things by using that." I missed the subtle difference in behavior between the two, and because I considered this simplification to be a trivial change amidst a much larger re-working of code, it took me quite a while to get to the bottom of the problem.

To me this illustrates the danger in encouraging users to write functions that are substantially the same as ones provided by Qiskit but with a "minor" twist. It is nicer to just make the built-in functions work the way users want, especially when doing so is trivial and would not require any significant breaking changes. In particular, if there is a concern that folks out there are really passing in unsorted lists to `indices` and relying on the use of `sorted()` in that line to achieve their desired behavior of marginalizing without reordering, then an additional optional argument could be added to allow users to specify whether to reorder or not, and it could default to the current behavior. The presence of this optional argument in the documentation would also help highlight the fact that the default behavior may be surprising to some users.

If no one objects, I would be happy to take a stab at writing the code I proposed in the previous paragraph.
Yeah this sounds good to me @dtmcclure

Resolving patch:
diff --git a/qiskit/__init__.py b/qiskit/__init__.py
--- a/qiskit/__init__.py
+++ b/qiskit/__init__.py
@@ -28,6 +28,8 @@
 sys.modules["qiskit._accelerate.pauli_expval"] = qiskit._accelerate.pauli_expval
 sys.modules["qiskit._accelerate.dense_layout"] = qiskit._accelerate.dense_layout
 sys.modules["qiskit._accelerate.sparse_pauli_op"] = qiskit._accelerate.sparse_pauli_op
+sys.modules["qiskit._accelerate.results"] = qiskit._accelerate.results
+
 
 # qiskit errors operator
 from qiskit.exceptions import QiskitError, MissingOptionalLibraryError
diff --git a/qiskit/result/__init__.py b/qiskit/result/__init__.py
--- a/qiskit/result/__init__.py
+++ b/qiskit/result/__init__.py
@@ -24,6 +24,7 @@
    ResultError
    Counts
    marginal_counts
+   marginal_distribution
 
 Distributions
 =============
@@ -48,6 +49,7 @@
 from .result import Result
 from .exceptions import ResultError
 from .utils import marginal_counts
+from .utils import marginal_distribution
 from .counts import Counts
 
 from .distributions.probability import ProbDistribution
diff --git a/qiskit/result/utils.py b/qiskit/result/utils.py
--- a/qiskit/result/utils.py
+++ b/qiskit/result/utils.py
@@ -10,6 +10,8 @@
 # copyright notice, and modified files need to carry a notice indicating
 # that they have been altered from the originals.
 
+# pylint: disable=c-extension-no-member
+
 """Utility functions for working with Results."""
 
 from typing import List, Union, Optional, Dict
@@ -18,8 +20,15 @@
 
 from qiskit.exceptions import QiskitError
 from qiskit.result.result import Result
+from qiskit.result.counts import Counts
+from qiskit.result.distributions.probability import ProbDistribution
+from qiskit.result.distributions.quasi import QuasiDistribution
+
 from qiskit.result.postprocess import _bin_to_hex, _hex_to_bin
 
+# pylint: disable=import-error, no-name-in-module
+from qiskit._accelerate import results as results_rs
+
 
 def marginal_counts(
     result: Union[dict, Result],
@@ -118,10 +127,54 @@ def _adjust_creg_sizes(creg_sizes, indices):
     return new_creg_sizes
 
 
+def marginal_distribution(
+    counts: dict, indices: Optional[List[int]] = None, format_marginal: bool = False
+) -> Dict[str, int]:
+    """Marginalize counts from an experiment over some indices of interest.
+
+    Unlike :func:`~.marginal_counts` this function respects the order of
+    the input ``indices``. If the input ``indices`` list is specified, the order
+    the bit indices will be the output order of the bitstrings
+    in the marginalized output.
+
+    Args:
+        counts: result to be marginalized
+        indices: The bit positions of interest
+            to marginalize over. If ``None`` (default), do not marginalize at all.
+        format_marginal: Default: False. If True, takes the output of
+            marginalize and formats it with placeholders between cregs and
+            for non-indices.
+    Returns:
+        dict(str, int): A marginalized dictionary
+    Raises:
+        QiskitError: If any value in ``indices`` is invalid or the ``counts`` dict
+        is invalid.
+    """
+    num_clbits = len(max(counts.keys()).replace(" ", ""))
+    if indices is not None and (not indices or not set(indices).issubset(range(num_clbits))):
+        raise QiskitError(f"indices must be in range [0, {num_clbits - 1}].")
+
+    if isinstance(counts, Counts):
+        res = results_rs.marginal_counts(counts, indices)
+    elif isinstance(counts, (ProbDistribution, QuasiDistribution)):
+        res = results_rs.marginal_distribution(counts, indices)
+    else:
+        first_value = next(iter(counts.values()))
+        if isinstance(first_value, int):
+            res = results_rs.marginal_counts(counts, indices)
+        elif isinstance(first_value, float):
+            res = results_rs.marginal_distribution(counts, indices)
+        else:
+            raise QiskitError("Values of counts must be an int or float")
+
+    if format_marginal and indices is not None:
+        return _format_marginal(counts, res, indices)
+    return res
+
+
 def _marginalize(counts, indices=None):
     """Get the marginal counts for the given set of indices"""
     num_clbits = len(next(iter(counts)).replace(" ", ""))
-
     # Check if we do not need to marginalize and if so, trim
     # whitespace and '_' and return
     if (indices is None) or set(range(num_clbits)) == set(indices):


Test patch:
diff --git a/test/python/result/test_counts.py b/test/python/result/test_counts.py
--- a/test/python/result/test_counts.py
+++ b/test/python/result/test_counts.py
@@ -43,6 +43,13 @@ def test_marginal_counts(self):
         result = utils.marginal_counts(counts_obj, [0, 1])
         self.assertEqual(expected, result)
 
+    def test_marginal_distribution(self):
+        raw_counts = {"0x0": 4, "0x1": 7, "0x2": 10, "0x6": 5, "0x9": 11, "0xD": 9, "0xE": 8}
+        expected = {"00": 4, "01": 27, "10": 23}
+        counts_obj = counts.Counts(raw_counts, creg_sizes=[["c0", 4]], memory_slots=4)
+        result = utils.marginal_distribution(counts_obj, [0, 1])
+        self.assertEqual(expected, result)
+
     def test_int_outcomes(self):
         raw_counts = {"0x0": 21, "0x2": 12, "0x3": 5, "0x2E": 265}
         expected = {0: 21, 2: 12, 3: 5, 46: 265}
@@ -90,6 +97,13 @@ def test_marginal_int_counts(self):
         result = utils.marginal_counts(counts_obj, [0, 1])
         self.assertEqual(expected, result)
 
+    def test_marginal_distribution_int_counts(self):
+        raw_counts = {0: 4, 1: 7, 2: 10, 6: 5, 9: 11, 13: 9, 14: 8}
+        expected = {"00": 4, "01": 27, "10": 23}
+        counts_obj = counts.Counts(raw_counts, creg_sizes=[["c0", 4]], memory_slots=4)
+        result = utils.marginal_distribution(counts_obj, [0, 1])
+        self.assertEqual(expected, result)
+
     def test_int_outcomes_with_int_counts(self):
         raw_counts = {0: 21, 2: 12, 3: 5, 46: 265}
         counts_obj = counts.Counts(raw_counts)
@@ -139,6 +153,13 @@ def test_marginal_bitstring_counts(self):
         result = utils.marginal_counts(counts_obj, [0, 1])
         self.assertEqual(expected, result)
 
+    def test_marginal_distribution_bitstring_counts(self):
+        raw_counts = {"0": 4, "1": 7, "10": 10, "110": 5, "1001": 11, "1101": 9, "1110": 8}
+        expected = {"00": 4, "01": 27, "10": 23}
+        counts_obj = counts.Counts(raw_counts, creg_sizes=[["c0", 4]], memory_slots=4)
+        result = utils.marginal_distribution(counts_obj, [0, 1])
+        self.assertEqual(expected, result)
+
     def test_int_outcomes_with_bitstring_counts(self):
         raw_counts = {"0": 21, "10": 12, "11": 5, "101110": 265}
         expected = {0: 21, 2: 12, 3: 5, 46: 265}
@@ -268,6 +289,21 @@ def test_marginal_0b_string_counts(self):
         result = utils.marginal_counts(counts_obj, [0, 1])
         self.assertEqual(expected, result)
 
+    def test_marginal_distribution_0b_string_counts(self):
+        raw_counts = {
+            "0b0": 4,
+            "0b1": 7,
+            "0b10": 10,
+            "0b110": 5,
+            "0b1001": 11,
+            "0b1101": 9,
+            "0b1110": 8,
+        }
+        expected = {"00": 4, "01": 27, "10": 23}
+        counts_obj = counts.Counts(raw_counts, creg_sizes=[["c0", 4]], memory_slots=4)
+        result = utils.marginal_distribution(counts_obj, [0, 1])
+        self.assertEqual(expected, result)
+
     def test_int_outcomes_with_0b_bitstring_counts(self):
         raw_counts = {"0b0": 21, "0b10": 12, "0b11": 5, "0b101110": 265}
         expected = {0: 21, 2: 12, 3: 5, 46: 265}
diff --git a/test/python/result/test_result.py b/test/python/result/test_result.py
--- a/test/python/result/test_result.py
+++ b/test/python/result/test_result.py
@@ -16,6 +16,7 @@
 
 from qiskit.result import models
 from qiskit.result import marginal_counts
+from qiskit.result import marginal_distribution
 from qiskit.result import Result
 from qiskit.qobj import QobjExperimentHeader
 from qiskit.test import QiskitTestCase
@@ -181,6 +182,34 @@ def test_marginal_counts(self):
         self.assertEqual(marginal_counts(result.get_counts(), [0, 1]), expected_marginal_counts)
         self.assertEqual(marginal_counts(result.get_counts(), [1, 0]), expected_marginal_counts)
 
+    def test_marginal_distribution(self):
+        """Test that counts are marginalized correctly."""
+        raw_counts = {"0x0": 4, "0x1": 7, "0x2": 10, "0x6": 5, "0x9": 11, "0xD": 9, "0xE": 8}
+        data = models.ExperimentResultData(counts=raw_counts)
+        exp_result_header = QobjExperimentHeader(creg_sizes=[["c0", 4]], memory_slots=4)
+        exp_result = models.ExperimentResult(
+            shots=54, success=True, data=data, header=exp_result_header
+        )
+        result = Result(results=[exp_result], **self.base_result_args)
+        expected_marginal_counts = {"00": 4, "01": 27, "10": 23}
+        expected_reverse = {"00": 4, "10": 27, "01": 23}
+
+        self.assertEqual(
+            marginal_distribution(result.get_counts(), [0, 1]), expected_marginal_counts
+        )
+        self.assertEqual(marginal_distribution(result.get_counts(), [1, 0]), expected_reverse)
+        # test with register spacing, bitstrings are in form of "00 00" for register split
+        data = models.ExperimentResultData(counts=raw_counts)
+        exp_result_header = QobjExperimentHeader(creg_sizes=[["c0", 2], ["c1", 2]], memory_slots=4)
+        exp_result = models.ExperimentResult(
+            shots=54, success=True, data=data, header=exp_result_header
+        )
+        result = Result(results=[exp_result], **self.base_result_args)
+        self.assertEqual(
+            marginal_distribution(result.get_counts(), [0, 1]), expected_marginal_counts
+        )
+        self.assertEqual(marginal_distribution(result.get_counts(), [1, 0]), expected_reverse)
+
     def test_marginal_counts_result(self):
         """Test that a Result object containing counts marginalizes correctly."""
         raw_counts_1 = {"0x0": 4, "0x1": 7, "0x2": 10, "0x6": 5, "0x9": 11, "0xD": 9, "0xE": 8}