Source code for mpqp.core.instruction.measurement.basis

"""When we measure a quantum state, we project it in an orthonormal basis of the
associated Hilbert space. By default,
:class:`BasisMeasure<mpqp.core.instruction.measurement.basis_measure.BasisMeasure>`
operates in the computational basis, but you may want to measure the state in a
custom basis, like it can be the case in the Bell game. For this purpose, you
can use the :class:`Basis` class.

On the other hand, some common basis are available for you to use:
:class:`ComputationalBasis` and :class:`HadamardBasis`."""

from __future__ import annotations

from abc import abstractmethod
from functools import reduce
from typing import Optional

import numpy as np
import numpy.typing as npt
from typeguard import typechecked

from mpqp.tools.generics import clean_array
from mpqp.tools.maths import atol, matrix_eq


[docs]@typechecked class Basis: """Represents a basis of the Hilbert space used for measuring a qubit. Args: basis_vectors: List of vector composing the basis. nb_qubits: Number of qubits associated with this basis. If not specified, it will be automatically inferred from ``basis_vectors``'s dimensions. Example: >>> Basis([np.array([1,0]), np.array([0,-1])]).pretty_print() Basis: [ [1, 0], [0, -1] ] """ def __init__( self, basis_vectors: list[npt.NDArray[np.complex64]], nb_qubits: Optional[int] = None, ): # TODO : add the possibility to give the symbols for the '0' and '1' of # the custom basis. This should then appear in the Sample # binary_representation of the basis state. For instance in the Hadamard # basis, the symbols will be '+' and '-'. If the user wants '↑' and '↓' # for his custom basis, when we print samples we would have something # like: # State: ↑↑↓, Index: 1, Count: 512, Probability: 0.512 if len(basis_vectors) == 0: self.nb_qubits = nb_qubits self.basis_vectors = basis_vectors return if nb_qubits is None: nb_qubits = int(np.log2(len(basis_vectors[0]))) if len(basis_vectors) != 2**nb_qubits: raise ValueError( "Incorrect number of vector for the basis: given " f"{len(basis_vectors)} but there should be {2**nb_qubits}" ) if any(len(vector) != 2**nb_qubits for vector in basis_vectors): raise ValueError("All vectors of the given basis are not the same size") if any( abs(np.linalg.norm(vector) - 1) # pyright: ignore[reportGeneralTypeIssues] > atol for vector in basis_vectors ): raise ValueError("All vectors of the given basis are not normalized") m = np.array([vector for vector in basis_vectors]) if not matrix_eq( m.transpose().dot(m), np.eye(len(basis_vectors)), # pyright: ignore[reportArgumentType] ): raise ValueError("The given basis is not orthogonal") self.nb_qubits = nb_qubits """See parameter description.""" self.basis_vectors = basis_vectors """See parameter description."""
[docs] def pretty_print(self): """Nicer print for the basis, with human readable formatting. Example: >>> Basis([np.array([1,0]), np.array([0,-1])]).pretty_print() Basis: [ [1, 0], [0, -1] ] """ joint_vectors = ",\n ".join(map(clean_array, self.basis_vectors)) print(f"Basis: [\n {joint_vectors}\n]")
def __repr__(self) -> str: return f"{type(self).__name__}({self.basis_vectors}, {self.nb_qubits})"
@typechecked class VariableSizeBasis(Basis): """3M-TODO""" @abstractmethod def __init__(self, nb_qubits: Optional[int] = None): super().__init__([], nb_qubits) pass @abstractmethod def set_size(self, nb_qubits: int): """To allow the user to use a basis without having to specify the size (because implicitly the size should be the number of qubits of the circuit), we use this method, that will only be called once the circuit's size is definitive (i.e. at the last moment before the circuit is ran) Args: nb_qubits: number of qubits in the basis """ pass def __repr__(self) -> str: return f"{type(self).__name__}()"
[docs]class ComputationalBasis(VariableSizeBasis): """Basis representing the computational basis, also called Z-basis or canonical basis. Args: nb_qubits: number of qubits of the space, if not given as input (for example if unknown at the moment of creation) ``set_size`` will have to be executed before the basis is used (in a measure for example). Examples: >>> ComputationalBasis(3).pretty_print() Basis: [ [1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1] ] >>> b = ComputationalBasis() >>> b.set_size(2) >>> b.pretty_print() Basis: [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ] """ def __init__(self, nb_qubits: Optional[int] = None): basis = [] Basis.__init__(self, basis, nb_qubits) if nb_qubits is not None: self.set_size(nb_qubits)
[docs] def set_size(self, nb_qubits: int): self.basis_vectors = [ np.array([0] * i + [1] + [0] * (2**nb_qubits - 1 - i), dtype=np.complex64) for i in range(2**nb_qubits) ] self.nb_qubits = nb_qubits
[docs]class HadamardBasis(VariableSizeBasis): """Basis representing the Hadamard basis, also called X-basis or +/- basis. Args: nb_qubits: number of qubits in the basis Example: >>> HadamardBasis(2).pretty_print() Basis: [ [0.5, 0.5, 0.5, 0.5], [0.5, -0.5, 0.5, -0.5], [0.5, 0.5, -0.5, -0.5], [0.5, -0.5, -0.5, 0.5] ] """ def __init__(self, nb_qubits: Optional[int] = None): basis = [] Basis.__init__(self, basis, nb_qubits) if nb_qubits is not None: self.set_size(nb_qubits)
[docs] def set_size(self, nb_qubits: int): H = np.array([[1, 1], [1, -1]], dtype=np.complex64) / np.sqrt(2) Hn = reduce(np.kron, [H] * nb_qubits, np.eye(1)) self.basis_vectors = [line for line in Hn] self.nb_qubits = nb_qubits