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