Repository: qutip/qutip

Issue:
Fidelity greater than one
fidelity() returns >1 by a significant amount for certain pure states. States are normalised, and taking the inner product returns 1. Code below provides two examples of states which give fidelity >1 with themselves.

```
#to test qutip's fidelity routine
import qutip as qu

psi0=qu.Qobj()

i=0
while i<=1:
    j=0
    while j<=1:
        psi0+=qu.state_number_qobj([2,2],[i,j]) #even superposition of qubit states
        j+=1
    i+=1

psi0=psi0.unit() #normalise

print(qu.fidelity(psi0,psi0))
print((psi0.dag()*psi0).norm()**2)
print("\n")

psi0=qu.tensor(psi0,qu.basis(10,1)) #tensor product with Fock state

print(qu.fidelity(psi0,psi0))
print((psi0.dag()*psi0).norm()**2)

```

Output:

```
1.00000002107
1.0


1.00000003485
1.0
```



Discussion:
I suspect this is because of `A.sqrtm()`, which is not necessary for a pure state. I'll go on and try to see what I can do with it.


Resolving patch:
diff --git a/qutip/metrics.py b/qutip/metrics.py
--- a/qutip/metrics.py
+++ b/qutip/metrics.py
@@ -71,15 +71,31 @@ def fidelity(A, B):
 
     """
     if A.isket or A.isbra:
-        A = ket2dm(A)
-    if B.isket or B.isbra:
-        B = ket2dm(B)
-
-    if A.dims != B.dims:
+        # Take advantage of the fact that the density operator for A
+        # is a projector to avoid a sqrtm call.
+        sqrtmA = ket2dm(A)
+        # Check whether we have to turn B into a density operator, too.
+        if B.isket or B.isbra:
+            B = ket2dm(B)
+    else:
+        if B.isket or B.isbra:
+            # Swap the order so that we can take a more numerically
+            # stable square root of B.
+            return fidelity(B, A)
+        # If we made it here, both A and B are operators, so
+        # we have to take the sqrtm of one of them.
+        sqrtmA = A.sqrtm()
+
+    if sqrtmA.dims != B.dims:
         raise TypeError('Density matrices do not have same dimensions.')
 
-    A = A.sqrtm()
-    return float(np.real((A * (B * A)).sqrtm().tr()))
+    # We don't actually need the whole matrix here, just the trace
+    # of its square root, so let's just get its eigenenergies instead.
+    # We also truncate negative eigenvalues to avoid nan propagation;
+    # even for positive semidefinite matrices, small negative eigenvalues
+    # can be reported.
+    eig_vals = (sqrtmA * B * sqrtmA).eigenenergies()
+    return float(np.real(np.sqrt(eig_vals[eig_vals > 0]).sum()))
 
 
 def process_fidelity(U1, U2, normalize=True):


Test patch:
diff --git a/qutip/tests/test_metrics.py b/qutip/tests/test_metrics.py
--- a/qutip/tests/test_metrics.py
+++ b/qutip/tests/test_metrics.py
@@ -36,6 +36,8 @@
 #    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 #    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ###############################################################################
+from __future__ import division
+
 import numpy as np
 from numpy import abs, sqrt
 from numpy.testing import assert_, assert_almost_equal, run_module_suite
@@ -44,10 +46,11 @@
 from qutip.operators import (
     create, destroy, jmat, identity, qdiags, sigmax, sigmay, sigmaz, qeye
 )
-from qutip.states import fock_dm
+from qutip.states import fock_dm, basis
 from qutip.propagator import propagator
 from qutip.random_objects import (
-    rand_herm, rand_dm, rand_unitary, rand_ket, rand_super_bcsz
+    rand_herm, rand_dm, rand_unitary, rand_ket, rand_super_bcsz,
+    rand_ket_haar, rand_dm_ginibre
 )
 from qutip.qobj import Qobj
 from qutip.superop_reps import to_super
@@ -95,6 +98,85 @@ def test_fidelity2():
         assert_(abs((F-FU)/F) < 1e-5)
 
 
+def test_fidelity_max():
+    """
+    Metrics: Fidelity of a pure state w/ itself should be 1.
+    """
+    for _ in range(10):
+        psi = rand_ket_haar(13)
+        assert_almost_equal(fidelity(psi, psi), 1)
+
+
+def test_fidelity_bounded_purepure(tol=1e-7):
+    """
+    Metrics: Fidelity of pure states within [0, 1].
+    """
+    for _ in range(10):
+        psi = rand_ket_haar(17)
+        phi = rand_ket_haar(17)
+        F = fidelity(psi, phi)
+        assert_(-tol <= F <= 1 + tol)
+
+
+def test_fidelity_bounded_puremixed(tol=1e-7):
+    """
+    Metrics: Fidelity of pure states against mixed states within [0, 1].
+    """
+    for _ in range(10):
+        psi = rand_ket_haar(11)
+        sigma = rand_dm_ginibre(11)
+        F = fidelity(psi, sigma)
+        assert_(-tol <= F <= 1 + tol)
+
+
+def test_fidelity_bounded_mixedmixed(tol=1e-7):
+    """
+    Metrics: Fidelity of mixed states within [0, 1].
+    """
+    for _ in range(10):
+        rho = rand_dm_ginibre(11)
+        sigma = rand_dm_ginibre(11)
+        F = fidelity(rho, sigma)
+        assert_(-tol <= F <= 1 + tol)
+
+
+def test_fidelity_known_cases():
+    """
+    Metrics: Checks fidelity against known cases.
+    """
+    ket0 = basis(2, 0)
+    ket1 = basis(2, 1)
+    ketp = (ket0 + ket1).unit()
+    # A state that almost overlaps with |+> should serve as
+    # a nice test case, especially since we can analytically
+    # calculate its overlap with |+>.
+    ketpy = (ket0 + np.exp(1j * np.pi / 4) * ket1).unit()
+
+    mms = qeye(2).unit()
+
+    assert_almost_equal(fidelity(ket0, ketp), 1 / np.sqrt(2))
+    assert_almost_equal(fidelity(ket0, ket1), 0)
+    assert_almost_equal(fidelity(ket0, mms),  1 / np.sqrt(2))
+    assert_almost_equal(fidelity(ketp, ketpy),
+        np.sqrt(
+            (1 / 8) + (1 / 2 + 1 / (2 * np.sqrt(2))) ** 2
+        )
+    )
+
+
+def test_fidelity_overlap():
+    """
+    Metrics: Checks fidelity against pure-state overlap. (#631)
+    """
+    for _ in range(10):
+        psi = rand_ket_haar(7)
+        phi = rand_ket_haar(7)
+
+        assert_almost_equal(
+            fidelity(psi, phi),
+            np.abs((psi.dag() * phi)[0, 0])
+        )
+
 def test_tracedist1():
     """
     Metrics: Trace dist., invariance under unitary trans.