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
« 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.
15from typing import Any
17import numpy as np
19from pytket.circuit import BasisOrder
21StateTuple = tuple[int, ...]
22CountsDict = dict[StateTuple, int | float]
23KwargTypes = Any
26class BitPermuter:
27 """Class for permuting the bits in an integer
29 Enables inverse permuation and uses caching to speed up common uses.
31 """
33 def __init__(self, permutation: tuple[int, ...]):
34 """Constructor
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]] = ({}, {})
48 def permute(self, val: int, inverse: bool = False) -> int:
49 """Return input with bit values permuted.
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]
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)
71 perm_map[val] = res
72 other_map[res] = val
73 return res
75 def permute_all(self) -> list[int]:
76 """Permute all integers within bit-width specified by permutation.
78 :return: List of permuted outputs.
79 :rtype: List
80 """
81 return list(map(self.permute, range(1 << self.n_bits)))
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.
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)}
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.
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()}
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 )
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.
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)
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.
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
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`.
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}
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`.
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}
192def get_n_qb_from_statevector(state: np.ndarray) -> int:
193 """Given a statevector, returns the number of qubits described
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
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
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")
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.
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])
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()]
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.
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)
268 result: np.ndarray = matrix[permuter.permute_all(), ...]
269 return result
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.
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, :]
293def compare_statevectors(first: np.ndarray, second: np.ndarray) -> bool:
294 """Check approximate equality up to global phase for statevectors.
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))
306def compare_unitaries(first: np.ndarray, second: np.ndarray) -> bool:
307 """Check approximate equality up to global phase for unitaries.
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]))