def rand_super_bcsz(N=2, enforce_tp=True, rank=None, dims=None): """ Returns a random superoperator drawn from the Bruzda et al ensemble for CPTP maps [BCSZ08]_. Note that due to finite numerical precision, for ranks less than full-rank, zero eigenvalues may become slightly negative, such that the returned operator is not actually completely positive. Parameters ---------- N : int Square root of the dimension of the superoperator to be returned. enforce_tp : bool If True, the trace-preserving condition of [BCSZ08]_ is enforced; otherwise only complete positivity is enforced. rank : int or 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]]]. Returns ------- rho : Qobj 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]]] if rank is None: rank = N ** 2 if rank > N ** 2: raise ValueError("Rank cannot exceed superoperator dimension.") # We use mainly dense matrices here for speed in low # dimensions. In the future, it would likely be better to switch off # between sparse and dense matrices as the dimension grows. # 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") # Precompute X X⁺, as we'll need it in two different places. XXdag = np.dot(X, X.T.conj()) if enforce_tp: # We do the partial trace over the first index by using dense reshape # operations, so that we can avoid bouncing to a sparse representation # and back. Y = np.einsum("ijik->jk", XXdag.reshape((N, N, N, N))) # Now we have the matrix 𝟙 ⊗ Y^{-1/2}, which we can find by doing # the square root and the inverse separately. As a possible improvement, # iterative methods exist to find inverse square root matrices directly, # as this is important in statistics. Z = np.kron(np.eye(N), sqrtm(la.inv(Y))) # Finally, we dot everything together and pack it into a Qobj, # marking the dimensions as that of a type=super (that is, # with left and right compound indices, each representing # left and right indices on the underlying Hilbert space). D = Qobj(np.dot(Z, np.dot(XXdag, Z))) else: D = N * Qobj(XXdag / np.trace(XXdag)) D.dims = [ # Left dims [[N], [N]], # Right dims [[N], [N]], ] # Since [BCSZ08] gives a row-stacking Choi matrix, but QuTiP # expects a column-stacking Choi matrix, we must permute the indices. D = D.permute([[1], [0]]) D.dims = dims # Mark that we've made a Choi matrix. D.superrep = "choi" return sr.to_super(D)
def rand_super_bcsz(N=2, enforce_tp=True, rank=None, dims=None): """ Returns a random superoperator drawn from the Bruzda et al ensemble for CPTP maps [BCSZ08]_. Note that due to finite numerical precision, for ranks less than full-rank, zero eigenvalues may become slightly negative, such that the returned operator is not actually completely positive. Parameters ---------- N : int Square root of the dimension of the superoperator to be returned. enforce_tp : bool If True, the trace-preserving condition of [BCSZ08]_ is enforced; otherwise only complete positivity is enforced. rank : int or 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]]]. Returns ------- rho : Qobj 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]]] if rank is None: rank = N**2 if rank > N**2: raise ValueError("Rank cannot exceed superoperator dimension.") # We use mainly dense matrices here for speed in low # dimensions. In the future, it would likely be better to switch off # between sparse and dense matrices as the dimension grows. # 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') # Precompute X X⁺, as we'll need it in two different places. XXdag = np.dot(X, X.T.conj()) if enforce_tp: # We do the partial trace over the first index by using dense reshape # operations, so that we can avoid bouncing to a sparse representation # and back. Y = np.einsum('ijik->jk', XXdag.reshape((N, N, N, N))) # Now we have the matrix 𝟙 ⊗ Y^{-1/2}, which we can find by doing # the square root and the inverse separately. As a possible improvement, # iterative methods exist to find inverse square root matrices directly, # as this is important in statistics. Z = np.kron( np.eye(N), sqrtm(la.inv(Y)) ) # Finally, we dot everything together and pack it into a Qobj, # marking the dimensions as that of a type=super (that is, # with left and right compound indices, each representing # left and right indices on the underlying Hilbert space). D = Qobj(np.dot(Z, np.dot(XXdag, Z))) else: D = N * Qobj(XXdag / np.trace(XXdag)) D.dims = [ # Left dims [[N], [N]], # Right dims [[N], [N]] ] # Since [BCSZ08] gives a row-stacking Choi matrix, but QuTiP # expects a column-stacking Choi matrix, we must permute the indices. D = D.permute([[1], [0]]) D.dims = dims # Mark that we've made a Choi matrix. D.superrep = 'choi' return sr.to_super(D)