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

Qiskit / qiskit / 23548889554

25 Mar 2026 03:22PM UTC coverage: 87.191% (-0.04%) from 87.235%
23548889554

Pull #15836

github

web-flow
Merge 4df6d9082 into 3e8032600
Pull Request #15836: Use `np.bitwise_count` in `BitArray.bitcount`

1 of 5 new or added lines in 1 file covered. (20.0%)

1085 existing lines in 41 files now uncovered.

103626 of 118850 relevant lines covered (87.19%)

1001189.12 hits per line

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

95.39
/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 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
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 Literal
1✔
23
from collections.abc import Callable, Iterable, Mapping, Sequence
1✔
24

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

28
from qiskit import _numpy_compat
1✔
29
from qiskit.exceptions import QiskitError
1✔
30
from qiskit.result import Counts, sampled_expectation_value
1✔
31

32
from .observables_array import ObservablesArray, ObservablesArrayLike
1✔
33
from .shape import ShapedMixin, ShapeInput, shape_tuple
1✔
34

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

38

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

43

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

49

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

60

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

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

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

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

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

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

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

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

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

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

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

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

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

140
    def __invert__(self) -> BitArray:
1✔
141
        return BitArray(np.bitwise_not(self._array), self.num_bits)
1✔
142

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

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

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

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

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

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

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

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

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

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

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

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

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

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

208
        Returns:
209
            A ``numpy.uint64``-array with shape ``(*shape, num_shots)``.
210
        """
NEW
211
        if _numpy_compat.VERSION_PARTS[0] >= 2:
×
NEW
212
            bitcounts = np.bitwise_count(self._array).sum(axis=-1)
×
213
        else:
NEW
214
            bitcounts = _WEIGHT_LOOKUP[self._array].sum(axis=-1)
×
NEW
215
        return bitcounts
×
216

217
    @staticmethod
1✔
218
    def from_bool_array(
1✔
219
        array: NDArray[np.bool_], order: Literal["big", "little"] = "big"
220
    ) -> BitArray:
221
        """Construct a new bit array from an array of bools.
222

223
        Args:
224
            array: The array to convert, with "bitstrings" along the last axis.
225
            order: One of ``"big"`` or ``"little"``, indicating whether ``array[..., 0]``
226
                correspond to the most significant bits or the least significant bits of each
227
                bitstring, respectively.
228

229
        Returns:
230
            A new bit array.
231
        """
232
        array = np.asarray(array, dtype=bool)
1✔
233

234
        if array.ndim < 2:
1✔
235
            raise ValueError("Expecting at least two dimensions.")
×
236

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

251
        return BitArray(np.packbits(array, axis=-1), num_bits=num_bits)
1✔
252

253
    @staticmethod
1✔
254
    def from_counts(
1✔
255
        counts: Mapping[str | int, int] | Iterable[Mapping[str | int, int]],
256
        num_bits: int | None = None,
257
    ) -> BitArray:
258
        """Construct a new bit array from one or more ``Counts``-like objects.
259

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

263
        Args:
264
            counts: One or more counts-like mappings with the same number of shots.
265
            num_bits: The desired number of bits per shot. If unset, the biggest value found sets
266
                this value, with a minimum of one bit.
267

268
        Returns:
269
            A new bit array with shape ``()`` for single input counts, or ``(N,)`` for an iterable
270
            of :math:`N` counts.
271

272
        Raises:
273
            ValueError: If different mappings have different numbers of shots.
274
            ValueError: If no counts dictionaries are supplied.
275
        """
276
        if singleton := isinstance(counts, Mapping):
1✔
277
            counts = [counts]
1✔
278
        else:
279
            counts = list(counts)
1✔
280
            if not counts:
1✔
281
                raise ValueError("At least one counts mapping expected.")
×
282

283
        counts = [
1✔
284
            mapping.int_outcomes() if isinstance(mapping, Counts) else mapping for mapping in counts
285
        ]
286

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

289
        bit_array = BitArray.from_samples(data, num_bits)
1✔
290
        if not singleton:
1✔
291
            if bit_array.num_shots % len(counts) > 0:
1✔
292
                raise ValueError("All of your mappings need to have the same number of shots.")
×
293
            bit_array = bit_array.reshape(len(counts), bit_array.num_shots // len(counts))
1✔
294
        return bit_array
1✔
295

296
    @staticmethod
1✔
297
    def from_samples(
1✔
298
        samples: Iterable[str] | Iterable[int], num_bits: int | None = None
299
    ) -> BitArray:
300
        """Construct a new bit array from an iterable of bitstrings, hexstrings, or integers.
301

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

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

307
        Args:
308
            samples: A list of bitstrings, a list of integers, or a list of hexstrings.
309
            num_bits: The desired number of bits per sample. If unset, the biggest sample provided
310
                is used to determine this value, with a minimum of one bit.
311

312
        Returns:
313
            A new bit array.
314

315
        Raises:
316
            ValueError: If no strings are given.
317
        """
318
        samples = iter(samples)
1✔
319
        try:
1✔
320
            first_sample = next(samples)
1✔
321
        except StopIteration as ex:
×
322
            raise ValueError("At least one sample is required.") from ex
×
323

324
        ints = chain([first_sample], samples)
1✔
325
        if isinstance(first_sample, str):
1✔
326
            base = 16 if first_sample.startswith("0x") else 2
1✔
327
            ints = (int(val, base=base) for val in ints)
1✔
328

329
        if num_bits is None:
1✔
330
            # we are forced to prematurely look at every iterand in this case
331
            ints = list(ints)
1✔
332
            num_bits = max(map(int.bit_length, ints))
1✔
333
            # convention: if the only value is 0, represent with one bit:
334
            if num_bits == 0:
1✔
335
                num_bits = 1
1✔
336

337
        num_bytes = _min_num_bytes(num_bits)
1✔
338
        data = b"".join(val.to_bytes(num_bytes, "big") for val in ints)
1✔
339
        array = np.frombuffer(data, dtype=np.uint8, count=len(data))
1✔
340
        return BitArray(array.reshape(-1, num_bytes), num_bits)
1✔
341

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

345
        Args:
346
            order: One of ``"big"`` or ``"little"``, respectively indicating whether the most significant
347
                bit or the least significant bit of each bitstring should be placed at ``[..., 0]``.
348

349
        Returns:
350
            A NumPy array of bools.
351

352
        Raises:
353
            ValueError: If the order is not one of ``"big"`` or ``"little"``.
354
        """
355
        if order not in ("big", "little"):
1✔
356
            raise ValueError(
1✔
357
                f"Invalid value for order: '{order}'. Valid values are 'big' and 'little'."
358
            )
359

360
        arr = np.unpackbits(self.array, axis=-1)[..., -self.num_bits :]
1✔
361
        if order == "little":
1✔
362
            arr = arr[..., ::-1]
1✔
363
        return arr.astype(np.bool_)
1✔
364

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

368
        Args:
369
            loc: Which entry of this array to return a dictionary for. If ``None``, counts from
370
                all positions in this array are unioned together.
371

372
        Returns:
373
            A dictionary mapping bitstrings to the number of occurrences of that bitstring.
374
        """
375
        mask = 2**self.num_bits - 1
1✔
376
        converter = partial(self._bytes_to_bitstring, num_bits=self.num_bits, mask=mask)
1✔
377
        return self._get_counts(loc=loc, converter=converter)
1✔
378

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

382
        Args:
383
            loc: Which entry of this array to return a dictionary for. If ``None``, counts from
384
                all positions in this array are unioned together.
385

386
        Returns:
387
            A dictionary mapping ``ints`` to the number of occurrences of that ``int``.
388

389
        """
390
        converter = partial(self._bytes_to_int, mask=2**self.num_bits - 1)
1✔
391
        return self._get_counts(loc=loc, converter=converter)
1✔
392

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

396
        Args:
397
            loc: Which entry of this array to return a dictionary for. If ``None``, counts from
398
                all positions in this array are unioned together.
399

400
        Returns:
401
            A list of bitstrings.
402
        """
403
        mask = 2**self.num_bits - 1
1✔
404
        converter = partial(self._bytes_to_bitstring, num_bits=self.num_bits, mask=mask)
1✔
405
        arr = self._array.reshape(-1, self._array.shape[-1]) if loc is None else self._array[loc]
1✔
406
        return [converter(shot_row.tobytes()) for shot_row in arr]
1✔
407

408
    def reshape(self, *shape: ShapeInput) -> BitArray:
1✔
409
        """Return a new reshaped bit array.
410

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

417
        Args:
418
            *shape: The new desired shape.
419

420
        Returns:
421
            A new bit array.
422

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

436
    def transpose(self, *axes) -> BitArray:
1✔
437
        """Return a bit array with axes transposed.
438

439
        Args:
440
            axes: None, tuple of ints or n ints. See `ndarray.transpose
441
                <https://numpy.org/doc/stable/reference/generated/
442
                numpy.ndarray.transpose.html#numpy.ndarray.transpose>`_
443
                for the details.
444

445
        Returns:
446
            BitArray: A bit array with axes permuted.
447

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

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

469
        .. note::
470

471
            The convention used by this method is that the index ``0`` corresponds to
472
            the least-significant bit in the :attr:`~array`, or equivalently
473
            the right-most bitstring entry as returned by
474
            :meth:`~get_counts` or :meth:`~get_bitstrings`, etc.
475

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

479
        Args:
480
            indices: The bit positions of interest to slice along.
481

482
        Returns:
483
            A bit array sliced along the bit axis.
484

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

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

506
        Args:
507
            indices: The shots positions of interest to slice along.
508

509
        Returns:
510
            A bit array sliced along the shots axis.
511

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

526
    def postselect(
1✔
527
        self,
528
        indices: Sequence[int] | int,
529
        selection: Sequence[bool | int] | bool | int,
530
    ) -> BitArray:
531
        """Post-select this bit array based on sliced equality with a given bitstring.
532

533
        .. note::
534
            If this bit array contains any shape axes, it is first flattened into a long list of shots
535
            before applying post-selection. This is done because :class:`~BitArray` cannot handle
536
            ragged numbers of shots across axes.
537

538
        Args:
539
            indices: A list of the indices of the cbits on which to postselect.
540
                If this bit array was produced by a sampler, then an index ``i`` corresponds to the
541
                :class:`~.ClassicalRegister` location ``creg[i]`` (as in :meth:`~slice_bits`).
542
                Negative indices are allowed.
543

544
            selection: A list of binary values (will be cast to ``bool``) of length matching
545
                ``indices``, with ``indices[i]`` corresponding to ``selection[i]``. Shots will be
546
                discarded unless all cbits specified by ``indices`` have the values given by
547
                ``selection``.
548

549
        Returns:
550
            A new bit array with ``shape=(), num_bits=data.num_bits, num_shots<=data.num_shots``.
551

552
        Raises:
553
            IndexError: If ``max(indices)`` is greater than or equal to :attr:`num_bits`.
554
            IndexError: If ``min(indices)`` is less than negative :attr:`num_bits`.
555
            ValueError: If the lengths of ``selection`` and ``indices`` do not match.
556
        """
557
        if isinstance(indices, int):
1✔
558
            indices = (indices,)
1✔
559
        if isinstance(selection, (bool, int)):
1✔
560
            selection = (selection,)
1✔
561
        selection = np.asarray(selection, dtype=bool)
1✔
562

563
        num_indices = len(indices)
1✔
564

565
        if len(selection) != num_indices:
1✔
566
            raise ValueError("Lengths of indices and selection do not match.")
1✔
567

568
        num_bytes = self._array.shape[-1]
1✔
569
        indices = np.asarray(indices)
1✔
570

571
        if num_indices > 0:
1✔
572
            if indices.max() >= self.num_bits:
1✔
573
                raise IndexError(
1✔
574
                    f"index {int(indices.max())} out of bounds for the number of bits {self.num_bits}."
575
                )
576
            if indices.min() < -self.num_bits:
1✔
577
                raise IndexError(
1✔
578
                    f"index {int(indices.min())} out of bounds for the number of bits {self.num_bits}."
579
                )
580

581
        flattened = self.reshape((), self.size * self.num_shots)
1✔
582

583
        # If no conditions, keep all data, but flatten as promised:
584
        if num_indices == 0:
1✔
585
            return flattened
1✔
586

587
        # Make negative bit indices positive:
588
        indices %= self.num_bits
1✔
589

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

594
        # Recall that creg[0] is the LSb:
595
        byte_significance, bit_significance = np.divmod(indices, 8)
1✔
596
        # least-significant byte is at last position:
597
        byte_idx = (num_bytes - 1) - byte_significance
1✔
598
        # least-significant bit is at position 0:
599
        bit_offset = bit_significance.astype(np.uint8)
1✔
600

601
        # Get bitpacked representation of `indices` (bitmask):
602
        bitmask = np.zeros(num_bytes, dtype=np.uint8)
1✔
603
        np.bitwise_or.at(bitmask, byte_idx, np.uint8(1) << bit_offset)
1✔
604

605
        # Get bitpacked representation of `selection` (desired bitstring):
606
        selection_bytes = np.zeros(num_bytes, dtype=np.uint8)
1✔
607
        ## This assumes no contradictions present, since those were already checked for:
608
        np.bitwise_or.at(
1✔
609
            selection_bytes, byte_idx, np.asarray(selection, dtype=np.uint8) << bit_offset
610
        )
611

612
        return BitArray(
1✔
613
            flattened._array[((flattened._array & bitmask) == selection_bytes).all(axis=-1)],
614
            num_bits=self.num_bits,
615
        )
616

617
    def expectation_values(self, observables: ObservablesArrayLike) -> NDArray[np.float64]:
1✔
618
        """Compute the expectation values of the provided observables, broadcasted against
619
        this bit array.
620

621
        .. note::
622

623
            This method returns the real part of the expectation value even if
624
            the operator has complex coefficients due to the specification of
625
            :func:`~.sampled_expectation_value`.
626

627
        Args:
628
            observables: The observable(s) to take the expectation value of.
629
                Must have a shape broadcastable with this bit array and
630
                the same number of qubits as the number of bits of this bit array.
631
                The observables must be diagonal (I, Z, 0 or 1) too.
632

633
        Returns:
634
            An array of expectation values whose shape is the broadcast shape of ``observables``
635
            and this bit array.
636

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

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

665
        Args:
666
            bit_arrays: The bit arrays must have (1) the same number of bits,
667
                (2) the same number of shots, and
668
                (3) the same shape, except in the dimension corresponding to axis
669
                (the first, by default).
670
            axis: The axis along which the arrays will be joined. Default is 0.
671

672
        Returns:
673
            The concatenated bit array.
674

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

712
    @staticmethod
1✔
713
    def concatenate_shots(bit_arrays: Sequence[BitArray]) -> BitArray:
1✔
714
        """Join a sequence of bit arrays along the shots axis.
715

716
        Args:
717
            bit_arrays: The bit arrays must have (1) the same number of bits,
718
                and (2) the same shape.
719

720
        Returns:
721
            The stacked bit array.
722

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

748
    @staticmethod
1✔
749
    def concatenate_bits(bit_arrays: Sequence[BitArray]) -> BitArray:
1✔
750
        """Join a sequence of bit arrays along the bits axis.
751

752
        .. note::
753
            This method is equivalent to per-shot bitstring concatenation.
754

755
        Args:
756
            bit_arrays: Bit arrays that have (1) the same number of shots,
757
                and (2) the same shape.
758

759
        Returns:
760
            The stacked bit array.
761

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