"""When we measure a quantum state, we project it in an orthonormal basis of the
associated Hilbert space. By default,
:class:`~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 ABC, abstractmethod
from functools import reduce
from typing import Optional
import numpy as np
import numpy.typing as npt
from typeguard import typechecked
from mpqp.core.instruction.gates.custom_gate import CustomGate
from mpqp.core.instruction.gates.gate_definition import UnitaryMatrix
from mpqp.tools.display import clean_1D_array, one_lined_repr
from mpqp.tools.maths import is_unitary
[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:
>>> custom_basis = Basis([np.array([1,0]), np.array([0,-1])], symbols=("↑", "↓"))
>>> custom_basis.pretty_print()
Basis: [
[1, 0],
[0, -1]
]
>>> circ = QCircuit([X(0), H(1), CNOT(1, 2), Y(2)])
>>> circ.add(BasisMeasure([0, 1, 2], basis=custom_basis, shots=10000))
>>> print(run(circ, IBMDevice.AER_SIMULATOR)) # doctest: +SKIP
Result: None, IBMDevice, AER_SIMULATOR
Counts: [0, 0, 0, 0, 0, 4936, 5064, 0]
Probabilities: [0, 0, 0, 0, 0, 0.4936, 0.5064, 0]
Samples:
State: ↓↑↓, Index: 5, Count: 4936, Probability: 0.4936
State: ↓↓↑, Index: 6, Count: 5064, Probability: 0.5064
Error: None
"""
def __init__(
self,
basis_vectors: list[npt.NDArray[np.complex64]],
nb_qubits: Optional[int] = None,
symbols: Optional[tuple[str, str]] = None,
basis_vectors_labels: Optional[list[str]] = None,
):
if symbols is not None and basis_vectors_labels is not None:
raise ValueError(
"You can only specify either symbols or basis_vectors_labels, "
"not both."
)
if symbols is None:
symbols = ("0", "1")
self.symbols = symbols
self.basis_vectors_labels = basis_vectors_labels
if len(basis_vectors) == 0:
if nb_qubits is None:
raise ValueError(
"Empty basis and no number of qubits specified. Please at "
"least specify one of these two."
)
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 not is_unitary(np.array([vector for vector in basis_vectors])):
raise ValueError(
"The given basis is not orthogonal: the matrix of the "
"concatenated vectors of the basis should be unitary."
)
self.nb_qubits = nb_qubits
"""See parameter description."""
self.basis_vectors = basis_vectors
"""See parameter description."""
[docs] def binary_to_custom(self, state: str) -> str:
"""Converts a binary string to a custom string representation.
By default, it uses "0" and "1" but can be customized based on the provided `symbols`.
Args:
state: The binary string (e.g., "01") to be converted.
Returns:
The custom string representation of the binary state.
Example:
>>> basis = Basis([np.array([1,0]), np.array([0,-1])], symbols=("+", "-"))
>>> custom_state = basis.binary_to_custom("01")
>>> custom_state
'+-'
"""
if self.basis_vectors_labels is not None:
return self.basis_vectors_labels[int(state, 2)]
return ''.join(self.symbols[int(bit)] for bit in state)
[docs] def pretty_print(self):
"""Nicer print for the basis, with human readable formatting."""
joint_vectors = ",\n ".join(map(clean_1D_array, self.basis_vectors))
print(f"Basis: [\n {joint_vectors}\n]")
def __repr__(self) -> str:
joint_vectors = ", ".join(map(one_lined_repr, self.basis_vectors))
qubits = "" if isinstance(self, VariableSizeBasis) else f", {self.nb_qubits}"
return f"{type(self).__name__}({joint_vectors}{qubits})"
[docs] def to_computational(self):
"""Converts the custom basis to the computational basis.
This method creates a quantum circuit with a custom gate represented by
a unitary transformation and applies it to all qubits before measurement.
Returns:
A quantum circuit representing the basis change circuit.
Example:
>>> basis = Basis([np.array([1, 0]), np.array([0, -1])])
>>> circuit = basis.to_computational()
>>> print(circuit)
┌─────────┐
q: ┤ Unitary ├
└─────────┘
"""
from mpqp.core.circuit import QCircuit
basis_change = np.array(self.basis_vectors).T.conjugate()
return QCircuit(
[
CustomGate(
UnitaryMatrix(basis_change), targets=list(range(self.nb_qubits))
)
]
)
[docs]@typechecked
class VariableSizeBasis(Basis, ABC):
"""A variable-size basis with a dynamically adjustable size to different qubit numbers
during circuit execution.
Args:
nb_qubits: number of qubits in the basis. If not provided,
the basis can be dynamically sized later using the `set_size` method.
symbols: custom symbols for representing basis states, defaults to ("0", "1").
"""
@abstractmethod
def __init__(
self, nb_qubits: Optional[int] = None, symbols: Optional[tuple[str, str]] = None
):
super().__init__([], 0, symbols=symbols)
if nb_qubits is not None:
self.set_size(nb_qubits)
[docs] @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 to measure, if not specified, the size will
be dynamic and automatically span across the whole circuit, even
through dimension change of the circuit.
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):
super().__init__(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] def to_computational(self):
from mpqp.core.circuit import QCircuit
return QCircuit(self.nb_qubits)
[docs]class HadamardBasis(VariableSizeBasis):
"""Basis representing the Hadamard basis, also called X-basis or +/- basis.
Args:
nb_qubits: number of qubits to measure, if not specified, the size will
be dynamic and automatically span across the whole circuit, even
through dimension change of the circuit.
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]
]
>>> circ = QCircuit([X(0), H(1), CNOT(1, 2), Y(2)])
>>> circ.add(BasisMeasure(basis=HadamardBasis()))
>>> print(run(circ, IBMDevice.AER_SIMULATOR)) # doctest: +SKIP
Result: None, IBMDevice, AER_SIMULATOR
Counts: [0, 261, 253, 0, 0, 244, 266, 0]
Probabilities: [0, 0.25488, 0.24707, 0, 0, 0.23828, 0.25977, 0]
Samples:
State: ++-, Index: 1, Count: 261, Probability: 0.2548828
State: +-+, Index: 2, Count: 253, Probability: 0.2470703
State: -+-, Index: 5, Count: 244, Probability: 0.2382812
State: --+, Index: 6, Count: 266, Probability: 0.2597656
Error: None
"""
def __init__(self, nb_qubits: Optional[int] = None):
super().__init__(nb_qubits, symbols=('+', '-'))
[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
[docs] def to_computational(self):
from mpqp.core.circuit import QCircuit
from mpqp.core.instruction.gates.native_gates import H
if self.nb_qubits == 0:
return QCircuit(self.nb_qubits)
return QCircuit([H(qb) for qb in range(self.nb_qubits)])