Repository: Qiskit/qiskit

Issue:
Code to compute concurrence is wrong
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**:  0.10.0
- **Python version**: 3.7
- **Operating system**: Linux

### What is the current behavior?

The routine 'concurrence' in qiskit/tools/qi/qi.py is wrong, the matrix

A = rho.dot(YY).dot(rho.conj()).dot(YY)

is not necessarily Hermitian but the Hermitian eigensolver is used to get it's eigenvalues. This can produce negative eigenvalues even though the matrix A is positive semidefinte.

### Steps to reproduce the problem

It is sufficient to use an entangled density matrix like

array([[0.20194612+0.j, 0.25967063+0.j, 0.27702504+0.j, 0.13035404+0.j],
       [0.25967063+0.j, 0.33389518+0.j, 0.3562102 +0.j, 0.16761458+0.j],
       [0.27702504+0.j, 0.3562102 +0.j, 0.38001657+0.j, 0.17881667+0.j],
       [0.13035404+0.j, 0.16761458+0.j, 0.17881667+0.j, 0.08414212+0.j]])

and check that the eigenvalues returned by the hermitian eigensolver are

[-0.0971868  -0.08020192 -0.0104574   0.39189013]

### What is the expected behavior?

For the density matrix above the correct eigenvalues are

[0.00000000e+00 0.00000000e+00 6.45239207e-09 4.51712320e-01]

### Suggested solutions

Replace the following 2 lines of the concurrence routine

 w = la.eigh(A, eigvals_only=True)
 w = np.sqrt(np.maximum(w, 0))

with the correct ones

w = la.eigvals(A).real
isort = np.argsort(w)
w = np.sqrt(np.maximum(w[isort[:]],0.))




Discussion:

Resolving patch:
diff --git a/qiskit/tools/qi/qi.py b/qiskit/tools/qi/qi.py
--- a/qiskit/tools/qi/qi.py
+++ b/qiskit/tools/qi/qi.py
@@ -354,8 +354,8 @@ def concurrence(state):
 
     YY = np.fliplr(np.diag([-1, 1, 1, -1]))
     A = rho.dot(YY).dot(rho.conj()).dot(YY)
-    w = la.eigh(A, eigvals_only=True)
-    w = np.sqrt(np.maximum(w, 0))
+    w = np.sort(np.real(la.eigvals(A)))
+    w = np.sqrt(np.maximum(w, 0.))
     return max(0.0, w[-1] - np.sum(w[0:-1]))
 
 


Test patch:
diff --git a/test/python/test_qi.py b/test/python/test_qi.py
--- a/test/python/test_qi.py
+++ b/test/python/test_qi.py
@@ -27,7 +27,6 @@
 from qiskit.tools.qi.qi import shannon_entropy, entropy, mutual_information
 from qiskit.tools.qi.qi import choi_to_pauli
 from qiskit.tools.qi.qi import entanglement_of_formation, is_pos_def
-from qiskit.tools.qi.qi import __eof_qubit as eof_qubit
 from qiskit.quantum_info import purity
 from qiskit.quantum_info.random import random_density_matrix
 from qiskit.exceptions import QiskitError
@@ -120,12 +119,10 @@ def test_concurrence(self):
                 [0, 0.5j, 0.5, 0], [0, 0, 0, 0]]
         rho3 = 0.5 * np.array(rho1) + 0.5 * np.array(rho2)
         rho4 = 0.75 * np.array(rho1) + 0.25 * np.array(rho2)
-        test_pass = (concurrence(psi1) == 0.0 and
-                     concurrence(rho1) == 1.0 and
-                     concurrence(rho2) == 1.0 and
-                     concurrence(rho3) == 0.0 and
-                     concurrence(rho4) == 0.5)
-        self.assertTrue(test_pass)
+        concurrences = [concurrence(state) for state
+                        in [psi1, rho1, rho2, rho3, rho4]]
+        targets = [0.0, 1.0, 1.0, 0.0, 0.5]
+        self.assertTrue(np.allclose(concurrences, targets))
 
     def test_concurrence_not_two_qubits(self):
         input_state = np.array([[0, 1], [1, 0]])
@@ -187,12 +184,16 @@ def test_mutual_information(self):
         self.assertAlmostEqual(-0.15821825498448047, res)
 
     def test_entanglement_of_formation(self):
-        input_state = np.array([[0.5, 0.25, 0.75, 1],
-                                [1, 0, 1, 0],
-                                [0.5, 0.5, 0.5, 0.5],
-                                [0, 1, 0, 1]])
-        res = entanglement_of_formation(input_state, 2)
-        self.assertAlmostEqual(0.6985340217364572, res)
+        psi1 = [1, 0, 0, 0]
+        rho1 = [[0.5, 0, 0, 0.5], [0, 0, 0, 0], [0, 0, 0, 0], [0.5, 0, 0, 0.5]]
+        rho2 = [[0, 0, 0, 0], [0, 0.5, -0.5j, 0],
+                [0, 0.5j, 0.5, 0], [0, 0, 0, 0]]
+        rho3 = 0.5 * np.array(rho1) + 0.5 * np.array(rho2)
+        rho4 = 0.75 * np.array(rho1) + 0.25 * np.array(rho2)
+        eofs = [entanglement_of_formation(state, 2)
+                for state in [psi1, rho1, rho2, rho3, rho4]]
+        targets = [0.0, 1.0, 1.0, 0.0, 0.35457890266527003]
+        self.assertTrue(np.allclose(eofs, targets))
 
     def test_entanglement_of_formation_1d_input(self):
         input_state = np.array([0.5, 0.25, 0.75, 1])
@@ -207,14 +208,6 @@ def test_entanglement_of_formation_invalid_input(self):
         self.assertEqual(fake_stout.getvalue().strip(), expected)
         self.assertIsNone(res)
 
-    def test__eof_qubit(self):
-        input_rho = np.array([[0.5, 0.25, 0.75, 1],
-                              [1, 0, 1, 0],
-                              [0.5, 0.5, 0.5, 0.5],
-                              [0, 1, 0, 1]])
-        res = eof_qubit(input_rho)
-        self.assertAlmostEqual(0.6985340217364572, res)
-
     def test_is_pos_def(self):
         input_x = np.array([[1, 0],
                             [0, 1]])