• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Qiskit / qiskit / 22357311496

24 Feb 2026 03:21PM UTC coverage: 87.695% (-0.3%) from 87.945%
22357311496

Pull #15684

github

web-flow
Merge 32623f7f6 into 3bba182dd
Pull Request #15684: Add Custom Gates/Instructions in Rust

153 of 324 new or added lines in 14 files covered. (47.22%)

3592 existing lines in 166 files now uncovered.

100321 of 114398 relevant lines covered (87.69%)

1147851.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.75
/qiskit/primitives/containers/bindings_array.py
1
# This code is part of Qiskit.
2
#
3
# (C) Copyright IBM 2024.
4
#
5
# This code is licensed under the Apache License, Version 2.0. You may
6
# obtain a copy of this license in the LICENSE.txt file in the root directory
7
# of this source tree or at https://www.apache.org/licenses/LICENSE-2.0.
8
#
9
# Any modifications or derivative works of this code must retain this
10
# copyright notice, and modified files need to carry a notice indicating
11
# that they have been altered from the originals.
12

13
"""
14
Bindings array class
15
"""
16
from __future__ import annotations
1✔
17

18

19
from collections.abc import Mapping
1✔
20
from collections.abc import Iterable, Mapping as _Mapping
1✔
21
from itertools import chain, islice
1✔
22

23
import numpy as np
1✔
24
from numpy.typing import ArrayLike
1✔
25

26
from qiskit.circuit import Parameter, QuantumCircuit
1✔
27

28
from .shape import ShapedMixin, ShapeInput, shape_tuple
1✔
29

30
# Public API classes
31
__all__ = ["BindingsArrayLike", "ParameterLike"]
1✔
32

33

34
ParameterLike = Parameter | str
1✔
35
"""A parameter or parameter name."""
1✔
36

37
BindingsArrayLike = Mapping[ParameterLike | tuple[ParameterLike, ...], ArrayLike]
1✔
38
"""A mapping of numeric bindings for circuit parameters.
1✔
39

40
This allows array values for single or multi-dimensional sweeps over parameter values.
41

42
The mapping keys can be single or tuple of Parameter or parameter name strings.
43
The values can be a float for a single parameter an N-dimensional array like where
44
the array's last dimension indexes the circuit parameters, with its leading dimensions
45
representing the array shape of its parameter bindings.
46

47
Examples:
48
- 0-d array (single binding): ``{"a": 4, ("b", "c"): [5, 6]}``
49
- Single array (last index for parameters): ``{ParameterVector("a", 100): np.ones((10, 10, 100)))``
50
- Multiple arrays (last index for parameters, flexible dimensions):
51
  ``{("c", "a"): np.ones((10, 10, 2)), "b": np.zeros((10, 10))}``
52
"""
53

54

55
class BindingsArray(ShapedMixin):
1✔
56
    r"""Stores parameter binding value sets for a :class:`qiskit.QuantumCircuit`.
57

58
    A single parameter binding set provides numeric values to bind to a circuit with free
59
    :class:`qiskit.circuit.Parameter`\s. An instance of this class stores an array-valued collection
60
    of such sets. The simplest example is a 0-d array consisting of a single parameter binding set,
61
    whereas an n-d array of parameter binding sets represents an n-d sweep over values.
62

63
    The storage format is a dictionary of arrays attached to parameters,
64
    ``{params_0: values_0,...}``. A convention is used where the last dimension of each array
65
    indexes (a subset of) circuit parameters. For example, if the last dimension of ``values_0`` is
66
    25, then it represents an array of possible binding values for the 25 distinct parameters
67
    ``params_0``, where its leading shape is the array :attr:`~.shape` of its binding array. This
68
    allows flexibility about whether values for different parameters are stored in one big array, or
69
    across several smaller arrays.
70

71
    .. plot::
72
       :include-source:
73
       :nofigs:
74

75
       import numpy as np
76
       from qiskit.primitives import BindingsArray
77

78
       # 0-d array (i.e. only one binding)
79
       BindingsArray({"a": 4, ("b", "c"): [5, 6]})
80

81
       # single array, last index is parameters
82
       parameters = tuple(f"a{idx}" for idx in range(100))
83
       BindingsArray({parameters: np.ones((10, 10, 100))})
84

85
       # multiple arrays, where each last index is parameters. notice that it's smart enough to
86
       # figure out that a missing last dimension corresponds to a single parameter.
87
       BindingsArray(
88
           {("c", "a"): np.zeros((10, 10, 2)), "b": np.ones((10, 10))}
89
       )
90
    """
91

92
    def __init__(
1✔
93
        self,
94
        data: BindingsArrayLike | None = None,
95
        shape: ShapeInput | None = None,
96
    ):
97
        r"""
98
        Initialize a :class:`~.BindingsArray`.
99

100
        The ``shape`` argument does not need to be provided whenever it can unambiguously
101
        be inferred from the provided arrays. Ambiguity arises whenever the key of an entry of
102
        ``data`` contains only one parameter and the corresponding array's shape ends in a one.
103
        In this case, it can't be decided whether that one is an index over parameters, or whether
104
        it should be incorporated in :attr:`~shape`.
105

106
        Since :class:`~.Parameter` objects are only allowed to represent float values, this
107
        class casts all given values to float. If an incompatible dtype is given, such as complex
108
        numbers, a ``TypeError`` will be raised.
109

110
        Args:
111
            data: A mapping from one or more parameters to arrays of values to bind
112
                them to, where the last axis is over parameters.
113
            shape: The leading shape of every array in these bindings.
114

115
        Raises:
116
            ValueError: If all inputs are ``None``.
117
            ValueError: If the shape cannot be automatically inferred from the arrays, or if there
118
                is some inconsistency in the shape of the given arrays.
119
            TypeError: If some of the values can't be cast to a float type.
120
        """
121
        super().__init__()
1✔
122

123
        if data is None:
1✔
124
            self._data = {}
1✔
125
        else:
126
            self._data = {
1✔
127
                (
128
                    _format_key((key,)) if isinstance(key, (Parameter, str)) else _format_key(key)
129
                ): np.asarray(val, dtype=float)
130
                for key, val in data.items()
131
            }
132

133
        self._shape = _infer_shape(self._data) if shape is None else shape_tuple(shape)
1✔
134
        self._num_parameters = None
1✔
135

136
        self.validate()
1✔
137

138
    def __getitem__(self, args) -> BindingsArray:
1✔
139
        # because the parameters live on the last axis, we don't need to do anything special to
140
        # accommodate them because there will always be an implicit slice(None, None, None)
141
        # on all unspecified trailing dimensions
142
        # separately, we choose to not disallow args which touch the last dimension, even though it
143
        # would not be a particularly friendly way to chop parameters
144
        data = {params: val[args] for params, val in self._data.items()}
1✔
145
        try:
1✔
146
            shape = next(iter(data.values())).shape[:-1]
1✔
147
        except StopIteration:
1✔
148
            shape = ()
1✔
149
        return BindingsArray(data, shape)
1✔
150

151
    def __repr__(self):
152
        descriptions = [f"shape={self.shape}", f"num_parameters={self.num_parameters}"]
153
        if self.num_parameters:
154
            names = list(islice(map(repr, chain.from_iterable(map(_format_key, self._data))), 5))
155
            if len(names) < self.num_parameters:
156
                names.append("...")
157
            descriptions.append(f"parameters=[{', '.join(names)}]")
158
        return f"{type(self).__name__}(<{', '.join(descriptions)}>)"
159

160
    @property
1✔
161
    def data(self) -> dict[tuple[str, ...], np.ndarray]:
1✔
162
        """The keyword values of this array."""
163
        return self._data
1✔
164

165
    @property
1✔
166
    def num_parameters(self) -> int:
1✔
167
        """The total number of parameters."""
168
        if self._num_parameters is None:
1✔
169
            self._num_parameters = sum(val.shape[-1] for val in self._data.values())
1✔
170
        return self._num_parameters
1✔
171

172
    def as_array(self, parameters: Iterable[ParameterLike] | None = None) -> np.ndarray:
1✔
173
        """Return the contents of this bindings array as a single NumPy array.
174

175
        The parameters are indexed along the last dimension of the returned array.
176

177
        Parameters:
178
            parameters: Optional parameters that determine the order of the output.
179

180
        Returns:
181
            This bindings array as a single NumPy array.
182

183
        Raises:
184
            ValueError: If ``parameters`` are provided, but do not match those found in ``data``.
185
        """
186
        position = 0
1✔
187
        ret = np.empty(shape_tuple(self.shape, self.num_parameters))
1✔
188

189
        if parameters is None:
1✔
190
            # preserve the order of both the dict and the parameters in the keys
191
            for arr in self.data.values():
1✔
192
                size = arr.shape[-1]
1✔
193
                ret[..., position : position + size] = arr
1✔
194
                position += size
1✔
195
        else:
196
            # use the order of the provided parameters
197
            parameters = list(parameters)
1✔
198
            if len(parameters) != self.num_parameters:
1✔
199
                raise ValueError(
1✔
200
                    f"Expected {self.num_parameters} parameters but {len(parameters)} received."
201
                )
202

203
            # If we make it through the following loop without a KeyError, we will know that the
204
            # data parameters are a subset of the given parameters. However, the above check
205
            # ensures there are at least as many of them as parameters. Thus we will know that
206
            # set(parameters) == set(chain(*data.values())).
207
            idx_lookup = {_param_name(parameter): idx for idx, parameter in enumerate(parameters)}
1✔
208
            for arr_params, arr in self.data.items():
1✔
209
                try:
1✔
210
                    idxs = [idx_lookup[_param_name(param)] + position for param in arr_params]
1✔
211
                except KeyError as ex:
1✔
212
                    missing = next(p for p in map(_param_name, arr_params) if p not in idx_lookup)
1✔
213
                    raise ValueError(f"Could not find placement for parameter '{missing}'.") from ex
1✔
214
                ret[..., idxs] = arr
1✔
215

216
        return ret
1✔
217

218
    def bind(self, circuit: QuantumCircuit, loc: tuple[int, ...]) -> QuantumCircuit:
1✔
219
        """Return a new circuit bound to the values at the provided index.
220

221
        Args:
222
            circuit: The circuit to bind.
223
            loc: A tuple of indices, one for each dimension of this array.
224

225
        Returns:
226
            The bound circuit.
227

228
        Raises:
229
            ValueError: If the index doesn't have the right number of values.
230
        """
231
        if len(loc) != self.ndim:
1✔
UNCOV
232
            raise ValueError(f"Expected {loc} to index all dimensions of {self.shape}.")
×
233

234
        parameters = {
1✔
235
            param: val
236
            for params, vals in self._data.items()
237
            for param, val in zip(params, vals[loc])
238
        }
239
        return circuit.assign_parameters(parameters)
1✔
240

241
    def bind_all(self, circuit: QuantumCircuit) -> np.ndarray:
1✔
242
        """Return an object array of bound circuits with the same shape.
243

244
        Args:
245
            circuit: The circuit to bind.
246

247
        Returns:
248
            An object array of the same shape containing all bound circuits.
249
        """
250
        arr = np.empty(self.shape, dtype=object)
1✔
251
        for idx in np.ndindex(self.shape):
1✔
252
            arr[idx] = self.bind(circuit, idx)
1✔
253
        return arr
1✔
254

255
    def ravel(self) -> BindingsArray:
1✔
256
        """Return a new :class:`~BindingsArray` with one dimension.
257

258
        The returned bindings array has a :attr:`shape` given by ``(size, )``, where the size is the
259
        :attr:`~size` of this bindings array.
260

261
        Returns:
262
            A new bindings array.
263
        """
264
        return self.reshape(self.size)
1✔
265

266
    def reshape(self, *shape: int | Iterable[int]) -> BindingsArray:
1✔
267
        """Return a new :class:`~BindingsArray` with a different shape.
268

269
        This results in a new view of the same arrays.
270

271
        Args:
272
            shape: The shape of the returned bindings array.
273

274
        Returns:
275
            A new bindings array.
276

277
        Raises:
278
            ValueError: If the provided shape has a different product than the current size.
279
        """
280
        shape = shape_tuple(shape)
1✔
281
        if any(dim < 0 for dim in shape):
1✔
282
            # to reliably catch the ValueError, we need to manually deal with negative values
283
            positive_size = np.prod([dim for dim in shape if dim >= 0], dtype=int)
1✔
284
            missing_dim = self.size // positive_size
1✔
285
            shape = tuple(dim if dim >= 0 else missing_dim for dim in shape)
1✔
286

287
        if np.prod(shape, dtype=int) != self.size:
1✔
UNCOV
288
            raise ValueError("Reshaping cannot change the total number of elements.")
×
289

290
        data = {ps: val.reshape(shape + val.shape[-1:]) for ps, val in self._data.items()}
1✔
291
        return BindingsArray(data, shape=shape)
1✔
292

293
    @classmethod
1✔
294
    def coerce(cls, bindings_array: BindingsArrayLike) -> BindingsArray:
1✔
295
        """Coerce an input that is :class:`~BindingsArrayLike` into a new :class:`~BindingsArray`.
296

297
        Args:
298
            bindings_array: An object to be bindings array.
299

300
        Returns:
301
            A new bindings array.
302
        """
303
        if bindings_array is None:
1✔
304
            bindings_array = cls()
1✔
305
        elif isinstance(bindings_array, _Mapping):
1✔
306
            bindings_array = cls(data=bindings_array)
1✔
307
        elif isinstance(bindings_array, BindingsArray):
1✔
308
            return bindings_array
1✔
309
        else:
UNCOV
310
            raise TypeError(f"Unsupported type {type(bindings_array)} is given.")
×
311
        return bindings_array
1✔
312

313
    def validate(self):
1✔
314
        """Validate the consistency in bindings_array."""
315
        for parameters, val in self._data.items():
1✔
316
            val = self._data[parameters] = _standardize_shape(val, self._shape)
1✔
317
            if len(parameters) != val.shape[-1]:
1✔
318
                raise ValueError(
1✔
319
                    f"Length of {parameters} inconsistent with last dimension of {val}"
320
                )
321

322

323
def _standardize_shape(val: np.ndarray, shape: tuple[int, ...]) -> np.ndarray:
1✔
324
    """Return ``val`` or ``val[..., None]``.
325

326
    Args:
327
        val: The array whose shape to standardize.
328
        shape: The shape to standardize to.
329

330
    Returns:
331
        An array with one more dimension than ``len(shape)``, and whose leading dimensions match
332
        ``shape``.
333

334
    Raises:
335
        ValueError: If the leading shape of ``val`` does not match the ``shape``.
336
    """
337
    if val.shape == shape:
1✔
338
        val = val[..., None]
1✔
339
    elif val.ndim - 1 != len(shape) or val.shape[:-1] != shape:
1✔
340
        raise ValueError(f"Array with shape {val.shape} inconsistent with {shape}")
1✔
341
    return val
1✔
342

343

344
def _infer_shape(data: dict[tuple[Parameter, ...], np.ndarray]) -> tuple[int, ...]:
1✔
345
    """Return a shape tuple that consistently defines the leading dimensions of all arrays.
346

347
    Args:
348
        data: A mapping from tuples to arrays, where the length of each tuple should match the
349
            last dimension of the corresponding array.
350

351
    Returns:
352
        A shape tuple that matches the leading dimension of every array.
353

354
    Raises:
355
        ValueError: If this cannot be done unambiguously.
356
    """
357
    only_possible_shapes = None
1✔
358

359
    def examine_array(*possible_shapes):
1✔
360
        nonlocal only_possible_shapes
361
        if only_possible_shapes is None:
1✔
362
            only_possible_shapes = set(possible_shapes)
1✔
363
        else:
364
            only_possible_shapes.intersection_update(possible_shapes)
1✔
365

366
    for parameters, val in data.items():
1✔
367
        if len(parameters) != 1:
1✔
368
            # the last dimension _has_ to be over parameters
369
            examine_array(val.shape[:-1])
1✔
370
        elif val.shape and val.shape[-1] == 1:
1✔
371
            # this case is a convention, and separated from the previous case for clarity:
372
            # if the last axis is 1-d, make an assumption that it is for our 1 parameter
373
            examine_array(val.shape[:-1])
1✔
374
        else:
375
            # otherwise, the user has left off the last axis and we'll be nice to them
376
            examine_array(val.shape)
1✔
377

378
    if only_possible_shapes is None:
1✔
379
        return ()
1✔
380
    if len(only_possible_shapes) == 1:
1✔
381
        return next(iter(only_possible_shapes))
1✔
382
    elif len(only_possible_shapes) == 0:
1✔
383
        raise ValueError("Could not find any consistent shape.")
1✔
UNCOV
384
    raise ValueError(
×
385
        "Could not unambiguously determine the intended shape, all shapes in "
386
        f"{only_possible_shapes} are consistent with the input; specify shape manually."
387
    )
388

389

390
def _format_key(key: tuple[Parameter | str, ...]):
1✔
391
    return tuple(map(_param_name, key))
1✔
392

393

394
def _param_name(param: Parameter | str) -> str:
1✔
395
    return param.name if isinstance(param, Parameter) else param
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc