Repository: qutip/qutip

Issue:
Unify random `Qobj` creation functions support of `dims`.
### Describe the Issue!

Most of the functions in `random_objects.py` take an `N` input and an optional `dims`. Those input are redundant as the size (`N`) can be obtained from the `dims`. However the way they handle this inconsistent:
- Many functions, such as `rand_super`, `rand_dm`, check that the dims matches the size and raise an error if it doesn't.
- `rand_dm_ginibre`, `rand_super_bcsz` just pass the `dims` to `Qobj` without any checks.
- `rand_ket` has a default `N=0` and obtain the size from `dims` when `N` is `0`, but it raise a division per zero error when neither `N` nor `dims` is passed.
- `rand_ket_haar` also tries to obtain the size from `dims` when `N` is `0`, but the default value of `N` is `2`, thus one need to pass `N=0` to use it. `N=0` would fail almost everywhere else.

Also some function have a default size, other not: `rand_unitary(N, ...)`, `rand_unitary_haar(N=2, ...)`.

The interface of these functions need to be unified.

Some options:
- `N` is always needed and `dims` are checked to match.
- A default of `None` is used for `N` and either `N` or `dims` is needed. If both are given, we check that they are compatible. If none are passed, we raise an appropriate error.
- (For `dev.major`) Remove the `dims` parameter and replace `N` with `dimensions`, which act as `N` if an integer is given and `dims` is a list is. This is already the behavior of `qeye` and `qzero`.





Discussion:
Hi, @Ericgig! I would like to work on this issue.

We cannot adopt the 3rd option in the master branch due to backwards compatibility, right? I'd rather avoid any single character variable wherever possible.

That said, I believe the second option you gave is the best one to adopt in the master branch. Once I finish, I can work on dev.major implementing the third option.
 
Hi @Leo-am,
Thank you for taking this issue.

Yes, with `dev.major` we can change the function call more easily, where as `master` should backwards compatibility.
So doing 2 first for v4.7 and 3 for v5 is the ideal.





Resolving patch:
diff --git a/qutip/random_objects.py b/qutip/random_objects.py
--- a/qutip/random_objects.py
+++ b/qutip/random_objects.py
@@ -1,33 +1,113 @@
-# -*- coding: utf-8 -*-
 """
 This module is a collection of random state and operator generators.
-The sparsity of the ouput Qobj's is controlled by varing the
-`density` parameter.
 """
 
 __all__ = [
     'rand_herm',
-    'rand_unitary', 'rand_unitary_haar',
-    'rand_dm', 'rand_dm_ginibre', 'rand_dm_hs',
+    'rand_unitary',
+    'rand_dm',
     'rand_stochastic',
-    'rand_ket', 'rand_ket_haar',
+    'rand_ket',
     'rand_kraus_map',
-    'rand_super_bcsz', 'rand_super'
+    'rand_super',
+    "rand_super_bcsz",
 ]
 
 import numbers
 
 import numpy as np
+from numpy.random import Generator, SeedSequence, default_rng
 import scipy.linalg
 import scipy.sparse as sp
 
-from . import Qobj, create, destroy, jmat, basis, to_super
+from . import (Qobj, create, destroy, jmat, basis,
+               to_super, to_choi, to_chi, to_kraus, to_stinespring)
 from .core import data as _data
+from .core.dimensions import flatten
 
+
+_RAND = default_rng()
 _UNITS = np.array([1, 1j])
 
 
-def rand_jacobi_rotation(A, *, seed=None):
+def _implicit_tensor_dimensions(dimensions, superoper=False):
+    """
+    Total flattened size and operator dimensions for operator creation routines
+    that automatically perform tensor products.
+
+    Parameters
+    ----------
+    dimensions : (int) or (list of int) or (list of list of int)
+        First dimension of an operator which can create an implicit tensor
+        product.  If the type is `int`, it is promoted first to `[dimensions]`.
+        From there, it should be one of the two-elements `dims` parameter of a
+        `qutip.Qobj` representing an `oper` or `super`, with possible tensor
+        products.
+
+    Returns
+    -------
+    size : int
+        Dimension of backing matrix required to represent operator.
+    dimensions : list
+        Dimension list in the form required by ``Qobj`` creation.
+    """
+    if not isinstance(dimensions, list):
+        dimensions = [dimensions]
+    flat = flatten(dimensions)
+    if not all(isinstance(x, numbers.Integral) and x >= 0 for x in flat):
+        raise ValueError("All dimensions must be integers >= 0")
+    N = np.prod(flat)
+    if superoper:
+        if isinstance(dimensions[0], numbers.Integral):
+            dimensions = [dimensions, dimensions]
+        else:
+            N = int(N**0.5)
+    return N, [dimensions, dimensions]
+
+
+def _get_generator(seed):
+    """
+    Obtain a random generator from a seed or generator.
+
+    Parameters
+    ----------
+    seed: int, SeedSequence, Generator, NoneType
+        Seed to create the generator. If it's already a generator return it.
+        When ``None`` is suplied, a default generator is provided.
+    """
+    if seed is None:
+        gen = _RAND
+    elif isinstance(seed, Generator):
+        gen = seed
+    else:
+        gen = default_rng(seed)
+    return gen
+
+
+def _randnz(shape, generator, norm=np.sqrt(0.5)):
+    """
+    Returns an array of standard normal complex random variates.
+    The Ginibre ensemble corresponds to setting ``norm = 1`` [Mis12]_.
+
+    Parameters
+    ----------
+    shape : tuple
+        Shape of the returned array of random variates.
+
+    generator : Generator
+        Random number generator.
+
+    norm : float
+        Scale of the returned random variates, or 'ginibre' to draw
+        from the Ginibre ensemble.
+    """
+    # This function is intended for internal use.
+    if norm == 'ginibre':
+        norm = 1
+    return np.sum(generator.normal(size=(shape + (2,))) * _UNITS, axis=-1) * norm
+
+
+def _rand_jacobi_rotation(A, generator):
     """Random Jacobi rotation of a matrix.
 
     Parameters
@@ -35,26 +115,24 @@ def rand_jacobi_rotation(A, *, seed=None):
     A : qutip.data.Data
         Matrix to rotate as a data layer object.
 
-    seed : int32
-        seed to reseed the random number generator.
+    generator : numpy.random.generator
+        Random number generator.
 
     Returns
     -------
     qutip.data.Data
         Rotated sparse matrix.
     """
-    if seed is not None:
-        np.random.seed(seed=seed)
     if A.shape[0] != A.shape[1]:
         raise ValueError('Input matrix must be square.')
     n = A.shape[0]
-    angle = 2 * np.random.random() * np.pi
+    angle = 2 * generator.random() * np.pi
     a = np.sqrt(0.5) * np.exp(-1j * angle)
     b = np.conj(a)
-    i = np.random.randint(n)
+    i = generator.integers(n)
     j = i
     while i == j:
-        j = np.random.randint(n)
+        j = generator.integers(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])
@@ -65,57 +143,105 @@ def rand_jacobi_rotation(A, *, seed=None):
     return _data.matmul(_data.matmul(R, A), R.adjoint())
 
 
-def _randnz(shape, norm=np.sqrt(0.5), *, seed=None):
+def _get_block_sizes(N, density, generator):
     """
-    Returns an array of standard normal complex random variates.
-    The Ginibre ensemble corresponds to setting ``norm = 1`` [Mis12]_.
+    Obtain a list of matrix block sizes in such a way that an NxN matrix
+    composed of full matrices of these sizes along the diagonal would be of
+    desired density.
+    """
+    if density <= 0:
+        return [1] * N
+    elif density >= 1:
+        return [N]
+    max_block_size = int((N**2 * density)**0.5)
+    min_block_size = 1
+    if density >= 0.5:
+        min_block_size = int(N / 2 * (1 + (2 * density - 1)**0.5))
+
+    if min_block_size >= max_block_size:
+        M = min(min_block_size, max_block_size)
+    else:
+        M = generator.integers(min_block_size, max_block_size) + 1
+    M += generator.integers(-1, 2)
+    M = min(max(M, 1), N)
 
-    Parameters
-    ----------
-    shape : tuple
-        Shape of the returned array of random variates.
+    other_blocks = []
+    if M < N:
+        other_N = N - M
+        other_density = (density * N**2 - M**2) / other_N**2
+        other_blocks = _get_block_sizes(other_N, other_density, generator)
 
-    norm : float
-        Scale of the returned random variates, or 'ginibre' to draw
-        from the Ginibre ensemble.
+    return [M] + other_blocks
 
-    seed : int32
-        seed to reseed the random number generator.
-    """
-    # This function is intended for internal use.
-    if seed is not None:
-        np.random.seed(seed=seed)
-    if norm == 'ginibre':
-        norm = 1
-    return np.sum(np.random.randn(*(shape + (2,))) * _UNITS, axis=-1) * norm
 
+def _merge_shuffle_blocks(blocks, generator):
+    """
+    For a list of block, merge them in one matrix along the diagonal and
+    shuffle the rows and columns.
+    """
+    N = sum(block.shape[0] for block in blocks)
+    D = sum(block.shape[0]**2 for block in blocks) / N**2
+    idx = generator.permutation(N)
+    end = 0
+    if D < 0.1:
+        # TODO: coo_array from sicpy 1.8 would allow to simplify this.
+        row = []
+        col = []
+        data = []
+        for block in blocks:
+            start = end
+            end = end + block.shape[0]
+            brow, bcol = np.meshgrid(idx[start:end], idx[start:end])
+            row.append(brow.ravel())
+            col.append(bcol.ravel())
+            data.append(block.ravel())
+        data = np.hstack(data)
+        row = np.hstack(row)
+        col = np.hstack(col)
+        matrix = sp.coo_matrix((data, (row, col)), shape=(N, N))
+    else:
+        matrix = np.zeros((N,N), dtype=complex)
+        for block in blocks:
+            start = end
+            end = end + block.shape[0]
+            brow, bcol = np.meshgrid(idx[start:end], idx[start:end])
+            matrix[brow, bcol] = block
+    return _data.create(matrix, copy=False)
 
-def rand_herm(N, density=0.75, dims=None, pos_def=False,
-              *, seed=None, dtype=_data.CSR):
-    """Creates a random NxN sparse Hermitian quantum object.
 
-    If 'N' is an integer, uses :math:`H=0.5*(X+X^{+})` where :math:`X` is
-    a randomly generated quantum operator with a given `density`. Else uses
-    complex Jacobi rotations when 'N' is given by an array.
+def rand_herm(dimensions, density=0.30, distribution="fill", *,
+              eigenvalues=(), seed=None, dtype=_data.CSR):
+    """Creates a random sparse Hermitian quantum object.
 
     Parameters
     ----------
-    N : int, list/ndarray
-        If int, then shape of output operator. If list/ndarray then eigenvalues
-        of generated operator.
+    dimensions : (int) or (list of int) or (list of list of int)
+        Dimension of Hilbert space. If provided as a list of ints, then the
+        dimension is the product over this list, but the ``dims`` property of
+        the new Qobj are set to this list.  This can produce either `oper` or
+        `super` depending on the passed `dimensions`.
 
-    density : float
+    density : float, [0.30]
         Density between [0,1] of output Hermitian operator.
 
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[N],[N]].
+    distribution : str {"fill", "pos_def", "eigen"}
+        Method used to obtain the density matrices.
+
+        - "fill" : Uses :math:`H=0.5*(X+X^{+})` where :math:`X` is a randomly
+          generated quantum operator with elements uniformly distributed
+          between ``[-1, 1] + [-1j, 1j]``.
+        - "eigen" : A density matrix with the given ``eigenvalues``. It uses
+          random complex Jacobi rotations to shuffle the operator.
+        - "pos_def" : Return a positive semi-definite matrix by diagonal
+          dominance.
 
-    pos_def : bool (default=False)
-        Return a positive semi-definite matrix (by diagonal dominance).
+    eigenvalues : array_like, optional
+        Eigenvalues of the output Hermitian matrix. The len must match the
+        shape of the matrix.
 
-    seed : int
-        seed for the random number generator
+    seed : int, SeedSequence, Generator, optional
+        Seed to create the random number generator or a pre prepared
+        generator. When none is suplied, a default generator is used.
 
     dtype : type or str
         Storage representation. Any data-layer known to `qutip.data.to` is
@@ -123,57 +249,54 @@ def rand_herm(N, density=0.75, dims=None, pos_def=False,
 
     Returns
     -------
-    oper : qobj
-        NxN Hermitian quantum operator.
+    oper : :class:`qobj`
+        Hermitian quantum operator.
 
     Notes
     -----
-    If given a list/ndarray as input 'N', this function returns a
-    random Hermitian object with eigenvalues given in the list/ndarray.
-    This is accomplished via complex Jacobi rotations.  While this method
-    is ~50% faster than the corresponding (real only) Matlab code, it should
-    not be repeatedly used for generating matrices larger than ~1000x1000.
+    If given a list of eigenvalues the object is created using complex Jacobi
+    rotations.  While this method is fast for small matrices, it should not be
+    repeatedly used for generating matrices larger than ~1000x1000.
     """
-    if seed is not None:
-        np.random.seed(seed=seed)
-    if isinstance(N, (np.ndarray, list)):
-        M = _data.diag[_data.CSR](N, 0)
-        N = len(N)
-        if dims:
-            _check_dims(dims, N, N)
+    N, dims = _implicit_tensor_dimensions(dimensions)
+    generator = _get_generator(seed)
+    if distribution not in ["eigen", "fill", "pos_def"]:
+        raise ValueError("distribution must be one of {'eigen', 'fill', "
+                         "'pos_def'}")
+
+    if distribution == "eigen":
+        if N != len(eigenvalues):
+            raise ValueError("The number of eigenvalues does not match the "
+                             "desired shape.")
+        out = _data.diag[_data.CSR](eigenvalues, 0)
         nvals = max([N**2 * density, 1])
-        M = rand_jacobi_rotation(M)
-        while _data.csr.nnz(M) < 0.95 * nvals:
-            M = rand_jacobi_rotation(M)
-
-    elif isinstance(N, numbers.Integral):
-        N = int(N)
-        if dims:
-            _check_dims(dims, N, N)
+        out = _rand_jacobi_rotation(out, generator)
+        while _data.csr.nnz(out) < 0.95 * nvals:
+            out = _rand_jacobi_rotation(out, generator)
+        out = Qobj(out, type='oper', dims=dims, isherm=True, copy=False)
+
+    else:
+        pos_def = distribution == "pos_def"
         if density < 0.5:
-            M = _rand_herm_sparse(N, density, pos_def)
+            M = _rand_herm_sparse(N, density, pos_def, generator)
         else:
-            M = _rand_herm_dense(N, density, pos_def)
+            M = _rand_herm_dense(N, density, pos_def, generator)
 
-    else:
-        raise TypeError('Input N must be an integer or array_like.')
-    out = Qobj(M, dims=dims or [[N]]*2, type='oper',
-               isherm=True, copy=False).to(dtype)
-    if dtype:
-        out = out.to(dtype)
-    return out
+        out = Qobj(M, type='oper', dims=dims, isherm=True, copy=False)
+
+    return out.to(dtype)
 
 
-def _rand_herm_sparse(N, density, pos_def):
+def _rand_herm_sparse(N, density, pos_def, generator):
     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 = int(num_elems)
-    data = (2 * np.random.rand(num_elems) - 1) + \
-           (2 * np.random.rand(num_elems) - 1) * 1j
+    data = (2 * generator.random(num_elems) - 1) + \
+           (2 * generator.random(num_elems) - 1) * 1j
     row_idx, col_idx = zip(*[
         divmod(index, N)
-        for index in np.random.choice(N*N, num_elems, replace=False)
+        for index in generator.choice(N*N, num_elems, replace=False)
     ])
     M = sp.coo_matrix((data, (row_idx, col_idx)),
                       dtype=complex, shape=(N, N))
@@ -189,17 +312,17 @@ def _rand_herm_sparse(N, density, pos_def):
     return rand_mat
 
 
-def _rand_herm_dense(N, density, pos_def):
+def _rand_herm_dense(N, density, pos_def, generator):
     M = (
-        (2*np.random.rand(N, N) - 1)
-        + 1j*(2*np.random.rand(N, N) - 1)
+        (2*generator.random((N, N)) - 1)
+        + 1j*(2*generator.random((N, N)) - 1)
     )
     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 = int(num_remove)
-    for index in np.random.choice(N*N, num_remove, replace=False):
+    for index in generator.choice(N*N, num_remove, replace=False):
         row, col = divmod(index, N)
         M[col, row] = 0
         M[row, col] = 0
@@ -208,27 +331,31 @@ def _rand_herm_dense(N, density, pos_def):
     return _data.create(M)
 
 
-
-def rand_unitary(N, density=0.75, dims=None, *, seed=None, dtype=_data.Dense):
-    r"""Creates a random NxN sparse unitary quantum object.
-
-    Uses :math:`\exp(-iH)` where H is a randomly generated
-    Hermitian operator.
+def rand_unitary(dimensions, density=1, distribution="haar", *,
+                 seed=None, dtype=_data.Dense):
+    r"""Creates a random sparse unitary quantum object.
 
     Parameters
     ----------
-    N : int
-        Shape of output quantum operator.
+    dimensions : (int) or (list of int) or (list of list of int)
+        Dimension of Hilbert space. If provided as a list of ints, then the
+        dimension is the product over this list, but the ``dims`` property of
+        the new Qobj are set to this list.  This can produce either `oper` or
+        `super` depending on the passed `dimensions`.
 
-    density : float
-        Density between [0,1] of output Unitary operator.
+    density : float, [1]
+        Density between [0,1] of output unitary operator.
+
+    distribution : ["haar", "exp"]
+        Method used to obtain the unitary matrices.
 
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[N],[N]].
+        - haar : Haar random unitary matrix using the algorithm of [Mez07]_.
+        - exp : Uses :math:`\exp(-iH)`, where H is a randomly generated
+          Hermitian operator.
 
-    seed : int
-        seed for the random number generator
+    seed : int, SeedSequence, Generator, optional
+        Seed to create the random number generator or a pre prepared
+        generator. When none is suplied, a default generator is used.
 
     dtype : type or str
         Storage representation. Any data-layer known to `qutip.data.to` is
@@ -237,17 +364,32 @@ def rand_unitary(N, density=0.75, dims=None, *, seed=None, dtype=_data.Dense):
     Returns
     -------
     oper : qobj
-        NxN Unitary quantum operator.
-
+        Unitary quantum operator.
     """
-    if dims:
-        _check_dims(dims, N, N)
-    return (-1.0j * rand_herm(N, density, dims=dims,
-                              seed=seed, dtype=dtype)
-            ).expm().to(dtype)
-
-
-def rand_unitary_haar(N=2, dims=None, *, seed=None, dtype=_data.Dense):
+    N, dims = _implicit_tensor_dimensions(dimensions)
+    if distribution not in ["haar", "exp"]:
+        raise ValueError("distribution must be one of {'haar', 'exp'}")
+    generator = _get_generator(seed)
+    block_sizes = _get_block_sizes(N, density, generator)
+
+    blocks = []
+    for block in block_sizes:
+        if distribution == "haar":
+            mat = _rand_unitary_haar(block, generator)
+        elif distribution == "exp":
+            mat = scipy.linalg.expm(
+                -1.0j *
+                _rand_herm_dense(block, 1, False, generator).as_ndarray()
+            )
+        blocks.append(mat)
+
+    merged = _merge_shuffle_blocks(blocks, generator)
+
+    mat = Qobj(merged, type='oper', dims=dims, isunitary=True, copy=False)
+    return mat.to(dtype)
+
+
+def _rand_unitary_haar(N, generator):
     """
     Returns a Haar random unitary matrix of dimension
     ``dim``, using the algorithm of [Mez07]_.
@@ -257,31 +399,17 @@ def rand_unitary_haar(N=2, dims=None, *, seed=None, dtype=_data.Dense):
     N : int
         Dimension of the unitary to be returned.
 
-    dims : list of lists of int, or None
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[N],[N]].
-
-    seed : int
-        seed for the random number generator
-
-    dtype : type or str
-        Storage representation. Any data-layer known to `qutip.data.to` is
-        accepted.
+    generator : numpy.random.generator
+        Random number generator.
 
     Returns
     -------
-    U : Qobj
-        Unitary of dims ``[[dim], [dim]]`` drawn from the Haar
-        measure.
+    U : qutip.core.data.Dense
+        Unitary of shape ``(N, N)`` drawn from the Haar measure.
     """
-    if dims is not None:
-        _check_dims(dims, N, N)
-    else:
-        dims = [[N], [N]]
-
     # Mez01 STEP 1: Generate an N × N matrix Z of complex standard
     #               normal random variates.
-    Z = _randnz((N, N), seed=seed)
+    Z = _randnz((N, N), generator)
 
     # Mez01 STEP 2: Find a QR decomposition Z = Q · R.
     Q, R = scipy.linalg.qr(Z)
@@ -302,30 +430,35 @@ def rand_unitary_haar(N=2, dims=None, *, seed=None, dtype=_data.Dense):
     #       As NumPy arrays, Q has shape (N, N) and
     #       Lambda has shape (N, ), such that the broadcasting
     #       represents precisely Q_ij λ_j.
-    return Qobj(Q * Lambda, dims=dims,
-                type='oper', isunitary=True, copy=False).to(dtype)
+    return Q * Lambda
 
 
-def rand_ket(N=None, density=1, dims=None, *, seed=None, dtype=_data.Dense):
-    """Creates a random Nx1 sparse ket vector.
+def rand_ket(dimensions, density=1, distribution="haar", *,
+             seed=None, dtype=_data.Dense):
+    """Creates a random ket vector.
 
     Parameters
     ----------
-    N : int
-        Number of rows for output quantum operator.
-        If None or 0, N is deduced from dims.
+    dimensions : (int) or (list of int) or (list of list of int)
+        Dimension of Hilbert space. If provided as a list of ints, then the
+        dimension is the product over this list, but the ``dims`` property of
+        the new Qobj are set to this list.  This can produce either `oper` or
+        `super` depending on the passed `dimensions`.
 
-    density : float
-        Density between [0,1] of output ket state.
+    density : float, [1]
+        Density between [0,1] of output ket state when using the ``fill``
+        method.
+
+    distribution : str {"haar", "fill"}
+        Method used to obtain the kets.
 
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[N],[1]].
-    seed : int
-        Seed for the random number generator.
+        - haar : Haar random pure state obtained by applying a Haar random
+          unitary to a fixed pure state.
+        - fill : Fill the ket with uniformly distributed random complex number.
 
-    seed : int
-        seed for the random number generator
+    seed : int, SeedSequence, Generator, optional
+        Seed to create the random number generator or a pre prepared
+        generator. When none is suplied, a default generator is used.
 
     dtype : type or str
         Storage representation. Any data-layer known to `qutip.data.to` is
@@ -334,109 +467,70 @@ def rand_ket(N=None, density=1, dims=None, *, seed=None, dtype=_data.Dense):
     Returns
     -------
     oper : qobj
-        Nx1 ket quantum state vector.
-
-    Raises
-    -------
-    ValueError
-        If neither `N` or `dims` are specified.
-
+        Ket quantum state vector.
     """
-    if seed is not None:
-        np.random.seed(seed=seed)
-    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 = np.prod(dims[0])
-        _check_dims(dims, N, 1)
+    generator = _get_generator(seed)
+    N, dims = _implicit_tensor_dimensions(dimensions)
+    if distribution not in ["haar", "fill"]:
+        raise ValueError("distribution must be one of {'haar', 'fill'}")
+
+    if distribution == "haar":
+        ket = rand_unitary(N, density, "haar", seed=generator) @ basis(N, 0)
     else:
-        dims = [[N], [1]]
-    X = scipy.sparse.rand(N, 1, density, format='csr')
-    while X.nnz == 0:
-        # ensure that the ket is not all zeros.
-        X = scipy.sparse.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)
-    X = _data.csr.CSR(X + Y)
-    return Qobj(_data.mul(X, 1 / _data.norm.l2(X)),
-                dims=dims,
-                copy=False,
-                type='ket',
-                isherm=False,
-                isunitary=False).to(dtype)
-
-
-def rand_ket_haar(N=None, dims=None, *, seed=None, dtype=_data.Dense):
-    """
-    Returns a Haar random pure state of dimension ``dim`` by
-    applying a Haar random unitary to a fixed pure state.
+        X = scipy.sparse.rand(N, 1, density, format='csr',
+                              random_state=generator)
+        while X.nnz == 0:
+            # ensure that the ket is not all zeros.
+            X = scipy.sparse.rand(N, 1, density+1/N, format='csr',
+                                  random_state=generator)
+        X.data = X.data - 0.5
+        Y = X.copy()
+        Y.data = 1.0j * (generator.random(len(X.data)) - 0.5)
+        X = _data.csr.CSR(X + Y)
+        ket = Qobj(_data.mul(X, 1 / _data.norm.l2(X)),
+                   copy=False, type="ket", isherm=False, isunitary=False)
+    ket.dims = [dims[0], [1]]
+    return ket.to(dtype)
+
+
+def rand_dm(dimensions, density=0.75, distribution="ginibre", *,
+            eigenvalues=(), rank=None, seed=None, dtype=_data.CSR):
+    r"""Creates a random density matrix of the desired dimensions.
 
     Parameters
     ----------
-    N : int
-        Dimension of the state vector to be returned.
-        If None or 0, N is deduced from dims.
-
-    dims : list of ints, or None
-        Dimensions of the resultant quantum object.
-        If None, [[N],[1]] is used.
-
-    seed : int
-        seed for the random number generator
-
-    dtype : type or str
-        Storage representation. Any data-layer known to `qutip.data.to` is
-        accepted.
+    dimensions : (int) or (list of int) or (list of list of int)
+        Dimension of Hilbert space. If provided as a list of ints, then the
+        dimension is the product over this list, but the ``dims`` property of
+        the new Qobj are set to this list.  This can produce either ``oper`` or
+        ``super`` depending on the passed ``dimensions``.
 
-    Returns
-    -------
-    psi : Qobj
-        A random state vector drawn from the Haar measure.
+    density : float
+        Density between [0,1] of output density matrix. Used by the "pure",
+        "eigen" and "herm".
 
-    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 = np.prod(dims[0])
-        _check_dims(dims, N, 1)
-    else:
-        dims = [[N], [1]]
-    U = rand_unitary_haar(N, seed=seed, dims=[dims[0], dims[0]])
-    return (U @ basis(dims[0], [0]*len(dims[0]))).to(dtype)
+    distribution : str {"ginibre", "hs", "pure", "eigen", "uniform"}
+        Method used to obtain the density matrices.
 
+        - "ginibre" : Ginibre random density operator of rank ``rank`` by using
+          the algorithm of [BCSZ08]_.
+        - "hs" : Hilbert-Schmidt ensemble, equivalent to a full rank ginibre
+          operator.
+        - "pure" : Density matrix created from a random ket.
+        - "eigen" : A density matrix with the given ``eigenvalues``.
+        - "herm" : Build from a random hermitian matrix using ``rand_herm``.
 
-def rand_dm(N, density=0.75, pure=False, dims=None, *,
-            seed=None, dtype=_data.CSR):
-    r"""Creates a random NxN density matrix.
+    eigenvalues : array_like, optional
+        Eigenvalues of the output Hermitian matrix. The len must match the
+        shape of the matrix.
 
-    Parameters
-    ----------
-    N : int, ndarray, list
-        If int, then shape of output operator. If list/ndarray then eigenvalues
-        of generated density matrix.
+    rank : int, optional
+        When using the "ginibre" distribution, rank of the density matrix.
+        Will default to a full rank operator when not provided.
 
-    density : float
-        Density between [0,1] of output density matrix.
-
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[N],[N]].
-    seed : int
-        Seed for the random number generator.
-
-    seed : int
-        seed for the random number generator
+    seed : int, SeedSequence, Generator, optional
+        Seed to create the random number generator or a pre prepared
+        generator. When none is suplied, a default generator is used.
 
     dtype : type or str
         Storage representation. Any data-layer known to `qutip.data.to` is
@@ -445,59 +539,57 @@ def rand_dm(N, density=0.75, pure=False, dims=None, *,
     Returns
     -------
     oper : qobj
-        NxN density matrix quantum operator.
-
-    Notes
-    -----
-    For small density matrices., choosing a low density will result in an error
-    as no diagonal elements will be generated such that :math:`Tr(\rho)=1`.
+        Density matrix quantum operator.
     """
-    if seed is not None:
-        np.random.seed(seed=seed)
-    if isinstance(N, (np.ndarray, list)):
-        if np.abs(np.sum(N)-1.0) > 1e-15:
+    generator = _get_generator(seed)
+    N, dims = _implicit_tensor_dimensions(dimensions)
+    distributions = set(["eigen", "ginibre", "hs", "pure", "herm"])
+    if distribution not in distributions:
+        raise ValueError(f"distribution must be one of {distributions}")
+
+    if distribution == "eigen":
+        if len(eigenvalues) != N:
+            raise ValueError("Number of eigenvalues does not match the shape.")
+        if np.abs(np.sum(eigenvalues)-1.0) > 1e-15 * N:
             raise ValueError('Eigenvalues of a density matrix '
-                             'must sum to one.')
-        H = _data.diag(N, 0)
-        N = len(N)
-        if dims:
-            _check_dims(dims, N, N)
+                             f'must sum to one, not {np.sum(eigenvalues)}')
+        H = _data.diag(eigenvalues, 0)
         nvals = N**2 * density
-        H = rand_jacobi_rotation(H)
+        H = _rand_jacobi_rotation(H, generator)
         while _data.csr.nnz(H) < 0.95*nvals:
-            H = rand_jacobi_rotation(H)
-    elif isinstance(N, numbers.Integral):
-        N = int(N)
-        if dims:
-            _check_dims(dims, N, N)
-        if pure:
-            dm_density = np.sqrt(density)
-            psi = rand_ket(N, dm_density, dtype=dtype)
-            H = psi.proj().data
-        else:
-            trace = 0
-            tries = 0
-            while trace == 0 and tries < 10:
-                H = rand_herm(N, density, seed=seed, dtype=dtype)
-                H = H.dag() @ H
-                trace = H.tr()
-                tries += 1
-            if tries >= 10:
-                raise ValueError(
-                    "Requested density is too low to generate density matrix.")
-            H /= trace
-            H = H.data
+            H = _rand_jacobi_rotation(H, generator)
+    elif distribution == "ginibre":
+        H = _rand_dm_ginibre(N, rank, generator)
+    elif distribution == "hs":
+        H = _rand_dm_ginibre(N, N, generator)
+    elif distribution == "pure":
+        dm_density = np.sqrt(density)
+        psi = rand_ket(N, density=dm_density,
+                       distribution="fill", seed=generator)
+        H = psi.proj().data
+    elif distribution == "herm":
+        block_sizes = _get_block_sizes(N, density, generator)
+        blocks = []
+        first = True
+        for block in block_sizes:
+            if block != 1:
+                mat = rand_herm(block, 1, seed=generator, dtype="dense").full()
+            else:
+                mat = np.ones((1,1))
+                if not first:
+                    mat *= max(generator.random() - 0.5, 0)
+                first = False
+            blocks.append(mat.T.conj() @ mat)
+        H = _merge_shuffle_blocks(blocks, generator)
+        H /= H.trace()
 
-    else:
-        raise TypeError('Input N must be an integer or array_like.')
     return Qobj(H, dims=dims, type='oper', isherm=True, copy=False).to(dtype)
 
 
-def rand_dm_ginibre(N=2, rank=None, dims=None, *, seed=None, dtype=_data.CSR):
+def _rand_dm_ginibre(N, rank, generator):
     """
-    Returns a Ginibre random density operator of dimension
-    ``dim`` and rank ``rank`` by using the algorithm of
-    [BCSZ08]_. If ``rank`` is `None`, a full-rank
+    Returns a Ginibre random density operator of  rank ``rank`` by using the
+    algorithm of [BCSZ08]_. If ``rank`` is `None`, a full-rank
     (Hilbert-Schmidt ensemble) random density operator will be
     returned.
 
@@ -505,87 +597,45 @@ def rand_dm_ginibre(N=2, rank=None, dims=None, *, seed=None, dtype=_data.CSR):
     ----------
     N : int
         Dimension of the density operator to be returned.
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[N],[N]].
 
     rank : int or None
         Rank of the sampled density operator. If None, a full-rank
         density operator is generated.
 
-    seed : int
-        seed for the random number generator
-
-    dtype : type or str
-        Storage representation. Any data-layer known to `qutip.data.to` is
-        accepted.
-
     Returns
     -------
     rho : Qobj
         An N × N density operator sampled from the Ginibre
         or Hilbert-Schmidt distribution.
     """
-    if rank is None:
+    if not rank:
         rank = N
     if rank > N:
         raise ValueError("Rank cannot exceed dimension.")
 
-    X = _randnz((N, rank), norm='ginibre', seed=seed)
+    X = _randnz((N, rank), generator, norm='ginibre')
     rho = np.dot(X, X.T.conj())
     rho /= np.trace(rho)
 
-    return Qobj(rho, dims=dims, type='oper', isherm=True, copy=False).to(dtype)
-
-
-def rand_dm_hs(N=2, dims=None, *, seed=None, dtype=_data.CSR):
-    """
-    Returns a Hilbert-Schmidt random density operator of dimension
-    ``dim`` and rank ``rank`` by using the algorithm of
-    [BCSZ08]_.
-
-    Parameters
-    ----------
-    N : int
-        Dimension of the density operator to be returned.
-
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[N],[N]].
-
-    seed : int
-        seed for the random number generator
-
-    dtype : type or str
-        Storage representation. Any data-layer known to `qutip.data.to` is
-        accepted.
-
-    Returns
-    -------
-    rho : Qobj
-        A dim × dim density operator sampled from the Ginibre
-        or Hilbert-Schmidt distribution.
-
-    """
-    return rand_dm_ginibre(N, rank=None, dims=dims, seed=seed, dtype=dtype)
+    return rho
 
 
-def rand_kraus_map(N, dims=None, *, seed=None, dtype=_data.Dense):
+def rand_kraus_map(dimensions, *, seed=None, dtype=_data.Dense):
     """
     Creates a random CPTP map on an N-dimensional Hilbert space in Kraus
     form.
 
     Parameters
     ----------
-    N : int
-        Length of input/output density matrix.
-
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[N],[N]].
+    dimensions : (int) or (list of int) or (list of list of int)
+        Dimension of Hilbert space. If provided as a list of ints, then the
+        dimension is the product over this list, but the ``dims`` property of
+        the new Qobj are set to this list.  This can produce either `oper` or
+        `super` depending on the passed `dimensions`.
 
-    seed : int
-        seed for the random number generator
+    seed : int, SeedSequence, Generator, optional
+        Seed to create the random number generator or a pre prepared
+        generator. When none is suplied, a default generator is used.
 
     dtype : type or str
         Storage representation. Any data-layer known to `qutip.data.to` is
@@ -597,8 +647,7 @@ def rand_kraus_map(N, dims=None, *, seed=None, dtype=_data.Dense):
         N^2 x N x N qobj operators.
 
     """
-    if dims:
-        _check_dims(dims, N, N)
+    N, dims = _implicit_tensor_dimensions(dimensions)
 
     # Random unitary (Stinespring Dilation)
     big_unitary = rand_unitary(N ** 3, seed=seed, dtype=dtype).full()
@@ -608,44 +657,48 @@ def rand_kraus_map(N, dims=None, *, seed=None, dtype=_data.Dense):
             for x in oper_list]
 
 
-def rand_super(N, dims=None, *, seed=None, dtype=_data.Dense):
+def rand_super(dimensions, *, superrep="super", seed=None, dtype=_data.Dense):
     """
     Returns a randomly drawn superoperator acting on operators acting on
     N dimensions.
 
     Parameters
     ----------
-    N : int
-        Square root of the dimension of the superoperator to be returned.
+    dimensions : (int) or (list of int) or (list of list of int)
+        Dimension of Hilbert space. If provided as a list of ints, then the
+        dimension is the product over this list, but the ``dims`` property of
+        the new Qobj are set to this list.  This can produce either `oper` or
+        `super` depending on the passed `dimensions`.
 
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[[N],[N]], [[N],[N]]].
+    superrop : str, optional, {"super"}
+        Representation of the super operator
 
-    seed : int
-        seed for the random number generator
+    seed : int, SeedSequence, Generator, optional
+        Seed to create the random number generator or a pre prepared
+        generator. When none is suplied, a default generator is used.
 
     dtype : type or str
         Storage representation. Any data-layer known to `qutip.data.to` is
         accepted.
     """
+    generator = _get_generator(seed)
     from .solver.propagator import propagator
-    if dims is not None:
-        # TODO: check!
-        _check_dims(dims, N**2, N**2)
-        pass
-    else:
-        dims = [[[N], [N]], [[N], [N]]]
-    H = rand_herm(N, seed=seed, dtype=dtype)
-    S = propagator(H, np.random.rand(), [
+    N, dims = _implicit_tensor_dimensions(dimensions, superoper=True)
+    H = rand_herm(N, seed=generator, dtype=dtype)
+    S = propagator(H, generator.random(), [
         create(N), destroy(N), jmat(float(N - 1) / 2.0, 'z')
     ])
     S.dims = dims
-    return S.to(dtype)
+    out = {
+            "choi" : to_choi,
+            "chi" : to_chi,
+            "super": to_super,
+        }[superrep](S).to(dtype)
+    return out
 
 
-def rand_super_bcsz(N=2, enforce_tp=True, rank=None, dims=None, *,
-                    seed=None, dtype=_data.CSR):
+def rand_super_bcsz(dimensions, enforce_tp=True, rank=None, *,
+                    superrep="super", seed=None, dtype=_data.CSR):
     """
     Returns a random superoperator drawn from the Bruzda
     et al ensemble for CPTP maps [BCSZ08]_. Note that due to
@@ -655,8 +708,13 @@ def rand_super_bcsz(N=2, enforce_tp=True, rank=None, dims=None, *,
 
     Parameters
     ----------
-    N : int
-        Square root of the dimension of the superoperator to be returned.
+    dimensions : (int) or (list of int) or (list of list of int)
+        Dimension of Hilbert space. If an int is provided, it is understood as
+        the Square root of the dimension of the superoperator to be returned,
+        with the corresponding dims as ``[[[N],[N]], [[N],[N]]]``. If provided
+        as a list of ints, then the dimensions is understood as the space of
+        density matrices this superoperator is applied to: ``dimensions=[2,2]``
+        ``dims=[[[2,2],[2,2]], [[2,2],[2,2]]]``.
 
     enforce_tp : bool
         If True, the trace-preserving condition of [BCSZ08]_ is enforced;
@@ -666,12 +724,12 @@ def rand_super_bcsz(N=2, enforce_tp=True, rank=None, dims=None, *,
         Rank of the sampled superoperator. If None, a full-rank
         superoperator is generated.
 
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[[N],[N]], [[N],[N]]].
+    seed : int, SeedSequence, Generator, optional
+        Seed to create the random number generator or a pre prepared
+        generator. When none is suplied, a default generator is used.
 
-    seed : int
-        seed for the random number generator
+    superrop : str, optional, {"super"}
+        representation of the
 
     dtype : type or str
         Storage representation. Any data-layer known to `qutip.data.to` is
@@ -683,11 +741,8 @@ def rand_super_bcsz(N=2, enforce_tp=True, rank=None, dims=None, *,
         A superoperator acting on vectorized dim × dim density operators,
         sampled from the BCSZ distribution.
     """
-    if dims is not None:
-        # TODO: check!
-        pass
-    else:
-        dims = [[[N], [N]], [[N], [N]]]
+    generator = _get_generator(seed)
+    N, dims = _implicit_tensor_dimensions(dimensions, superoper=True)
 
     if rank is None:
         rank = N**2
@@ -700,7 +755,7 @@ def rand_super_bcsz(N=2, enforce_tp=True, rank=None, dims=None, *,
 
     # We start with a Ginibre uniform matrix X of the appropriate rank,
     # and use it to construct a positive semidefinite matrix X X⁺.
-    X = _randnz((N**2, rank), norm='ginibre', seed=seed)
+    X = _randnz((N**2, rank), generator, norm='ginibre')
 
     # Precompute X X⁺, as we'll need it in two different places.
     XXdag = np.dot(X, X.T.conj())
@@ -736,31 +791,35 @@ def rand_super_bcsz(N=2, enforce_tp=True, rank=None, dims=None, *,
     # Mark that we've made a Choi matrix.
     D.superrep = 'choi'
     D.dims = dims
-
-    return to_super(D).to(dtype)
+    out = {
+            "choi" : to_choi,
+            "chi" : to_chi,
+            "super": to_super,
+        }[superrep](D).to(dtype)
+    return out
 
 
-def rand_stochastic(N, density=0.75, kind='left', dims=None,
+def rand_stochastic(dimensions, density=0.75, kind='left',
                     *, seed=None, dtype=_data.CSR):
     """Generates a random stochastic matrix.
 
     Parameters
     ----------
-    N : int
-        Dimension of matrix.
+    dimensions : (int) or (list of int) or (list of list of int)
+        Dimension of Hilbert space. If provided as a list of ints, then the
+        dimension is the product over this list, but the ``dims`` property of
+        the new Qobj are set to this list.  This can produce either `oper` or
+        `super` depending on the passed `dimensions`.
 
-    density : float
+    density : float, [0.75]
         Density between [0,1] of output density matrix.
 
     kind : str (Default = 'left')
         Generate 'left' or 'right' stochastic matrix.
 
-    dims : list
-        Dimensions of quantum object.  Used for specifying
-        tensor structure. Default is dims=[[N],[N]].
-
-    seed : int
-        seed for the random number generator
+    seed : int, SeedSequence, Generator, optional
+        Seed to create the random number generator or a pre prepared
+        generator. When none is suplied, a default generator is used.
 
     dtype : type or str
         Storage representation. Any data-layer known to `qutip.data.to` is
@@ -771,17 +830,15 @@ def rand_stochastic(N, density=0.75, kind='left', dims=None,
     oper : qobj
         Quantum operator form of stochastic matrix.
     """
-    if seed is not None:
-        np.random.seed(seed=seed)
-    if dims:
-        _check_dims(dims, N, N)
+    generator = _get_generator(seed)
+    N, dims = _implicit_tensor_dimensions(dimensions)
     num_elems = max([int(np.ceil(N*(N+1)*density)/2), N])
-    data = np.random.rand(num_elems)
+    data = generator.random(num_elems)
     # Ensure an element on every row and column
-    row_idx = np.hstack([np.random.permutation(N),
-                         np.random.choice(N, num_elems-N)])
-    col_idx = np.hstack([np.random.permutation(N),
-                         np.random.choice(N, num_elems-N)])
+    row_idx = np.hstack([generator.permutation(N),
+                         generator.choice(N, num_elems-N)])
+    col_idx = np.hstack([generator.permutation(N),
+                         generator.choice(N, num_elems-N)])
     M = sp.coo_matrix((data, (row_idx, col_idx)),
                       dtype=np.complex128, shape=(N, N)).tocsr()
     M = 0.5 * (M + M.conj().transpose())
@@ -793,19 +850,4 @@ def rand_stochastic(N, density=0.75, kind='left', dims=None,
         M.data[row_start:row_end] /= row_sum
     if kind == 'left':
         M = M.transpose()
-    if dims:
-        return Qobj(M, dims=dims).to(dtype)
-    else:
-        return Qobj(M).to(dtype)
-
-
-def _check_dims(dims, N1, N2):
-    if len(dims) != 2:
-        raise Exception("Qobj dimensions must be list of length 2.")
-    if (not isinstance(dims[0], list)) or (not isinstance(dims[1], list)):
-        raise TypeError(
-            "Qobj dimension components must be lists. i.e. dims=[[N],[N]]")
-    if np.prod(dims[0]) != N1 or np.prod(dims[1]) != N2:
-        raise ValueError("Qobj dimensions must match matrix shape.")
-    if len(dims[0]) != len(dims[1]):
-        raise TypeError("Qobj dimension components must have same length.")
+    return Qobj(M, dims=dims).to(dtype)


Test patch:
diff --git a/qutip/tests/core/test_eigen.py b/qutip/tests/core/test_eigen.py
--- a/qutip/tests/core/test_eigen.py
+++ b/qutip/tests/core/test_eigen.py
@@ -42,7 +42,7 @@ def test_eigen_rand_oper(rand, sparse, dtype, order):
     else:
         assert np.all(np.diff(spvals).real <= 0)
     is_eigen_set(H, spvals, spvecs)
-    np.testing.assert_allclose(spvals, sp_energies)
+    np.testing.assert_allclose(spvals, sp_energies, atol=5e-15)
 
 
 @pytest.mark.parametrize(["sparse", 'dtype'], [
diff --git a/qutip/tests/core/test_eigenstates.py b/qutip/tests/core/test_eigenstates.py
--- a/qutip/tests/core/test_eigenstates.py
+++ b/qutip/tests/core/test_eigenstates.py
@@ -69,13 +69,18 @@ def test_known_eigensystem(hamiltonian, eigenvalues, eigenstates):
 
 # Specify parametrisation over a random Hamiltonian by specifying the
 # dimensions, rather than duplicating that logic.
-@pytest.fixture(params=[pytest.param([10], id="simple"),
-                        pytest.param([5, 3, 4], id="tensor"),
-                        pytest.param([[0, 1, 1]]*3, id="degenerate")])
+@pytest.fixture(params=[
+    pytest.param([10], id="simple"),
+    pytest.param([5, 3, 4], id="tensor"),
+    pytest.param([3, 3, 3], id="degenerate")])
 def random_hamiltonian(request):
     dimensions = request.param
-    return qutip.tensor(*[qutip.rand_herm(dim_or_evecs)
-                          for dim_or_evecs in dimensions])
+    eigen = None
+    dist = "fill"
+    if dimensions == [3, 3, 3]:
+        eigen = [0, 1, 1] * 9
+        dist = "eigen"
+    return qutip.rand_herm(dimensions, distribution=dist, eigenvalues=eigen)
 
 
 @pytest.mark.parametrize('sparse', [True, False])
diff --git a/qutip/tests/core/test_metrics.py b/qutip/tests/core/test_metrics.py
--- a/qutip/tests/core/test_metrics.py
+++ b/qutip/tests/core/test_metrics.py
@@ -7,8 +7,8 @@
 
 from qutip import (
     Qobj, tensor, fock_dm, basis, destroy, qdiags, sigmax, sigmay, sigmaz,
-    qeye, rand_ket, rand_super_bcsz, rand_ket_haar, rand_dm_ginibre, rand_dm,
-    rand_unitary, rand_unitary_haar, to_super, to_choi, kraus_to_choi,
+    qeye, rand_ket, rand_super_bcsz, rand_dm,
+    rand_unitary, to_super, to_choi, kraus_to_choi,
     to_chi, to_kraus,
 )
 from qutip.core.gates import snot, swap
@@ -38,8 +38,8 @@ def dimension(request):
 
 
 @pytest.fixture(scope="function", params=[
-    pytest.param(rand_ket_haar, id="pure"),
-    pytest.param(rand_dm_ginibre, id="mixed"),
+    pytest.param(rand_ket, id="pure"),
+    pytest.param(rand_dm, id="mixed"),
 ])
 def state(request, dimension):
     return request.param(dimension)
@@ -59,8 +59,8 @@ def state(request, dimension):
 class Test_fidelity:
     def test_mixed_state_inequality(self, dimension):
         tol = 1e-7
-        rho1 = rand_dm(dimension, 0.25)
-        rho2 = rand_dm(dimension, 0.25)
+        rho1 = rand_dm(dimension, density=0.25)
+        rho2 = rand_dm(dimension, density=0.25)
         F = fidelity(rho1, rho2)
         assert 1 - F <= np.sqrt(1 - F*F) + tol
 
@@ -76,9 +76,9 @@ def test_orthogonal(self, left_dm, right_dm, dimension):
         assert fidelity(left, right) == pytest.approx(0, abs=1e-6)
 
     def test_invariant_under_unitary_transformation(self, dimension):
-        rho1 = rand_dm(dimension, 0.25)
-        rho2 = rand_dm(dimension, 0.25)
-        U = rand_unitary(dimension, 0.25)
+        rho1 = rand_dm(dimension, density=0.25)
+        rho2 = rand_dm(dimension, density=0.25)
+        U = rand_unitary(dimension)
         F = fidelity(rho1, rho2)
         FU = fidelity(U*rho1*U.dag(), U*rho2*U.dag())
         assert F == pytest.approx(FU, rel=1e-5, abs=1e-7)
@@ -93,8 +93,8 @@ def test_bounded(self, left, right, dimension):
 
     def test_pure_state_equivalent_to_overlap(self, dimension):
         """Check fidelity against pure-state overlap, see gh-361."""
-        psi = rand_ket_haar(dimension)
-        phi = rand_ket_haar(dimension)
+        psi = rand_ket(dimension)
+        phi = rand_ket(dimension)
         overlap = np.abs(psi.overlap(phi))
         assert fidelity(psi, phi) == pytest.approx(overlap, abs=1e-7)
 
@@ -131,9 +131,9 @@ def test_orthogonal(self, left_dm, right_dm, dimension):
         assert tracedist(left, right) == pytest.approx(1, abs=1e-6)
 
     def test_invariant_under_unitary_transformation(self, dimension):
-        rho1 = rand_dm(dimension, 0.25)
-        rho2 = rand_dm(dimension, 0.25)
-        U = rand_unitary(dimension, 0.25)
+        rho1 = rand_dm(dimension, density=0.25)
+        rho2 = rand_dm(dimension, density=0.25)
+        U = rand_unitary(dimension)
         D = tracedist(rho1, rho2)
         DU = tracedist(U*rho1*U.dag(), U*rho2*U.dag())
         assert D == pytest.approx(DU, rel=1e-5)
@@ -195,7 +195,7 @@ def test_bounded(self, dimension):
     def test_unitaries_equal_1(self, dimension):
         """Tests that for random unitaries U, AGF(U, U) = 1."""
         tol = 1e-7
-        U = rand_unitary_haar(dimension)
+        U = rand_unitary(dimension)
         SU = to_super(U)
         assert average_gate_fidelity(SU, target=U) == pytest.approx(1, abs=tol)
 
@@ -441,7 +441,7 @@ def test_qubit_triangle(self, dimension):
     @pytest.mark.repeat(3)
     @pytest.mark.parametrize("generator", [
         pytest.param(rand_super_bcsz, id="super"),
-        pytest.param(rand_unitary_haar, id="unitary"),
+        pytest.param(rand_unitary, id="unitary"),
     ])
     def test_force_solve(self, dimension, generator):
         """
@@ -482,7 +482,7 @@ def test_process_fidelity_identical_channels(superrep_conversion):
     """
     num_qubits = 2
     for k in range(10):
-        oper = rand_super_bcsz(2**num_qubits, dims=2*[2*[num_qubits*[2]]])
+        oper = rand_super_bcsz(num_qubits*[2])
         oper = superrep_conversion(oper)
         f = process_fidelity(oper, oper)
         assert f == pytest.approx(1)
@@ -494,7 +494,7 @@ def test_process_fidelity_identical_unitaries():
     """
     num_qubits = 3
     for k in range(10):
-        oper = rand_unitary(2**num_qubits, dims=2*[num_qubits*[2]])
+        oper = rand_unitary(num_qubits * [2])
         f = process_fidelity(oper, oper)
         assert f == pytest.approx(1)
 
@@ -507,8 +507,8 @@ def test_process_fidelity_consistency():
     for k in range(10):
         fidelities_u_to_u = []
         fidelities_u_to_id = []
-        u1 = rand_unitary(2**num_qubits, dims=2*[num_qubits*[2]])
-        u2 = rand_unitary(2**num_qubits, dims=2*[num_qubits*[2]])
+        u1 = rand_unitary(num_qubits * [2])
+        u2 = rand_unitary(num_qubits * [2])
         for map1 in [lambda x:x, to_super, to_choi, to_chi, to_kraus]:
             fidelities_u_to_id.append(process_fidelity(map1(u1)))
             for map2 in [lambda x:x, to_super, to_choi, to_chi, to_kraus]:
diff --git a/qutip/tests/core/test_ptrace.py b/qutip/tests/core/test_ptrace.py
--- a/qutip/tests/core/test_ptrace.py
+++ b/qutip/tests/core/test_ptrace.py
@@ -38,7 +38,7 @@ def dm(request):
 @pytest.fixture
 def state(dtype, dm):
     dims = [2, 3, 4]
-    state = qutip.rand_ket(np.prod(dims), dims=[dims, [1]*len(dims)])
+    state = qutip.rand_ket(dims)
     if dm:
         state = state.proj()
     return state.to(dtype)
@@ -98,7 +98,7 @@ def test_ptrace_fails_on_invalid_input(state, selection, exception):
                              ([2]*6, [0, 1]),
                          ])
 def test_ptrace_rand_ket(dtype, dims, sel):
-    A = qutip.rand_ket(np.prod(dims), dims=[dims, [1]*len(dims)])
+    A = qutip.rand_ket(dims)
     assert A.ptrace(sel) == expected(A, sel)
 
 
@@ -111,7 +111,7 @@ def test_ptrace_rand_ket(dtype, dims, sel):
                               'trace_multiple_not_sorted',
                               ])
 def test_ptrace_rand_dm(dtype, sel):
-    A = qutip.rand_dm(64, 0.5, dims=[[4, 4, 4], [4, 4, 4]]).to(dtype)
+    A = qutip.rand_dm([4, 4, 4], density=0.5).to(dtype)
     assert A.ptrace(sel) == expected(A, sel)
 
 
diff --git a/qutip/tests/core/test_qobj.py b/qutip/tests/core/test_qobj.py
--- a/qutip/tests/core/test_qobj.py
+++ b/qutip/tests/core/test_qobj.py
@@ -595,9 +595,9 @@ def test_QobjNorm():
     np.testing.assert_allclose(A.norm('fro'), scipy.linalg.norm(x, 'fro'),
                                atol=1e-12)
     # operator trace norm
-    a = qutip.rand_herm(10, 0.25)
+    a = qutip.rand_herm(10, density=0.25)
     np.testing.assert_allclose(a.norm(), (a*a.dag()).sqrtm().tr().real)
-    b = qutip.rand_herm(10, 0.25) - 1j*qutip.rand_herm(10, 0.25)
+    b = qutip.rand_herm(10, density=0.25) - 1j*qutip.rand_herm(10, density=0.25)
     np.testing.assert_allclose(b.norm(), (b*b.dag()).sqrtm().tr().real)
 
 
@@ -668,8 +668,7 @@ def test_QobjPermute():
 
     for _ in range(3):
         super_dims = [3, 5, 4]
-        U = qutip.rand_unitary(np.prod(super_dims), density=0.02,
-                               dims=[super_dims, super_dims])
+        U = qutip.rand_unitary(super_dims)
         Unew = U.permute([2, 1, 0])
         S_tens = qutip.to_super(U)
         S_tens_new = qutip.to_super(Unew)
@@ -941,10 +940,10 @@ def test_dual_channel(sub_dimensions, n_trials=50):
     duals = []
 
     for _ in [None]*n_trials:
-        X = qutip.rand_dm_ginibre(out_dim)
+        X = qutip.rand_dm(out_dim)
         X.dims = left_dims
         X = qutip.operator_to_vector(X)
-        Y = qutip.rand_dm_ginibre(in_dim)
+        Y = qutip.rand_dm(in_dim)
         Y.dims = right_dims
         Y = qutip.operator_to_vector(Y)
 
@@ -960,7 +959,7 @@ def test_call():
     """
     # Make test objects.
     psi = qutip.rand_ket(3)
-    rho = qutip.rand_dm_ginibre(3)
+    rho = qutip.rand_dm(3)
     U = qutip.rand_unitary(3)
     S = qutip.rand_super_bcsz(3)
 
@@ -986,10 +985,10 @@ def test_mat_elem():
     """
     for _ in range(10):
         N = 20
-        H = qutip.rand_herm(N, 0.2)
-        L = qutip.rand_ket(N, 0.3)
+        H = qutip.rand_herm(N, density=0.2)
+        L = qutip.rand_ket(N, density=0.3)
         Ld = L.dag()
-        R = qutip.rand_ket(N, 0.3)
+        R = qutip.rand_ket(N, density=0.3)
         ans = Ld * H * R
         # bra-ket
         out1 = H.matrix_element(Ld, R)
@@ -1005,7 +1004,7 @@ def test_projection():
     """
     for _ in range(10):
         N = 5
-        K = qutip.tensor(qutip.rand_ket(N, 0.75), qutip.rand_ket(N, 0.75))
+        K = qutip.rand_ket([N, N], density=0.75)
         B = K.dag()
         ans = K * K.dag()
         out1 = K.proj()
@@ -1020,9 +1019,9 @@ def test_overlap():
     """
     for _ in range(10):
         N = 10
-        A = qutip.rand_ket(N, 0.75)
+        A = qutip.rand_ket(N, density=0.75)
         Ad = A.dag()
-        B = qutip.rand_ket(N, 0.75)
+        B = qutip.rand_ket(N, density=0.75)
         Bd = B.dag()
         ans = A.dag() * B
         np.testing.assert_allclose(A.overlap(B), ans)
@@ -1115,7 +1114,7 @@ def test_sum_buildin(shape):
 def test_groundstate():
     eigenvals = np.sort(np.random.rand(10))
     eigenvals[1:] += 0.1  # Ensure no degenerate groundstate
-    qobj = qutip.rand_herm(eigenvals)
+    qobj = qutip.rand_herm(10, distribution="eigen", eigenvalues=eigenvals)
     groundenergy, groundstate = qobj.groundstate()
     assert groundenergy == pytest.approx(eigenvals[0])
     assert qutip.expect(qobj, groundstate) == pytest.approx(eigenvals[0])
diff --git a/qutip/tests/core/test_superop_reps.py b/qutip/tests/core/test_superop_reps.py
--- a/qutip/tests/core/test_superop_reps.py
+++ b/qutip/tests/core/test_superop_reps.py
@@ -12,7 +12,7 @@
 
 from qutip import (
     Qobj, basis, identity, sigmax, sigmay, qeye, create, rand_super,
-    rand_super_bcsz, rand_dm_ginibre, tensor, super_tensor, kraus_to_choi,
+    rand_super_bcsz, rand_dm, tensor, super_tensor, kraus_to_choi,
     to_super, to_choi, to_kraus, to_chi, to_stinespring, operator_to_vector,
     vector_to_operator, sprepost, destroy
 )
@@ -275,7 +275,7 @@ def test_stinespring_agrees(self, dimension):
         """
 
         map = rand_super_bcsz(dimension)
-        state = rand_dm_ginibre(dimension)
+        state = rand_dm(dimension)
 
         S = to_super(map)
         A, B = to_stinespring(map)
diff --git a/qutip/tests/core/test_tensor.py b/qutip/tests/core/test_tensor.py
--- a/qutip/tests/core/test_tensor.py
+++ b/qutip/tests/core/test_tensor.py
@@ -219,7 +219,7 @@ def test_permutation_without_expansion(self, permutation):
     def test_general_qubit_expansion(self, n_targets):
         # Test all permutations with the given number of targets.
         n_qubits = 5
-        operation = qutip.rand_unitary(2**n_targets, dims=[[2]*n_targets]*2)
+        operation = qutip.rand_unitary([2]*n_targets)
         for targets in itertools.permutations(range(n_qubits), n_targets):
             expected = _tensor_with_entanglement([qutip.qeye(2)] * n_qubits,
                                                  operation, targets)
diff --git a/qutip/tests/solve/test_krylovsolve.py b/qutip/tests/solve/test_krylovsolve.py
--- a/qutip/tests/solve/test_krylovsolve.py
+++ b/qutip/tests/solve/test_krylovsolve.py
@@ -3,7 +3,7 @@
 from qutip.solve.solver import SolverOptions, Result
 from qutip import tensor, Qobj, basis, expect, ket
 from qutip import sigmax, sigmay, sigmaz, qeye, jmat
-from qutip import rand_herm, rand_ket, rand_unitary_haar, num, destroy, create
+from qutip import rand_herm, rand_ket, rand_unitary, num, destroy, create
 import os
 import math
 import types
@@ -42,7 +42,7 @@ def happy_breakdown_parameters(request):
 
 
 def h_sho(dim):
-    U = rand_unitary_haar(dim)
+    U = rand_unitary(dim)
     H = U * (num(dim) + 0.5) * U.dag()
     H = H / np.linalg.norm(H.full(), ord=2)
     return H
@@ -247,7 +247,7 @@ def test_03_simple_check_states_e_ops_H_ising_transverse(self):
         H = h_ising_transverse(N, hx=0.1, hz=0.5, Jx=1, Jy=0, Jz=1)
         _dims = H.dims
         _dims2 = [1] * N
-        psi0 = rand_ket(dim, dims=[_dims[0], _dims2])
+        psi0 = rand_ket(_dims[0])
         tlist = np.linspace(0, 20, 200)
 
         self.simple_check_states_e_ops(
diff --git a/qutip/tests/test_entropy.py b/qutip/tests/test_entropy.py
--- a/qutip/tests/test_entropy.py
+++ b/qutip/tests/test_entropy.py
@@ -36,7 +36,7 @@ def test_maximally_entangled(self, dm):
 
     @pytest.mark.repeat(10)
     def test_nonzero(self):
-        dm = qutip.rand_dm(4, dims=[[2, 2], [2, 2]])
+        dm = qutip.rand_dm([2, 2])
         assert qutip.concurrence(dm) >= 0
 
 
@@ -44,13 +44,13 @@ def test_nonzero(self):
 class TestMutualInformation:
     def test_pure_state_additive(self):
         # Verify mutual information = S(A) + S(B) for pure states.
-        dm = qutip.rand_dm(25, dims=[[5, 5], [5, 5]], pure=True)
+        dm = qutip.rand_dm([5, 5], distribution="pure")
         expect = (qutip.entropy_vn(dm.ptrace(0))
                   + qutip.entropy_vn(dm.ptrace(1)))
         assert abs(qutip.entropy_mutual(dm, [0], [1]) - expect) < 1e-13
 
     def test_component_selection(self):
-        dm = qutip.rand_dm(8, dims=[[2, 2, 2], [2, 2, 2]], pure=True)
+        dm = qutip.rand_dm([2, 2, 2], distribution="pure")
         expect = (qutip.entropy_vn(dm.ptrace([0, 2]))
                   + qutip.entropy_vn(dm.ptrace(1)))
         assert abs(qutip.entropy_mutual(dm, [0, 2], [1]) - expect) < 1e-13
@@ -163,14 +163,14 @@ def test_density_matrices_with_non_real_eigenvalues(self):
 
     @pytest.mark.repeat(20)
     def test_random_dm_with_self(self):
-        rho = qutip.rand_dm(8, pure=False)
+        rho = qutip.rand_dm(8)
         rel = qutip.entropy_relative(rho, rho)
         assert abs(rel) < 1e-13
 
     @pytest.mark.repeat(20)
     def test_random_rho_sigma(self):
-        rho = qutip.rand_dm(8, pure=False)
-        sigma = qutip.rand_dm(8, pure=False)
+        rho = qutip.rand_dm(8)
+        sigma = qutip.rand_dm(8)
         rel = qutip.entropy_relative(rho, sigma)
         assert rel >= 0
         assert rel == pytest.approx(
@@ -182,14 +182,14 @@ def test_random_rho_sigma(self):
 class TestConditionalEntropy:
     def test_inequality_3_qubits(self):
         # S(A | B,C) <= S(A|B)
-        full = qutip.rand_dm(8, dims=[[2]*3]*2, pure=True)
+        full = qutip.rand_dm([2]*3, distribution="pure")
         ab = full.ptrace([0, 1])
         assert (qutip.entropy_conditional(full, [1, 2])
                 <= qutip.entropy_conditional(ab, 1))
 
     def test_triangle_inequality_4_qubits(self):
         # S(A,B | C,D) <= S(A|C) + S(B|D)
-        full = qutip.rand_dm(16, dims=[[2]*4]*2, pure=True)
+        full = qutip.rand_dm([2]*4, distribution="pure")
         ac, bd = full.ptrace([0, 2]), full.ptrace([1, 3])
         assert (qutip.entropy_conditional(full, [2, 3])
                 <= (qutip.entropy_conditional(ac, 1)
diff --git a/qutip/tests/test_mkl.py b/qutip/tests/test_mkl.py
--- a/qutip/tests/test_mkl.py
+++ b/qutip/tests/test_mkl.py
@@ -26,7 +26,7 @@ def test_single_rhs_vector_real(self):
         np.testing.assert_allclose(x, x2)
 
     def test_single_rhs_vector_complex(self):
-        A = qutip.rand_herm(10, dtype='csr')
+        A = qutip.rand_herm(10, density=0.8, dtype='csr')
         x = qutip.rand_ket(10).full()
         b = A.full() @ x
         y = mkl_spsolve(A.data.as_scipy(), b, verbose=True)
@@ -82,7 +82,9 @@ def test_sparse_rhs(self):
 
     @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
     def test_symmetric_solver(self, dtype):
-        A = qutip.rand_herm(np.arange(1, 11), dtype='csr').data.as_scipy()
+        A = qutip.rand_herm(10, distribution="eigen",
+                            eigenvalues=np.arange(1, 11),
+                            dtype='csr').data.as_scipy()
         if dtype == np.float64:
             A = A.real
         x = np.ones(10, dtype=dtype)
diff --git a/qutip/tests/test_rand.py b/qutip/tests/test_rand.py
deleted file mode 100644
--- a/qutip/tests/test_rand.py
+++ /dev/null
@@ -1,145 +0,0 @@
-import numpy as np
-from qutip import (
-    rand_unitary, qeye, rand_herm, rand_dm, rand_ket, rand_stochastic,
-)
-from qutip.core.data import eigs_csr
-
-
-class TestRand:
-    """
-    A test class for the built-in random quantum object generators.
-    """
-
-    def testRandUnitary(self):
-        "random Unitary"
-
-        U = [rand_unitary(5) for k in range(5)]
-        for u in U:
-            assert u * u.dag() == qeye(5)
-
-    def testRandUnitarySeed(self):
-        "random Unitary with seed"
-
-        seed = 12345
-        U0 = rand_unitary(5, seed=seed)
-        U1 = rand_unitary(5, seed=None)
-        U2 = rand_unitary(5, seed=seed)
-        assert U0 != U1
-        assert U0 == U2
-
-    def testRandherm(self):
-        "random hermitian"
-
-        H = [rand_herm(5) for k in range(5)]
-        for h in H:
-            assert h.isherm
-
-    def testRandhermSeed(self):
-        "random hermitian with seed"
-
-        seed = 12345
-        U0 = rand_herm(5, seed=seed)
-        U1 = rand_herm(5, seed=None)
-        U2 = rand_herm(5, seed=seed)
-        assert U0 != U1
-        assert U0 == U2
-
-    def testRandhermPosDef(self):
-        "Random: Hermitian - Positive semi-def"
-
-        H = [rand_herm(5,pos_def=1) for k in range(5)]
-        for h in H:
-            assert not any(eigs_csr(h.data, h.isherm, vecs=False)) < 0
-
-    def testRandhermEigs(self):
-        "Random: Hermitian - Eigs given"
-
-        H = [rand_herm([1,2,3,4,5],0.5) for k in range(5)]
-        for h in H:
-            eigs = eigs_csr(h.data, h.isherm, vecs=False)
-            assert np.abs(np.sum(eigs)-15.0) < 1e-12
-
-    def testRanddm(self):
-        "random density matrix"
-
-        R = [rand_dm(5) for k in range(5)]
-        for r in R:
-            assert r.tr() - 1.0 < 1e-15
-            # verify all eigvals are >=0
-            assert not any(eigs_csr(r.data, r.isherm, vecs=False)) < 0
-            # verify hermitian
-            assert r.isherm
-
-    def testRandDmSeed(self):
-        "random density matrix with seed"
-
-        seed = 12345
-        U0 = rand_dm(5, seed=seed)
-        U1 = rand_dm(5, seed=None)
-        U2 = rand_dm(5, seed=seed)
-        assert U0 != U1
-        assert U0 == U2
-
-    def testRanddmEigs(self):
-        "Random: Density matrix - Eigs given"
-        R = []
-        for k in range(5):
-            eigs = np.random.random(5)
-            eigs /= np.sum(eigs)
-            R += [rand_dm(eigs)]
-        for r in R:
-            assert r.tr() - 1.0 < 1e-15
-            # verify all eigvals are >=0
-            assert not any(eigs_csr(r.data, r.isherm, vecs=False)) < 0
-            # verify hermitian
-            assert r.isherm
-
-    def testRandket(self):
-        "random ket"
-        P = [rand_ket(5) for k in range(5)]
-        for p in P:
-            assert p.type == 'ket'
-
-    def testRandketSeed(self):
-        "random ket with seed"
-
-        seed = 12345
-        U0 = rand_ket(5, seed=seed)
-        U1 = rand_ket(5, seed=None)
-        U2 = rand_ket(5, seed=seed)
-        assert U0 != U1
-        assert U0 == U2
-
-    def testRandStochasticLeft(self):
-        'Random: Stochastic - left'
-        Q = [rand_stochastic(10) for k in range(5)]
-        for i in range(5):
-            A = Q[i].full()
-            np.testing.assert_allclose(np.sum(A, axis=0), 1, atol=1e-15)
-
-    def testRandStochasticLeftSeed(self):
-        "Random: Stochastic - left with seed"
-
-        seed = 12345
-        U0 = rand_stochastic(10, seed=seed)
-        U1 = rand_stochastic(10, seed=None)
-        U2 = rand_stochastic(10, seed=seed)
-        assert U0 != U1
-        assert U0 == U2
-
-    def testRandStochasticRight(self):
-        'Random: Stochastic - right'
-        Q = [rand_stochastic(10, kind='right') for k in range(5)]
-        for i in range(5):
-            A = Q[i].full()
-            np.testing.assert_allclose(np.sum(A, axis=1), 1, atol=1e-15)
-
-    def testRandStochasticRightSeed(self):
-        "Random: Stochastic - right with seed"
-
-        seed = 12345
-        U0 = rand_stochastic(10, kind='right', seed=seed)
-        U1 = rand_stochastic(10, kind='right', seed=None)
-        U2 = rand_stochastic(10, kind='right', seed=seed)
-        assert U0 != U1
-        assert U0 == U2
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
@@ -1,199 +1,287 @@
 import numpy as np
+from numpy.random import SeedSequence, default_rng
 import scipy.sparse as sp
 import scipy.linalg as la
 import pytest
 
-from qutip import qeye
+from qutip import qeye, num, to_kraus, kraus_to_choi, CoreOptions
 from qutip import data as _data
-from qutip.random_objects import (rand_ket, rand_dm, rand_herm, rand_unitary,
-                                  rand_ket_haar, rand_dm_hs, rand_stochastic,
-                                  rand_super, rand_unitary_haar, rand_dm_ginibre,
-                                  rand_super_bcsz)
-
-@pytest.mark.repeat(5)
-@pytest.mark.parametrize('func', [rand_unitary, rand_unitary_haar])
-def test_rand_unitary(func):
-    """
-    Random Qobjs: Tests that unitaries are actually unitary.
-    """
-    random_qobj = func(5)
-    I = qeye(5)
-    assert random_qobj * random_qobj.dag() == I
-
-
-@pytest.mark.repeat(5)
+from qutip.random_objects import (
+    rand_herm,
+    rand_unitary,
+    rand_dm,
+    rand_ket,
+    rand_stochastic,
+    rand_super,
+    rand_super_bcsz,
+    rand_kraus_map,
+)
+
+
+@pytest.fixture(params=[
+    12,
+    [8],
+    [2, 2, 3],
+    [[2], [2]]
+], ids=["int", "list", "tensor", "super"])
+def dimensions(request):
+    return request.param
+
+
+@pytest.fixture(
+    params=list(_data.to.dtypes),
+    ids=lambda dtype: str(dtype)[:-2].split(".")[-1]
+)
+def dtype(request):
+    return request.param
+
+
+def _assert_density(qobj, density):
+    N = np.sum(qobj.full() != 0)
+    density = N / np.prod(qobj.shape)
+    assert density == pytest.approx(density, abs=0.2)
+
+
+def _assert_metadata(random_qobj, dims, dtype=None, super=False, ket=False):
+    if isinstance(dims, int):
+        dims = [dims]
+    N = np.prod(dims)
+    if super and not isinstance(dims[0], list):
+        target_dims_0 = [dims, dims]
+        shape0 = N**2
+    else:
+        target_dims_0 = dims
+        shape0 = N
+
+    if ket:
+        target_dims_1 = [1]
+        shape1 = 1
+    else:
+        target_dims_1 = target_dims_0
+        shape1 = shape0
+
+    assert random_qobj.dims[0] == target_dims_0
+    assert random_qobj.dims[1] == target_dims_1
+    assert random_qobj.shape == (shape0, shape1)
+
+    if dtype:
+        assert isinstance(random_qobj.data, dtype)
+
+
+@pytest.mark.repeat(3)
 @pytest.mark.parametrize('density', [0.2, 0.8], ids=["sparse", "dense"])
-@pytest.mark.parametrize('pos_def', [True, False])
-def test_rand_herm(density, pos_def):
+@pytest.mark.parametrize('distribution', ["fill", "pos_def"])
+def test_rand_herm(dimensions, density, distribution, dtype):
     """
     Random Qobjs: Hermitian matrix
     """
-    random_qobj = rand_herm(5, density=density, pos_def=pos_def)
-    if pos_def:
+    random_qobj = rand_herm(
+        dimensions,
+        density=density,
+        distribution=distribution,
+        dtype=dtype
+    )
+    if distribution == "pos_def":
         assert all(random_qobj.eigenenergies() > -1e14)
     assert random_qobj.isherm
+    assert _data.isherm(random_qobj.data)
+    _assert_metadata(random_qobj, dimensions, dtype)
+    _assert_density(random_qobj, density)
 
 
-@pytest.mark.repeat(5)
-def test_rand_herm_Eigs():
+@pytest.mark.repeat(3)
+@pytest.mark.parametrize('density', [0.2, 0.8], ids=["sparse", "dense"])
+def test_rand_herm_Eigs(dimensions, density):
     """
     Random Qobjs: Hermitian matrix - Eigs given
     """
-    eigs = np.random.random(5)
+    N = np.prod(dimensions)
+    eigs = np.random.random(N)
     eigs /= np.sum(eigs)
     eigs.sort()
-    random_qobj = rand_herm(eigs)
+    random_qobj = rand_herm(dimensions, density, "eigen", eigenvalues=eigs)
     np.testing.assert_allclose(random_qobj.eigenenergies(), eigs)
     # verify hermitian
     assert random_qobj.isherm
+    assert _data.isherm(random_qobj.data)
+    _assert_metadata(random_qobj, dimensions)
+    _assert_density(random_qobj, density)
 
 
 @pytest.mark.repeat(5)
-@pytest.mark.parametrize('func', [rand_dm, rand_dm_hs])
-def test_rand_dm(func):
+@pytest.mark.parametrize('distribution', ["haar", "exp"])
+@pytest.mark.parametrize('density', [0.2, 0.8])
+def test_rand_unitary(dimensions, distribution, density, dtype):
+    """
+    Random Qobjs: Tests that unitaries are actually unitary.
+    """
+    N = np.prod(dimensions)
+    random_qobj = rand_unitary(
+        dimensions, distribution=distribution,
+        density=density, dtype=dtype
+    )
+    I = qeye(dimensions)
+    assert random_qobj * random_qobj.dag() == I
+    _assert_metadata(random_qobj, dimensions, dtype)
+    if distribution == "exp":
+        _assert_density(random_qobj, density)
+
+
+@pytest.mark.repeat(3)
+@pytest.mark.parametrize(["distribution", "kw"], [
+    pytest.param("ginibre", {"rank": 3}),
+    pytest.param("ginibre", {"rank": 1}),
+    pytest.param("hs", {"rank": 0}),
+    pytest.param("pure", {"rank": 1, "density": 0.5}),
+    pytest.param("eigen", {"eigenvalues": True}),
+    pytest.param("herm", {"density": 0.7}),
+    pytest.param("herm", {"density": 0.3}),
+])
+def test_rand_dm(dimensions, kw, dtype, distribution):
     """
     Random Qobjs: Density matrix
     """
-    random_qobj = func(5)
+    N = np.prod(dimensions)
+    print(N, kw)
+    if "eigenvalues" in kw:
+        eigs = np.random.random(N)
+        eigs /= np.sum(eigs)
+        eigs.sort()
+        kw["eigenvalues"] = eigs
+
+    random_qobj = rand_dm(
+        dimensions,
+        distribution=distribution,
+        dtype=dtype,
+        **kw
+    )
     assert abs(random_qobj.tr() - 1.0) < 1e-14
     # verify all eigvals are >=0
     assert all(random_qobj.eigenenergies() >= -1e-14)
     # verify hermitian
     assert random_qobj.isherm
+    assert _data.isherm(random_qobj.data)
 
+    _assert_metadata(random_qobj, dimensions, dtype)
 
-@pytest.mark.repeat(5)
-def test_rand_dm_Eigs():
-    """
-    Random Qobjs: Density matrix - Eigs given
-    """
-    eigs = np.random.random(5)
-    eigs /= np.sum(eigs)
-    eigs.sort()
-    random_qobj = rand_dm(eigs)
-    assert abs(random_qobj.tr() - 1.0) < 1e-14
-    np.testing.assert_allclose(random_qobj.eigenenergies(), eigs)
-    # verify hermitian
-    assert random_qobj.isherm
+    eigenvalues = random_qobj.eigenenergies()
 
+    if "rank" in kw:
+        print(kw["rank"], N, kw["rank"] or N)
+        desired_rank = kw["rank"] or N  # 'hs' is full rank.
+        rank = sum([abs(E) >= 1e-10 for E in eigenvalues])
+        assert rank == desired_rank
 
-@pytest.mark.repeat(5)
-def test_rand_dm_ginibre_rank():
-    """
-    Random Qobjs: Ginibre-random density ops have correct rank.
-    """
-    random_qobj = rand_dm_ginibre(5, rank=3)
-    rank = sum([abs(E) >= 1e-10 for E in random_qobj.eigenenergies()])
-    assert rank == 3
+    if "eigenvalues" in kw:
+        np.testing.assert_allclose(eigenvalues, kw["eigenvalues"])
+
+    if "density" in kw:
+        _assert_density(random_qobj, kw["density"])
 
 
 @pytest.mark.repeat(5)
 @pytest.mark.parametrize('kind', ["left", "right"])
-def test_rand_stochastic(kind):
+def test_rand_stochastic(dimensions, kind, dtype):
     """
     Random Qobjs: Test random stochastic
     """
-    random_qobj = rand_stochastic(5, kind=kind)
+    random_qobj = rand_stochastic(dimensions, kind=kind, dtype=dtype)
     axis = {"left":0, "right":1}[kind]
-    np.testing.assert_allclose(np.sum(random_qobj.full(), axis=axis), 1,
-                               atol=1e-14)
+    np.testing.assert_allclose(
+        np.sum(random_qobj.full(), axis=axis),
+        1, atol=1e-14
+    )
+    _assert_metadata(random_qobj, dimensions, dtype)
 
 
 @pytest.mark.repeat(5)
-@pytest.mark.parametrize('func', [rand_ket, rand_ket_haar])
-def test_rand_ket(func):
+@pytest.mark.parametrize('distribution', ["haar", "fill"])
+def test_rand_ket(dimensions, distribution, dtype):
     """
     Random Qobjs: Test random ket type and norm.
     """
-    random_qobj = func(5)
+    random_qobj = rand_ket(dimensions, distribution=distribution, dtype=dtype)
+
     assert random_qobj.type == 'ket'
     assert abs(random_qobj.norm() - 1) < 1e-14
 
+    if isinstance(dimensions, int):
+        dims = [dimensions]
+    N = np.prod(dimensions)
+    _assert_metadata(random_qobj, dimensions, dtype, ket=True)
 
-@pytest.mark.repeat(5)
-def test_rand_super():
+
+@pytest.mark.repeat(2)
+@pytest.mark.parametrize('superrep', ["choi", "super"])
+def test_rand_super(dimensions, dtype, superrep):
     """
     Random Qobjs: Super operator.
     """
-    random_qobj = rand_super(5)
+    random_qobj = rand_super(dimensions, dtype=dtype, superrep=superrep)
     assert random_qobj.issuper
+    with CoreOptions(atol=1e-9):
+        assert random_qobj.iscptp
+    assert random_qobj.superrep == superrep
+    _assert_metadata(random_qobj, dimensions, dtype, super=True)
 
 
-@pytest.mark.repeat(5)
-def test_rand_super_bcsz_cptp():
+@pytest.mark.repeat(2)
+@pytest.mark.parametrize('rank', [None, 4])
+@pytest.mark.parametrize('superrep', ["choi", "super"])
+def test_rand_super_bcsz(dimensions, dtype, rank, superrep):
     """
     Random Qobjs: Tests that BCSZ-random superoperators are CPTP.
     """
-    random_qobj = rand_super_bcsz(5)
-    assert random_qobj.issuper
-    assert random_qobj.iscptp
 
-
-@pytest.mark.parametrize('func', [
-    rand_unitary, rand_unitary_haar, rand_herm,
-    rand_dm, rand_dm_hs, rand_dm_ginibre,
-    rand_ket, rand_ket_haar,
-    rand_super, rand_super_bcsz
+    random_qobj = rand_super_bcsz(dimensions, rank=rank,
+                                  dtype=dtype, superrep=superrep)
+    assert random_qobj.issuper
+    with CoreOptions(atol=1e-9):
+        assert random_qobj.iscptp
+    assert random_qobj.superrep == superrep
+    _assert_metadata(random_qobj, dimensions, dtype, super=True)
+    if (
+        not rank
+        and isinstance(dimensions, list)
+        and isinstance(dimensions[0], list)
+    ):
+        # dimensions = [[a], [a]], qobj.dims = [[[a], [a]], [[a], [a]]]
+        rank = np.prod(dimensions)
+    elif not rank:
+        # dimensions = [a], qobj.dims = [[[a], [a]], [[a], [a]]]
+        rank = np.prod(dimensions)**2
+    rank = rank or N
+    obtained_rank = len(to_kraus(random_qobj, tol=1e-13))
+    assert obtained_rank == rank
+
+
+@pytest.mark.parametrize("function", [
+    pytest.param(rand_herm, id="rand_herm"),
+    pytest.param(rand_unitary, id="rand_unitary"),
+    pytest.param(rand_dm, id="rand_dm"),
+    pytest.param(rand_ket, id="rand_ket"),
+    pytest.param(rand_stochastic, id="rand_stochastic"),
+    pytest.param(rand_super, id="rand_super"),
+    pytest.param(rand_super_bcsz, id="rand_super_bcsz"),
+])
+@pytest.mark.parametrize("seed", [
+    pytest.param(lambda : 123, id="int"),
+    pytest.param(lambda : SeedSequence(123), id="SeedSequence"),
+    pytest.param(lambda : default_rng(123), id="Generator")
 ])
-def test_random_seeds(func):
+def test_random_seeds(function, seed):
     """
     Random Qobjs: Random number generator seed
     """
-    seed = 12345
-    U0 = func(5, seed=seed)
-    U1 = func(5, seed=None)
-    U2 = func(5, seed=seed)
+    U0 = function(5, seed=seed())
+    U1 = function(5, seed=None)
+    U2 = function(5, seed=seed())
     assert U0 != U1
     assert U0 == U2
 
 
-@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, rand_stochastic,
-])
-@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
+def test_kraus_map(dimensions, dtype):
+    kmap = rand_kraus_map(dimensions, dtype=dtype)
+    _assert_metadata(kmap[0], dimensions, dtype)
+    with CoreOptions(atol=1e-9):
+        assert kraus_to_choi(kmap).iscptp
diff --git a/qutip/tests/test_wigner.py b/qutip/tests/test_wigner.py
--- a/qutip/tests/test_wigner.py
+++ b/qutip/tests/test_wigner.py
@@ -51,9 +51,9 @@ def test_failure_if_coordinates_not_1d(self, ndim):
     @pytest.mark.parametrize('dm', [True, False], ids=['dm', 'ket'])
     def test_failure_if_tensor_hilbert_space(self, dm):
         if dm:
-            state = qutip.rand_dm(4, dims=[[2, 2], [2, 2]])
+            state = qutip.rand_dm([2, 2])
         else:
-            state = qutip.rand_ket(4, dims=[[2, 2], [1, 1]])
+            state = qutip.rand_ket([2, 2])
         xs = np.linspace(-1, 1, 5)
         with pytest.raises(ValueError) as e:
             qutip.qfunc(state, xs, xs)
@@ -491,7 +491,7 @@ def test_wigner_compare_methods_dm():
     for n in range(10):
         # try ten different random density matrices
 
-        rho = rand_dm(N, 0.5 + rand() / 2)
+        rho = rand_dm(N, density=0.5 + rand() / 2)
 
         # calculate the wigner function using qutip and analytic formula
         W_qutip1 = wigner(rho, xvec, yvec, g=2)
@@ -523,7 +523,7 @@ def test_wigner_compare_methods_ket():
     for n in range(10):
         # try ten different random density matrices
 
-        psi = rand_ket(N, 0.5 + rand() / 2)
+        psi = rand_ket(N, density=0.5 + rand() / 2)
 
         # calculate the wigner function using qutip and analytic formula
         W_qutip1 = wigner(psi, xvec, yvec, g=2)
@@ -599,12 +599,12 @@ def test_wigner_clenshaw_sp_iter_dm():
     pytest.param(7, id="spin-seven")
 ])
 @pytest.mark.parametrize("pure", [
-    pytest.param(True, id="pure"),
-    pytest.param(False, id="mixed")
+    pytest.param("pure", id="pure"),
+    pytest.param("herm", id="mixed")
 ])
 def test_spin_q_function(spin, pure):
     d = int(2*spin + 1)
-    rho = rand_dm(d, pure=pure)
+    rho = rand_dm(d, distribution=pure)
 
     # Points at which to evaluate the spin Q function
     theta = np.linspace(0, np.pi, 16, endpoint=True)
@@ -623,12 +623,12 @@ def test_spin_q_function(spin, pure):
     pytest.param(7, id="spin-seven")
 ])
 @pytest.mark.parametrize("pure", [
-    pytest.param(True, id="pure"),
-    pytest.param(False, id="mixed")
+    pytest.param("pure", id="pure"),
+    pytest.param("herm", id="mixed")
 ])
 def test_spin_q_function_normalized(spin, pure):
     d = int(2 * spin + 1)
-    rho = rand_dm(d, pure=pure)
+    rho = rand_dm(d, distribution=pure)
 
     # Points at which to evaluate the spin Q function
     theta = np.linspace(0, np.pi, 128, endpoint=True)
@@ -646,12 +646,12 @@ def test_spin_q_function_normalized(spin, pure):
     pytest.param(2, id="spin-two")
 ])
 @pytest.mark.parametrize("pure", [
-    pytest.param(True, id="pure"),
-    pytest.param(False, id="mixed")
+    pytest.param("pure", id="pure"),
+    pytest.param("herm", id="mixed")
 ])
 def test_spin_wigner_normalized(spin, pure):
     d = int(2*spin + 1)
-    rho = rand_dm(d, pure=pure)
+    rho = rand_dm(d, distribution=pure)
 
     # Points at which to evaluate the spin Wigner function
     theta = np.linspace(0, np.pi, 256, endpoint=True)
@@ -668,12 +668,12 @@ def test_spin_wigner_normalized(spin, pure):
     pytest.param(2, id="spin-two")
 ])
 @pytest.mark.parametrize("pure", [
-    pytest.param(True, id="pure"),
-    pytest.param(False, id="mixed")
+    pytest.param("pure", id="pure"),
+    pytest.param("herm", id="mixed")
 ])
 def test_spin_wigner_overlap(spin, pure, n=5):
     d = int(2*spin + 1)
-    rho = rand_dm(d, pure=pure)
+    rho = rand_dm(d, distribution=pure)
 
     # Points at which to evaluate the spin Wigner function
     theta = np.linspace(0, np.pi, 256, endpoint=True)