Source code for pytket.utils.outcomearray

# Copyright Quantinuum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""`OutcomeArray` class and associated methods."""

# Needed for sphinx to set up type alias for ArrayLike
from __future__ import annotations

import operator
from collections import Counter
from functools import reduce
from typing import TYPE_CHECKING, Any, cast

import numpy as np
import numpy.typing as npt

if TYPE_CHECKING:
    from collections.abc import Sequence

    from numpy.typing import ArrayLike


[docs] class OutcomeArray(np.ndarray): """ Array of measured outcomes from qubits. Derived class of :py:class:`numpy.ndarray`. Bitwise outcomes are compressed into unsigned 8-bit integers, each representing up to 8 qubit measurements. Each row is a repeat measurement. :param width: Number of bit entries stored, less than or equal to the bit capacity of the array. :param n_outcomes: Number of outcomes stored. """ def __new__(cls, input_array: npt.ArrayLike, width: int) -> OutcomeArray: # Input array is an already formed ndarray instance # We first cast to be our class type obj = np.asarray(input_array).view(cls) # add the new attribute to the created instance if len(obj.shape) != 2 or obj.dtype != np.uint8: # noqa: PLR2004 raise ValueError( "OutcomeArray must be a two dimensional array of dtype uint8." ) bitcapacity = obj.shape[-1] * 8 if width > bitcapacity: raise ValueError( f"Width {width} is larger than maxium bitlength of " f"array: {bitcapacity}." ) obj._width = width # noqa: SLF001 # Finally, we must return the newly created object: return obj def __array_finalize__(self, obj: Any, *args: Any, **kwargs: Any) -> None: # see InfoArray.__array_finalize__ for comments if obj is None: return self._width: int | None = getattr(obj, "_width", None) @property def width(self) -> int: """Number of bit entries stored, less than or equal to the bit capacity of the array.""" assert type(self._width) is int return self._width @property def n_outcomes(self) -> Any: """Number of outcomes stored.""" return self.shape[0] # A numpy ndarray is explicitly unhashable (its __hash__ has type None). But as we # are dealing with integral arrays only it makes sense to define a hash. def __hash__(self) -> int: # type: ignore return hash((self.tobytes(), self.width)) def __eq__(self, other: object) -> bool: if isinstance(other, OutcomeArray): return bool(np.array_equal(self, other) and self.width == other.width) return False
[docs] @classmethod def from_readouts(cls, readouts: ArrayLike) -> OutcomeArray: """Create OutcomeArray from a 2D array like object of read-out integers, e.g. [[1, 1, 0], [0, 1, 1]]""" readouts_ar = np.array(readouts, dtype=int) return cls(np.packbits(readouts_ar, axis=-1), readouts_ar.shape[-1])
[docs] def to_readouts(self) -> np.ndarray: """Convert OutcomeArray to a 2D array of readouts, each row a separate outcome and each column a bit value.""" return cast( "np.ndarray", np.asarray(np.unpackbits(self, axis=-1))[..., : self.width] )
[docs] def to_readout(self) -> np.ndarray: """Convert a singleton to a single readout (1D array)""" if self.n_outcomes > 1: raise ValueError(f"Not a singleton: {self.n_outcomes} readouts") return cast("np.ndarray", self.to_readouts()[0])
[docs] def to_intlist(self, big_endian: bool = True) -> list[int]: """Express each outcome as an integer corresponding to the bit values. :param big_endian: whether to use big endian encoding (or little endian if False), defaults to True :return: List of integers, each corresponding to an outcome. """ if big_endian: array = self else: array = OutcomeArray.from_readouts(np.fliplr(self.to_readouts())) bitcapacity = array.shape[-1] * 8 intify = lambda bytear: reduce( operator.or_, (int(num) << (8 * i) for i, num in enumerate(bytear[::-1])), 0 ) >> (bitcapacity - array.width) intar = np.apply_along_axis(intify, -1, array) return list(intar)
[docs] @classmethod def from_ints( cls, ints: Sequence[int], width: int, big_endian: bool = True ) -> OutcomeArray: """Create OutcomeArray from iterator of integers corresponding to outcomes where the bitwise representation of the integer corresponds to the readouts. :param ints: Iterable of outcome integers :param width: Number of qubit measurements :param big_endian: whether to use big endian encoding (or little endian if False), defaults to True :return: OutcomeArray instance """ n_ints = len(ints) bitstrings = ( bin(int_val)[2:].zfill(width)[:: (-1) ** (not big_endian)] for int_val in ints ) bitar = np.frombuffer( "".join(bitstrings).encode("ascii"), dtype=np.uint8, count=n_ints * width ) - ord("0") bitar.resize((n_ints, width)) return cls.from_readouts(bitar)
[docs] def counts(self) -> Counter[OutcomeArray]: """Calculate counts of outcomes in OutcomeArray :return: Counter of outcome, number of instances """ ars, count_vals = np.unique(self, axis=0, return_counts=True) width = self.width oalist = [OutcomeArray(x[None, :], width) for x in ars] return Counter(dict(zip(oalist, count_vals, strict=False)))
[docs] def choose_indices(self, indices: list[int]) -> OutcomeArray: """Permute ordering of bits in outcomes or choose subset of bits. e.g. [1, 0, 2] acting on a bitstring of length 4 swaps bit locations 0 & 1, leaves 2 in the same place and deletes location 3. :param indices: New locations for readout bits. :return: New array corresponding to given permutation. """ return OutcomeArray.from_readouts(self.to_readouts()[..., indices])
[docs] def to_dict(self) -> dict[str, Any]: """Return a JSON serializable dictionary representation of the OutcomeArray. :return: JSON serializable dictionary """ return {"width": self.width, "array": self.tolist()}
[docs] @classmethod def from_dict(cls, ar_dict: dict[str, Any]) -> OutcomeArray: """Create an OutcomeArray from JSON serializable dictionary (as created by `to_dict`). :param dict: Dictionary representation of OutcomeArray. :return: Instance of OutcomeArray """ return OutcomeArray( np.array(ar_dict["array"], dtype=np.uint8), width=ar_dict["width"] )
[docs] def readout_counts( ctr: Counter[OutcomeArray], ) -> Counter[tuple[int, ...]]: """Convert counts from :py:class:`~.OutcomeArray` types to tuples of ints.""" return Counter({tuple(map(int, oa.to_readout())): int(n) for oa, n in ctr.items()})