Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/outcomearray.py: 91%
74 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-09 15:08 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-09 15:08 +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."""
17import operator
18from collections import Counter
19from collections.abc import Sequence
20from functools import reduce
21from typing import Any, cast
23import numpy as np
24import numpy.typing as npt
25from numpy.typing import ArrayLike
28class OutcomeArray(np.ndarray):
29 """
30 Array of measured outcomes from qubits. Derived class of `numpy.ndarray`.
32 Bitwise outcomes are compressed into unsigned 8-bit integers, each
33 representing up to 8 qubit measurements. Each row is a repeat measurement.
35 :param width: Number of bit entries stored, less than or equal to the bit
36 capacity of the array.
37 :type width: int
38 :param n_outcomes: Number of outcomes stored.
39 :type n_outcomes: int
40 """
42 def __new__(cls, input_array: npt.ArrayLike, width: int) -> "OutcomeArray":
43 # Input array is an already formed ndarray instance
44 # We first cast to be our class type
45 obj = np.asarray(input_array).view(cls)
46 # add the new attribute to the created instance
47 if len(obj.shape) != 2 or obj.dtype != np.uint8: # noqa: PLR2004 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true
48 raise ValueError(
49 "OutcomeArray must be a two dimensional array of dtype uint8."
50 )
51 bitcapacity = obj.shape[-1] * 8
52 if width > bitcapacity: 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 raise ValueError(
54 f"Width {width} is larger than maxium bitlength of "
55 f"array: {bitcapacity}."
56 )
57 obj._width = width # noqa: SLF001
58 # Finally, we must return the newly created object:
59 return obj
61 def __array_finalize__(self, obj: Any, *args: Any, **kwargs: Any) -> None:
62 # see InfoArray.__array_finalize__ for comments
63 if obj is None:
64 return
65 self._width: int | None = getattr(obj, "_width", None)
67 @property
68 def width(self) -> int:
69 """Number of bit entries stored, less than or equal to the bit capacity of the
70 array."""
71 assert type(self._width) is int
72 return self._width
74 @property
75 def n_outcomes(self) -> Any:
76 """Number of outcomes stored."""
77 return self.shape[0]
79 # A numpy ndarray is explicitly unhashable (its __hash__ has type None). But as we
80 # are dealing with integral arrays only it makes sense to define a hash.
81 def __hash__(self) -> int: # type: ignore
82 return hash((self.tobytes(), self.width))
84 def __eq__(self, other: object) -> bool:
85 if isinstance(other, OutcomeArray): 85 ↛ 87line 85 didn't jump to line 87 because the condition on line 85 was always true
86 return bool(np.array_equal(self, other) and self.width == other.width)
87 return False
89 @classmethod
90 def from_readouts(cls, readouts: ArrayLike) -> "OutcomeArray":
91 """Create OutcomeArray from a 2D array like object of read-out integers,
92 e.g. [[1, 1, 0], [0, 1, 1]]"""
93 readouts_ar = np.array(readouts, dtype=int)
94 return cls(np.packbits(readouts_ar, axis=-1), readouts_ar.shape[-1])
96 def to_readouts(self) -> np.ndarray:
97 """Convert OutcomeArray to a 2D array of readouts, each row a separate outcome
98 and each column a bit value."""
99 return cast(
100 "np.ndarray", np.asarray(np.unpackbits(self, axis=-1))[..., : self.width]
101 )
103 def to_readout(self) -> np.ndarray:
104 """Convert a singleton to a single readout (1D array)"""
105 if self.n_outcomes > 1: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 raise ValueError(f"Not a singleton: {self.n_outcomes} readouts")
107 return cast("np.ndarray", self.to_readouts()[0])
109 def to_intlist(self, big_endian: bool = True) -> list[int]:
110 """Express each outcome as an integer corresponding to the bit values.
112 :param big_endian: whether to use big endian encoding (or little endian
113 if False), defaults to True
114 :type big_endian: bool, optional
115 :return: List of integers, each corresponding to an outcome.
116 :rtype: List[int]
117 """
118 if big_endian:
119 array = self
120 else:
121 array = OutcomeArray.from_readouts(np.fliplr(self.to_readouts()))
122 bitcapacity = array.shape[-1] * 8
123 intify = lambda bytear: reduce(
124 operator.or_, (int(num) << (8 * i) for i, num in enumerate(bytear[::-1])), 0
125 ) >> (bitcapacity - array.width)
126 intar = np.apply_along_axis(intify, -1, array)
127 return list(intar)
129 @classmethod
130 def from_ints(
131 cls, ints: Sequence[int], width: int, big_endian: bool = True
132 ) -> "OutcomeArray":
133 """Create OutcomeArray from iterator of integers corresponding to outcomes
134 where the bitwise representation of the integer corresponds to the readouts.
136 :param ints: Iterable of outcome integers
137 :type ints: Iterable[int]
138 :param width: Number of qubit measurements
139 :type width: int
140 :param big_endian: whether to use big endian encoding (or little endian
141 if False), defaults to True
142 :type big_endian: bool, optional
143 :return: OutcomeArray instance
144 :rtype: OutcomeArray
145 """
146 n_ints = len(ints)
147 bitstrings = (
148 bin(int_val)[2:].zfill(width)[:: (-1) ** (not big_endian)]
149 for int_val in ints
150 )
151 bitar = np.frombuffer(
152 "".join(bitstrings).encode("ascii"), dtype=np.uint8, count=n_ints * width
153 ) - ord("0")
154 bitar.resize((n_ints, width))
155 return cls.from_readouts(bitar)
157 def counts(self) -> Counter["OutcomeArray"]:
158 """Calculate counts of outcomes in OutcomeArray
160 :return: Counter of outcome, number of instances
161 :rtype: Counter[OutcomeArray]
162 """
163 ars, count_vals = np.unique(self, axis=0, return_counts=True)
164 width = self.width
165 oalist = [OutcomeArray(x[None, :], width) for x in ars]
166 return Counter(dict(zip(oalist, count_vals, strict=False)))
168 def choose_indices(self, indices: list[int]) -> "OutcomeArray":
169 """Permute ordering of bits in outcomes or choose subset of bits.
170 e.g. [1, 0, 2] acting on a bitstring of length 4 swaps bit locations 0 & 1,
171 leaves 2 in the same place and deletes location 3.
173 :param indices: New locations for readout bits.
174 :type indices: List[int]
175 :return: New array corresponding to given permutation.
176 :rtype: OutcomeArray
177 """
178 return OutcomeArray.from_readouts(self.to_readouts()[..., indices])
180 def to_dict(self) -> dict[str, Any]:
181 """Return a JSON serializable dictionary representation of the OutcomeArray.
183 :return: JSON serializable dictionary
184 :rtype: Dict[str, Any]
185 """
186 return {"width": self.width, "array": self.tolist()}
188 @classmethod
189 def from_dict(cls, ar_dict: dict[str, Any]) -> "OutcomeArray":
190 """Create an OutcomeArray from JSON serializable dictionary (as created by
191 `to_dict`).
193 :param dict: Dictionary representation of OutcomeArray.
194 :type indices: Dict[str, Any]
195 :return: Instance of OutcomeArray
196 :rtype: OutcomeArray
197 """
198 return OutcomeArray(
199 np.array(ar_dict["array"], dtype=np.uint8), width=ar_dict["width"]
200 )
203def readout_counts(
204 ctr: Counter[OutcomeArray],
205) -> Counter[tuple[int, ...]]:
206 """Convert counts from :py:class:`OutcomeArray` types to tuples of ints."""
207 return Counter({tuple(map(int, oa.to_readout())): int(n) for oa, n in ctr.items()})