Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/results.py: 89%

83 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-14 10:02 +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 

15from typing import Any 

16 

17import numpy as np 

18 

19from pytket.circuit import BasisOrder 

20 

21StateTuple = tuple[int, ...] 

22CountsDict = dict[StateTuple, int | float] 

23KwargTypes = Any 

24 

25 

26class BitPermuter: 

27 """Class for permuting the bits in an integer 

28 

29 Enables inverse permuation and uses caching to speed up common uses. 

30 

31 """ 

32 

33 def __init__(self, permutation: tuple[int, ...]): 

34 """Constructor 

35 

36 :param permutation: Map from current bit index (big-endian) to its new position, 

37 encoded as a list. 

38 :type permutation: Tuple[int, ...] 

39 :raises ValueError: Input permutation is not valid complete permutation 

40 of all bits 

41 """ 

42 if sorted(permutation) != list(range(len(permutation))): 

43 raise ValueError("Permutation is not a valid complete permutation.") 

44 self.perm = tuple(permutation) 

45 self.n_bits = len(self.perm) 

46 self.int_maps: tuple[dict[int, int], dict[int, int]] = ({}, {}) 

47 

48 def permute(self, val: int, inverse: bool = False) -> int: 

49 """Return input with bit values permuted. 

50 

51 :param val: input integer 

52 :type val: int 

53 :param inverse: whether to use the inverse permutation, defaults to False 

54 :type inverse: bool, optional 

55 :return: permuted integer 

56 :rtype: int 

57 """ 

58 perm_map, other_map = self.int_maps[:: (-1) ** inverse] 

59 if val in perm_map: 59 ↛ 60line 59 didn't jump to line 60 because the condition on line 59 was never true

60 return perm_map[val] 

61 

62 res = 0 

63 for source_index, target_index in enumerate(self.perm): 

64 if inverse: 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true

65 target_index, source_index = source_index, target_index 

66 # if source bit set 

67 if val & (1 << (self.n_bits - 1 - source_index)): 

68 # set target bit 

69 res |= 1 << (self.n_bits - 1 - target_index) 

70 

71 perm_map[val] = res 

72 other_map[res] = val 

73 return res 

74 

75 def permute_all(self) -> list[int]: 

76 """Permute all integers within bit-width specified by permutation. 

77 

78 :return: List of permuted outputs. 

79 :rtype: List 

80 """ 

81 return list(map(self.permute, range(1 << self.n_bits))) 

82 

83 

84def counts_from_shot_table(shot_table: np.ndarray) -> dict[tuple[int, ...], int]: 

85 """Summarises a shot table into a dictionary of counts for each observed outcome. 

86 

87 :param shot_table: Table of shots from a pytket backend. 

88 :type shot_table: np.ndarray 

89 :return: Dictionary mapping observed readouts to the number of times observed. 

90 :rtype: Dict[Tuple[int, ...], int] 

91 """ 

92 shot_values, counts = np.unique(shot_table, axis=0, return_counts=True) 

93 return {tuple(s): c for s, c in zip(shot_values, counts)} 

94 

95 

96def probs_from_counts( 

97 counts: dict[tuple[int, ...], int], 

98) -> dict[tuple[int, ...], float]: 

99 """Converts raw counts of observed outcomes into the observed probability 

100 distribution. 

101 

102 :param counts: Dictionary mapping observed readouts to the number of times observed. 

103 :type counts: Dict[Tuple[int, ...], int] 

104 :return: Probability distribution over observed readouts. 

105 :rtype: Dict[Tuple[int, ...], float] 

106 """ 

107 total = sum(counts.values()) 

108 return {outcome: c / total for outcome, c in counts.items()} 

109 

110 

111def _index_to_readout( 

112 index: int, width: int, basis: BasisOrder = BasisOrder.ilo 

113) -> tuple[int, ...]: 

114 return tuple( 

115 (index >> i) & 1 for i in range(width)[:: (-1) ** (basis == BasisOrder.ilo)] 

116 ) 

117 

118 

119def _reverse_bits_of_index(index: int, width: int) -> int: 

120 """Reverse bits of a readout/statevector index to change :py:class:`BasisOrder`. 

121 Values in tket are ILO-BE (2 means [bit0, bit1] == [1, 0]). 

122 Values in qiskit are DLO-BE (2 means [bit1, bit0] == [1, 0]). 

123 Note: Since ILO-BE (DLO-BE) is indistinguishable from DLO-LE (ILO-LE), this can also 

124 be seen as changing the endianness of the value. 

125 

126 :param n: Value to reverse 

127 :type n: int 

128 :param width: Number of bits in bitstring 

129 :type width: int 

130 :return: Integer value of reverse bitstring 

131 :rtype: int 

132 """ 

133 permuter = BitPermuter(tuple(range(width - 1, -1, -1))) 

134 return permuter.permute(index) 

135 

136 

137def _compute_probs_from_state(state: np.ndarray, min_p: float = 1e-10) -> np.ndarray: 

138 """ 

139 Converts statevector to a probability vector. 

140 Set probabilities lower than `min_p` to 0. 

141 

142 :param state: A statevector. 

143 :type state: np.ndarray 

144 :param min_p: Minimum probability to include in result 

145 :type min_p: float 

146 :return: Probability vector. 

147 :rtype: np.ndarray 

148 """ 

149 probs = state.real**2 + state.imag**2 

150 probs /= sum(probs) 

151 ignore = probs < min_p 

152 probs[ignore] = 0 

153 probs /= sum(probs) 

154 return probs 

155 

156 

157def probs_from_state( 

158 state: np.ndarray, min_p: float = 1e-10 

159) -> dict[tuple[int, ...], float]: 

160 """ 

161 Converts statevector to the probability distribution over readouts in the 

162 computational basis. Ignores probabilities lower than `min_p`. 

163 

164 :param state: Full statevector with big-endian encoding. 

165 :type state: np.ndarray 

166 :param min_p: Minimum probability to include in result 

167 :type min_p: float 

168 :return: Probability distribution over readouts. 

169 :rtype: Dict[Tuple[int], float] 

170 """ 

171 width = get_n_qb_from_statevector(state) 

172 probs = _compute_probs_from_state(state, min_p) 

173 return {_index_to_readout(i, width): p for i, p in enumerate(probs) if p != 0} 

174 

175 

176def int_dist_from_state(state: np.ndarray, min_p: float = 1e-10) -> dict[int, float]: 

177 """ 

178 Converts statevector to the probability distribution over 

179 its indices. Ignores probabilities lower than `min_p`. 

180 

181 :param state: A statevector. 

182 :type state: np.ndarray 

183 :param min_p: Minimum probability to include in result 

184 :type min_p: float 

185 :return: Probability distribution over the vector's indices. 

186 :rtype: Dict[int, float] 

187 """ 

188 probs = _compute_probs_from_state(state, min_p) 

189 return {i: p for i, p in enumerate(probs) if p != 0} 

190 

191 

192def get_n_qb_from_statevector(state: np.ndarray) -> int: 

193 """Given a statevector, returns the number of qubits described 

194 

195 :param state: Statevector to inspect 

196 :type state: np.ndarray 

197 :raises ValueError: If the dimension of the statevector is not a power of 2 

198 :return: `n` such that `len(state) == 2 ** n` 

199 :rtype: int 

200 """ 

201 n_qb = int(np.log2(state.shape[0])) 

202 if 2**n_qb != state.shape[0]: 

203 raise ValueError("Size is not a power of 2") 

204 return n_qb 

205 

206 

207def _assert_compatible_state_permutation( 

208 state: np.ndarray, permutation: tuple[int, ...] 

209) -> None: 

210 """Asserts that a statevector and a permutation list both refer to the same number 

211 of qubits 

212 

213 :param state: Statevector 

214 :type state: np.ndarray 

215 :param permutation: Permutation of qubit indices, encoded as a list. 

216 :type permutation: Tuple[int, ...] 

217 :raises ValueError: [description] 

218 """ 

219 n_qb = len(permutation) 

220 if 2**n_qb != state.shape[0]: 

221 raise ValueError("Invalid permutation: length does not match number of qubits") 

222 

223 

224def permute_qubits_in_statevector( 

225 state: np.ndarray, permutation: tuple[int, ...] 

226) -> np.ndarray: 

227 """Rearranges a statevector according to a permutation of the qubit indices. 

228 

229 >>> # A 3-qubit state: 

230 >>> state = np.array([0.0, 0.0625, 0.1875, 0.25, 0.375, 0.4375, 0.5, 0.5625]) 

231 >>> permutation = [1, 0, 2] # swap qubits 0 and 1 

232 >>> # Apply the permutation that swaps indices 2 (="010") and 4 (="100"), and swaps 

233 >>> # indices 3 (="011") and 5 (="101"): 

234 >>> permute_qubits_in_statevector(state, permutation) 

235 array([0. , 0.0625, 0.375 , 0.4375, 0.1875, 0.25 , 0.5 , 0.5625]) 

236 

237 :param state: Original statevector. 

238 :type state: np.ndarray 

239 :param permutation: Map from current qubit index (big-endian) to its new position, 

240 encoded as a list. 

241 :type permutation: Tuple[int, ...] 

242 :return: Updated statevector. 

243 :rtype: np.ndarray 

244 """ 

245 _assert_compatible_state_permutation(state, permutation) 

246 permuter = BitPermuter(permutation) 

247 return state[permuter.permute_all()] 

248 

249 

250def permute_basis_indexing( 

251 matrix: np.ndarray, permutation: tuple[int, ...] 

252) -> np.ndarray: 

253 """Rearranges the first dimensions of an array (statevector or unitary) 

254 according to a permutation of the bit indices in the binary representation 

255 of row indices. 

256 

257 :param matrix: Original unitary matrix 

258 :type matrix: np.ndarray 

259 :param permutation: Map from current qubit index (big-endian) to its new position, 

260 encoded as a list 

261 :type permutation: Tuple[int, ...] 

262 :return: Updated unitary matrix 

263 :rtype: np.ndarray 

264 """ 

265 _assert_compatible_state_permutation(matrix, permutation) 

266 permuter = BitPermuter(permutation) 

267 

268 result: np.ndarray = matrix[permuter.permute_all(), ...] 

269 return result 

270 

271 

272def permute_rows_cols_in_unitary( 

273 matrix: np.ndarray, permutation: tuple[int, ...] 

274) -> np.ndarray: 

275 """Rearranges the rows of a unitary matrix according to a permutation of the qubit 

276 indices. 

277 

278 :param matrix: Original unitary matrix 

279 :type matrix: np.ndarray 

280 :param permutation: Map from current qubit index (big-endian) to its new position, 

281 encoded as a list 

282 :type permutation: Tuple[int, ...] 

283 :return: Updated unitary matrix 

284 :rtype: np.ndarray 

285 """ 

286 _assert_compatible_state_permutation(matrix, permutation) 

287 permuter = BitPermuter(permutation) 

288 all_perms = permuter.permute_all() 

289 permat: np.ndarray = matrix[:, all_perms] 

290 return permat[all_perms, :] 

291 

292 

293def compare_statevectors(first: np.ndarray, second: np.ndarray) -> bool: 

294 """Check approximate equality up to global phase for statevectors. 

295 

296 :param first: First statevector. 

297 :type first: np.ndarray 

298 :param second: Second statevector. 

299 :type second: np.ndarray 

300 :return: Approximate equality. 

301 :rtype: bool 

302 """ 

303 return bool(np.isclose(np.abs(np.vdot(first, second)), 1)) 

304 

305 

306def compare_unitaries(first: np.ndarray, second: np.ndarray) -> bool: 

307 """Check approximate equality up to global phase for unitaries. 

308 

309 :param first: First unitary. 

310 :type first: np.ndarray 

311 :param second: Second unitary. 

312 :type second: np.ndarray 

313 :return: Approximate equality. 

314 :rtype: bool 

315 """ 

316 conjug_prod = first @ second.conjugate().transpose() 

317 identity = np.identity(conjug_prod.shape[0], dtype=complex) 

318 return bool(np.allclose(conjug_prod, identity * conjug_prod[0, 0]))