Exemplo n.º 1
0
    def test_sparse_lindblad_bases(self):
        sparsePP = Basis.cast("pp", 16, sparse=True)
        mxs = sparsePP.elements
        for lbl, mx in zip(sparsePP.labels, mxs):
            print("{}: {} matrix with {} nonzero entries (of {} total)".format(
                lbl, mx.shape, mx.nnz, mx.shape[0] * mx.shape[1]))
            print(mx.toarray())
        print("{} basis elements".format(len(sparsePP)))
        self.assertEqual(len(sparsePP), 16)

        # TODO assert correctness

        M = np.ones((16, 16), 'd')
        v = np.ones(16, 'd')
        S = scipy.sparse.identity(16, 'd', 'csr')

        print("Test types after basis change by sparse basis:")
        Mout = bt.change_basis(M, sparsePP, 'std')
        vout = bt.change_basis(v, sparsePP, 'std')
        Sout = bt.change_basis(S, sparsePP, 'std')
        print("{} -> {}".format(type(M), type(Mout)))
        print("{} -> {}".format(type(v), type(vout)))
        print("{} -> {}".format(type(S), type(Sout)))
        self.assertIsInstance(Mout, np.ndarray)
        self.assertIsInstance(vout, np.ndarray)
        self.assertIsInstance(Sout, scipy.sparse.csr_matrix)
Exemplo n.º 2
0
    def test_general(self):
        std = Basis.cast('std', 4)
        std4 = Basis.cast('std', 16)
        std2x2 = Basis.cast([('std', 4), ('std', 4)])
        gm = Basis.cast('gm', 4)

        from_basis, to_basis = bt.build_basis_pair(np.identity(4, 'd'), "std",
                                                   "gm")
        from_basis, to_basis = bt.build_basis_pair(np.identity(4, 'd'), std,
                                                   "gm")
        from_basis, to_basis = bt.build_basis_pair(np.identity(4, 'd'), "std",
                                                   gm)

        mx = np.array([[1, 0, 0, 1], [0, 1, 2, 0], [0, 2, 1, 0], [1, 0, 0, 1]])

        bt.change_basis(mx, 'std', 'gm')  # shortname lookup
        bt.change_basis(mx, std, gm)  # object
        bt.change_basis(mx, std, 'gm')  # combination
        bt.flexible_change_basis(mx, std, gm)  # same dimension
        I2x2 = np.identity(8, 'd')
        I4 = bt.flexible_change_basis(I2x2, std2x2, std4)
        self.assertArraysAlmostEqual(
            bt.flexible_change_basis(I4, std4, std2x2), I2x2)

        with self.assertRaises(AssertionError):
            bt.change_basis(mx, std, std4)  # basis size mismatch

        mxInStdBasis = np.array(
            [[1, 0, 0, 2], [0, 0, 0, 0], [0, 0, 0, 0], [3, 0, 0, 4]], 'd')

        begin = Basis.cast('std', [1, 1])
        end = Basis.cast('std', 4)
        mxInReducedBasis = bt.resize_std_mx(mxInStdBasis, 'contract', end,
                                            begin)
        original = bt.resize_std_mx(mxInReducedBasis, 'expand', begin, end)
Exemplo n.º 3
0
    def to_dense(self, on_space='minimal', scratch=None):
        """
        Return this state vector as a (dense) numpy array.

        The memory in `scratch` maybe used when it is not-None.

        Parameters
        ----------
        on_space : {'minimal', 'Hilbert', 'HilbertSchmidt'}
            The space that the returned dense operation acts upon.  For unitary matrices and bra/ket vectors,
            use `'Hilbert'`.  For superoperator matrices and super-bra/super-ket vectors use `'HilbertSchmidt'`.
            `'minimal'` means that `'Hilbert'` is used if possible given this operator's evolution type, and
            otherwise `'HilbertSchmidt'` is used.

        scratch : numpy.ndarray, optional
            scratch space available for use.

        Returns
        -------
        numpy.ndarray
        """
        assert (on_space in ('minimal', 'HilbertSchmidt'))
        dmVec_std = _ot.state_to_dmvec(
            self.pure_state.to_dense(on_space='Hilbert'))
        return _bt.change_basis(dmVec_std, 'std', self.basis)
Exemplo n.º 4
0
 def create_target_gate(self, v):
     phi, theta = v
     target_unitary = (np.cos(theta / 2) * sigI + 1.j * np.sin(theta / 2) *
                       (np.cos(phi) * sigX + np.sin(phi) * sigY))
     superop = change_basis(np.kron(target_unitary.conj(), target_unitary),
                            'col', 'pp')
     return superop
Exemplo n.º 5
0
def jamiolkowski_iso_inv(choi_mx, choi_mx_basis='pp', op_mx_basis='pp'):
    """
    Given a choi matrix, return the corresponding operation matrix.

    This function performs the inverse of :function:`jamiolkowski_iso`.

    Parameters
    ----------
    choi_mx : numpy array
        the Choi matrix, normalized to have trace == 1, to compute operation matrix for.

    choi_mx_basis : Basis object
        The source and destination basis, respectively.  Allowed
        values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp),
        and Qutrit (qt) (or a custom basis object).

    op_mx_basis : Basis object
        The source and destination basis, respectively.  Allowed
        values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp),
        and Qutrit (qt) (or a custom basis object).

    Returns
    -------
    numpy array
        operation matrix in the desired basis.
    """
    choi_mx = _np.asarray(choi_mx)  # will have "expanded" dimension even if bases are for reduced...
    N = choi_mx.shape[0]  # dimension of full-basis (expanded) operation matrix
    if not isinstance(choi_mx_basis, _Basis):  # if we're not given a basis, build
        choi_mx_basis = _Basis.cast(choi_mx_basis, N)  # one with the full dimension

    dmDim = int(round(_np.sqrt(N)))  # density matrix dimension

    #get full list of basis matrices (in std basis)
    BVec = _bt.basis_matrices(choi_mx_basis.create_simple_equivalent(), N)
    assert(len(BVec) == N)  # make sure the number of basis matrices matches the dim of the choi matrix given

    # Invert normalization
    choiMx_unnorm = choi_mx * dmDim

    opMxInStdBasis = _np.zeros((N, N), 'complex')  # in matrix unit basis of entire density matrix
    for i in range(N):
        for j in range(N):
            BiBj = _np.kron(BVec[i], _np.conjugate(BVec[j]))
            opMxInStdBasis += choiMx_unnorm[i, j] * BiBj

    if not isinstance(op_mx_basis, _Basis):
        op_mx_basis = _Basis.cast(op_mx_basis, N)  # make sure op_mx_basis is a Basis; we'd like dimension to be N

    #project operation matrix so it acts only on the space given by the desired state space blocks
    opMxInStdBasis = _bt.resize_std_mx(opMxInStdBasis, 'contract',
                                       op_mx_basis.create_simple_equivalent('std'),
                                       op_mx_basis.create_equivalent('std'))

    #transform operation matrix into appropriate basis
    return _bt.change_basis(opMxInStdBasis, op_mx_basis.create_equivalent('std'), op_mx_basis)
Exemplo n.º 6
0
 def create_process_matrix(self, v, comm=None, return_generator=False):
     processes = []
     phi, theta, t = v
     theta = theta * t
     target_unitary = (np.cos(theta / 2) * sigI + 1.j * np.sin(theta / 2) *
                       (np.cos(phi) * sigX + np.sin(phi) * sigY))
     superop = change_basis(np.kron(target_unitary.conj(), target_unitary),
                            'col', 'pp')
     processes += [superop]
     return np.array(processes) if (processes is not None) else None
Exemplo n.º 7
0
def create_effect_from_dmvec(superket_vector, effect_type, basis='pp', evotype='default', state_space=None,
                             on_construction_error='warn'):
    effect_type_preferences = (effect_type,) if isinstance(effect_type, str) else effect_type
    if state_space is None:
        state_space = _statespace.default_space_for_dim(len(superket_vector))

    for typ in effect_type_preferences:
        try:
            if typ == "static":
                ef = StaticPOVMEffect(superket_vector, evotype, state_space)
            elif typ == "full":
                ef = FullPOVMEffect(superket_vector, evotype, state_space)
            elif _ot.is_valid_lindblad_paramtype(typ):
                from ..operations import LindbladErrorgen as _LindbladErrorgen, ExpErrorgenOp as _ExpErrorgenOp
                try:
                    dmvec = _bt.change_basis(superket_vector, basis, 'std')
                    purevec = _ot.dmvec_to_state(dmvec)  # raises error if dmvec does not correspond to a pure state
                    static_effect = StaticPOVMPureEffect(purevec, basis, evotype, state_space)
                except ValueError:
                    static_effect = StaticPOVMEffect(superket_vector, evotype, state_space)
                proj_basis = 'PP' if state_space.is_entirely_qubits else basis
                errorgen = _LindbladErrorgen.from_error_generator(state_space.dim, typ, proj_basis,
                                                                  basis, truncate=True, evotype=evotype)
                ef = ComposedPOVMEffect(static_effect, _ExpErrorgenOp(errorgen))
            else:
                # Anything else we try to convert to a pure vector and convert the pure state vector
                dmvec = _bt.change_basis(superket_vector, basis, 'std')
                purevec = _ot.dmvec_to_state(dmvec)  # raises error if dmvec does not correspond to a pure state

                ef = create_effect_from_pure_vector(purevec, typ, basis, evotype, state_space)
            return ef
        except (ValueError, AssertionError) as err:
            if on_construction_error == 'raise':
                raise err
            elif on_construction_error == 'warn':
                print('Failed to construct effect with type "{}" with error: {}'.format(typ, str(err)))
            pass  # move on to next type

    raise ValueError("Could not create an effect of type(s) %s from the given superket vector!" % (str(effect_type)))
Exemplo n.º 8
0
 def create_process_matrices(self, v, grouped_v, comm=None):
     assert (len(grouped_v) == 1)  # we expect a single "grouped" parameter
     processes = []
     times = grouped_v[0]
     phi_in, theta_in = v
     for t in times:
         phi = phi_in
         theta = theta_in * t
         target_unitary = (np.cos(theta / 2) * sigI +
                           1.j * np.sin(theta / 2) *
                           (np.cos(phi) * sigX + np.sin(phi) * sigY))
         superop = change_basis(
             np.kron(target_unitary.conj(), target_unitary), 'col', 'pp')
         processes += [superop]
     return np.array(processes) if (processes is not None) else None
Exemplo n.º 9
0
def fast_jamiolkowski_iso_std(operation_mx, op_mx_basis):
    """
    The corresponding Choi matrix in the standard basis that is normalized to have trace == 1.

    This routine *only* computes the case of the Choi matrix being in the
    standard (matrix unit) basis, but does so more quickly than
    :func:`jamiolkowski_iso` and so is particuarly useful when only the
    eigenvalues of the Choi matrix are needed.

    Parameters
    ----------
    operation_mx : numpy array
        the operation matrix to compute Choi matrix of.

    op_mx_basis : Basis object
        The source and destination basis, respectively.  Allowed
        values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp),
        and Qutrit (qt) (or a custom basis object).

    Returns
    -------
    numpy array
        the Choi matrix, normalized to have trace == 1, in the std basis.
    """

    #first, get operation matrix into std basis
    operation_mx = _np.asarray(operation_mx)
    op_mx_basis = _bt.create_basis_for_matrix(operation_mx, op_mx_basis)
    opMxInStdBasis = _bt.change_basis(operation_mx, op_mx_basis, op_mx_basis.create_equivalent('std'))

    #expand operation matrix so it acts on entire space of dmDim x dmDim density matrices
    opMxInStdBasis = _bt.resize_std_mx(opMxInStdBasis, 'expand', op_mx_basis.create_equivalent('std'),
                                       op_mx_basis.create_simple_equivalent('std'))

    #Shuffle indices to go from process matrix to Jamiolkowski matrix (they vectorize differently)
    N2 = opMxInStdBasis.shape[0]; N = int(_np.sqrt(N2))
    assert(N * N == N2)  # make sure N2 is a perfect square
    Jmx = opMxInStdBasis.reshape((N, N, N, N))
    Jmx = _np.swapaxes(Jmx, 1, 2).flatten()
    Jmx = Jmx.reshape((N2, N2))

    # This construction results in a Jmx with trace == dim(H) = sqrt(gateMxInPauliBasis.shape[0])
    #  but we'd like a Jmx with trace == 1, so normalize:
    Jmx_norm = Jmx / N
    return Jmx_norm
Exemplo n.º 10
0
def create_from_dmvecs(superket_vectors, povm_type, basis='pp', evotype='default', state_space=None,
                       on_construction_error='warn'):
    """ TODO: docstring -- create a POVM from a list/dict of (key, pure-vector) pairs """
    povm_type_preferences = (povm_type,) if isinstance(povm_type, str) else povm_type
    if not isinstance(superket_vectors, dict):  # then assume it's a list of (key, value) pairs
        superket_vectors = _collections.OrderedDict(superket_vectors)

    for typ in povm_type_preferences:
        try:
            if typ in ("full", "static"):
                effects = [(lbl, create_effect_from_dmvec(dmvec, typ, basis, evotype, state_space))
                           for lbl, dmvec in superket_vectors.items()]
                povm = UnconstrainedPOVM(effects, evotype, state_space)
            elif typ == 'full TP':
                effects = [(lbl, create_effect_from_dmvec(dmvec, 'full', basis, evotype, state_space))
                           for lbl, dmvec in superket_vectors.items()]
                povm = TPPOVM(effects, evotype, state_space)
            elif _ot.is_valid_lindblad_paramtype(typ):
                from ..operations import LindbladErrorgen as _LindbladErrorgen, ExpErrorgenOp as _ExpErrorgenOp
                base_povm = create_from_dmvecs(superket_vectors, ('computational', 'static'),
                                               basis, evotype, state_space)

                proj_basis = 'PP' if state_space.is_entirely_qubits else basis
                errorgen = _LindbladErrorgen.from_error_generator(state_space.dim, typ, proj_basis, basis,
                                                                  truncate=True, evotype=evotype,
                                                                  state_space=state_space)
                povm = ComposedPOVM(_ExpErrorgenOp(errorgen), base_povm, mx_basis=basis)
            elif typ in ('computational', 'static pure', 'full pure'):
                # RESHAPE NOTE: .flatten() added to line below (to convert pure *col* vec -> 1D) to fix unit tests
                pure_vectors = {k: _ot.dmvec_to_state(_bt.change_basis(superket, basis, 'std')).flatten()
                                for k, superket in superket_vectors.items()}
                povm = create_from_pure_vectors(pure_vectors, typ, basis, evotype, state_space)
            else:
                raise ValueError("Unknown POVM type '%s'!" % str(typ))

            return povm  # if we get to here, then we've successfully created a state to return
        except (ValueError, AssertionError) as err:
            if on_construction_error == 'raise':
                raise err
            elif on_construction_error == 'warn':
                print('Failed to construct povm with type "{}" with error: {}'.format(typ, str(err)))
            pass  # move on to next type

    raise ValueError("Could not create a POVM of type(s) %s from the given pure vectors!" % (str(povm_type)))
Exemplo n.º 11
0
    def test_unitary_to_pauligate(self):
        theta = np.pi
        sigmax = np.array([[0, 1], [1, 0]])
        ex = 1j * theta * sigmax / 2
        U = scipy.linalg.expm(ex)
        # U is 2x2 unitary matrix operating on single qubit in [0,1] basis (X(pi) rotation)

        op = ot.unitary_to_pauligate(U)
        op_ans = np.array([[1., 0., 0., 0.], [0., 1., 0., 0.],
                           [0., 0., -1., 0.], [0., 0., 0., -1.]], 'd')
        self.assertArraysAlmostEqual(op, op_ans)

        U_2Q = np.identity(4, 'complex')
        U_2Q[2:, 2:] = U
        # U_2Q is 4x4 unitary matrix operating on isolated two-qubit space (CX(pi) rotation)

        op_2Q = ot.unitary_to_pauligate(U_2Q)
        op_2Q_inv = ot.process_mx_to_unitary(
            bt.change_basis(op_2Q, 'pp', 'std'))
        self.assertArraysAlmostEqual(U_2Q, op_2Q_inv)
Exemplo n.º 12
0
def fast_jamiolkowski_iso_std_inv(choi_mx, op_mx_basis):
    """
    Given a choi matrix in the standard basis, return the corresponding operation matrix.

    This function performs the inverse of :function:`fast_jamiolkowski_iso_std`.

    Parameters
    ----------
    choi_mx : numpy array
        the Choi matrix in the standard (matrix units) basis, normalized to
        have trace == 1, to compute operation matrix for.

    op_mx_basis : Basis object
        The source and destination basis, respectively.  Allowed
        values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp),
        and Qutrit (qt) (or a custom basis object).

    Returns
    -------
    numpy array
        operation matrix in the desired basis.
    """

    #Shuffle indices to go from process matrix to Jamiolkowski matrix (they vectorize differently)
    N2 = choi_mx.shape[0]; N = int(_np.sqrt(N2))
    assert(N * N == N2)  # make sure N2 is a perfect square
    opMxInStdBasis = choi_mx.reshape((N, N, N, N)) * N
    opMxInStdBasis = _np.swapaxes(opMxInStdBasis, 1, 2).flatten()
    opMxInStdBasis = opMxInStdBasis.reshape((N2, N2))
    op_mx_basis = _bt.create_basis_for_matrix(opMxInStdBasis, op_mx_basis)

    #project operation matrix so it acts only on the space given by the desired state space blocks
    opMxInStdBasis = _bt.resize_std_mx(opMxInStdBasis, 'contract',
                                       op_mx_basis.create_simple_equivalent('std'),
                                       op_mx_basis.create_equivalent('std'))

    #transform operation matrix into appropriate basis
    return _bt.change_basis(opMxInStdBasis, op_mx_basis.create_equivalent('std'), op_mx_basis)
Exemplo n.º 13
0
    def test_lind_errgen_projects(self):
        mx_basis = Basis.cast('pp', 4)
        basis = Basis.cast('PP', 4)
        X = basis['X']
        Y = basis['Y']
        Z = basis['Z']

        # Build known combination to project back to
        errgen = 0.1 * lt.create_elementary_errorgen('H', Z) \
            - 0.01 * lt.create_elementary_errorgen('H', X) \
            + 0.2 * lt.create_elementary_errorgen('S', X) \
            + 0.25 * lt.create_elementary_errorgen('S', Y) \
            + 0.05 * lt.create_elementary_errorgen('C', X, Y) \
            - 0.01 * lt.create_elementary_errorgen('A', X, Y)
        errgen = bt.change_basis(errgen, 'std', mx_basis)

        Hblk = LindbladCoefficientBlock('ham', basis)
        ODblk = LindbladCoefficientBlock('other_diagonal', basis)
        Oblk = LindbladCoefficientBlock('other', basis)

        Hblk.set_from_errorgen_projections(errgen, errorgen_basis=mx_basis)
        ODblk.set_from_errorgen_projections(errgen, errorgen_basis=mx_basis)
        Oblk.set_from_errorgen_projections(errgen, errorgen_basis=mx_basis)

        self.assertArraysAlmostEqual(Hblk.block_data, [-0.01, 0, 0.1])
        self.assertArraysAlmostEqual(ODblk.block_data, [0.2, 0.25, 0])
        self.assertArraysAlmostEqual(
            Oblk.block_data,
            np.array([[0.2, 0.05 + 0.01j, 0], [0.05 - 0.01j, 0.25, 0],
                      [0, 0, 0]]))

        def dicts_equal(d, f):
            f = {LEEL.cast(k): v for k, v in f.items()}
            if set(d.keys()) != set(f.keys()): return False
            for k in d:
                if abs(d[k] - f[k]) > 1e-12: return False
            return True

        self.assertTrue(
            dicts_equal(Hblk.elementary_errorgens, {
                ('H', 'Z'): 0.1,
                ('H', 'X'): -0.01,
                ('H', 'Y'): 0
            }))
        self.assertTrue(
            dicts_equal(ODblk.elementary_errorgens, {
                ('S', 'X'): 0.2,
                ('S', 'Y'): 0.25,
                ('S', 'Z'): 0
            }))
        self.assertTrue(
            dicts_equal(
                Oblk.elementary_errorgens, {
                    ('S', 'X'): 0.2,
                    ('S', 'Y'): 0.25,
                    ('S', 'Z'): 0.0,
                    ('C', 'X', 'Y'): 0.05,
                    ('A', 'X', 'Y'): -0.01,
                    ('C', 'X', 'Z'): 0,
                    ('A', 'X', 'Z'): 0,
                    ('C', 'Y', 'Z'): 0,
                    ('A', 'Y', 'Z'): 0,
                }))
Exemplo n.º 14
0
def run_process_tomography(state_to_density_matrix_fn,
                           n_qubits=1,
                           comm=None,
                           verbose=False,
                           basis='pp',
                           time_dependent=False,
                           opt_args={}):
    """
    A function to compute the process matrix for a quantum channel given a function
    that maps a pure input state to an output density matrix.

    Args:
        state_to_density_matrix_fn : (function: array -> array)
            The function that computes the output density matrix from an input pure state.

        n_qubits : (int, optional, default 1)
            The number of qubits expected by the function. Defaults to 1.

        comm : (MPI.comm object, optional)
            An MPI communicator object for parallel computation. Defaults to local comm.

        verbose : (bool, optional, default False)
            How much detail to send to stdout

        basis : (str, optional, default 'pp')
            The basis in which to return the process matrix

        time_dependent : (bool, optional, default False )
            If the process is time dependent, then expect the density matrix function to
            return a list of density matrices, one at each time point.

        opt_args : (dict, optional)
            Optional keyword arguments for state_to_density_matrix_fn

    Returns:
        numpy.ndarray
            The process matrix representation of the quantum channel in the basis
            specified by 'basis'. If 'time_dependent'=True, then this will be an array
            of process matrices.
    """
    if comm is not None:
        rank = comm.Get_rank()
        size = comm.Get_size()
    else:
        rank = 0
        size = 1
    if verbose:
        print('Running process tomography as %d of %d on %s.' %
              (comm.Get_rank(), comm.Get_size(), comm.Get_name()))

    # Define and preprocess the input test states
    one_qubit_states = _np.array([[1, 0], [0, 1], [1, 1], [1., 1.j]],
                                 dtype='complex')
    one_qubit_states = [state / _lin.norm(state) for state in one_qubit_states]
    states = _itertools.product(one_qubit_states, repeat=n_qubits)
    states = [multi_kron(*state) for state in states]
    in_density_matrices = [_np.outer(state, state.conj()) for state in states]
    in_states = _np.column_stack(
        list([vec(rho) for rho in in_density_matrices]))
    my_states = split(size, states)[rank]
    if verbose:
        print("Process %d of %d evaluating %d input states." %
              (rank, size, len(my_states)))
    if time_dependent:
        my_out_density_matrices = [
            state_to_density_matrix_fn(state, **opt_args)
            for state in my_states
        ]
    else:
        my_out_density_matrices = [[
            state_to_density_matrix_fn(state, **opt_args)
        ] for state in my_states]

    # Assemble the outputs
    if comm is not None:
        gathered_out_density_matrices = comm.gather(my_out_density_matrices,
                                                    root=0)
    else:
        gathered_out_density_matrices = [my_out_density_matrices]

    if rank == 0:
        # Postprocess the output states to compute the process matrix
        # Flatten over processors
        out_density_matrices = _np.array(
            [y for x in gathered_out_density_matrices for y in x])
        # Sort the list by time
        out_density_matrices = _np.transpose(out_density_matrices,
                                             [1, 0, 2, 3])
        out_states = [
            _np.column_stack(
                list([vec(rho) for rho in density_matrices_at_time]))
            for density_matrices_at_time in out_density_matrices
        ]
        process_matrices = [
            _np.dot(out_states_at_time, _lin.inv(in_states))
            for out_states_at_time in out_states
        ]
        process_matrices = [
            change_basis(process_matrix_at_time, 'col', basis)
            for process_matrix_at_time in process_matrices
        ]

        if not time_dependent:
            return process_matrices[0]
        else:
            return process_matrices
    else:
        # print(f'Rank {rank} returning NONE from comm {comm}.')
        return None
Exemplo n.º 15
0
def jamiolkowski_iso(operation_mx, op_mx_basis='pp', choi_mx_basis='pp'):
    """
    Given a operation matrix, return the corresponding Choi matrix that is normalized to have trace == 1.

    Parameters
    ----------
    operation_mx : numpy array
        the operation matrix to compute Choi matrix of.

    op_mx_basis : Basis object
        The source and destination basis, respectively.  Allowed
        values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp),
        and Qutrit (qt) (or a custom basis object).

    choi_mx_basis : Basis object
        The source and destination basis, respectively.  Allowed
        values are Matrix-unit (std), Gell-Mann (gm), Pauli-product (pp),
        and Qutrit (qt) (or a custom basis object).

    Returns
    -------
    numpy array
        the Choi matrix, normalized to have trace == 1, in the desired basis.
    """
    operation_mx = _np.asarray(operation_mx)
    op_mx_basis = _bt.create_basis_for_matrix(operation_mx, op_mx_basis)
    opMxInStdBasis = _bt.change_basis(operation_mx, op_mx_basis, op_mx_basis.create_equivalent('std'))

    #expand operation matrix so it acts on entire space of dmDim x dmDim density matrices
    #  so that we can take dot products with the BVec matrices below
    opMxInStdBasis = _bt.resize_std_mx(opMxInStdBasis, 'expand', op_mx_basis.create_equivalent(
        'std'), op_mx_basis.create_simple_equivalent('std'))

    N = opMxInStdBasis.shape[0]  # dimension of the full-basis (expanded) gate
    dmDim = int(round(_np.sqrt(N)))  # density matrix dimension

    #Note: we need to use the *full* basis of Matrix Unit, Gell-Mann, or Pauli-product matrices when
    # generating the Choi matrix, even when the original operation matrix doesn't include the entire basis.
    # This is because even when the original operation matrix doesn't include a certain basis element (B0 say),
    # conjugating with this basis element and tracing, i.e. trace(B0^dag * Operation * B0), is not necessarily zero.

    #get full list of basis matrices (in std basis) -- i.e. we use dmDim
    if not isinstance(choi_mx_basis, _Basis):
        choi_mx_basis = _Basis.cast(choi_mx_basis, N)  # we'd like a basis of dimension N

    BVec = choi_mx_basis.create_simple_equivalent().elements
    M = len(BVec)  # can be < N if basis has multiple block dims
    assert(M == N), 'Expected {}, got {}'.format(M, N)

    choiMx = _np.empty((N, N), 'complex')
    for i in range(M):
        for j in range(M):
            BiBj = _np.kron(BVec[i], _np.conjugate(BVec[j]))
            BiBj_dag = _np.transpose(_np.conjugate(BiBj))
            choiMx[i, j] = _mt.trace(_np.dot(opMxInStdBasis, BiBj_dag)) \
                / _mt.trace(_np.dot(BiBj, BiBj_dag))

    # This construction results in a Jmx with trace == dim(H) = sqrt(operation_mx.shape[0])
    #  (dimension of density matrix) but we'd like a Jmx with trace == 1, so normalize:
    choiMx_normalized = choiMx / dmDim
    return choiMx_normalized