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

Qiskit / qiskit / 19082828935

04 Nov 2025 09:06PM UTC coverage: 88.178% (-0.009%) from 88.187%
19082828935

Pull #15208

github

web-flow
Merge 2824c324c into 56db921d1
Pull Request #15208: Add ways to iterate over the `Target` in C.

102 of 126 new or added lines in 2 files covered. (80.95%)

989 existing lines in 42 files now uncovered.

93963 of 106561 relevant lines covered (88.18%)

1143131.27 hits per line

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

96.32
/qiskit/primitives/containers/bit_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 http://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
BitArray
15
"""
16

17
from __future__ import annotations
1✔
18

19
from collections import defaultdict
1✔
20
from functools import partial
1✔
21
from itertools import chain, repeat
1✔
22
from typing import Callable, Iterable, Literal, Mapping, Sequence
1✔
23

24
import numpy as np
1✔
25
from numpy.typing import NDArray
1✔
26

27
from qiskit.exceptions import QiskitError
1✔
28
from qiskit.result import Counts, sampled_expectation_value
1✔
29

30
from .observables_array import ObservablesArray, ObservablesArrayLike
1✔
31
from .shape import ShapedMixin, ShapeInput, shape_tuple
1✔
32

33
# this lookup table tells you how many bits are 1 in each uint8 value
34
_WEIGHT_LOOKUP = np.unpackbits(np.arange(256, dtype=np.uint8).reshape(-1, 1), axis=1).sum(axis=1)
1✔
35

36

37
def _min_num_bytes(num_bits: int) -> int:
1✔
38
    """Return the minimum number of bytes needed to store ``num_bits``."""
39
    return num_bits // 8 + (num_bits % 8 > 0)
1✔
40

41

42
def _unpack(bit_array: BitArray) -> NDArray[np.uint8]:
1✔
43
    arr = np.unpackbits(bit_array.array, axis=-1, bitorder="big")
1✔
44
    arr = arr[..., -1 : -bit_array.num_bits - 1 : -1]
1✔
45
    return arr
1✔
46

47

48
def _pack(arr: NDArray[np.uint8]) -> tuple[NDArray[np.uint8], int]:
1✔
49
    arr = arr[..., ::-1]
1✔
50
    num_bits = arr.shape[-1]
1✔
51
    pad_size = -num_bits % 8
1✔
52
    if pad_size > 0:
1✔
53
        pad_width = [(0, 0)] * (arr.ndim - 1) + [(pad_size, 0)]
1✔
54
        arr = np.pad(arr, pad_width, constant_values=0)
1✔
55
    arr = np.packbits(arr, axis=-1, bitorder="big")
1✔
56
    return arr, num_bits
1✔
57

58

59
class BitArray(ShapedMixin):
1✔
60
    """Stores an array of bit values.
61

62
    This object contains a single, contiguous block of data that represents an array of bitstrings.
63
    The last axis is over packed bits, the second last axis is over shots, and the preceding axes
64
    correspond to the shape of the pub that was executed to sample these bits.
65

66
    You typically get this object back as one part of a :class:`.DataBin` accessed through
67
    a single :class:`.PubResult.data`.  Users do not typically create this class themselves, however
68
    if you have bitstring-like data in an alternate form that you would like to convert to a
69
    :class:`BitArray`, you can use one of :meth:`from_bool_array`, :meth:`from_counts` or
70
    :meth:`from_samples`.
71

72
    You can "unpack" the bitstrings into expanded array of :class:`bool` by using the
73
    :meth:`to_bool_array` method.
74

75
    This class supports the bitwise ``&`` (and), ``|`` (or), ``^`` (xor) and ``~`` (not) operators,
76
    where the binary operators act on two :class:`BitArray` instances.
77

78
    The class also supports the "indexing" syntax ``bit_array[indices]``.  These ``indices`` select
79
    a single entry, or multi-dimensional slice of entries, from the same shape as the corresponding
80
    pub.  The allowed indices match :class:`numpy.ndarray`: you can use single integers, slices
81
    (``a:b``) or Numpy arrays for each dimension, and use a tuple of items to slice multiple
82
    dimensions at once. The indexing syntax cannot be used to slice along the "shots" or "bits"
83
    axes; for these, use :meth:`slice_shots` and :meth:`slice_bits`, respectively.
84
    """
85

86
    def __init__(self, array: NDArray[np.uint8], num_bits: int):
1✔
87
        """
88
        Args:
89
            array: The ``uint8`` data array.
90
            num_bits: How many bit are in each outcome.
91

92
        Raises:
93
            TypeError: If the input is not a NumPy array with type ``numpy.uint8``.
94
            ValueError: If the input array has fewer than two axes, or the size of the last axis
95
                is not the smallest number of bytes that can contain ``num_bits``.
96
        """
97
        super().__init__()
1✔
98

99
        if not isinstance(array, np.ndarray):
1✔
100
            raise TypeError(f"Input must be a numpy.ndarray not {type(array)}")
1✔
101
        if array.dtype != np.uint8:
1✔
102
            raise TypeError(f"Input array must have dtype uint8, not {array.dtype}.")
1✔
103
        if array.ndim < 2:
1✔
104
            raise ValueError("The input array must have at least two axes.")
1✔
105
        if array.shape[-1] != (expected := _min_num_bytes(num_bits)):
1✔
106
            raise ValueError(f"The input array is expected to have {expected} bytes per shot.")
1✔
107

108
        self._array = array
1✔
109
        self._num_bits = num_bits
1✔
110
        # second last dimension is shots, last dimension is packed bits
111
        self._shape = self._array.shape[:-2]
1✔
112

113
    def _prepare_broadcastable(self, other: "BitArray") -> tuple[NDArray[np.uint8], ...]:
1✔
114
        """Validation and broadcasting of two bit arrays before element-wise binary operation."""
115
        if self.num_bits != other.num_bits:
1✔
UNCOV
116
            raise ValueError(f"'num_bits' must match in {self} and {other}.")
×
117
        self_shape = self.shape + (self.num_shots,)
1✔
118
        other_shape = other.shape + (other.num_shots,)
1✔
119
        try:
1✔
120
            shape = np.broadcast_shapes(self_shape, other_shape) + (self._array.shape[-1],)
1✔
UNCOV
121
        except ValueError as ex:
×
UNCOV
122
            raise ValueError(f"{self} and {other} are not compatible for this operation.") from ex
×
123
        return np.broadcast_to(self.array, shape), np.broadcast_to(other.array, shape)
1✔
124

125
    def __and__(self, other: "BitArray") -> "BitArray":
1✔
126
        return BitArray(np.bitwise_and(*self._prepare_broadcastable(other)), self.num_bits)
1✔
127

128
    def __eq__(self, other: "BitArray") -> bool:
1✔
129
        if (n := self.num_bits) != other.num_bits:
1✔
130
            return False
1✔
131
        arrs = [self._array, other._array]
1✔
132
        if n % 8 > 0:
1✔
133
            # ignore straggling bits on the left
134
            mask = np.array([255 >> ((-n) % 8)] + [255] * (n // 8), dtype=np.uint8)
1✔
135
            arrs = [np.bitwise_and(arr, mask) for arr in arrs]
1✔
136
        return np.array_equal(*arrs, equal_nan=False)
1✔
137

138
    def __invert__(self) -> "BitArray":
1✔
139
        return BitArray(np.bitwise_not(self._array), self.num_bits)
1✔
140

141
    def __or__(self, other: "BitArray") -> "BitArray":
1✔
142
        return BitArray(np.bitwise_or(*self._prepare_broadcastable(other)), self.num_bits)
1✔
143

144
    def __xor__(self, other: "BitArray") -> "BitArray":
1✔
145
        return BitArray(np.bitwise_xor(*self._prepare_broadcastable(other)), self.num_bits)
1✔
146

147
    def __repr__(self):
148
        desc = f"<shape={self.shape}, num_shots={self.num_shots}, num_bits={self.num_bits}>"
149
        return f"BitArray({desc})"
150

151
    def __getitem__(self, indices):
1✔
152
        if isinstance(indices, tuple):
1✔
153
            if len(indices) == self.ndim + 1:
1✔
154
                raise IndexError(
1✔
155
                    "BitArray cannot be sliced along the shots axis, use slice_shots() instead."
156
                )
157
            if len(indices) >= self.ndim + 2:
1✔
158
                raise IndexError(
1✔
159
                    "BitArray cannot be sliced along the bits axis, use slice_bits() instead."
160
                )
161
        return BitArray(self._array[indices], self.num_bits)
1✔
162

163
    @property
1✔
164
    def array(self) -> NDArray[np.uint8]:
1✔
165
        """The raw NumPy array of data."""
166
        return self._array
1✔
167

168
    @property
1✔
169
    def num_bits(self) -> int:
1✔
170
        """The number of bits in the register that this array stores data for.
171

172
        For example, a ``ClassicalRegister(5, "meas")`` would result in ``num_bits=5``.
173
        """
174
        return self._num_bits
1✔
175

176
    @property
1✔
177
    def num_shots(self) -> int:
1✔
178
        """The number of shots sampled from the register in each configuration.
179

180
        More precisely, the length of the second last axis of :attr:`~.array`.
181
        """
182
        return self._array.shape[-2]
1✔
183

184
    @staticmethod
1✔
185
    def _bytes_to_bitstring(data: bytes, num_bits: int, mask: int) -> str:
1✔
186
        val = int.from_bytes(data, "big") & mask
1✔
187
        return bin(val)[2:].zfill(num_bits)
1✔
188

189
    @staticmethod
1✔
190
    def _bytes_to_int(data: bytes, mask: int) -> int:
1✔
191
        return int.from_bytes(data, "big") & mask
1✔
192

193
    def _get_counts(
1✔
194
        self, *, loc: int | tuple[int, ...] | None, converter: Callable[[bytes], str | int]
195
    ) -> dict[str, int] | dict[int, int]:
196
        arr = self._array.reshape(-1, self._array.shape[-1]) if loc is None else self._array[loc]
1✔
197

198
        counts = defaultdict(int)
1✔
199
        for shot_row in arr:
1✔
200
            counts[converter(shot_row.tobytes())] += 1
1✔
201
        return dict(counts)
1✔
202

203
    def bitcount(self) -> NDArray[np.uint64]:
1✔
204
        """Compute the number of ones appearing in the binary representation of each shot.
205

206
        Returns:
207
            A ``numpy.uint64``-array with shape ``(*shape, num_shots)``.
208
        """
UNCOV
209
        return _WEIGHT_LOOKUP[self._array].sum(axis=-1)
×
210

211
    @staticmethod
1✔
212
    def from_bool_array(
1✔
213
        array: NDArray[np.bool_], order: Literal["big", "little"] = "big"
214
    ) -> "BitArray":
215
        """Construct a new bit array from an array of bools.
216

217
        Args:
218
            array: The array to convert, with "bitstrings" along the last axis.
219
            order: One of ``"big"`` or ``"little"``, indicating whether ``array[..., 0]``
220
                correspond to the most significant bits or the least significant bits of each
221
                bitstring, respectively.
222

223
        Returns:
224
            A new bit array.
225
        """
226
        array = np.asarray(array, dtype=bool)
1✔
227

228
        if array.ndim < 2:
1✔
UNCOV
229
            raise ValueError("Expecting at least two dimensions.")
×
230

231
        if order == "little":
1✔
232
            # np.unpackbits assumes "big"
233
            array = array[..., ::-1]
1✔
234
        elif order != "big":
1✔
235
            raise ValueError(
1✔
236
                f"unknown value for order: '{order}'. Valid values are 'big' and 'little'."
237
            )
238
        num_bits = array.shape[-1]
1✔
239
        if remainder := (-num_bits) % 8:
1✔
240
            # unpackbits pads with zeros on the wrong side with respect to what we want, so
241
            # we manually pad to the nearest byte
242
            pad = np.zeros(shape_tuple(array.shape[:-1], remainder), dtype=bool)
1✔
243
            array = np.concatenate([pad, array], axis=-1)
1✔
244

245
        return BitArray(np.packbits(array, axis=-1), num_bits=num_bits)
1✔
246

247
    @staticmethod
1✔
248
    def from_counts(
1✔
249
        counts: Mapping[str | int, int] | Iterable[Mapping[str | int, int]],
250
        num_bits: int | None = None,
251
    ) -> "BitArray":
252
        """Construct a new bit array from one or more ``Counts``-like objects.
253

254
        The ``counts`` can have keys that are (uniformly) integers, hexstrings, or bitstrings.
255
        Their values represent numbers of occurrences of that value.
256

257
        Args:
258
            counts: One or more counts-like mappings with the same number of shots.
259
            num_bits: The desired number of bits per shot. If unset, the biggest value found sets
260
                this value, with a minimum of one bit.
261

262
        Returns:
263
            A new bit array with shape ``()`` for single input counts, or ``(N,)`` for an iterable
264
            of :math:`N` counts.
265

266
        Raises:
267
            ValueError: If different mappings have different numbers of shots.
268
            ValueError: If no counts dictionaries are supplied.
269
        """
270
        if singleton := isinstance(counts, Mapping):
1✔
271
            counts = [counts]
1✔
272
        else:
273
            counts = list(counts)
1✔
274
            if not counts:
1✔
UNCOV
275
                raise ValueError("At least one counts mapping expected.")
×
276

277
        counts = [
1✔
278
            mapping.int_outcomes() if isinstance(mapping, Counts) else mapping for mapping in counts
279
        ]
280

281
        data = (v for mapping in counts for vs, count in mapping.items() for v in repeat(vs, count))
1✔
282

283
        bit_array = BitArray.from_samples(data, num_bits)
1✔
284
        if not singleton:
1✔
285
            if bit_array.num_shots % len(counts) > 0:
1✔
UNCOV
286
                raise ValueError("All of your mappings need to have the same number of shots.")
×
287
            bit_array = bit_array.reshape(len(counts), bit_array.num_shots // len(counts))
1✔
288
        return bit_array
1✔
289

290
    @staticmethod
1✔
291
    def from_samples(
1✔
292
        samples: Iterable[str] | Iterable[int], num_bits: int | None = None
293
    ) -> "BitArray":
294
        """Construct a new bit array from an iterable of bitstrings, hexstrings, or integers.
295

296
        All samples are assumed to be integers if the first one is. Strings are all assumed to be
297
        bitstrings whenever the first string doesn't start with ``"0x"``.
298

299
        Consider pairing this method with :meth:`~reshape` if your samples represent nested data.
300

301
        Args:
302
            samples: A list of bitstrings, a list of integers, or a list of hexstrings.
303
            num_bits: The desired number of bits per sample. If unset, the biggest sample provided
304
                is used to determine this value, with a minimum of one bit.
305

306
        Returns:
307
            A new bit array.
308

309
        Raises:
310
            ValueError: If no strings are given.
311
        """
312
        samples = iter(samples)
1✔
313
        try:
1✔
314
            first_sample = next(samples)
1✔
UNCOV
315
        except StopIteration as ex:
×
UNCOV
316
            raise ValueError("At least one sample is required.") from ex
×
317

318
        ints = chain([first_sample], samples)
1✔
319
        if isinstance(first_sample, str):
1✔
320
            base = 16 if first_sample.startswith("0x") else 2
1✔
321
            ints = (int(val, base=base) for val in ints)
1✔
322

323
        if num_bits is None:
1✔
324
            # we are forced to prematurely look at every iterand in this case
325
            ints = list(ints)
1✔
326
            num_bits = max(map(int.bit_length, ints))
1✔
327
            # convention: if the only value is 0, represent with one bit:
328
            if num_bits == 0:
1✔
329
                num_bits = 1
1✔
330

331
        num_bytes = _min_num_bytes(num_bits)
1✔
332
        data = b"".join(val.to_bytes(num_bytes, "big") for val in ints)
1✔
333
        array = np.frombuffer(data, dtype=np.uint8, count=len(data))
1✔
334
        return BitArray(array.reshape(-1, num_bytes), num_bits)
1✔
335

336
    def to_bool_array(self, order: Literal["big", "little"] = "big") -> NDArray[np.bool_]:
1✔
337
        """Convert this :class:`~BitArray` to a boolean array.
338

339
        Args:
340
            order: One of ``"big"`` or ``"little"``, respectively indicating whether the most significant
341
                bit or the least significant bit of each bitstring should be placed at ``[..., 0]``.
342

343
        Returns:
344
            A NumPy array of bools.
345

346
        Raises:
347
            ValueError: If the order is not one of ``"big"`` or ``"little"``.
348
        """
349
        if order not in ("big", "little"):
1✔
350
            raise ValueError(
1✔
351
                f"Invalid value for order: '{order}'. Valid values are 'big' and 'little'."
352
            )
353

354
        arr = np.unpackbits(self.array, axis=-1)[..., -self.num_bits :]
1✔
355
        if order == "little":
1✔
356
            arr = arr[..., ::-1]
1✔
357
        return arr.astype(np.bool_)
1✔
358

359
    def get_counts(self, loc: int | tuple[int, ...] | None = None) -> dict[str, int]:
1✔
360
        """Return a counts dictionary with bitstring keys.
361

362
        Args:
363
            loc: Which entry of this array to return a dictionary for. If ``None``, counts from
364
                all positions in this array are unioned together.
365

366
        Returns:
367
            A dictionary mapping bitstrings to the number of occurrences of that bitstring.
368
        """
369
        mask = 2**self.num_bits - 1
1✔
370
        converter = partial(self._bytes_to_bitstring, num_bits=self.num_bits, mask=mask)
1✔
371
        return self._get_counts(loc=loc, converter=converter)
1✔
372

373
    def get_int_counts(self, loc: int | tuple[int, ...] | None = None) -> dict[int, int]:
1✔
374
        r"""Return a counts dictionary, where bitstrings are stored as ``int``\s.
375

376
        Args:
377
            loc: Which entry of this array to return a dictionary for. If ``None``, counts from
378
                all positions in this array are unioned together.
379

380
        Returns:
381
            A dictionary mapping ``ints`` to the number of occurrences of that ``int``.
382

383
        """
384
        converter = partial(self._bytes_to_int, mask=2**self.num_bits - 1)
1✔
385
        return self._get_counts(loc=loc, converter=converter)
1✔
386

387
    def get_bitstrings(self, loc: int | tuple[int, ...] | None = None) -> list[str]:
1✔
388
        """Return a list of bitstrings.
389

390
        Args:
391
            loc: Which entry of this array to return a dictionary for. If ``None``, counts from
392
                all positions in this array are unioned together.
393

394
        Returns:
395
            A list of bitstrings.
396
        """
397
        mask = 2**self.num_bits - 1
1✔
398
        converter = partial(self._bytes_to_bitstring, num_bits=self.num_bits, mask=mask)
1✔
399
        arr = self._array.reshape(-1, self._array.shape[-1]) if loc is None else self._array[loc]
1✔
400
        return [converter(shot_row.tobytes()) for shot_row in arr]
1✔
401

402
    def reshape(self, *shape: ShapeInput) -> "BitArray":
1✔
403
        """Return a new reshaped bit array.
404

405
        The :attr:`~num_shots` axis is either included or excluded from the reshaping procedure
406
        depending on which picture the new shape is compatible with. For example, for a bit array
407
        with shape ``(20, 5)`` and ``64`` shots, a reshape to ``(100,)`` would leave the
408
        number of shots intact, whereas a reshape to ``(200, 32)`` would change the number of
409
        shots to ``32``.
410

411
        Args:
412
            *shape: The new desired shape.
413

414
        Returns:
415
            A new bit array.
416

417
        Raises:
418
            ValueError: If the size corresponding to your new shape is not equal to either
419
                :attr:`~size`, or the product of :attr:`~size` and :attr:`~num_shots`.
420
        """
421
        shape = shape_tuple(shape)
1✔
422
        if (size := np.prod(shape, dtype=int)) == self.size:
1✔
423
            shape = shape_tuple(shape, self._array.shape[-2:])
1✔
424
        elif size == self.size * self.num_shots:
1✔
425
            shape = shape_tuple(shape, self._array.shape[-1:])
1✔
426
        else:
UNCOV
427
            raise ValueError("Cannot change the size of the array.")
×
428
        return BitArray(self._array.reshape(shape), self.num_bits)
1✔
429

430
    def transpose(self, *axes) -> "BitArray":
1✔
431
        """Return a bit array with axes transposed.
432

433
        Args:
434
            axes: None, tuple of ints or n ints. See `ndarray.transpose
435
                <https://numpy.org/doc/stable/reference/generated/
436
                numpy.ndarray.transpose.html#numpy.ndarray.transpose>`_
437
                for the details.
438

439
        Returns:
440
            BitArray: A bit array with axes permuted.
441

442
        Raises:
443
            ValueError: If ``axes`` don't match this bit array.
444
            ValueError: If ``axes`` includes any indices that are out of bounds.
445
        """
446
        if len(axes) == 0:
1✔
447
            axes = tuple(reversed(range(self.ndim)))
1✔
448
        if len(axes) == 1 and isinstance(axes[0], Sequence):
1✔
449
            axes = axes[0]
1✔
450
        if len(axes) != self.ndim:
1✔
451
            raise ValueError("axes don't match bit array")
1✔
452
        for i in axes:
1✔
453
            if i >= self.ndim or self.ndim + i < 0:
1✔
454
                raise ValueError(
1✔
455
                    f"axis {i} is out of bounds for bit array of dimension {self.ndim}."
456
                )
457
        axes = tuple(i if i >= 0 else self.ndim + i for i in axes) + (-2, -1)
1✔
458
        return BitArray(self._array.transpose(axes), self.num_bits)
1✔
459

460
    def slice_bits(self, indices: int | Sequence[int]) -> "BitArray":
1✔
461
        """Return a bit array sliced along the bit axis of some indices of interest.
462

463
        .. note::
464

465
            The convention used by this method is that the index ``0`` corresponds to
466
            the least-significant bit in the :attr:`~array`, or equivalently
467
            the right-most bitstring entry as returned by
468
            :meth:`~get_counts` or :meth:`~get_bitstrings`, etc.
469

470
            If this bit array was produced by a sampler, then an index ``i`` corresponds to the
471
            :class:`~.ClassicalRegister` location ``creg[i]``.
472

473
        Args:
474
            indices: The bit positions of interest to slice along.
475

476
        Returns:
477
            A bit array sliced along the bit axis.
478

479
        Raises:
480
            IndexError: If there are any invalid indices of the bit axis.
481
        """
482
        if isinstance(indices, int):
1✔
483
            indices = (indices,)
1✔
484
        for index in indices:
1✔
485
            if index < 0 or index >= self.num_bits:
1✔
486
                raise IndexError(
1✔
487
                    f"index {index} is out of bounds for the number of bits {self.num_bits}."
488
                )
489
        # This implementation introduces a temporary 8x memory overhead due to bit
490
        # unpacking. This could be fixed using bitwise functions, at the expense of a
491
        # more complicated implementation.
492
        arr = _unpack(self)
1✔
493
        arr = arr[..., indices]
1✔
494
        arr, num_bits = _pack(arr)
1✔
495
        return BitArray(arr, num_bits)
1✔
496

497
    def slice_shots(self, indices: int | Sequence[int]) -> "BitArray":
1✔
498
        """Return a bit array sliced along the shots axis of some indices of interest.
499

500
        Args:
501
            indices: The shots positions of interest to slice along.
502

503
        Returns:
504
            A bit array sliced along the shots axis.
505

506
        Raises:
507
            IndexError: If there are any invalid indices of the shots axis.
508
        """
509
        if isinstance(indices, int):
1✔
510
            indices = (indices,)
1✔
511
        for index in indices:
1✔
512
            if index < 0 or index >= self.num_shots:
1✔
513
                raise IndexError(
1✔
514
                    f"index {index} is out of bounds for the number of shots {self.num_shots}."
515
                )
516
        arr = self._array
1✔
517
        arr = arr[..., indices, :]
1✔
518
        return BitArray(arr, self.num_bits)
1✔
519

520
    def postselect(
1✔
521
        self,
522
        indices: Sequence[int] | int,
523
        selection: Sequence[bool | int] | bool | int,
524
    ) -> BitArray:
525
        """Post-select this bit array based on sliced equality with a given bitstring.
526

527
        .. note::
528
            If this bit array contains any shape axes, it is first flattened into a long list of shots
529
            before applying post-selection. This is done because :class:`~BitArray` cannot handle
530
            ragged numbers of shots across axes.
531

532
        Args:
533
            indices: A list of the indices of the cbits on which to postselect.
534
                If this bit array was produced by a sampler, then an index ``i`` corresponds to the
535
                :class:`~.ClassicalRegister` location ``creg[i]`` (as in :meth:`~slice_bits`).
536
                Negative indices are allowed.
537

538
            selection: A list of binary values (will be cast to ``bool``) of length matching
539
                ``indices``, with ``indices[i]`` corresponding to ``selection[i]``. Shots will be
540
                discarded unless all cbits specified by ``indices`` have the values given by
541
                ``selection``.
542

543
        Returns:
544
            A new bit array with ``shape=(), num_bits=data.num_bits, num_shots<=data.num_shots``.
545

546
        Raises:
547
            IndexError: If ``max(indices)`` is greater than or equal to :attr:`num_bits`.
548
            IndexError: If ``min(indices)`` is less than negative :attr:`num_bits`.
549
            ValueError: If the lengths of ``selection`` and ``indices`` do not match.
550
        """
551
        if isinstance(indices, int):
1✔
552
            indices = (indices,)
1✔
553
        if isinstance(selection, (bool, int)):
1✔
554
            selection = (selection,)
1✔
555
        selection = np.asarray(selection, dtype=bool)
1✔
556

557
        num_indices = len(indices)
1✔
558

559
        if len(selection) != num_indices:
1✔
560
            raise ValueError("Lengths of indices and selection do not match.")
1✔
561

562
        num_bytes = self._array.shape[-1]
1✔
563
        indices = np.asarray(indices)
1✔
564

565
        if num_indices > 0:
1✔
566
            if indices.max() >= self.num_bits:
1✔
567
                raise IndexError(
1✔
568
                    f"index {int(indices.max())} out of bounds for the number of bits {self.num_bits}."
569
                )
570
            if indices.min() < -self.num_bits:
1✔
571
                raise IndexError(
1✔
572
                    f"index {int(indices.min())} out of bounds for the number of bits {self.num_bits}."
573
                )
574

575
        flattened = self.reshape((), self.size * self.num_shots)
1✔
576

577
        # If no conditions, keep all data, but flatten as promised:
578
        if num_indices == 0:
1✔
579
            return flattened
1✔
580

581
        # Make negative bit indices positive:
582
        indices %= self.num_bits
1✔
583

584
        # Handle special-case of contradictory conditions:
585
        if np.intersect1d(indices[selection], indices[np.logical_not(selection)]).size > 0:
1✔
586
            return BitArray(np.empty((0, num_bytes), dtype=np.uint8), num_bits=self.num_bits)
1✔
587

588
        # Recall that creg[0] is the LSb:
589
        byte_significance, bit_significance = np.divmod(indices, 8)
1✔
590
        # least-significant byte is at last position:
591
        byte_idx = (num_bytes - 1) - byte_significance
1✔
592
        # least-significant bit is at position 0:
593
        bit_offset = bit_significance.astype(np.uint8)
1✔
594

595
        # Get bitpacked representation of `indices` (bitmask):
596
        bitmask = np.zeros(num_bytes, dtype=np.uint8)
1✔
597
        np.bitwise_or.at(bitmask, byte_idx, np.uint8(1) << bit_offset)
1✔
598

599
        # Get bitpacked representation of `selection` (desired bitstring):
600
        selection_bytes = np.zeros(num_bytes, dtype=np.uint8)
1✔
601
        ## This assumes no contradictions present, since those were already checked for:
602
        np.bitwise_or.at(
1✔
603
            selection_bytes, byte_idx, np.asarray(selection, dtype=np.uint8) << bit_offset
604
        )
605

606
        return BitArray(
1✔
607
            flattened._array[((flattened._array & bitmask) == selection_bytes).all(axis=-1)],
608
            num_bits=self.num_bits,
609
        )
610

611
    def expectation_values(self, observables: ObservablesArrayLike) -> NDArray[np.float64]:
1✔
612
        """Compute the expectation values of the provided observables, broadcasted against
613
        this bit array.
614

615
        .. note::
616

617
            This method returns the real part of the expectation value even if
618
            the operator has complex coefficients due to the specification of
619
            :func:`~.sampled_expectation_value`.
620

621
        Args:
622
            observables: The observable(s) to take the expectation value of.
623
                Must have a shape broadcastable with with this bit array and
624
                the same number of qubits as the number of bits of this bit array.
625
                The observables must be diagonal (I, Z, 0 or 1) too.
626

627
        Returns:
628
            An array of expectation values whose shape is the broadcast shape of ``observables``
629
            and this bit array.
630

631
        Raises:
632
            ValueError: If the provided observables does not have a shape broadcastable with
633
                this bit array.
634
            ValueError: If the provided observables does not have the same number of qubits as
635
                the number of bits of this bit array.
636
            ValueError: If the provided observables are not diagonal.
637
        """
638
        observables = ObservablesArray.coerce(observables)
1✔
639
        arr_indices = np.fromiter(np.ndindex(self.shape), dtype=object).reshape(self.shape)
1✔
640
        bc_indices, bc_obs = np.broadcast_arrays(arr_indices, observables)
1✔
641
        counts = {}
1✔
642
        arr = np.zeros_like(bc_indices, dtype=float)
1✔
643
        for index in np.ndindex(bc_indices.shape):
1✔
644
            loc = bc_indices[index]
1✔
645
            for pauli, coeff in bc_obs[index].items():
1✔
646
                if loc not in counts:
1✔
647
                    counts[loc] = self.get_counts(loc)
1✔
648
                try:
1✔
649
                    expval = sampled_expectation_value(counts[loc], pauli)
1✔
650
                except QiskitError as ex:
1✔
651
                    raise ValueError(ex.message) from ex
1✔
652
                arr[index] += expval * coeff
1✔
653
        return arr
1✔
654

655
    @staticmethod
1✔
656
    def concatenate(bit_arrays: Sequence[BitArray], axis: int = 0) -> BitArray:
1✔
657
        """Join a sequence of bit arrays along an existing axis.
658

659
        Args:
660
            bit_arrays: The bit arrays must have (1) the same number of bits,
661
                (2) the same number of shots, and
662
                (3) the same shape, except in the dimension corresponding to axis
663
                (the first, by default).
664
            axis: The axis along which the arrays will be joined. Default is 0.
665

666
        Returns:
667
            The concatenated bit array.
668

669
        Raises:
670
            ValueError: If the sequence of bit arrays is empty.
671
            ValueError: If any bit arrays has a different number of bits.
672
            ValueError: If any bit arrays has a different number of shots.
673
            ValueError: If any bit arrays has a different number of dimensions.
674
        """
675
        if len(bit_arrays) == 0:
1✔
676
            raise ValueError("Need at least one bit array to concatenate")
1✔
677
        num_bits = bit_arrays[0].num_bits
1✔
678
        num_shots = bit_arrays[0].num_shots
1✔
679
        ndim = bit_arrays[0].ndim
1✔
680
        if ndim == 0:
1✔
UNCOV
681
            raise ValueError("Zero-dimensional bit arrays cannot be concatenated")
×
682
        for i, ba in enumerate(bit_arrays):
1✔
683
            if ba.num_bits != num_bits:
1✔
684
                raise ValueError(
1✔
685
                    "All bit arrays must have same number of bits, "
686
                    f"but the bit array at index 0 has {num_bits} bits "
687
                    f"and the bit array at index {i} has {ba.num_bits} bits."
688
                )
689
            if ba.num_shots != num_shots:
1✔
690
                raise ValueError(
1✔
691
                    "All bit arrays must have same number of shots, "
692
                    f"but the bit array at index 0 has {num_shots} shots "
693
                    f"and the bit array at index {i} has {ba.num_shots} shots."
694
                )
695
            if ba.ndim != ndim:
1✔
696
                raise ValueError(
1✔
697
                    "All bit arrays must have same number of dimensions, "
698
                    f"but the bit array at index 0 has {ndim} dimension(s) "
699
                    f"and the bit array at index {i} has {ba.ndim} dimension(s)."
700
                )
701
        if axis < 0 or axis >= ndim:
1✔
702
            raise ValueError(f"axis {axis} is out of bounds for bit array of dimension {ndim}.")
1✔
703
        data = np.concatenate([ba.array for ba in bit_arrays], axis=axis)
1✔
704
        return BitArray(data, num_bits)
1✔
705

706
    @staticmethod
1✔
707
    def concatenate_shots(bit_arrays: Sequence[BitArray]) -> BitArray:
1✔
708
        """Join a sequence of bit arrays along the shots axis.
709

710
        Args:
711
            bit_arrays: The bit arrays must have (1) the same number of bits,
712
                and (2) the same shape.
713

714
        Returns:
715
            The stacked bit array.
716

717
        Raises:
718
            ValueError: If the sequence of bit arrays is empty.
719
            ValueError: If any bit arrays has a different number of bits.
720
            ValueError: If any bit arrays has a different shape.
721
        """
722
        if len(bit_arrays) == 0:
1✔
723
            raise ValueError("Need at least one bit array to stack")
1✔
724
        num_bits = bit_arrays[0].num_bits
1✔
725
        shape = bit_arrays[0].shape
1✔
726
        for i, ba in enumerate(bit_arrays):
1✔
727
            if ba.num_bits != num_bits:
1✔
728
                raise ValueError(
1✔
729
                    "All bit arrays must have same number of bits, "
730
                    f"but the bit array at index 0 has {num_bits} bits "
731
                    f"and the bit array at index {i} has {ba.num_bits} bits."
732
                )
733
            if ba.shape != shape:
1✔
734
                raise ValueError(
1✔
735
                    "All bit arrays must have same shape, "
736
                    f"but the bit array at index 0 has shape {shape} "
737
                    f"and the bit array at index {i} has shape {ba.shape}."
738
                )
739
        data = np.concatenate([ba.array for ba in bit_arrays], axis=-2)
1✔
740
        return BitArray(data, num_bits)
1✔
741

742
    @staticmethod
1✔
743
    def concatenate_bits(bit_arrays: Sequence[BitArray]) -> BitArray:
1✔
744
        """Join a sequence of bit arrays along the bits axis.
745

746
        .. note::
747
            This method is equivalent to per-shot bitstring concatenation.
748

749
        Args:
750
            bit_arrays: Bit arrays that have (1) the same number of shots,
751
                and (2) the same shape.
752

753
        Returns:
754
            The stacked bit array.
755

756
        Raises:
757
            ValueError: If the sequence of bit arrays is empty.
758
            ValueError: If any bit arrays has a different number of shots.
759
            ValueError: If any bit arrays has a different shape.
760
        """
761
        if len(bit_arrays) == 0:
1✔
762
            raise ValueError("Need at least one bit array to stack")
1✔
763
        num_shots = bit_arrays[0].num_shots
1✔
764
        shape = bit_arrays[0].shape
1✔
765
        for i, ba in enumerate(bit_arrays):
1✔
766
            if ba.num_shots != num_shots:
1✔
767
                raise ValueError(
1✔
768
                    "All bit arrays must have same number of shots, "
769
                    f"but the bit array at index 0 has {num_shots} shots "
770
                    f"and the bit array at index {i} has {ba.num_shots} shots."
771
                )
772
            if ba.shape != shape:
1✔
773
                raise ValueError(
1✔
774
                    "All bit arrays must have same shape, "
775
                    f"but the bit array at index 0 has shape {shape} "
776
                    f"and the bit array at index {i} has shape {ba.shape}."
777
                )
778
        # This implementation introduces a temporary 8x memory overhead due to bit
779
        # unpacking. This could be fixed using bitwise functions, at the expense of a
780
        # more complicated implementation.
781
        data = np.concatenate([_unpack(ba) for ba in bit_arrays], axis=-1)
1✔
782
        data, num_bits = _pack(data)
1✔
783
        return BitArray(data, num_bits)
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