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

76 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-02 12:44 +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: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true

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