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

81 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 

15from typing import Any 

16 

17import numpy as np 

18 

19from pytket.circuit import BasisOrder 

20 

21KwargTypes = Any 

22 

23 

24class BitPermuter: 

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

26 

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

28 

29 """ 

30 

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

32 """Constructor 

33 

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

35 encoded as a list. 

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

37 of all bits 

38 """ 

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

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

41 self.perm = tuple(permutation) 

42 self.n_bits = len(self.perm) 

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

44 

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

46 """Return input with bit values permuted. 

47 

48 :param val: input integer 

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

50 :return: permuted integer 

51 """ 

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

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

54 return perm_map[val] 

55 

56 res = 0 

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

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

59 target_index, source_index = source_index, target_index # noqa: PLW2901 

60 # if source bit set 

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

62 # set target bit 

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

64 

65 perm_map[val] = res 

66 other_map[res] = val 

67 return res 

68 

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

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

71 

72 :return: List of permuted outputs. 

73 """ 

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

75 

76 

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

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

79 

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

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

82 """ 

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

84 return {tuple(s): c for s, c in zip(shot_values, counts, strict=False)} 

85 

86 

87def probs_from_counts( 

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

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

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

91 distribution. 

92 

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

94 :return: Probability distribution over observed readouts. 

95 """ 

96 total = sum(counts.values()) 

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

98 

99 

100def _index_to_readout( 

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

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

103 return tuple( 

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

105 ) 

106 

107 

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

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

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

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

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

113 be seen as changing the endianness of the value. 

114 

115 :param n: Value to reverse 

116 :param width: Number of bits in bitstring 

117 :return: Integer value of reverse bitstring 

118 """ 

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

120 return permuter.permute(index) 

121 

122 

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

124 """ 

125 Converts statevector to a probability vector. 

126 Set probabilities lower than `min_p` to 0. 

127 

128 :param state: A statevector. 

129 :param min_p: Minimum probability to include in result 

130 :return: Probability vector. 

131 """ 

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

133 probs /= sum(probs) 

134 ignore = probs < min_p 

135 probs[ignore] = 0 

136 probs /= sum(probs) 

137 return probs # type: ignore 

138 

139 

140def probs_from_state( 

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

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

143 """ 

144 Converts statevector to the probability distribution over readouts in the 

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

146 

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

148 :param min_p: Minimum probability to include in result 

149 :return: Probability distribution over readouts. 

150 """ 

151 width = get_n_qb_from_statevector(state) 

152 probs = _compute_probs_from_state(state, min_p) 

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

154 

155 

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

157 """ 

158 Converts statevector to the probability distribution over 

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

160 

161 :param state: A statevector. 

162 :param min_p: Minimum probability to include in result 

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

164 """ 

165 probs = _compute_probs_from_state(state, min_p) 

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

167 

168 

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

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

171 

172 :param state: Statevector to inspect 

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

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

175 """ 

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

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

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

179 return n_qb 

180 

181 

182def _assert_compatible_state_permutation( 

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

184) -> None: 

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

186 of qubits 

187 

188 :param state: Statevector 

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

190 :raises ValueError: [description] 

191 """ 

192 n_qb = len(permutation) 

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

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

195 

196 

197def permute_qubits_in_statevector( 

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

199) -> np.ndarray: 

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

201 

202 >>> # A 3-qubit state: 

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

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

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

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

207 >>> permute_qubits_in_statevector(state, permutation) 

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

209 

210 :param state: Original statevector. 

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

212 encoded as a list. 

213 :return: Updated statevector. 

214 """ 

215 _assert_compatible_state_permutation(state, permutation) 

216 permuter = BitPermuter(permutation) 

217 return state[permuter.permute_all()] 

218 

219 

220def permute_basis_indexing( 

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

222) -> np.ndarray: 

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

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

225 of row indices. 

226 

227 :param matrix: Original unitary matrix 

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

229 encoded as a list 

230 :return: Updated unitary matrix 

231 """ 

232 _assert_compatible_state_permutation(matrix, permutation) 

233 permuter = BitPermuter(permutation) 

234 

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

236 return result 

237 

238 

239def permute_rows_cols_in_unitary( 

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

241) -> np.ndarray: 

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

243 indices. 

244 

245 :param matrix: Original unitary matrix 

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

247 encoded as a list 

248 :return: Updated unitary matrix 

249 """ 

250 _assert_compatible_state_permutation(matrix, permutation) 

251 permuter = BitPermuter(permutation) 

252 all_perms = permuter.permute_all() 

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

254 return permat[all_perms, :] 

255 

256 

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

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

259 

260 :param first: First statevector. 

261 :param second: Second statevector. 

262 :return: Approximate equality. 

263 """ 

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

265 

266 

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

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

269 

270 :param first: First unitary. 

271 :param second: Second unitary. 

272 :return: Approximate equality. 

273 """ 

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

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

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