Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/outcomearray.py: 88%
76 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-02 12:44 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-02 12:44 +0000
1# Copyright Quantinuum
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""`OutcomeArray` class and associated methods."""
17# Needed for sphinx to set up type alias for ArrayLike
18from __future__ import annotations
20import operator
21from collections import Counter
22from functools import reduce
23from typing import TYPE_CHECKING, Any, cast
25import numpy as np
26import numpy.typing as npt
28if TYPE_CHECKING: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 from collections.abc import Sequence
31 from numpy.typing import ArrayLike
34class OutcomeArray(np.ndarray):
35 """
36 Array of measured outcomes from qubits. Derived class of :py:class:`numpy.ndarray`.
38 Bitwise outcomes are compressed into unsigned 8-bit integers, each
39 representing up to 8 qubit measurements. Each row is a repeat measurement.
41 :param width: Number of bit entries stored, less than or equal to the bit
42 capacity of the array.
43 :param n_outcomes: Number of outcomes stored.
44 """
46 def __new__(cls, input_array: npt.ArrayLike, width: int) -> OutcomeArray:
47 # Input array is an already formed ndarray instance
48 # We first cast to be our class type
49 obj = np.asarray(input_array).view(cls)
50 # add the new attribute to the created instance
51 if len(obj.shape) != 2 or obj.dtype != np.uint8: # noqa: PLR2004 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true
52 raise ValueError(
53 "OutcomeArray must be a two dimensional array of dtype uint8."
54 )
55 bitcapacity = obj.shape[-1] * 8
56 if width > bitcapacity: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 raise ValueError(
58 f"Width {width} is larger than maxium bitlength of "
59 f"array: {bitcapacity}."
60 )
61 obj._width = width # noqa: SLF001
62 # Finally, we must return the newly created object:
63 return obj
65 def __array_finalize__(self, obj: Any, *args: Any, **kwargs: Any) -> None:
66 # see InfoArray.__array_finalize__ for comments
67 if obj is None:
68 return
69 self._width: int | None = getattr(obj, "_width", None)
71 @property
72 def width(self) -> int:
73 """Number of bit entries stored, less than or equal to the bit capacity of the
74 array."""
75 assert type(self._width) is int
76 return self._width
78 @property
79 def n_outcomes(self) -> Any:
80 """Number of outcomes stored."""
81 return self.shape[0]
83 # A numpy ndarray is explicitly unhashable (its __hash__ has type None). But as we
84 # are dealing with integral arrays only it makes sense to define a hash.
85 def __hash__(self) -> int: # type: ignore
86 return hash((self.tobytes(), self.width))
88 def __eq__(self, other: object) -> bool:
89 if isinstance(other, OutcomeArray): 89 ↛ 91line 89 didn't jump to line 91 because the condition on line 89 was always true
90 return bool(np.array_equal(self, other) and self.width == other.width)
91 return False
93 @classmethod
94 def from_readouts(cls, readouts: ArrayLike) -> OutcomeArray:
95 """Create OutcomeArray from a 2D array like object of read-out integers,
96 e.g. [[1, 1, 0], [0, 1, 1]]"""
97 readouts_ar = np.array(readouts, dtype=int)
98 return cls(np.packbits(readouts_ar, axis=-1), readouts_ar.shape[-1])
100 def to_readouts(self) -> np.ndarray:
101 """Convert OutcomeArray to a 2D array of readouts, each row a separate outcome
102 and each column a bit value."""
103 return cast(
104 "np.ndarray", np.asarray(np.unpackbits(self, axis=-1))[..., : self.width]
105 )
107 def to_readout(self) -> np.ndarray:
108 """Convert a singleton to a single readout (1D array)"""
109 if self.n_outcomes > 1: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 raise ValueError(f"Not a singleton: {self.n_outcomes} readouts")
111 return cast("np.ndarray", self.to_readouts()[0])
113 def to_intlist(self, big_endian: bool = True) -> list[int]:
114 """Express each outcome as an integer corresponding to the bit values.
116 :param big_endian: whether to use big endian encoding (or little endian
117 if False), defaults to True
118 :return: List of integers, each corresponding to an outcome.
119 """
120 if big_endian:
121 array = self
122 else:
123 array = OutcomeArray.from_readouts(np.fliplr(self.to_readouts()))
124 bitcapacity = array.shape[-1] * 8
125 intify = lambda bytear: reduce(
126 operator.or_, (int(num) << (8 * i) for i, num in enumerate(bytear[::-1])), 0
127 ) >> (bitcapacity - array.width)
128 intar = np.apply_along_axis(intify, -1, array)
129 return list(intar)
131 @classmethod
132 def from_ints(
133 cls, ints: Sequence[int], width: int, big_endian: bool = True
134 ) -> OutcomeArray:
135 """Create OutcomeArray from iterator of integers corresponding to outcomes
136 where the bitwise representation of the integer corresponds to the readouts.
138 :param ints: Iterable of outcome integers
139 :param width: Number of qubit measurements
140 :param big_endian: whether to use big endian encoding (or little endian
141 if False), defaults to True
142 :return: OutcomeArray instance
143 """
144 n_ints = len(ints)
145 bitstrings = (
146 bin(int_val)[2:].zfill(width)[:: (-1) ** (not big_endian)]
147 for int_val in ints
148 )
149 bitar = np.frombuffer(
150 "".join(bitstrings).encode("ascii"), dtype=np.uint8, count=n_ints * width
151 ) - ord("0")
152 bitar.resize((n_ints, width))
153 return cls.from_readouts(bitar)
155 def counts(self) -> Counter[OutcomeArray]:
156 """Calculate counts of outcomes in OutcomeArray
158 :return: Counter of outcome, number of instances
159 """
160 ars, count_vals = np.unique(self, axis=0, return_counts=True)
161 width = self.width
162 oalist = [OutcomeArray(x[None, :], width) for x in ars]
163 return Counter(dict(zip(oalist, count_vals, strict=False)))
165 def choose_indices(self, indices: list[int]) -> OutcomeArray:
166 """Permute ordering of bits in outcomes or choose subset of bits.
167 e.g. [1, 0, 2] acting on a bitstring of length 4 swaps bit locations 0 & 1,
168 leaves 2 in the same place and deletes location 3.
170 :param indices: New locations for readout bits.
171 :return: New array corresponding to given permutation.
172 """
173 return OutcomeArray.from_readouts(self.to_readouts()[..., indices])
175 def to_dict(self) -> dict[str, Any]:
176 """Return a JSON serializable dictionary representation of the OutcomeArray.
178 :return: JSON serializable dictionary
179 """
180 return {"width": self.width, "array": self.tolist()}
182 @classmethod
183 def from_dict(cls, ar_dict: dict[str, Any]) -> OutcomeArray:
184 """Create an OutcomeArray from JSON serializable dictionary (as created by
185 `to_dict`).
187 :param dict: Dictionary representation of OutcomeArray.
188 :return: Instance of OutcomeArray
189 """
190 return OutcomeArray(
191 np.array(ar_dict["array"], dtype=np.uint8), width=ar_dict["width"]
192 )
195def readout_counts(
196 ctr: Counter[OutcomeArray],
197) -> Counter[tuple[int, ...]]:
198 """Convert counts from :py:class:`~.OutcomeArray` types to tuples of ints."""
199 return Counter({tuple(map(int, oa.to_readout())): int(n) for oa, n in ctr.items()})