Repository: Qiskit/qiskit

Issue:
marginal_distribution raises for NumPy array indices
### Environment

- **Qiskit Terra version**: 0.21
- **Python version**: 3.10
- **Operating system**: osx


### What is happening?

`marginal_distribution` raises error when passes a NumPy array of indices:

```python

marginal_distribution(counts, np.asarray([0, 2]))
```


--> 225 if indices is not None and (not indices or not set(indices).issubset(range(num_clbits))):
    226     raise QiskitError(f"indices must be in range [0, {num_clbits - 1}].")
    228 if isinstance(counts, Counts):

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()


### How can we reproduce the issue?

Try above

### What should happen?

It should work for both lists and arrays.

### Any suggestions?

_No response_


Discussion:
This should be easy to fix and backport for 0.21.1, we just need to change the is empty check on the input validation there to something that works with lists and numpy arrays. I'm pretty sure the python->rust boundary will work fine as is if it gets a numpy array (although it won't be as efficient as declaring an input type as a numpy array, which is easy to add support for in addition to a generic sequence input)
Yeah it is a simple fix.  Was going to do it myself but saw that I would need to modify type hints, and I draw the line at type hinting.

Resolving patch:
diff --git a/qiskit/result/utils.py b/qiskit/result/utils.py
--- a/qiskit/result/utils.py
+++ b/qiskit/result/utils.py
@@ -14,7 +14,7 @@
 
 """Utility functions for working with Results."""
 
-from typing import List, Union, Optional, Dict
+from typing import Sequence, Union, Optional, Dict, List
 from collections import Counter
 from copy import deepcopy
 
@@ -199,7 +199,9 @@ def marginal_memory(
 
 
 def marginal_distribution(
-    counts: dict, indices: Optional[List[int]] = None, format_marginal: bool = False
+    counts: dict,
+    indices: Optional[Sequence[int]] = None,
+    format_marginal: bool = False,
 ) -> Dict[str, int]:
     """Marginalize counts from an experiment over some indices of interest.
 
@@ -222,7 +224,7 @@ def marginal_distribution(
         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))):
+    if indices is not None and (len(indices) == 0 or not set(indices).issubset(range(num_clbits))):
         raise QiskitError(f"indices must be in range [0, {num_clbits - 1}].")
 
     if isinstance(counts, Counts):


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
@@ -16,6 +16,8 @@
 
 import unittest
 
+import numpy as np
+
 from qiskit.result import counts
 from qiskit import exceptions
 from qiskit.result import utils
@@ -50,6 +52,14 @@ def test_marginal_distribution(self):
         result = utils.marginal_distribution(counts_obj, [0, 1])
         self.assertEqual(expected, result)
 
+    def test_marginal_distribution_numpy_indices(self):
+        raw_counts = {"0x0": 4, "0x1": 7, "0x2": 10, "0x6": 5, "0x9": 11, "0xD": 9, "0xE": 8}
+        expected = {"00": 4, "01": 27, "10": 23}
+        indices = np.asarray([0, 1])
+        counts_obj = counts.Counts(raw_counts, creg_sizes=[["c0", 4]], memory_slots=4)
+        result = utils.marginal_distribution(counts_obj, indices)
+        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}