Repository: qutip/qutip

Issue:
Rand_ket does not work as intended
**Describe the bug**
qutip.random_objects.rand_ket does not work as intended
rand_ket(N=0, density=1, dims=None, seed=None) should be to figure out what N is, given dims is assigned when N is set to be 0 or None.

**To Reproduce**

```python
from qutip.random_objects import rand_ket
rand_ket(None, dims=[[2],[1]])
```
The terminal out put is
```
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-61-18d662b8b1db> in <module>
----> 1 rand_ket(None, dims=[[2],[1]])

~\Anaconda3\envs\quspin_env\lib\site-packages\qutip\random_objects.py in rand_ket(N, density, dims, seed)
    326         _check_dims(dims, N, 1)
    327     elif dims:
--> 328         N = prod(dims[0])
    329         _check_dims(dims, N, 1)
    330     else:

NameError: name 'prod' is not defined
```

**Expected behavior**
rand_ket should give you the same answer as rand_ket(2, dims=[[2],[1]])


**Your Environment**
QuTiP Version:      4.6.0
Numpy Version:      1.19.2
Scipy Version:      1.6.2
Cython Version:     None
Matplotlib Version: 3.3.4
Python Version:     3.7.10
Number of CPUs:     8
BLAS Info:          INTEL MKL
OPENMP Installed:   False
INTEL MKL Ext:      True
Platform Info:      Windows (AMD64)




Discussion:
Whoops, you're right.  That `prod` should say `np.prod` instead and there's a similar bug in `rand_ket_haar`.  Thanks for reporting it!

I also wouldn't be averse if someone wants to change the signature of `rand_ket` to
```python
def rand_ket(N=None, dims=None, ...):
   ...
```
so that it becomes possible to do `rand_ket(dims=[[2], [1]])` without needing to specify the first argument.  Obviously there'll need to be a couple more tests in the function to ensure that at least one of `N` and `dims` was passed.

Resolving patch:
diff --git a/qutip/random_objects.py b/qutip/random_objects.py
--- a/qutip/random_objects.py
+++ b/qutip/random_objects.py
@@ -300,14 +300,14 @@ def rand_unitary_haar(N=2, dims=None, seed=None):
     return U
 
 
-def rand_ket(N=0, density=1, dims=None, seed=None):
+def rand_ket(N=None, density=1, dims=None, seed=None):
     """Creates a random Nx1 sparse ket vector.
 
     Parameters
     ----------
     N : int
-        Number of rows for output quantum operator.
-        If None or 0, N is deduced from dims.
+        Number of rows for output quantum vector.
+        If None, N is deduced from dims.
     density : float
         Density between [0,1] of output ket state.
     dims : list
@@ -317,18 +317,26 @@ def rand_ket(N=0, density=1, dims=None, seed=None):
     Returns
     -------
     oper : qobj
-        Nx1 ket state quantum operator.
+        Nx1 ket quantum state vector.
+
+    Raises
+    -------
+    ValueError
+        If neither `N` or `dims` are specified.
 
     """
     if seed is not None:
         np.random.seed(seed=seed)
-    if N and dims:
+    if N is None and dims is None:
+        raise ValueError('Specify either the number of rows of state vector'
+                         '(N) or dimensions of quantum object (dims)')
+    if N is not None and dims:
         _check_dims(dims, N, 1)
     elif dims:
-        N = prod(dims[0])
+        N = np.prod(dims[0])
         _check_dims(dims, N, 1)
     else:
-        dims = [[N],[1]]
+        dims = [[N], [1]]
     X = sp.rand(N, 1, density, format='csr')
     while X.nnz == 0:
         # ensure that the ket is not all zeros.
@@ -342,7 +350,7 @@ def rand_ket(N=0, density=1, dims=None, seed=None):
     return Qobj(X / X.norm(), dims=dims)
 
 
-def rand_ket_haar(N=2, dims=None, seed=None):
+def rand_ket_haar(N=None, dims=None, seed=None):
     """
     Returns a Haar random pure state of dimension ``dim`` by
     applying a Haar random unitary to a fixed pure state.
@@ -351,7 +359,7 @@ def rand_ket_haar(N=2, dims=None, seed=None):
     ----------
     N : int
         Dimension of the state vector to be returned.
-        If None or 0, N is deduced from dims.
+        If None, N is deduced from dims.
     dims : list of ints, or None
         Dimensions of the resultant quantum object.
         If None, [[N],[1]] is used.
@@ -360,14 +368,22 @@ def rand_ket_haar(N=2, dims=None, seed=None):
     -------
     psi : Qobj
         A random state vector drawn from the Haar measure.
+
+    Raises
+    -------
+    ValueError
+        If neither `N` or `dims` are specified.
     """
+    if N is None and dims is None:
+        raise ValueError('Specify either the number of rows of state vector'
+                         '(N) or dimensions of quantum object (dims)')
     if N and dims:
         _check_dims(dims, N, 1)
     elif dims:
-        N = prod(dims[0])
+        N = np.prod(dims[0])
         _check_dims(dims, N, 1)
     else:
-        dims = [[N],[1]]
+        dims = [[N], [1]]
     psi = rand_unitary_haar(N, seed=seed) * basis(N, 0)
     psi.dims = dims
     return psi


Test patch:
diff --git a/qutip/tests/test_random.py b/qutip/tests/test_random.py
--- a/qutip/tests/test_random.py
+++ b/qutip/tests/test_random.py
@@ -31,16 +31,13 @@
 #    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ###############################################################################
 
-import scipy.sparse as sp
-import scipy.linalg as la
 import numpy as np
-from numpy.testing import assert_equal, assert_, run_module_suite
+from qutip import (
+    rand_ket, rand_dm, rand_herm, rand_unitary, rand_ket_haar, rand_dm_hs,
+    rand_super, rand_unitary_haar, rand_dm_ginibre, rand_super_bcsz, qeye,
+)
+import pytest
 
-from qutip.random_objects import (rand_ket, rand_dm, rand_herm, rand_unitary,
-                                  rand_ket_haar, rand_dm_hs,
-                                  rand_super, rand_unitary_haar, rand_dm_ginibre,
-                                  rand_super_bcsz)
-from qutip.operators import qeye
 
 def test_rand_unitary_haar_unitarity():
     """
@@ -48,48 +45,73 @@ def test_rand_unitary_haar_unitarity():
     """
     U = rand_unitary_haar(5)
     I = qeye(5)
+    assert U * U.dag() == I
 
-    assert_(U * U.dag() == I)
 
 def test_rand_dm_ginibre_rank():
     """
     Random Qobjs: Ginibre-random density ops have correct rank.
     """
     rho = rand_dm_ginibre(5, rank=3)
-
     rank = sum([abs(E) >= 1e-10 for E in rho.eigenenergies()])
-    assert_(rank == 3)
+    assert rank == 3
+
 
 def test_rand_super_bcsz_cptp():
     """
     Random Qobjs: Tests that BCSZ-random superoperators are CPTP.
     """
     S = rand_super_bcsz(5)
-    assert_(S.iscptp)
-
-def check_func_dims(func, args, kwargs, dims):
-    # TODO: promote this out of test_random, as it's generically useful
-    #       in writing tests.
-    resdims = func(*args, **kwargs).dims
-    assert_(resdims == dims, "Checking {}; epected dimensions of {}, got {}.".format(func.__name__, dims, resdims))
-
-def test_rand_vector_dims():
-    FUNCS = [rand_ket, rand_ket_haar]
-    for func in FUNCS:
-        check_func_dims( func, (7, ), {}, [[7], [1]])
-        check_func_dims( func, (6, ), {'dims': [[2,3], [1,1]]}, [[2,3], [1,1]])
-
-def test_rand_oper_dims():
-    FUNCS = [rand_unitary, rand_herm, rand_dm, rand_unitary_haar, rand_dm_ginibre, rand_dm_hs]
-    for func in FUNCS:
-        check_func_dims( func, (7, ), {}, [[7], [7]])
-        check_func_dims( func, (6, ), {'dims': [[2, 3], [2, 3]]}, [[2, 3], [2, 3]])
-
-def test_rand_super_dims():
-    FUNCS = [rand_super, rand_super_bcsz]
-    for func in FUNCS:
-        check_func_dims(func, (7, ), {}, [[[7], [7]]] * 2)
-        check_func_dims(func, (6, ), {'dims': [[[2, 3], [2, 3]], [[2, 3], [2, 3]]]}, [[[2, 3], [2, 3]], [[2, 3], [2, 3]]])
-
-if __name__ == "__main__":
-    run_module_suite()
+    assert S.iscptp
+
+
+@pytest.mark.parametrize('func', [rand_ket, rand_ket_haar])
+@pytest.mark.parametrize(('args', 'kwargs', 'dims'), [
+    pytest.param((6,), {}, [[6], [1]], id="N"),
+    pytest.param((), {'dims': [[2, 3], [1, 1]]}, [[2, 3], [1, 1]], id="dims"),
+    pytest.param((6,), {'dims': [[2, 3], [1, 1]]}, [[2, 3], [1, 1]],
+                 id="both"),
+])
+def test_rand_vector_dims(func, args, kwargs, dims):
+    shape = np.prod(dims[0]), np.prod(dims[1])
+    output = func(*args, **kwargs)
+    assert output.shape == shape
+    assert output.dims == dims
+
+
+@pytest.mark.parametrize('func', [rand_ket, rand_ket_haar])
+def test_rand_ket_raises_if_no_args(func):
+    with pytest.raises(ValueError):
+        func()
+
+
+@pytest.mark.parametrize('func', [
+    rand_unitary, rand_herm, rand_dm, rand_unitary_haar, rand_dm_ginibre,
+    rand_dm_hs,
+])
+@pytest.mark.parametrize(('args', 'kwargs', 'dims'), [
+    pytest.param((6,), {}, [[6], [6]], id="N"),
+    pytest.param((6,), {'dims': [[2, 3], [2, 3]]}, [[2, 3], [2, 3]],
+                 id="both"),
+])
+def test_rand_oper_dims(func, args, kwargs, dims):
+    shape = np.prod(dims[0]), np.prod(dims[1])
+    output = func(*args, **kwargs)
+    assert output.shape == shape
+    assert output.dims == dims
+
+
+_super_dims = [[[2, 3], [2, 3]], [[2, 3], [2, 3]]]
+
+
+@pytest.mark.parametrize('func', [rand_super, rand_super_bcsz])
+@pytest.mark.parametrize(('args', 'kwargs', 'dims'), [
+    pytest.param((6,), {}, [[[6]]*2]*2, id="N"),
+    pytest.param((6,), {'dims': _super_dims}, _super_dims,
+                 id="both"),
+])
+def test_rand_super_dims(func, args, kwargs, dims):
+    shape = np.prod(dims[0]), np.prod(dims[1])
+    output = func(*args, **kwargs)
+    assert output.shape == shape
+    assert output.dims == dims