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

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

16import operator 

17from collections import Counter 

18from collections.abc import Sequence 

19from functools import reduce 

20from typing import Any, cast 

21 

22import numpy as np 

23import numpy.typing as npt 

24from numpy.typing import ArrayLike 

25 

26 

27class OutcomeArray(np.ndarray): 

28 """ 

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

30 

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

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

33 

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

40 

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 

59 

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) 

65 

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 

72 

73 @property 

74 def n_outcomes(self) -> Any: 

75 """Number of outcomes stored.""" 

76 return self.shape[0] 

77 

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

82 

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 

87 

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

94 

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 ) 

101 

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

107 

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

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

110 

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) 

127 

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. 

134 

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) 

155 

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

157 """Calculate counts of outcomes in OutcomeArray 

158 

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

166 

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. 

171 

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

178 

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

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

181 

182 :return: JSON serializable dictionary 

183 :rtype: Dict[str, Any] 

184 """ 

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

186 

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

191 

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 ) 

200 

201 

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