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
« 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.
15from typing import Any
17import numpy as np
19from pytket.circuit import BasisOrder
21KwargTypes = Any
24class BitPermuter:
25 """Class for permuting the bits in an integer
27 Enables inverse permuation and uses caching to speed up common uses.
29 """
31 def __init__(self, permutation: tuple[int, ...]):
32 """Constructor
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]] = ({}, {})
45 def permute(self, val: int, inverse: bool = False) -> int:
46 """Return input with bit values permuted.
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]
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)
65 perm_map[val] = res
66 other_map[res] = val
67 return res
69 def permute_all(self) -> list[int]:
70 """Permute all integers within bit-width specified by permutation.
72 :return: List of permuted outputs.
73 """
74 return list(map(self.permute, range(1 << self.n_bits)))
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.
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)}
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.
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()}
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 )
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.
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)
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.
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
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`.
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}
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`.
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}
169def get_n_qb_from_statevector(state: np.ndarray) -> int:
170 """Given a statevector, returns the number of qubits described
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
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
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")
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.
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])
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()]
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.
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)
235 result: np.ndarray = matrix[permuter.permute_all(), ...]
236 return result
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.
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, :]
257def compare_statevectors(first: np.ndarray, second: np.ndarray) -> bool:
258 """Check approximate equality up to global phase for statevectors.
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))
267def compare_unitaries(first: np.ndarray, second: np.ndarray) -> bool:
268 """Check approximate equality up to global phase for unitaries.
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]))