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

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 

17import operator 

18from collections import Counter 

19from collections.abc import Sequence 

20from functools import reduce 

21from typing import Any, cast 

22 

23import numpy as np 

24import numpy.typing as npt 

25from numpy.typing import ArrayLike 

26 

27 

28class OutcomeArray(np.ndarray): 

29 """ 

30 Array of measured outcomes from qubits. Derived class of `numpy.ndarray`. 

31 

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

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

34 

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

41 

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 

60 

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) 

66 

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 

73 

74 @property 

75 def n_outcomes(self) -> Any: 

76 """Number of outcomes stored.""" 

77 return self.shape[0] 

78 

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

83 

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 

88 

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

95 

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 ) 

102 

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

108 

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

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

111 

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) 

128 

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. 

135 

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) 

156 

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

158 """Calculate counts of outcomes in OutcomeArray 

159 

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

167 

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. 

172 

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

179 

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

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

182 

183 :return: JSON serializable dictionary 

184 :rtype: Dict[str, Any] 

185 """ 

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

187 

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

192 

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 ) 

201 

202 

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