Repository: Qiskit/qiskit

Issue:
BindingsArray and ObservablesArray reshape have wrong signature
### Environment

- **Qiskit Terra version**: 1.0
- **Python version**: All
- **Operating system**: All


### What is happening?

Making an issue to track that this gets fixed. The reshape method of BindingsArray and ObservablesArray is currently

```python
def reshape(self, shape: int | Iterable[int])
```

which is inconsistent with the more general signature of `BitArray.reshape` which is

```python
def reshape(self, *shape: ShapeInput)
```

### How can we reproduce the issue?

N/A

### What should happen?

N/A

### Any suggestions?

These methods need to be updated so they all take the same `(*ShapeInput)` args before 1.0 is released.


Discussion:

Resolving patch:
diff --git a/qiskit/primitives/containers/bindings_array.py b/qiskit/primitives/containers/bindings_array.py
--- a/qiskit/primitives/containers/bindings_array.py
+++ b/qiskit/primitives/containers/bindings_array.py
@@ -169,72 +169,6 @@ def vals(self) -> list[np.ndarray]:
         """The non-keyword values of this array."""
         return self._vals
 
-    def as_array(self, parameters: Iterable[Parameter | str] | None = None) -> np.ndarray:
-        """Return the contents of this bindings array as a single NumPy array.
-
-        As with each :attr:`~vals` and :attr:`~kwvals` array, the parameters are indexed along the
-        last dimension.
-
-        The order of the :attr:`~vals` portion of this bindings array is always preserved, and
-        always comes first in the returned array, irrespective of whether ``parameters`` are
-        provided.
-
-        If ``parameters`` are provided, then they determine the order of any :attr:`~kwvals`
-        present in this bindings array. If :attr:`~vals` are present in addition to :attr:`~kwvals`,
-        then they appear before the :attr:`~kwvals` always.
-
-        Parameters:
-            parameters: Optional parameters that determine the order of the output.
-
-        Returns:
-            This bindings array as a single NumPy array.
-
-        Raises:
-            RuntimeError: If these bindings contain multiple dtypes.
-            ValueError: If ``parameters`` are provided, but do not match those found in ``kwvals``.
-        """
-        dtypes = {arr.dtype for arr in self.vals}
-        dtypes.update(arr.dtype for arr in self.kwvals.values())
-        if len(dtypes) > 1:
-            raise RuntimeError(f"Multiple dtypes ({dtypes}) were found.")
-        dtype = next(iter(dtypes)) if dtypes else float
-
-        if self.num_parameters == 0 and not self.shape:
-            # we want this special case to look like a single binding on no parameters
-            return np.empty((1, 0), dtype=dtype)
-
-        ret = np.empty(shape_tuple(self.shape, self.num_parameters), dtype=dtype)
-
-        # always start by placing the vals in the returned array
-        pos = 0
-        for arr in self.vals:
-            size = arr.shape[-1]
-            ret[..., pos : pos + size] = arr
-            pos += size
-
-        if parameters is None:
-            # preserve the order of the kwvals
-            for arr in self.kwvals.values():
-                size = arr.shape[-1]
-                ret[..., pos : pos + size] = arr
-                pos += size
-        elif self.kwvals:
-            # use the order of the provided parameters
-            parameters = list(parameters)
-            if len(parameters) != (num_kwval := sum(arr.shape[-1] for arr in self.kwvals.values())):
-                raise ValueError(f"Expected {num_kwval} parameters but {len(parameters)} received.")
-
-            idx_lookup = {_param_name(parameter): idx for idx, parameter in enumerate(parameters)}
-            for arr_params, arr in self.kwvals.items():
-                try:
-                    idxs = [idx_lookup[_param_name(param)] + pos for param in arr_params]
-                except KeyError as ex:
-                    missing = next(p for p in map(_param_name, arr_params) if p not in idx_lookup)
-                    raise ValueError(f"Could not find placement for parameter '{missing}'.") from ex
-                ret[..., idxs] = arr
-
-        return ret
-
     def bind(self, circuit: QuantumCircuit, loc: tuple[int, ...]) -> QuantumCircuit:
         """Return a new circuit bound to the values at the provided index.
 
@@ -290,7 +224,7 @@ def ravel(self) -> BindingsArray:
         """
         return self.reshape(self.size)
 
-    def reshape(self, shape: int | Iterable[int]) -> BindingsArray:
+    def reshape(self, *shape: int | Iterable[int]) -> BindingsArray:
         """Return a new :class:`~BindingsArray` with a different shape.
 
         This results in a new view of the same arrays.
@@ -304,12 +238,19 @@ def reshape(self, shape: int | Iterable[int]) -> BindingsArray:
         Raises:
             ValueError: If the provided shape has a different product than the current size.
         """
-        shape = (shape, -1) if isinstance(shape, int) else (*shape, -1)
-        if np.prod(shape[:-1]).astype(int) != self.size:
+        shape = shape_tuple(shape)
+        if any(dim < 0 for dim in shape):
+            # to reliably catch the ValueError, we need to manually deal with negative values
+            positive_size = np.prod([dim for dim in shape if dim >= 0], dtype=int)
+            missing_dim = self.size // positive_size
+            shape = tuple(dim if dim >= 0 else missing_dim for dim in shape)
+
+        if np.prod(shape, dtype=int) != self.size:
             raise ValueError("Reshaping cannot change the total number of elements.")
-        vals = [val.reshape(shape) for val in self._vals]
-        kwvals = {params: val.reshape(shape) for params, val in self._kwvals.items()}
-        return BindingsArray(vals, kwvals, shape[:-1])
+
+        vals = [val.reshape(shape + val.shape[-1:]) for val in self._vals]
+        kwvals = {ps: val.reshape(shape + val.shape[-1:]) for ps, val in self._kwvals.items()}
+        return BindingsArray(vals, kwvals, shape=shape)
 
     @classmethod
     def coerce(cls, bindings_array: BindingsArrayLike) -> BindingsArray:
diff --git a/qiskit/primitives/containers/observables_array.py b/qiskit/primitives/containers/observables_array.py
--- a/qiskit/primitives/containers/observables_array.py
+++ b/qiskit/primitives/containers/observables_array.py
@@ -28,7 +28,7 @@
 from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp
 
 from .object_array import object_array
-from .shape import ShapedMixin
+from .shape import ShapedMixin, shape_tuple
 
 BasisObservable = Mapping[str, complex]
 """Representation type of a single observable."""
@@ -119,7 +119,7 @@ def __getitem__(self, args):
             return item
         return ObservablesArray(item, copy=False, validate=False)
 
-    def reshape(self, shape: int | Iterable[int]) -> ObservablesArray:
+    def reshape(self, *shape: int | Iterable[int]) -> ObservablesArray:
         """Return a new array with a different shape.
 
         This results in a new view of the same arrays.
@@ -130,6 +130,7 @@ def reshape(self, shape: int | Iterable[int]) -> ObservablesArray:
         Returns:
             A new array.
         """
+        shape = shape_tuple(*shape)
         return ObservablesArray(self._array.reshape(shape), copy=False, validate=False)
 
     def ravel(self) -> ObservablesArray:


Test patch:
diff --git a/test/python/primitives/containers/test_bindings_array.py b/test/python/primitives/containers/test_bindings_array.py
--- a/test/python/primitives/containers/test_bindings_array.py
+++ b/test/python/primitives/containers/test_bindings_array.py
@@ -77,95 +77,6 @@ def test_repr(self):
             ).startswith("BindingsArray")
         )
 
-    def test_as_array_dtype_raises(self):
-        """Test as_array() raises when multiple dtypes are present."""
-        ba = BindingsArray([np.empty((50, 5), dtype=float), np.empty((50, 2), dtype=int)])
-        with self.assertRaises(RuntimeError):
-            ba.as_array()
-
-    @ddt.idata([(True, True), (True, False), (False, True), (False, False)])
-    @ddt.unpack
-    def test_as_array_bad_param_raises(self, kwvals_str, args_str):
-        """Test as_array() raises when a parameter key is missing."""
-        kwval_param = lambda param: Parameter(param) if kwvals_str else param
-        args_param = lambda param: Parameter(param) if args_str else param
-
-        ba = BindingsArray(kwvals={(kwval_param("a"), kwval_param("b")): np.empty((5, 2))})
-        with self.assertRaisesRegex(ValueError, "Expected 2 parameters but 1 received"):
-            ba.as_array([args_param("b")])
-
-        ba = BindingsArray(kwvals={(kwval_param("a"), kwval_param("b")): np.empty((5, 2))})
-        with self.assertRaisesRegex(ValueError, "Expected 2 parameters but 3 received"):
-            ba.as_array([args_param("b"), args_param("a"), args_param("b")])
-
-        with self.assertRaisesRegex(ValueError, "Could not find placement for parameter 'a'"):
-            ba.as_array([args_param("b"), args_param("c")])
-
-    def test_as_array_vals_only(self):
-        """Test as_array() works when only vals are present."""
-        # empty
-        ba = BindingsArray(shape=())
-        np.testing.assert_allclose(ba.as_array(), np.array([[]]))
-
-        # one array
-        arr = np.linspace(0, 20, 250).reshape((25, 5, 2))
-        ba = BindingsArray(arr)
-        np.testing.assert_allclose(ba.as_array(), arr)
-
-        # two arrays
-        arr_a = np.linspace(0, 20, 250).reshape((25, 5, 2))
-        arr_b = np.linspace(0, 5, 1000).reshape((25, 5, 8))
-        ba = BindingsArray([arr_a, arr_b])
-        np.testing.assert_allclose(ba.as_array(), np.concatenate([arr_a, arr_b], axis=2))
-
-    @ddt.idata([(True, True), (True, False), (False, True), (False, False)])
-    @ddt.unpack
-    def test_as_array_kwvals_only(self, kwvals_str, args_str):
-        """Test as_array() works when only kwvals are present."""
-        kwval_param = lambda param: Parameter(param) if kwvals_str else param
-        args_param = lambda param: Parameter(param) if args_str else param
-
-        arr_a = np.linspace(0, 20, 250).reshape((25, 5, 2))
-        arr_b = np.linspace(0, 5, 375).reshape((25, 5, 3))
-        ba = BindingsArray(
-            kwvals={
-                (kwval_param("a"), kwval_param("b")): arr_a,
-                (kwval_param("c"), kwval_param("d"), kwval_param("e")): arr_b,
-            }
-        )
-        np.testing.assert_allclose(ba.as_array(), np.concatenate([arr_a, arr_b], axis=2))
-
-        params = map(args_param, "dabec")
-        expected = [arr_b[..., 1], arr_a[..., 0], arr_a[..., 1], arr_b[..., 2], arr_b[..., 0]]
-        expected = np.concatenate([arr[..., None] for arr in expected], axis=2)
-        np.testing.assert_allclose(ba.as_array(params), expected)
-
-    @ddt.idata([(True, True), (True, False), (False, True), (False, False)])
-    @ddt.unpack
-    def test_as_array_mixed_vals(self, kwvals_str, args_str):
-        """Test as_array() works when both vals and kwvals are present."""
-        kwval_param = lambda param: Parameter(param) if kwvals_str else param
-        args_param = lambda param: Parameter(param) if args_str else param
-
-        arr_a = np.linspace(0, 20, 375).reshape((25, 5, 3))
-        arr_b = np.linspace(0, 5, 250).reshape((25, 5, 2))
-        arr_c = np.linspace(0, 5, 375).reshape((25, 5, 3))
-        ba = BindingsArray(
-            arr_a,
-            kwvals={
-                (kwval_param("a"), kwval_param("b")): arr_b,
-                (kwval_param("c"), kwval_param("d"), kwval_param("e")): arr_c,
-            },
-        )
-
-        expected = np.concatenate([arr_a, arr_b, arr_c], axis=2)
-        np.testing.assert_allclose(ba.as_array(), expected)
-
-        params = map(args_param, "dabec")
-        expected = [arr_c[..., 1], arr_b[..., 0], arr_b[..., 1], arr_c[..., 2], arr_c[..., 0]]
-        expected = np.concatenate([arr_a] + [arr[..., None] for arr in expected], axis=2)
-        np.testing.assert_allclose(ba.as_array(params), expected)
-
     def test_bind_at_idx(self):
         """Test binding at a specified index"""
         vals = np.linspace(0, 1, 1000).reshape((5, 4, 50))
@@ -257,9 +168,8 @@ def test_ravel(self):
 
     def test_reshape(self):
         """Test reshape"""
-        vals = np.linspace(0, 1, 300).reshape((2, 3, 50))
-
         with self.subTest("reshape"):
+            vals = np.linspace(0, 1, 300).reshape((2, 3, 50))
             ba = BindingsArray(vals)
             reshape_ba = ba.reshape((3, 2))
             self.assertEqual(reshape_ba.num_parameters, 50)
@@ -270,17 +180,17 @@ def test_reshape(self):
             reshape_vals = vals.reshape((3, 2, 50))
             np.testing.assert_allclose(reshape_ba.vals, reshape_vals.reshape((1, 3, 2, 50)))
 
-            circuit = self.circuit
-            bound_circuits = reshape_ba.bind_all(circuit)
-            self.assertEqual(bound_circuits.shape, (3, 2))
-            self.assertEqual(bound_circuits[0, 0], circuit.assign_parameters(reshape_vals[0, 0]))
-            self.assertEqual(bound_circuits[0, 1], circuit.assign_parameters(reshape_vals[0, 1]))
-            self.assertEqual(bound_circuits[1, 0], circuit.assign_parameters(reshape_vals[1, 0]))
-            self.assertEqual(bound_circuits[1, 1], circuit.assign_parameters(reshape_vals[1, 1]))
-            self.assertEqual(bound_circuits[2, 0], circuit.assign_parameters(reshape_vals[2, 0]))
-            self.assertEqual(bound_circuits[2, 1], circuit.assign_parameters(reshape_vals[2, 1]))
+            bound_circuits = list(reshape_ba.bind_all(self.circuit).ravel())
+            self.assertEqual(len(bound_circuits), 6)
+            self.assertEqual(bound_circuits[0], self.circuit.assign_parameters(reshape_vals[0, 0]))
+            self.assertEqual(bound_circuits[1], self.circuit.assign_parameters(reshape_vals[0, 1]))
+            self.assertEqual(bound_circuits[2], self.circuit.assign_parameters(reshape_vals[1, 0]))
+            self.assertEqual(bound_circuits[3], self.circuit.assign_parameters(reshape_vals[1, 1]))
+            self.assertEqual(bound_circuits[4], self.circuit.assign_parameters(reshape_vals[2, 0]))
+            self.assertEqual(bound_circuits[5], self.circuit.assign_parameters(reshape_vals[2, 1]))
 
         with self.subTest("flatten"):
+            vals = np.linspace(0, 1, 300).reshape((2, 3, 50))
             ba = BindingsArray(vals)
             reshape_ba = ba.reshape(6)
             self.assertEqual(reshape_ba.num_parameters, 50)
@@ -291,11 +201,38 @@ def test_reshape(self):
             reshape_vals = vals.reshape(-1, 50)
             np.testing.assert_allclose(reshape_ba.vals, reshape_vals.reshape((1, 6, 50)))
 
-            bound_circuits = list(reshape_ba.bind_all(self.circuit))
+            bound_circuits = list(reshape_ba.bind_all(self.circuit).ravel())
             self.assertEqual(len(bound_circuits), 6)
             for i in range(6):
                 self.assertEqual(bound_circuits[i], self.circuit.assign_parameters(reshape_vals[i]))
 
+        with self.subTest("various_formats"):
+            ba = BindingsArray(
+                [np.empty((16, 7)), np.empty((16, 3))],
+                {Parameter("a"): np.empty(16), (Parameter("b"), Parameter("c")): np.empty((16, 2))},
+            )
+
+            def various_formats(shape):
+                # call reshape with a single argument
+                yield [shape]
+                yield [(-1,) + shape[1:]]
+                yield [np.array(shape)]
+                yield [list(shape)]
+                yield [list(map(np.int64, shape))]
+                yield [tuple(map(np.int64, shape))]
+
+                # call reshape with multiple arguments
+                yield shape
+                yield np.array(shape)
+                yield list(shape)
+                yield list(map(np.int64, shape))
+                yield tuple(map(np.int64, shape))
+
+            for shape in [(16,), (4, 4), (2, 4, 2), (2, 2, 2, 2), (1, 8, 1, 2)]:
+                for input_shape in various_formats(shape):
+                    reshaped_ba = ba.reshape(input_shape)
+                    self.assertEqual(reshaped_ba.shape, shape)
+
     def test_kwvals(self):
         """Test constructor with kwvals"""
         with self.subTest("binding a single value"):
diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py
--- a/test/python/primitives/containers/test_observables_array.py
+++ b/test/python/primitives/containers/test_observables_array.py
@@ -295,15 +295,34 @@ def test_reshape(self):
         labels = np.array(bases.to_labels(), dtype=object)
         obs = ObservablesArray(qi.pauli_basis(2))
 
+        def various_formats(shape):
+            # call reshape with a single argument
+            yield [shape]
+            yield [(-1,) + shape[1:]]
+            yield [np.array(shape)]
+            yield [list(shape)]
+            yield [list(map(np.int64, shape))]
+            yield [tuple(map(np.int64, shape))]
+
+            # call reshape with multiple arguments
+            yield shape
+            yield np.array(shape)
+            yield list(shape)
+            yield list(map(np.int64, shape))
+            yield tuple(map(np.int64, shape))
+
         for shape in [(16,), (4, 4), (2, 4, 2), (2, 2, 2, 2), (1, 8, 1, 2)]:
             with self.subTest(shape):
-                obs_rs = obs.reshape(shape)
-                self.assertEqual(obs_rs.shape, shape)
-                labels_rs = labels.reshape(shape)
-                for idx in np.ndindex(shape):
-                    self.assertEqual(
-                        obs_rs[idx], {labels_rs[idx]: 1}, msg=f"failed for shape {shape}"
-                    )
+                for input_shape in various_formats(shape):
+                    obs_rs = obs.reshape(*input_shape)
+                    self.assertEqual(obs_rs.shape, shape)
+                    labels_rs = labels.reshape(shape)
+                    for idx in np.ndindex(shape):
+                        self.assertEqual(
+                            obs_rs[idx],
+                            {labels_rs[idx]: 1},
+                            msg=f"failed for shape {shape} with input format {input_shape}",
+                        )
 
     def test_validate(self):
         """Test the validate method"""