Repository: qutip/qutip

Issue:
the inverse of an operator
I tried to calculate the inverse of an operator as following:
```
>>> import qutip as qt
>>> from scipy.sparse.linalg import inv
>>> inv(qt.sigmaz().data)
C:\Users\username\Miniconda3\lib\site-packages\scipy\sparse\linalg\dsolve\linsolve.py:296: SparseEfficiencyWarning: splu requires CSC matrix format
  warn('splu requires CSC matrix format', SparseEfficiencyWarning)
C:\Users\username\Miniconda3\lib\site-packages\scipy\sparse\linalg\dsolve\linsolve.py:203: SparseEfficiencyWarning: spsolve is more efficient when sparse b is in the CSC matrix format
  'is in the CSC matrix format', SparseEfficiencyWarning)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\username\Miniconda3\lib\site-packages\scipy\sparse\linalg\matfuncs.py", line 78, in inv       
    Ainv = spsolve(A, I)
  File "C:\Users\username\Miniconda3\lib\site-packages\scipy\sparse\linalg\dsolve\linsolve.py", line 223, in spsolve
    shape=b.shape, dtype=A.dtype)
  File "C:\Users\username\Miniconda3\lib\site-packages\qutip\fastsparse.py", line 59, in __init__
    if args[1].shape[0] and args[1].dtype != np.int32:
AttributeError: 'tuple' object has no attribute 'shape'
```
I don't know why this doesn't work.
Although I can convert it to be dense, and use `scipy.linalg.inv` to do the same job,
the super operaters to be inverted in my actual problems are extremely large and sparse, and the inverses are also expected to be sparse.
So I want to find a way without converting them to dense.
But I didn't find any function in QuTiP to do that. Or maybe I missed it?

BTW, the version I use is:
```
QuTiP Version:      4.4.1
Numpy Version:      1.16.5
Scipy Version:      1.3.1
Cython Version:     0.29.13
Matplotlib Version: 3.1.1
Python Version:     3.7.3
Number of CPUs:     4
BLAS Info:          INTEL MKL
OPENMP Installed:   False
INTEL MKL Ext:      True
Platform Info:      Windows (AMD64)
Installation path:  C:\Users\username\Miniconda3\lib\site-packages\qutip
```


Discussion:
Hi @HAyAsIiI ,
would you consider doing the following?

```
import qutip as qt
from scipy.sparse.linalg import inv
import numpy as np
from scipy import sparse

sMat = sparse.csr_matrix(np.asarray(qt.sigmaz( )))
invrs = inv(sMat)
```

Since a few version back, we've added a __ array __ method to Qobj. So now you can have sMat be your sparse matrix.


Resolving patch:
diff --git a/qutip/qobj.py b/qutip/qobj.py
--- a/qutip/qobj.py
+++ b/qutip/qobj.py
@@ -62,7 +62,8 @@
 from qutip.permute import _permute
 from qutip.sparse import (sp_eigs, sp_expm, sp_fro_norm, sp_max_norm,
                           sp_one_norm, sp_L2_norm)
-from qutip.dimensions import type_from_dims, enumerate_flat, collapse_dims_super
+from qutip.dimensions import (type_from_dims, enumerate_flat,
+                              collapse_dims_super)
 from qutip.cy.spmath import (zcsr_transpose, zcsr_adjoint, zcsr_isherm,
                             zcsr_trace, zcsr_proj, zcsr_inner)
 from qutip.cy.spmatfuncs import zcsr_mat_elem
@@ -74,7 +75,7 @@
     from itertools import izip_longest
     zip_longest = izip_longest
 
-#OPENMP stuff
+# OPENMP stuff
 from qutip.cy.openmp.utilities import use_openmp
 if settings.has_openmp:
     from qutip.cy.openmp.omp_sparse_utils import omp_tidyup
@@ -176,6 +177,8 @@ class Qobj(object):
     groundstate(sparse=False, tol=0, maxiter=100000)
         Returns eigenvalue and eigenket for the groundstate of a quantum
         object.
+    inv()
+        Return a Qobj corresponding to the matrix inverse of the operator.
     matrix_element(bra, ket)
         Returns the matrix element of operator between `bra` and `ket` vectors.
     norm(norm='tr', sparse=False, tol=0, maxiter=100000)
@@ -319,12 +322,12 @@ def __init__(self, inpt=None, dims=[[], []], shape=[],
             inpt = np.array([[0]])
             _tmp = sp.csr_matrix(inpt, dtype=complex, copy=copy)
             self._data = fast_csr_matrix((_tmp.data, _tmp.indices, _tmp.indptr),
-                                        shape = _tmp.shape)
+                                        shape=_tmp.shape)
             self.dims = [[int(inpt.shape[0])], [int(inpt.shape[1])]]
 
         if type == 'super':
             # Type is not super, i.e. dims not explicitly passed, but oper shape
-            if dims== [[], []] and self.shape[0] == self.shape[1]:
+            if dims == [[], []] and self.shape[0] == self.shape[1]:
                 sub_shape = np.sqrt(self.shape[0])
                 # check if root of shape is int
                 if (sub_shape % 1) != 0:
@@ -349,7 +352,7 @@ def copy(self):
 
     def get_data(self):
         return self._data
-    #Here we perfrom a check of the csr matrix type during setting of Q.data
+    # Here we perfrom a check of the csr matrix type during setting of Q.data
     def set_data(self, data):
         if not isinstance(data, fast_csr_matrix):
             raise TypeError('Qobj data must be in fast_csr format.')
@@ -546,7 +549,7 @@ def __mul__(self, other):
                 raise TypeError("Incompatible Qobj shapes")
 
         elif isinstance(other, np.ndarray):
-            if other.dtype=='object':
+            if other.dtype == 'object':
                 return np.array([self * item for item in other],
                                 dtype=object)
             else:
@@ -583,7 +586,7 @@ def __rmul__(self, other):
         MULTIPLICATION with Qobj on RIGHT [ ex. 4*Qobj ]
         """
         if isinstance(other, np.ndarray):
-            if other.dtype=='object':
+            if other.dtype == 'object':
                 return np.array([item * self for item in other],
                                             dtype=object)
             else:
@@ -1015,16 +1018,15 @@ def proj(self):
             Projection operator.
         """
         if self.isket:
-            _out = zcsr_proj(self.data,1)
-            _dims = [self.dims[0],self.dims[0]]
+            _out = zcsr_proj(self.data, 1)
+            _dims = [self.dims[0], self.dims[0]]
         elif self.isbra:
-            _out = zcsr_proj(self.data,0)
-            _dims = [self.dims[1],self.dims[1]]
+            _out = zcsr_proj(self.data, 0)
+            _dims = [self.dims[1], self.dims[1]]
         else:
             raise TypeError('Projector can only be formed from a bra or ket.')
 
-        return Qobj(_out,dims=_dims)
-
+        return Qobj(_out, dims=_dims)
 
     def tr(self):
         """Trace of a quantum object.
@@ -1199,7 +1201,6 @@ def sqrtm(self, sparse=False, tol=0, maxiter=100000):
         else:
             raise TypeError('Invalid operand for matrix square root')
 
-
     def cosm(self):
         """Cosine of a quantum operator.
 
@@ -1221,11 +1222,10 @@ def cosm(self):
 
         """
         if self.dims[0][0] == self.dims[1][0]:
-             return 0.5 * ((1j * self).expm() + (-1j * self).expm())
+            return 0.5 * ((1j * self).expm() + (-1j * self).expm())
         else:
             raise TypeError('Invalid operand for matrix square root')
 
-
     def sinm(self):
         """Sine of a quantum operator.
 
@@ -1247,11 +1247,32 @@ def sinm(self):
 
         """
         if self.dims[0][0] == self.dims[1][0]:
-             return -0.5j * ((1j * self).expm() - (-1j * self).expm())
+            return -0.5j * ((1j * self).expm() - (-1j * self).expm())
         else:
             raise TypeError('Invalid operand for matrix square root')
 
+    def inv(self, sparse=False):
+        """Matrix inverse of a quantum operator
+
+        Operator must be square.
+
+        Returns
+        -------
+        oper : :class:`qutip.Qobj`
+            Matrix inverse of operator.
 
+        Raises
+        ------
+        TypeError
+            Quantum object is not square.
+        """
+        if self.shape[0] != self.shape[1]:
+            raise TypeError('Invalid operand for matrix inverse')
+        if sparse:
+            inv_mat = sp.linalg.inv(sp.csc_matrix(self.data))
+        else:
+            inv_mat = np.linalg.inv(self.full())
+        return Qobj(inv_mat, dims=self.dims[::-1])
 
     def unit(self, inplace=False,
             norm=None, sparse=False,
@@ -1295,7 +1316,7 @@ def unit(self, inplace=False,
         else:
             raise Exception('inplace kwarg must be bool.')
 
-    def ptrace(self, sel):
+    def ptrace(self, sel, sparse=None):
         """Partial trace of the quantum object.
 
         Parameters
@@ -1315,9 +1336,17 @@ def ptrace(self, sel):
         that has been deprecated.
 
         """
-        q = Qobj()
-        q.data, q.dims, _ = _ptrace(self, sel)
-        return q.tidyup() if settings.auto_tidyup else q
+        if sparse is None:
+            if self.isket:
+                sparse = False
+            elif (self.data.nnz / (self.shape[0] * self.shape[1])) >= 0.1:
+                sparse = False
+        if sparse:
+            q = Qobj()
+            q.data, q.dims, _ = _ptrace(self, sel)
+            return q.tidyup() if settings.auto_tidyup else q
+        else:
+            return _ptrace_dense(self, sel)
 
     def permute(self, order):
         """Permutes a composite quantum object.
@@ -1354,14 +1383,14 @@ def tidyup(self, atol=settings.auto_tidyup_atol):
 
         """
         if self.data.nnz:
-            #This does the tidyup and returns True if
-            #The sparse data needs to be shortened
+            # This does the tidyup and returns True if
+            # The sparse data needs to be shortened
             if use_openmp() and self.data.nnz > 500:
-                if omp_tidyup(self.data.data,atol,self.data.nnz,
+                if omp_tidyup(self.data.data,atol, self.data.nnz,
                             settings.num_cpus):
                             self.data.eliminate_zeros()
             else:
-                if cy_tidyup(self.data.data,atol,self.data.nnz):
+                if cy_tidyup(self.data.data,atol, self.data.nnz):
                     self.data.eliminate_zeros()
             return self
         else:
@@ -1445,8 +1474,6 @@ def transform(self, inpt, inverse=False, sparse=True):
         else:
             return out
 
-
-
     def trunc_neg(self, method="clip"):
         """Truncates negative eigenvalues and renormalizes.
 
@@ -1509,7 +1536,6 @@ def trunc_neg(self, method="clip"):
             ], Qobj(np.zeros(self.shape), dims=self.dims)
         ).unit()
 
-
     def matrix_element(self, bra, ket):
         """Calculates a matrix element.
 
@@ -1540,10 +1566,10 @@ def matrix_element(self, bra, ket):
 
         else:
             if bra.isbra and ket.isket:
-                return zcsr_mat_elem(self.data,bra.data,ket.data,1)
+                return zcsr_mat_elem(self.data, bra.data, ket.data, 1)
 
             elif bra.isket and ket.isket:
-                return zcsr_mat_elem(self.data,bra.data,ket.data,0)
+                return zcsr_mat_elem(self.data, bra.data, ket.data, 0)
             else:
                 err = "Can only calculate matrix elements for bra"
                 err += " and ket vectors."
@@ -1586,8 +1612,8 @@ def overlap(self, other):
                 if other.isket:
                     return zcsr_inner(self.data, other.data, 1)
                 elif other.isbra:
-                    #Since we deal mainly with ket vectors, the bra-bra combo
-                    #is not common, and not optimized.
+                    # Since we deal mainly with ket vectors, the bra-bra combo
+                    # is not common, and not optimized.
                     return zcsr_inner(self.data, other.dag().data, 1)
                 elif other.isoper:
                     return (qutip.states.ket2dm(self).dag() * other).tr()
@@ -1618,7 +1644,6 @@ def overlap(self, other):
 
         raise TypeError("Can only calculate overlap for state vector Qobjs")
 
-
     def eigenstates(self, sparse=False, sort='low',
                     eigvals=0, tol=0, maxiter=100000):
         """Eigenstates and eigenenergies.
@@ -1668,7 +1693,6 @@ def eigenstates(self, sparse=False, sort='low',
         norms = np.array([ket.norm() for ket in ekets])
         return evals, ekets / norms
 
-
     def eigenenergies(self, sparse=False, sort='low',
                       eigvals=0, tol=0, maxiter=100000):
         """Eigenenergies of a quantum object.
@@ -1852,7 +1876,6 @@ def dnorm(self, B=None):
         """
         return mts.dnorm(self, B)
 
-
     @property
     def ishp(self):
         # FIXME: this needs to be cached in the same ways as isherm.
@@ -1935,7 +1958,6 @@ def iscptp(self):
 
     @property
     def isherm(self):
-
         if self._isherm is not None:
             # used previously computed value
             return self._isherm
@@ -2102,7 +2124,6 @@ def qobj_list_evaluate(qobj_list, t, args):
 # A collection of tests used to determine the type of quantum objects, and some
 # functions for increased compatibility with quantum optics toolbox.
 #
-
 def dag(A):
     """Adjont operator (dagger) of a quantum object.
 
@@ -2156,6 +2177,36 @@ def ptrace(Q, sel):
     return Q.ptrace(sel)
 
 
+def _ptrace_dense(Q, sel):
+    rd = np.asarray(Q.dims[0], dtype=np.int32).ravel()
+    nd = len(rd)
+    if isinstance(sel, int):
+        sel = np.array([sel])
+    else:
+        sel = np.asarray(sel)
+    sel = list(np.sort(sel))
+    dkeep = (rd[sel]).tolist()
+    qtrace = list(set(np.arange(nd)) - set(sel))
+    dtrace = (rd[qtrace]).tolist()
+    rd = list(rd)
+    if isket(Q):
+        vmat = (Q.full()
+                .reshape(rd)
+                .transpose(sel + qtrace)
+                .reshape([np.prod(dkeep), np.prod(dtrace)]))
+        rhomat = vmat.dot(vmat.conj().T)
+    else:
+        rhomat = np.trace(Q.full()
+                          .reshape(rd + rd)
+                          .transpose(qtrace + [nd + q for q in qtrace] +
+                                     sel + [nd + q for q in sel])
+                          .reshape([np.prod(dtrace),
+                                    np.prod(dtrace),
+                                    np.prod(dkeep),
+                                    np.prod(dkeep)]))
+    return Qobj(rhomat, dims=[dkeep, dkeep])
+
+
 def dims(inpt):
     """Returns the dims attribute of a quantum object.
 
diff --git a/qutip/random_objects.py b/qutip/random_objects.py
--- a/qutip/random_objects.py
+++ b/qutip/random_objects.py
@@ -82,13 +82,14 @@ def rand_jacobi_rotation(A, seed=None):
     b = 1.0/np.sqrt(2)*np.exp(1j*angle)
     i = np.int(np.floor(np.random.random()*n))
     j = i
-    while (i==j):
+    while (i == j):
         j = np.int(np.floor(np.random.random()*n))
-    data = np.hstack((np.array([a,-b,a,b],dtype=complex),np.ones(n-2,dtype=complex)))
-    diag = np.delete(np.arange(n),[i,j])
-    rows = np.hstack(([i,i,j,j],diag))
-    cols = np.hstack(([i,j,i,j],diag))
-    R = sp.coo_matrix((data,(rows,cols)),shape=[n,n], dtype=complex).tocsr()
+    data = np.hstack((np.array([a,-b,a,b], dtype=complex),
+                      np.ones(n-2, dtype=complex)))
+    diag = np.delete(np.arange(n), [i,j])
+    rows = np.hstack(([i,i,j,j], diag))
+    cols = np.hstack(([i,j,i,j], diag))
+    R = sp.coo_matrix((data,(rows,cols)), shape=[n,n], dtype=complex).tocsr()
     A = R*A*R.conj().transpose()
     return A
 
@@ -132,6 +133,8 @@ def rand_herm(N, density=0.75, dims=None, pos_def=False, seed=None):
         tensor structure. Default is dims=[[N],[N]].
     pos_def : bool (default=False)
         Return a positive semi-definite matrix (by diagonal dominance).
+    seed : int
+        seed for the random number generator
 
     Returns
     -------
@@ -147,39 +150,73 @@ def rand_herm(N, density=0.75, dims=None, pos_def=False, seed=None):
     not be repeatedly used for generating matrices larger than ~1000x1000.
 
     """
-    if isinstance(N,(np.ndarray,list)):
+    if seed is not None:
+        np.random.seed(seed=seed)
+    if isinstance(N, (np.ndarray,list)):
         M = sp.diags(N,0, dtype=complex, format='csr')
         N = len(N)
         if dims:
             _check_dims(dims, N, N)
-        nvals = N**2*density
-        M = rand_jacobi_rotation(M, seed=seed)
-        while M.nnz < 0.95*nvals:
+        nvals = max([N**2 * density, 1])
+        M = rand_jacobi_rotation(M)
+        while M.nnz < 0.95 * nvals:
             M = rand_jacobi_rotation(M)
+        M.sort_indices()
     elif isinstance(N, (int, np.int32, np.int64)):
-        if seed is not None:
-            np.random.seed(seed=seed)
         if dims:
             _check_dims(dims, N, N)
-        num_elems = np.int(np.ceil(N*(N+1)*density)/2)
-        data = (2*np.random.rand(num_elems)-1)+1j*(2*np.random.rand(num_elems)-1)
-        row_idx = np.random.choice(N, num_elems)
-        col_idx = np.random.choice(N, num_elems)
-        M = sp.coo_matrix((data, (row_idx,col_idx)), dtype=complex, shape=(N,N)).tocsr()
-        M = 0.5*(M+M.conj().transpose())
-        if pos_def:
-            M = M.tocoo()
-            M.setdiag(np.abs(M.diagonal())+np.sqrt(2)*N)
-            M = M.tocsr()
+        if density < 0.5:
+            M = _rand_herm_sparse(N, density, pos_def)
+        else:
+            M = _rand_herm_dense(N, density, pos_def)
     else:
         raise TypeError('Input N must be an integer or array_like.')
-    M.sort_indices()
     if dims:
         return Qobj(M, dims=dims)
     else:
         return Qobj(M)
 
 
+def _rand_herm_sparse(N, density, pos_def):
+    target = (1-(1-density)**0.5)
+    num_elems = (N**2 - 0.666 * N) * target + 0.666 * N * density
+    num_elems = max([num_elems, 1])
+    num_elems = np.int(num_elems)
+    data = (2 * np.random.rand(num_elems) - 1) + \
+           (2 * np.random.rand(num_elems) - 1) * 1j
+    row_idx, col_idx = zip(*[divmod(index, N) for index
+                             in np.random.choice(N*N,
+                                                 num_elems,
+                                                 replace=False)])
+    M = sp.coo_matrix((data, (row_idx,col_idx)),
+                      dtype=complex, shape=(N,N)).tocsr()
+    M = 0.5 * (M + M.conj().transpose())
+    if pos_def:
+        M.setdiag(np.abs(M.diagonal())+np.sqrt(2)*N)
+    M.sort_indices()
+    return M
+
+
+def _rand_herm_dense(N, density, pos_def):
+    M = (2 * np.random.rand(N,N) - 1) + \
+        (2 * np.random.rand(N,N) - 1) * 1j
+    M = 0.5 * (M + M.conj().transpose())
+    target = (1-(density)**0.5)
+    num_remove = N * (N - 0.666) * target + 0.666 * N * (1 - density)
+    num_remove = max([num_remove, 1])
+    num_remove = np.int(num_remove)
+    for row, col in [divmod(index, N)
+                     for index in np.random.choice(N*N,
+                                                   num_remove,
+                                                   replace=False)]:
+        M[col, row] = 0
+        M[row, col] = 0
+    if pos_def:
+        as_vec = M.ravel()
+        M[::N+1] = (np.abs(M.diagonal())+np.sqrt(2)*N)
+    return M
+
+
 def rand_unitary(N, density=0.75, dims=None, seed=None):
     """Creates a random NxN sparse unitary quantum object.
 
@@ -211,6 +248,7 @@ def rand_unitary(N, density=0.75, dims=None, seed=None):
     else:
         return Qobj(U)
 
+
 def rand_unitary_haar(N=2, dims=None, seed=None):
     """
     Returns a Haar random unitary matrix of dimension
@@ -293,6 +331,9 @@ def rand_ket(N=0, density=1, dims=None, seed=None):
     else:
         dims = [[N],[1]]
     X = sp.rand(N, 1, density, format='csr')
+    while X.nnz == 0:
+        # ensure that the ket is not all zeros.
+        X = sp.rand(N, 1, density+1/N, format='csr')
     X.data = X.data - 0.5
     Y = X.copy()
     Y.data = 1.0j * (np.random.random(len(X.data)) - 0.5)
@@ -358,10 +399,11 @@ def rand_dm(N, density=0.75, pure=False, dims=None, seed=None):
     as no diagonal elements will be generated such that :math:`Tr(\\rho)=1`.
 
     """
-    if isinstance(N,(np.ndarray,list)):
+    if isinstance(N, (np.ndarray, list)):
         if np.abs(np.sum(N)-1.0) > 1e-15:
-            raise ValueError('Eigenvalues of a density matrix must sum to one.')
-        H = sp.diags(N,0, dtype=complex, format='csr')
+            raise ValueError('Eigenvalues of a density matrix '
+                             'must sum to one.')
+        H = sp.diags(N, 0, dtype=complex, format='csr')
         N = len(N)
         if dims:
             _check_dims(dims, N, N)
@@ -436,6 +478,7 @@ def rand_dm_ginibre(N=2, rank=None, dims=None, seed=None):
 
     return Qobj(rho, dims=dims)
 
+
 def rand_dm_hs(N=2, dims=None, seed=None):
     """
     Returns a Hilbert-Schmidt random density operator of dimension
@@ -576,9 +619,9 @@ def rand_super_bcsz(N=2, enforce_tp=True, rank=None, dims=None, seed=None):
         Y = np.einsum('ijik->jk', XXdag.reshape((N, N, N, N)))
 
         # Now we have the matrix 𝟙 ⊗ Y^{-1/2}, which we can find by doing
-        # the square root and the inverse separately. As a possible improvement,
-        # iterative methods exist to find inverse square root matrices directly,
-        # as this is important in statistics.
+        # the square root and the inverse separately. As a possible
+        # improvement, iterative methods exist to find inverse square root
+        # matrices directly, as this is important in statistics.
         Z = np.kron(
             np.eye(N),
             sqrtm(la.inv(Y))
@@ -642,36 +685,23 @@ def rand_stochastic(N, density=0.75, kind='left', dims=None, seed=None):
                          np.random.choice(N, num_elems-N)])
     col_idx = np.hstack([np.random.permutation(N),
                          np.random.choice(N, num_elems-N)])
+    M = sp.coo_matrix((data, (row_idx, col_idx)),
+                      dtype=float, shape=(N,N)).tocsr()
+    M = 0.5 * (M + M.conj().transpose())
+    num_rows = M.indptr.shape[0]-1
+    for row in range(num_rows):
+        row_start = M.indptr[row]
+        row_end = M.indptr[row+1]
+        row_sum = np.sum(M.data[row_start:row_end])
+        M.data[row_start:row_end] /= row_sum
     if kind=='left':
-        M = sp.coo_matrix((data, (row_idx,col_idx)), dtype=float, shape=(N,N)).tocsc()
-    else:
-        M = sp.coo_matrix((data, (row_idx,col_idx)), dtype=float, shape=(N,N)).tocsr()
-    M = 0.5*(M+M.conj().transpose())
-    if kind=='left':
-        num_cols = M.indptr.shape[0]-1
-        for col in range(num_cols):
-            col_start = M.indptr[col]
-            col_end = M.indptr[col+1]
-            col_sum = np.sum(M.data[col_start:col_end])
-            M.data[col_start:col_end] /= col_sum
-        M = M.tocsr()
-    else:
-        num_rows = M.indptr.shape[0]-1
-        for row in range(num_rows):
-            row_start = M.indptr[row]
-            row_end = M.indptr[row+1]
-            row_sum = np.sum(M.data[row_start:row_end])
-            M.data[row_start:row_end] /= row_sum
+        M = M.transpose()
     if dims:
         return Qobj(M, dims=dims, shape=[N, N])
     else:
         return Qobj(M)
 
 
-
-
-
-
 def _check_dims(dims, N1, N2):
     if len(dims) != 2:
         raise Exception("Qobj dimensions must be list of length 2.")


Test patch:
diff --git a/qutip/tests/test_ptrace.py b/qutip/tests/test_ptrace.py
--- a/qutip/tests/test_ptrace.py
+++ b/qutip/tests/test_ptrace.py
@@ -35,76 +35,144 @@
 from qutip.legacy.ptrace import _ptrace as _pt
 
 def test_ptrace_rand():
-    'ptrace : randomized tests'
-    for k in range(10):
+    'ptrace : randomized tests, sparse'
+    for k in range(5):
         A = tensor(rand_ket(5), rand_ket(2), rand_ket(3))
-        B = A.ptrace([1,2])
+        B = A.ptrace([1,2], True)
         bdat,bd,bs = _pt(A, [1,2])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
-        B = A.ptrace([0,2])
+
+        B = A.ptrace([0,2], True)
         bdat,bd,bs = _pt(A, [0,2])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
-        B = A.ptrace([0,1])
+
+        B = A.ptrace([0,1], True)
         bdat,bd,bs = _pt(A, [0,1])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
 
-
-    for k in range(10):
+    for k in range(5):
         A = tensor(rand_dm(2), thermal_dm(10,1), rand_unitary(3))
-        B = A.ptrace([1,2])
+        B = A.ptrace([1,2], True)
         bdat,bd,bs = _pt(A, [1,2])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
-        B = A.ptrace([0,2])
+
+        B = A.ptrace([0,2], True)
         bdat,bd,bs = _pt(A, [0,2])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
-        B = A.ptrace([0,1])
+
+        B = A.ptrace([0,1], True)
         bdat,bd,bs = _pt(A, [0,1])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
-        
-    for k in range(10):
+
+    for k in range(5):
         A = tensor(rand_ket(2),rand_ket(2),rand_ket(2),
                     rand_ket(2),rand_ket(2),rand_ket(2))
-        B = A.ptrace([3,2])
+        B = A.ptrace([3,2], True)
         bdat,bd,bs = _pt(A, [3,2])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
-        B = A.ptrace([0,2])
+
+        B = A.ptrace([0,2], True)
         bdat,bd,bs = _pt(A, [0,2])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
-        B = A.ptrace([0,1])
+
+        B = A.ptrace([0,1], True)
         bdat,bd,bs = _pt(A, [0,1])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
 
+    for k in range(5):
+        A = rand_dm(64,0.5,dims=[[4,4,4],[4,4,4]])
+        B = A.ptrace([0], True)
+        bdat,bd,bs = _pt(A, [0])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+        B = A.ptrace([1], True)
+        bdat,bd,bs = _pt(A, [1])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
 
-    for k in range(10):
+        B = A.ptrace([0,2], True)
+        bdat,bd,bs = _pt(A, [0,2])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+
+def test_ptrace_rand():
+    'ptrace : randomized tests, dense'
+    for k in range(5):
+        A = tensor(rand_ket(5), rand_ket(2), rand_ket(3))
+        B = A.ptrace([1,2], False)
+        bdat,bd,bs = _pt(A, [1,2])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+        B = A.ptrace([0,2], False)
+        bdat,bd,bs = _pt(A, [0,2])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+        B = A.ptrace([0,1], False)
+        bdat,bd,bs = _pt(A, [0,1])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+    for k in range(5):
+        A = tensor(rand_dm(2), thermal_dm(10,1), rand_unitary(3))
+        B = A.ptrace([1,2], False)
+        bdat,bd,bs = _pt(A, [1,2])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+        B = A.ptrace([0,2], False)
+        bdat,bd,bs = _pt(A, [0,2])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+        B = A.ptrace([0,1], False)
+        bdat,bd,bs = _pt(A, [0,1])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+    for k in range(5):
+        A = tensor(rand_ket(2),rand_ket(2),rand_ket(2),
+                    rand_ket(2),rand_ket(2),rand_ket(2))
+        B = A.ptrace([3,2], False)
+        bdat,bd,bs = _pt(A, [3,2])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+        B = A.ptrace([0,2], False)
+        bdat,bd,bs = _pt(A, [0,2])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+        B = A.ptrace([0,1], False)
+        bdat,bd,bs = _pt(A, [0,1])
+        C = Qobj(bdat,dims=bd)
+        assert_(B==C)
+
+    for k in range(5):
         A = rand_dm(64,0.5,dims=[[4,4,4],[4,4,4]])
-        B = A.ptrace([0])
+        B = A.ptrace([0], False)
         bdat,bd,bs = _pt(A, [0])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
-        B = A.ptrace([1])
+
+        B = A.ptrace([1], False)
         bdat,bd,bs = _pt(A, [1])
         C = Qobj(bdat,dims=bd)
         assert_(B==C)
-        
-        B = A.ptrace([0,2])
+
+        B = A.ptrace([0,2], False)
         bdat,bd,bs = _pt(A, [0,2])
         C = Qobj(bdat,dims=bd)
-        assert_(B==C)
\ No newline at end of file
+        assert_(B==C)
diff --git a/qutip/tests/test_qobj.py b/qutip/tests/test_qobj.py
--- a/qutip/tests/test_qobj.py
+++ b/qutip/tests/test_qobj.py
@@ -51,6 +51,16 @@
 from functools import partial
 from contextlib import contextmanager
 
+def _random_not_singular(N):
+    """
+    return a N*N complex array with determinant not 0.
+    """
+    data = np.zeros((1,1))
+    while np.linalg.det(data) == 0:
+        data = np.random.random((N, N)) + \
+               1j * np.random.random((N, N)) - (0.5 + 0.5j)
+    return data
+
 @contextmanager
 def expect_exception(ex_class):
     """
@@ -91,16 +101,14 @@ def assert_hermicity(oper, hermicity, msg=""):
 def test_QobjData():
     "Qobj data"
     N = 10
-    data1 = np.random.random(
-        (N, N)) + 1j * np.random.random((N, N)) - (0.5 + 0.5j)
+    data1 = _random_not_singular(N)
     q1 = Qobj(data1)
     # check if data is a csr_matrix if originally array
     assert_equal(sp.isspmatrix_csr(q1.data), True)
     # check if dense ouput is equal to original data
     assert_(np.all(q1.data.todense() - np.matrix(data1) == 0))
 
-    data2 = np.random.random(
-        (N, N)) + 1j * np.random.random((N, N)) - (0.5 + 0.5j)
+    data2 = _random_not_singular(N)
     data2 = sp.csr_matrix(data2)
     q2 = Qobj(data2)
     # check if data is a csr_matrix if originally csr_matrix
@@ -111,8 +119,7 @@ def test_QobjData():
     # check if data is a csr_matrix if originally int
     assert_equal(sp.isspmatrix_csr(q3.data), True)
 
-    data4 = np.random.random(
-        (N, N)) + 1j * np.random.random((N, N)) - (0.5 + 0.5j)
+    data4 = _random_not_singular(N)
     data4 = np.matrix(data4)
     q4 = Qobj(data4)
     # check if data is a csr_matrix if originally csr_matrix
@@ -154,8 +161,7 @@ def test_QobjType():
 def test_QobjHerm():
     "Qobj Hermicity"
     N = 10
-    data = np.random.random(
-        (N, N)) + 1j * np.random.random((N, N)) - (0.5 + 0.5j)
+    data = _random_not_singular(N)
     q = Qobj(data)
     assert_equal(q.isherm, False)
 
@@ -222,8 +228,7 @@ def test_QobjUnitaryOper():
 def test_QobjDimsShape():
     "Qobj shape"
     N = 10
-    data = np.random.random(
-        (N, N)) + 1j * np.random.random((N, N)) - (0.5 + 0.5j)
+    data = _random_not_singular(N)
 
     q1 = Qobj(data)
     assert_equal(q1.dims, [[10], [10]])
@@ -238,8 +243,7 @@ def test_QobjDimsShape():
 
     N = 4
 
-    data = np.random.random(
-        (N, N)) + 1j * np.random.random((N, N)) - (0.5 + 0.5j)
+    data = _random_not_singular(N)
 
     q1 = Qobj(data, dims=[[2, 2], [2, 2]])
     assert_equal(q1.dims, [[2, 2], [2, 2]])
@@ -327,12 +331,10 @@ def test_QobjAddition():
 
 def test_QobjSubtraction():
     "Qobj subtraction"
-    data1 = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data1 = _random_not_singular(5)
     q1 = Qobj(data1)
 
-    data2 = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data2 = _random_not_singular(5)
     q2 = Qobj(data2)
 
     q3 = q1 - q2
@@ -364,8 +366,7 @@ def test_QobjMultiplication():
 
 def test_QobjDivision():
     "Qobj division"
-    data = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data = _random_not_singular(5)
     q = Qobj(data)
     randN = 10 * np.random.random()
     q = q / randN
@@ -374,8 +375,7 @@ def test_QobjDivision():
 
 def test_QobjPower():
     "Qobj power"
-    data = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data = _random_not_singular(5)
     q = Qobj(data)
 
     q2 = q ** 2
@@ -387,8 +387,7 @@ def test_QobjPower():
 
 def test_QobjNeg():
     "Qobj negation"
-    data = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data = _random_not_singular(5)
     q = Qobj(data)
     x = -q
     assert_(np.all(x.data.todense() + np.matrix(data) == 0))
@@ -398,8 +397,7 @@ def test_QobjNeg():
 
 def test_QobjEquals():
     "Qobj equals"
-    data = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data = _random_not_singular(5)
     q1 = Qobj(data)
     q2 = Qobj(data)
     assert_(q1 == q2)
@@ -411,8 +409,7 @@ def test_QobjEquals():
 
 def test_QobjGetItem():
     "Qobj getitem"
-    data = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data = _random_not_singular(5)
     q = Qobj(data)
     assert_equal(q[0, 0], data[0, 0])
     assert_equal(q[-1, 2], data[-1, 2])
@@ -489,8 +486,7 @@ def test_Qobj_Spmv():
 
 def test_QobjConjugate():
     "Qobj conjugate"
-    data = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data = _random_not_singular(5)
     A = Qobj(data)
     B = A.conj()
     assert_(np.all(B.data.todense() - np.matrix(data.conj()) == 0))
@@ -501,8 +497,7 @@ def test_QobjConjugate():
 
 def test_QobjDagger():
     "Qobj adjoint (dagger)"
-    data = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data = _random_not_singular(5)
     A = Qobj(data)
     B = A.dag()
     assert_(np.all(B.data.todense() - np.matrix(data.conj().T) == 0))
@@ -513,8 +508,7 @@ def test_QobjDagger():
 
 def test_QobjDiagonals():
     "Qobj diagonals"
-    data = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data = _random_not_singular(5)
     A = Qobj(data)
     b = A.diag()
     assert_(np.all(b - np.diag(data) == 0))
@@ -553,8 +547,7 @@ def test_QobjEigenStates():
 
 def test_QobjExpm():
     "Qobj expm (dense)"
-    data = np.random.random(
-        (15, 15)) + 1j * np.random.random((15, 15)) - (0.5 + 0.5j)
+    data = _random_not_singular(15)
     A = Qobj(data)
     B = A.expm()
     assert_((B.data.todense() - np.matrix(la.expm(data)) < 1e-10).all())
@@ -562,8 +555,7 @@ def test_QobjExpm():
 
 def test_QobjExpmExplicitlySparse():
     "Qobj expm (sparse)"
-    data = np.random.random(
-        (15, 15)) + 1j * np.random.random((15, 15)) - (0.5 + 0.5j)
+    data = _random_not_singular(15)
     A = Qobj(data)
     B = A.expm(method='sparse')
     assert_((B.data.todense() - np.matrix(la.expm(data)) < 1e-10).all())
@@ -578,17 +570,27 @@ def test_QobjExpmZeroOper():
 
 def test_Qobj_sqrtm():
     "Qobj sqrtm"
-    data = np.random.random(
-        (5, 5)) + 1j * np.random.random((5, 5)) - (0.5 + 0.5j)
+    data = _random_not_singular(5)
     A = Qobj(data)
     B = A.sqrtm()
     assert_(A == B * B)
 
 
+def test_Qobj_inv():
+    "Qobj inv"
+    data = _random_not_singular(5)
+    A = Qobj(data)
+    B = A.inv()
+    assert_(qeye(5) == A * B)
+    assert_(qeye(5) == B * A)
+    B = A.inv(sparse=True)
+    assert_(qeye(5) == A * B)
+    assert_(qeye(5) == B * A)
+
+
 def test_QobjFull():
     "Qobj full"
-    data = np.random.random(
-        (15, 15)) + 1j * np.random.random((15, 15)) - (0.5 + 0.5j)
+    data = _random_not_singular(15)
     A = Qobj(data)
     b = A.full()
     assert_(np.all(b - data == 0))
@@ -622,12 +624,12 @@ def test_QobjPurity():
     assert_almost_equal(psi.purity(), 1)
     # check purity of pure ket state (superposition)
     psi2 = basis(2, 0)
-    psi_tot = (psi+psi2).unit() 
+    psi_tot = (psi+psi2).unit()
     assert_almost_equal(psi_tot.purity(), 1)
-    # check purity of density matrix of pure state 
+    # check purity of density matrix of pure state
     assert_almost_equal(ket2dm(psi_tot).purity(), 1)
     # check purity of maximally mixed density matrix
-    rho_mixed = (ket2dm(psi)+ket2dm(psi2)).unit() 
+    rho_mixed = (ket2dm(psi)+ket2dm(psi2)).unit()
     assert_almost_equal(rho_mixed.purity(), 0.5)
 
 def test_QobjPermute():