from dataclasses import dataclass
from typing import Union
from qibo import Circuit, gates
from qibo.backends import Backend, _check_backend
from qibo.config import raise_error
from qibo.hamiltonians import Hamiltonian, Z
from qibo.result import CircuitResult, MeasurementOutcomes, QuantumState
from qiboml import ndarray
[docs]@dataclass
class QuantumDecoding:
"""
Abstract decoder class.
Args:
nqubits (int): total number of qubits.
qubits (tuple[int], optional): set of qubits it acts on, by default ``range(nqubits)``.
nshots (int, optional): number of shots used for circuit execution and sampling.
backend (Backend, optional): backend used for computation, by default the globally-set backend is used.
"""
nqubits: int
qubits: tuple[int] = None
nshots: int = None
backend: Backend = None
_circuit: Circuit = None
def __post_init__(self):
"""Ancillary post initialization operations."""
self.qubits = (
tuple(range(self.nqubits)) if self.qubits is None else tuple(self.qubits)
)
self._circuit = Circuit(self.nqubits)
self.backend = _check_backend(self.backend)
self._circuit.add(gates.M(*self.qubits))
[docs] def __call__(
self, x: Circuit
) -> Union[CircuitResult, QuantumState, MeasurementOutcomes]:
"""Combine the input circuir with the internal one and execute them with the internal backend.
Args:
x (Circuit): input circuit.
Returns:
(CircuitResult | QuantumState | MeasurementOutcomes): the execution ``qibo.result`` object.
"""
return self.backend.execute_circuit(x + self._circuit, nshots=self.nshots)
@property
def circuit(
self,
) -> Circuit:
"""A copy of the internal circuit.
Returns:
(Circuit): a copy of the internal circuit.
"""
return self._circuit.copy()
[docs] def set_backend(self, backend: Backend):
"""Set the internal backend.
Args:
backend (Backend): backend to be set.
"""
self.backend = backend
@property
def output_shape(self):
"""The shape of the decoded outputs."""
raise_error(NotImplementedError)
@property
def analytic(self) -> bool:
"""Whether the decoder is analytic, i.e. the gradient is ananlytically computable, or not
(e.g. if sampling is involved).
Returns:
(bool): ``True`` if ``nshots`` is ``None``, ``False`` otherwise.
"""
if self.nshots is None:
return True
return False
def __hash__(self) -> int:
return hash((self.qubits, self.nshots, self.backend))
[docs]class Probabilities(QuantumDecoding):
"""The probabilities decoder."""
# TODO: collapse on ExpectationDecoding if not analytic
[docs] def __call__(self, x: Circuit) -> ndarray:
"""Computes the final state probabilities.
Args:
x (Circuit): input circuit.
Returns:
(ndarray): the final probabilities.
"""
return super().__call__(x).probabilities(self.qubits)
@property
def output_shape(self) -> tuple[int, int]:
"""Shape of the output probabilities.
Returns:
(tuple[int, int]): a ``(1, 2**nqubits)`` shape.
"""
return (1, 2**self.nqubits)
@property
def analytic(self) -> bool:
return True
[docs]@dataclass
class Expectation(QuantumDecoding):
r"""The expectation value decoder.
Args:
observable (Hamiltonian | ndarray): the observable to calculate the expectation value of,
by default :math:`Z_0\otimes Z_1\otimes ... \otimes Z_n` is used.
"""
observable: Union[ndarray, Hamiltonian] = None
def __post_init__(self):
"""Ancillary post initialization operations."""
if self.observable is None:
self.observable = Z(self.nqubits, dense=False, backend=self.backend)
super().__post_init__()
[docs] def __call__(self, x: Circuit) -> ndarray:
"""Execute the input circuit and calculate the expectation value of the internal observable on
the final state
Args:
x (Circuit): input Circuit.
Returns:
(ndarray): the calculated expectation value.
"""
if self.analytic:
return self.observable.expectation(
super().__call__(x).state(),
).reshape(1, 1)
else:
return self.observable.expectation_from_samples(
super().__call__(x).frequencies(),
qubit_map=self.qubits,
).reshape(1, 1)
@property
def output_shape(self) -> tuple[int, int]:
"""Shape of the output expectation value.
Returns:
(tuple[int, int]): a ``(1, 1)`` shape.
"""
return (1, 1)
[docs] def set_backend(self, backend: Backend):
"""Set the internal and observable's backends.
Args:
backend (Backend): backend to be set.
"""
super().set_backend(backend)
self.observable.backend = backend
def __hash__(self) -> int:
return hash((self.qubits, self.nshots, self.backend, self.observable))
[docs]class State(QuantumDecoding):
"""The state decoder."""
[docs] def __call__(self, x: Circuit) -> ndarray:
"""Compute the final state of the input circuit and separates it in its real and
imaginary parts stacked on top of each other.
Args:
x (Circuit): input Circuit.
Returns:
(ndarray): the final state.
"""
state = super().__call__(x).state()
return self.backend.np.vstack(
(self.backend.np.real(state), self.backend.np.imag(state))
).reshape(self.output_shape)
@property
def output_shape(self) -> tuple[int, int, int]:
"""Shape of the output state.
Returns:
(tuple[int, int, int]): a ``(2, 1, 2**nqubits)`` shape.
"""
return (2, 1, 2**self.nqubits)
@property
def analytic(self) -> bool:
return True
[docs]class Samples(QuantumDecoding):
"""The samples decoder."""
def __post_init__(self):
super().__post_init__()
[docs] def __call__(self, x: Circuit) -> ndarray:
"""Sample the final state of the circuit.
Args:
x (Circuit): input Circuit.
Returns:
(ndarray): the generated samples.
"""
return self.backend.cast(super().__call__(x).samples(), self.backend.precision)
@property
def output_shape(self) -> tuple[int, int]:
"""Shape of the output samples.
Returns:
(tuple[int, int]): a ``(nshots, nqubits)`` shape.
"""
return (self.nshots, len(self.qubits))
@property
def analytic(self) -> bool: # pragma: no cover
return False