Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/outcomearray.py: 91%

73 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-10 11:51 +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. 

14 

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

16 

17# Needed for sphinx to set up type alias for ArrayLike 

18from __future__ import annotations 

19 

20import operator 

21from collections import Counter 

22from functools import reduce 

23from typing import TYPE_CHECKING, Any, cast 

24 

25import numpy as np 

26import numpy.typing as npt 

27 

28if TYPE_CHECKING: 

29 from collections.abc import Sequence 

30 

31 from numpy.typing import ArrayLike 

32 

33 

34class OutcomeArray(np.ndarray): 

35 """ 

36 Array of measured outcomes from qubits. Derived class of :py:class:`numpy.ndarray`. 

37 

38 Bitwise outcomes are compressed into unsigned 8-bit integers, each 

39 representing up to 8 qubit measurements. Each row is a repeat measurement. 

40 

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 """ 

45 

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 maximum 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 

64 

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) 

70 

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 

77 

78 @property 

79 def n_outcomes(self) -> Any: 

80 """Number of outcomes stored.""" 

81 return self.shape[0] 

82 

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)) 

87 

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 

92 

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]) 

99 

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 ) 

106 

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]) 

112 

113 def to_intlist(self, big_endian: bool = True) -> list[int]: 

114 """Express each outcome as an integer corresponding to the bit values. 

115 

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) 

130 

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. 

137 

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) 

154 

155 def counts(self) -> Counter[OutcomeArray]: 

156 """Calculate counts of outcomes in OutcomeArray 

157 

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))) 

164 

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. 

169 

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]) 

174 

175 def to_dict(self) -> dict[str, Any]: 

176 """Return a JSON serializable dictionary representation of the OutcomeArray. 

177 

178 :return: JSON serializable dictionary 

179 """ 

180 return {"width": self.width, "array": self.tolist()} 

181 

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`). 

186 

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 ) 

193 

194 

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()})