# Copyright 2019-2024 Cambridge Quantum Computing
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any
import numpy as np
from pytket.circuit import BasisOrder
StateTuple = tuple[int, ...]
CountsDict = dict[StateTuple, int | float]
KwargTypes = Any
class BitPermuter:
"""Class for permuting the bits in an integer
Enables inverse permuation and uses caching to speed up common uses.
"""
def __init__(self, permutation: tuple[int, ...]):
"""Constructor
:param permutation: Map from current bit index (big-endian) to its new position,
encoded as a list.
:type permutation: Tuple[int, ...]
:raises ValueError: Input permutation is not valid complete permutation
of all bits
"""
if sorted(permutation) != list(range(len(permutation))):
raise ValueError("Permutation is not a valid complete permutation.")
self.perm = tuple(permutation)
self.n_bits = len(self.perm)
self.int_maps: tuple[dict[int, int], dict[int, int]] = ({}, {})
def permute(self, val: int, inverse: bool = False) -> int:
"""Return input with bit values permuted.
:param val: input integer
:type val: int
:param inverse: whether to use the inverse permutation, defaults to False
:type inverse: bool, optional
:return: permuted integer
:rtype: int
"""
perm_map, other_map = self.int_maps[:: (-1) ** inverse]
if val in perm_map:
return perm_map[val]
res = 0
for source_index, target_index in enumerate(self.perm):
if inverse:
target_index, source_index = source_index, target_index
# if source bit set
if val & (1 << (self.n_bits - 1 - source_index)):
# set target bit
res |= 1 << (self.n_bits - 1 - target_index)
perm_map[val] = res
other_map[res] = val
return res
def permute_all(self) -> list[int]:
"""Permute all integers within bit-width specified by permutation.
:return: List of permuted outputs.
:rtype: List
"""
return list(map(self.permute, range(1 << self.n_bits)))
[docs]
def counts_from_shot_table(shot_table: np.ndarray) -> dict[tuple[int, ...], int]:
"""Summarises a shot table into a dictionary of counts for each observed outcome.
:param shot_table: Table of shots from a pytket backend.
:type shot_table: np.ndarray
:return: Dictionary mapping observed readouts to the number of times observed.
:rtype: Dict[Tuple[int, ...], int]
"""
shot_values, counts = np.unique(shot_table, axis=0, return_counts=True)
return {tuple(s): c for s, c in zip(shot_values, counts)}
[docs]
def probs_from_counts(
counts: dict[tuple[int, ...], int]
) -> dict[tuple[int, ...], float]:
"""Converts raw counts of observed outcomes into the observed probability
distribution.
:param counts: Dictionary mapping observed readouts to the number of times observed.
:type counts: Dict[Tuple[int, ...], int]
:return: Probability distribution over observed readouts.
:rtype: Dict[Tuple[int, ...], float]
"""
total = sum(counts.values())
return {outcome: c / total for outcome, c in counts.items()}
def _index_to_readout(
index: int, width: int, basis: BasisOrder = BasisOrder.ilo
) -> tuple[int, ...]:
return tuple(
(index >> i) & 1 for i in range(width)[:: (-1) ** (basis == BasisOrder.ilo)]
)
def _reverse_bits_of_index(index: int, width: int) -> int:
"""Reverse bits of a readout/statevector index to change :py:class:`BasisOrder`.
Values in tket are ILO-BE (2 means [bit0, bit1] == [1, 0]).
Values in qiskit are DLO-BE (2 means [bit1, bit0] == [1, 0]).
Note: Since ILO-BE (DLO-BE) is indistinguishable from DLO-LE (ILO-LE), this can also
be seen as changing the endianness of the value.
:param n: Value to reverse
:type n: int
:param width: Number of bits in bitstring
:type width: int
:return: Integer value of reverse bitstring
:rtype: int
"""
permuter = BitPermuter(tuple(range(width - 1, -1, -1)))
return permuter.permute(index)
def _compute_probs_from_state(state: np.ndarray, min_p: float = 1e-10) -> np.ndarray:
"""
Converts statevector to a probability vector.
Set probabilities lower than `min_p` to 0.
:param state: A statevector.
:type state: np.ndarray
:param min_p: Minimum probability to include in result
:type min_p: float
:return: Probability vector.
:rtype: np.ndarray
"""
probs = state.real**2 + state.imag**2
probs /= sum(probs)
ignore = probs < min_p
probs[ignore] = 0
probs /= sum(probs)
return probs
[docs]
def probs_from_state(
state: np.ndarray, min_p: float = 1e-10
) -> dict[tuple[int, ...], float]:
"""
Converts statevector to the probability distribution over readouts in the
computational basis. Ignores probabilities lower than `min_p`.
:param state: Full statevector with big-endian encoding.
:type state: np.ndarray
:param min_p: Minimum probability to include in result
:type min_p: float
:return: Probability distribution over readouts.
:rtype: Dict[Tuple[int], float]
"""
width = get_n_qb_from_statevector(state)
probs = _compute_probs_from_state(state, min_p)
return {_index_to_readout(i, width): p for i, p in enumerate(probs) if p != 0}
def int_dist_from_state(state: np.ndarray, min_p: float = 1e-10) -> dict[int, float]:
"""
Converts statevector to the probability distribution over
its indices. Ignores probabilities lower than `min_p`.
:param state: A statevector.
:type state: np.ndarray
:param min_p: Minimum probability to include in result
:type min_p: float
:return: Probability distribution over the vector's indices.
:rtype: Dict[int, float]
"""
probs = _compute_probs_from_state(state, min_p)
return {i: p for i, p in enumerate(probs) if p != 0}
def get_n_qb_from_statevector(state: np.ndarray) -> int:
"""Given a statevector, returns the number of qubits described
:param state: Statevector to inspect
:type state: np.ndarray
:raises ValueError: If the dimension of the statevector is not a power of 2
:return: `n` such that `len(state) == 2 ** n`
:rtype: int
"""
n_qb = int(np.log2(state.shape[0]))
if 2**n_qb != state.shape[0]:
raise ValueError("Size is not a power of 2")
return n_qb
def _assert_compatible_state_permutation(
state: np.ndarray, permutation: tuple[int, ...]
) -> None:
"""Asserts that a statevector and a permutation list both refer to the same number
of qubits
:param state: Statevector
:type state: np.ndarray
:param permutation: Permutation of qubit indices, encoded as a list.
:type permutation: Tuple[int, ...]
:raises ValueError: [description]
"""
n_qb = len(permutation)
if 2**n_qb != state.shape[0]:
raise ValueError("Invalid permutation: length does not match number of qubits")
[docs]
def permute_qubits_in_statevector(
state: np.ndarray, permutation: tuple[int, ...]
) -> np.ndarray:
"""Rearranges a statevector according to a permutation of the qubit indices.
>>> # A 3-qubit state:
>>> state = np.array([0.0, 0.0625, 0.1875, 0.25, 0.375, 0.4375, 0.5, 0.5625])
>>> permutation = [1, 0, 2] # swap qubits 0 and 1
>>> # Apply the permutation that swaps indices 2 (="010") and 4 (="100"), and swaps
>>> # indices 3 (="011") and 5 (="101"):
>>> permute_qubits_in_statevector(state, permutation)
array([0. , 0.0625, 0.375 , 0.4375, 0.1875, 0.25 , 0.5 , 0.5625])
:param state: Original statevector.
:type state: np.ndarray
:param permutation: Map from current qubit index (big-endian) to its new position,
encoded as a list.
:type permutation: Tuple[int, ...]
:return: Updated statevector.
:rtype: np.ndarray
"""
_assert_compatible_state_permutation(state, permutation)
permuter = BitPermuter(permutation)
return state[permuter.permute_all()]
[docs]
def permute_basis_indexing(
matrix: np.ndarray, permutation: tuple[int, ...]
) -> np.ndarray:
"""Rearranges the first dimensions of an array (statevector or unitary)
according to a permutation of the bit indices in the binary representation
of row indices.
:param matrix: Original unitary matrix
:type matrix: np.ndarray
:param permutation: Map from current qubit index (big-endian) to its new position,
encoded as a list
:type permutation: Tuple[int, ...]
:return: Updated unitary matrix
:rtype: np.ndarray
"""
_assert_compatible_state_permutation(matrix, permutation)
permuter = BitPermuter(permutation)
result: np.ndarray = matrix[permuter.permute_all(), ...]
return result
[docs]
def permute_rows_cols_in_unitary(
matrix: np.ndarray, permutation: tuple[int, ...]
) -> np.ndarray:
"""Rearranges the rows of a unitary matrix according to a permutation of the qubit
indices.
:param matrix: Original unitary matrix
:type matrix: np.ndarray
:param permutation: Map from current qubit index (big-endian) to its new position,
encoded as a list
:type permutation: Tuple[int, ...]
:return: Updated unitary matrix
:rtype: np.ndarray
"""
_assert_compatible_state_permutation(matrix, permutation)
permuter = BitPermuter(permutation)
all_perms = permuter.permute_all()
permat: np.ndarray = matrix[:, all_perms]
return permat[all_perms, :]
[docs]
def compare_statevectors(first: np.ndarray, second: np.ndarray) -> bool:
"""Check approximate equality up to global phase for statevectors.
:param first: First statevector.
:type first: np.ndarray
:param second: Second statevector.
:type second: np.ndarray
:return: Approximate equality.
:rtype: bool
"""
return bool(np.isclose(np.abs(np.vdot(first, second)), 1))
[docs]
def compare_unitaries(first: np.ndarray, second: np.ndarray) -> bool:
"""Check approximate equality up to global phase for unitaries.
:param first: First unitary.
:type first: np.ndarray
:param second: Second unitary.
:type second: np.ndarray
:return: Approximate equality.
:rtype: bool
"""
conjug_prod = first @ second.conjugate().transpose()
identity = np.identity(conjug_prod.shape[0], dtype=complex)
return bool(np.allclose(conjug_prod, identity * conjug_prod[0, 0]))