diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index b2b1e9075d..e3a14acf64 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -1704,6 +1704,12 @@ Entanglement of formation .. autofunction:: qibo.quantum_info.entanglement_of_formation +Negativity +"""""""""" + +.. autofunction:: qibo.quantum_info.negativity + + Entanglement fidelity """"""""""""""""""""" diff --git a/src/qibo/quantum_info/entanglement.py b/src/qibo/quantum_info/entanglement.py index cc79980bae..2eb670dc62 100644 --- a/src/qibo/quantum_info/entanglement.py +++ b/src/qibo/quantum_info/entanglement.py @@ -4,7 +4,11 @@ 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.linalg_operations import ( + matrix_power, + partial_trace, + partial_transpose, +) from qibo.quantum_info.metrics import fidelity, purity @@ -116,6 +120,39 @@ def entanglement_of_formation( return ent_of_form +def negativity(state, bipartition, backend=None): + """Calculates the negativity of a bipartite quantum state. + + Given a bipartite state :math:`\\rho \\in \\mathcal{H}_{A} \\otimes \\mathcal{H}_{B}`, + the negativity :math:`\\operatorname{Neg}(\\rho)` is given by + + .. math:: + \\operatorname{Neg}(\\rho) = \\frac{1}{2} \\, + \\left( \\norm{\\rho_{B}}_{1} - 1 \\right) \\, , + + where :math:`\\rho_{B}` is the reduced density matrix after tracing out qubits in + partition :math:`A`, and :math:`\\norm{\\cdot}_{1}` is the Schatten :math:`1`-norm + (also known as nuclear norm or trace norm). + + Args: + state (ndarray): statevector or density matrix. + bipartition (list or tuple or ndarray): qubits in the subsystem to be traced out. + 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: Negativity :math:`\\operatorname{Neg}(\\rho)` of state :math:`\\rho`. + """ + backend = _check_backend(backend) + + reduced = partial_transpose(state, bipartition, backend) + reduced = backend.np.conj(reduced.T) @ reduced + norm = backend.np.trace(matrix_power(reduced, 1 / 2, backend)) + + return float(backend.np.real((norm - 1) / 2)) + + def entanglement_fidelity( channel, nqubits: int, state=None, check_hermitian: bool = False, backend=None ): diff --git a/tests/test_quantum_info_entanglement.py b/tests/test_quantum_info_entanglement.py index 01a08cd3ca..a461c16495 100644 --- a/tests/test_quantum_info_entanglement.py +++ b/tests/test_quantum_info_entanglement.py @@ -9,6 +9,7 @@ entanglement_of_formation, entangling_capability, meyer_wallach_entanglement, + negativity, ) from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector @@ -66,6 +67,27 @@ def test_concurrence_and_formation(backend, bipartition, base, check_purity): backend.assert_allclose(ent_form, 0.0, atol=PRECISION_TOL) +@pytest.mark.parametrize("p", [1 / 5, 1 / 3 + 0.01, 1.0]) +def test_negativity(backend, p): + # werner state + zero, one = np.array([1, 0]), np.array([0, 1]) + psi = (np.kron(zero, one) - np.kron(one, zero)) / np.sqrt(2) + psi = np.outer(psi, psi.T) + psi = backend.cast(psi) + state = p * psi + (1 - p) * backend.identity_density_matrix(2, normalize=True) + + neg = negativity(state, [0], backend=backend) + + if p == 1 / 5: + target = 0.0 + elif p == 1.0: + target = 1 / 2 + else: + target = 3 / 400 + + backend.assert_allclose(neg, target, atol=1e-10) + + @pytest.mark.parametrize("check_hermitian", [False, True]) @pytest.mark.parametrize("nqubits", [4, 6]) @pytest.mark.parametrize("channel", [gates.DepolarizingChannel])