"""Submodules with entanglement measures."""
import numpy as np
from qibo.backends import _check_backend
from qibo.config import PRECISION_TOL, raise_error
from qibo.quantum_info.linalg_operations import partial_trace
from qibo.quantum_info.metrics import fidelity, purity
[docs]def concurrence(state, bipartition, check_purity: bool = True, backend=None):
"""Calculates concurrence of a pure bipartite quantum state
:math:`\\rho \\in \\mathcal{H}_{A} \\otimes \\mathcal{H}_{B}` as
.. math::
C(\\rho) = \\sqrt{2 \\, (\\text{tr}^{2}(\\rho) - \\text{tr}(\\rho_{A}^{2}))} \\, ,
where :math:`\\rho_{A} = \\text{tr}_{B}(\\rho)` is the reduced density operator
obtained by tracing out the qubits in the ``bipartition`` :math:`B`.
Args:
state (ndarray): statevector or density matrix.
bipartition (list or tuple or ndarray): qubits in the subsystem to be traced out.
check_purity (bool, optional): if ``True``, checks if ``state`` is pure. If ``False``,
it assumes ``state`` is pure . Defaults to ``True``.
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
Defaults to ``None``.
Returns:
float: Concurrence of :math:`\\rho`.
"""
backend = _check_backend(backend)
if (
(len(state.shape) not in [1, 2])
or (len(state) == 0)
or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
):
raise_error(
TypeError,
f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
)
if not isinstance(check_purity, bool):
raise_error(
TypeError,
f"check_purity must be type bool, but it is type {type(check_purity)}.",
)
if check_purity is True:
purity_total_system = purity(state, backend=backend)
mixed = bool(abs(purity_total_system - 1.0) > PRECISION_TOL)
if mixed is True:
raise_error(
NotImplementedError,
"concurrence only implemented for pure quantum states.",
)
reduced_density_matrix = partial_trace(state, bipartition, backend=backend)
purity_reduced = purity(reduced_density_matrix, backend=backend)
if purity_reduced - 1.0 > 0.0:
purity_reduced = round(purity_reduced, 7)
concur = np.sqrt(2 * (1 - purity_reduced))
return concur
[docs]def entanglement_fidelity(
channel, nqubits: int, state=None, check_hermitian: bool = False, backend=None
):
"""Entanglement fidelity :math:`F_{\\mathcal{E}}` of a ``channel`` :math:`\\mathcal{E}`
on ``state`` :math:`\\rho` is given by
.. math::
F_{\\mathcal{E}}(\\rho) = F(\\rho_{f}, \\rho)
where :math:`F` is the :func:`qibo.quantum_info.fidelity` function for states,
and :math:`\\rho_{f} = \\mathcal{E}_{A} \\otimes I_{B}(\\rho)`
is the state after the channel :math:`\\mathcal{E}` was applied to
partition :math:`A`.
Args:
channel (:class:`qibo.gates.channels.Channel`): quantum channel
acting on partition :math:`A`.
nqubits (int): total number of qubits in ``state``.
state (ndarray, optional): statevector or density matrix to be evolved
by ``channel``. If ``None``, defaults to the maximally entangled state
:math:`\\frac{1}{2^{n}} \\, \\sum_{k} \\, \\ket{k}\\ket{k}`, where
:math:`n` is ``nqubits``. Defaults to ``None``.
check_hermitian (bool, optional): if ``True``, checks if the final state
:math:`\\rho_{f}` is Hermitian. If ``False``, it assumes it is Hermitian.
Defaults to ``False``.
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
Defaults to ``None``.
Returns:
float: Entanglement fidelity :math:`F_{\\mathcal{E}}`.
"""
if not isinstance(nqubits, int):
raise_error(
TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
)
if nqubits <= 0:
raise_error(
ValueError, f"nqubits must be a positive integer, but it is {nqubits}."
)
if state is not None and (
(len(state.shape) not in [1, 2])
or (len(state) == 0)
or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
):
raise_error(
TypeError,
f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
)
if not isinstance(check_hermitian, bool):
raise_error(
TypeError,
f"check_hermitian must be type bool, but it is type {type(check_hermitian)}.",
)
backend = _check_backend(backend)
if state is None:
state = backend.plus_density_matrix(nqubits)
# necessary because this function do support repeated execution,
# so it has to default to density matrices
if len(state.shape) == 1:
state = np.outer(state, np.conj(state))
state_final = backend.apply_channel_density_matrix(channel, state, nqubits)
entang_fidelity = fidelity(
state_final, state, check_hermitian=check_hermitian, backend=backend
)
return entang_fidelity
[docs]def meyer_wallach_entanglement(circuit, backend=None):
"""Computes the Meyer-Wallach entanglement Q of the `circuit`,
.. math::
Q(\\theta) = 1 - \\frac{1}{N} \\, \\sum_{k} \\,
\\text{tr}\\left(\\rho_{k^{2}}(\\theta)\\right) \\, .
Args:
circuit (:class:`qibo.models.Circuit`): Parametrized circuit.
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
Defaults to ``None``.
Returns:
float: Meyer-Wallach entanglement.
"""
backend = _check_backend(backend)
circuit.density_matrix = True
nqubits = circuit.nqubits
rho = backend.execute_circuit(circuit).state()
ent = 0
for j in range(nqubits):
trace_q = list(range(nqubits))
trace_q.pop(j)
rho_r = partial_trace(rho, trace_q, backend=backend)
trace = purity(rho_r, backend=backend)
ent += trace
entanglement = 1 - ent / nqubits
return entanglement
[docs]def entangling_capability(circuit, samples: int, seed=None, backend=None):
"""Returns the entangling capability :math:`\\text{Ent}` of a parametrized
circuit, which is average Meyer-Wallach entanglement Q of the circuit, i.e.
.. math::
\\text{Ent} = \\frac{2}{S}\\sum_{k}Q_k \\, ,
where :math:`S` is the number of samples.
Args:
circuit (:class:`qibo.models.Circuit`): Parametrized circuit.
samples (int): number of samples to estimate the integral.
seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
numbers or a fixed seed to initialize a generator. If ``None``, initializes
a generator with a random seed. Default: ``None``.
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
Defaults to ``None``.
Returns:
float: Entangling capability.
"""
if not isinstance(samples, int):
raise_error(
TypeError, f"samples must be type int, but it is type {type(samples)}."
)
if (
seed is not None
and not isinstance(seed, int)
and not isinstance(seed, np.random.Generator)
):
raise_error(
TypeError, "seed must be either type int or numpy.random.Generator."
)
backend = _check_backend(backend)
local_state = (
np.random.default_rng(seed) if seed is None or isinstance(seed, int) else seed
)
res = []
for _ in range(samples):
params = local_state.uniform(-np.pi, np.pi, circuit.trainable_gates.nparams)
circuit.set_parameters(params)
entanglement = meyer_wallach_entanglement(circuit, backend=backend)
res.append(entanglement)
capability = 2 * np.real(np.sum(res)) / samples
return capability