def MaxEnt_state(dim, normalized=True, density_matrix=True): ''' Generates the dim-dimensional maximally entangled state, which is defined as (1/sqrt(dim))*(|0>|0>+|1>|1>+...+|d-1>|d-1>). If normalized=False, then the function returns the unnormalized maximally entangled vector. If density_matrix=True, then the function returns the state as a density matrix. ''' if normalized: Bell = (1. / np.sqrt(dim)) * np.sum( [ket(dim, [i, i]) for i in range(dim)], 0) if density_matrix: return Bell @ dag(Bell) else: return Bell else: Gamma = np.sum([ket(dim, [i, i]) for i in range(dim)], 0) if density_matrix: return Gamma @ dag(Gamma) else: return Gamma
def nQubit_Pauli_coeff(X, n, return_dict=False): ''' Generates the coefficients of the matrix X in the n-qubit Pauli basis. The coefficients c_{alpha} are such that X=(1/2^n)\sum_{alpha} c_alpha \sigma_alpha The coefficients are returned in lexicographical ordering. ''' indices = list(itertools.product(*[range(0, 4)] * n)) if return_dict: C = {} else: C = [] for index in indices: sigma_i = generate_nQubit_Pauli(index) if return_dict: C[index] = Tr(dag(sigma_i) @ X) else: C.append(Tr(dag(sigma_i) @ X)) return C
def generate_channel_isometry(K, dimA, dimB): ''' Generates an isometric extension of the channel specified by the Kraus operators K. dimA is the dimension of the input space of the channel, and dimB is the dimension of the output space of the channel. If dimA=dimB, then the function also outputs a unitary extension of the channel given by a particular construction. ''' dimE = len(K) V = np.sum([tensor(K[i], ket(dimE, i)) for i in range(dimE)], 0) if dimA == dimB: # In this case, the unitary we generate has dimensions dimA*dimE x dimA*dimE U = tensor(V, dag(ket(dimE, 0))) states = [V @ ket(dimA, i) for i in range(dimA)] for i in range(dimA * dimE - dimA): states.append(RandomStateVector(dimA * dimE)) states_new = gram_schmidt(states, dimA * dimE) count = dimA for i in range(dimA): for j in range(1, dimE): U = U + tensor(states_new[count], dag(ket(dimA, i)), dag(ket(dimE, j))) count += 1 return V, np.array(U) else: return V
def coherent_state_fermi(A,rep='JW',density_matrix=False): ''' Generates the fermionic coherent state vector for n modes, where A is a complex anti-symmetric n x n matrix. The matrix A should be at least 2 x 2 -- for one mode, the coherent state is the just the vacuum. The definition being used here comes from A. Perelomov. Generalized Coherent States and Their Applications (Sec. 9.2) ''' n=np.shape(A)[0] # Number of modes a,_=jordan_wigner(n) At=np.zeros((2**n,2**n),dtype=complex) N=np.linalg.det(eye(n)+A@dag(A))**(1/4) for i in range(1,n+1): for j in range(1,n+1): At=At+(-1/2)*A[i-1,j-1]*dag(a[j])@dag(a[i]) vac=tensor([ket(2,0),n]) if not density_matrix: return (1/N)*expm(At)@vac else: coh=(1/N)*expm(At)@vac return coh@dag(coh)
def CZ_ij(i, j, n): ''' CZ gate on qubits i and j, i being the control and j being the target. The total number of qubits is n. (Note that for the CZ gate it does matter which qubit is the control and which qubit is the target.) ''' dims = 2 * np.ones(n) dims = dims.astype(int) indices = np.linspace(1, n, n) indices_diff = np.setdiff1d(indices, [i, j]) perm_arrange = np.append(np.array([i, j]), indices_diff) perm_rearrange = np.zeros(n) for i in range(n): perm_rearrange[i] = np.argwhere(perm_arrange == i + 1)[0][0] + 1 perm_rearrange = perm_rearrange.astype(int) Sz = np.array([[1, 0], [0, -1]]) CZ = tensor(ket(2, 0) @ dag(ket(2, 0)), eye(2)) + tensor( ket(2, 1) @ dag(ket(2, 1)), Sz) out_temp = tensor(CZ, [eye(2), n - 2]) out = syspermute(out_temp, perm_rearrange, dims) return out
def graph_state(A_G,n,density_matrix=False,return_CZ=False,alt=True): ''' Generates the graph state corresponding to the undirected graph G with n vertices. A_G denotes the adjacency matrix of G, which for an undirected graph is a binary symmetric matrix indicating which vertices are connected. See the following book chapter for a review: ``Cluster States'' in Compedium of Quantum Physics, pp. 96-105, by H. J. Briegel. ''' plus=(1/np.sqrt(2))*(ket(2,0)+ket(2,1)) plus_n=tensor([plus,n]) G=eye(2**n) for i in range(n): for j in range(i,n): if A_G[i,j]==1: G=G@CZ_ij(i+1,j+1,n) if density_matrix: plus_n=plus_n@dag(plus_n) if return_CZ: return G@plus_n@dag(G),G else: return G@plus_n@dag(G) else: if return_CZ: return G@plus_n,G else: return G@plus_n
def Kraus_representation(P, dimA, dimB): ''' Takes a Choi representation P of a channel and returns its Kraus representation. The Choi representation is defined with the channel acting on the second half of the maximally entangled vector. ''' D, U = eig(P) U_cols = U.shape[1] # Need to check if the matrix U generated by eig is unitary (up to numerical precision) check1 = np.allclose(eye(dimA * dimB), U @ dag(U)) check2 = np.allclose(eye(dimA * dimB), dag(U) @ U) if check1 and check2: U = np.array(U) # If U is not unitary, use Gram-Schmidt to make it unitary (i.e., make the columns of U orthonormal) else: C = gram_schmidt([U[:, i] for i in range(U_cols)], dimA * dimB) U = np.sum([tensor(dag(ket(U_cols, i)), C[i]) for i in range(U_cols)], 0) #print(U) K = [] for i in range(U_cols): Col = U[:, i] K_tmp = np.array(np.sqrt(D[i]) * Col.reshape([dimA, dimB])) K.append(K_tmp.transpose()) return K
def jordan_wigner(n): ''' Generates the Jordan-Wigner representation of the fermionic creation, annihilation, and Majorana operators for an n-mode system. The convention for the Majorana operators is as follows: c_j=aj^{dag}+aj c_{n+j}=i(aj^{dag}-aj) ''' s = ket(2, 0) @ dag(ket(2, 1)) S = su_generators(2) a = {} # Dictionary for the annihilation operators c = {} # Dictionary for the Majorana operators for j in range(1, n + 1): a[j] = tensor([S[3], j - 1], s, [S[0], n - j]) c[j] = dag(a[j]) + a[j] c[n + j] = 1j * (dag(a[j]) - a[j]) return a, c
def Clifford_twirl_channel_one_qubit(K, rho, sys=1, dim=[2]): ''' Twirls the given channel with Kraus operators in K by the one-qubit Clifford group on the given subsystem (specified by sys). ''' n = int(np.log2(np.sum([d for d in dim]))) C1 = eye(2**n) C2 = Rx_i(sys, np.pi, n) C3 = Rx_i(sys, np.pi / 2., n) C4 = Rx_i(sys, -np.pi / 2., n) C5 = Rz_i(sys, np.pi, n) C6 = Rx_i(sys, np.pi, n) * Rz_i(sys, np.pi, n) C7 = Rx_i(sys, np.pi / 2., n) * Rz_i(sys, np.pi, n) C6 = Rx_i(sys, np.pi, n) * Rz_i(sys, np.pi, n) C8 = Rx_i(sys, -np.pi / 2., n) * Rz_i(sys, np.pi, n) C9 = Rz_i(sys, np.pi / 2., n) C10 = Ry_i(sys, np.pi, n) * Rz_i(sys, np.pi / 2., n) C11 = Ry_i(sys, -np.pi / 2., n) * Rz_i(sys, np.pi / 2., n) C12 = Ry_i(sys, np.pi / 2., n) * Rz_i(sys, np.pi / 2., n) C13 = Rz_i(sys, -np.pi / 2., n) C14 = Ry_i(sys, np.pi, n) * Rz_i(sys, -np.pi / 2., n) C15 = Ry_i(sys, -np.pi / 2., n) * Rz_i(sys, -np.pi / 2., n) C16 = Ry_i(sys, np.pi / 2., n) * Rz_i(sys, -np.pi / 2., n) C17 = Rz_i(sys, -np.pi / 2., n) * Rx_i(sys, np.pi / 2., n) * Rz_i( sys, np.pi / 2., n) C18 = Rz_i(sys, np.pi / 2., n) * Rx_i(sys, np.pi / 2., n) * Rz_i( sys, np.pi / 2., n) C19 = Rz_i(sys, np.pi, n) * Rx_i(sys, np.pi / 2., n) * Rz_i( sys, np.pi / 2., n) C20 = Rx_i(sys, np.pi / 2., n) * Rz_i(sys, np.pi / 2., n) C21 = Rz_i(sys, np.pi / 2., n) * Rx_i(sys, -np.pi / 2., n) * Rz_i( sys, np.pi / 2., n) C22 = Rz_i(sys, -np.pi / 2., n) * Rx_i(sys, -np.pi / 2., n) * Rz_i( sys, np.pi / 2., n) C23 = Rx_i(sys, -np.pi / 2., n) * Rz_i(sys, np.pi / 2., n) C24 = Rx_i(sys, np.pi, n) * Rx_i(sys, -np.pi / 2., n) * Rz_i( sys, np.pi / 2., n) C = [ C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C13, C14, C15, C16, C17, C18, C19, C20, C21, C22, C23, C24 ] rho_twirl = 0 for i in range(len(C)): rho_twirl += (1. / 24.) * C[i] @ apply_channel(K, dag(C[i]) @ rho @ C[i], sys, dim) @ dag(C[i]) return rho_twirl, C
def discrete_Weyl_X(d): ''' Generates the X shift operators. ''' X=ket(d,1)@dag(ket(d,0)) for i in range(1,d): X=X+ket(d,(i+1)%d)@dag(ket(d,i)) return X
def discrete_Weyl_Z(d): ''' Generates the Z phase operators. ''' w = np.exp(2 * np.pi * 1j / d) Z = ket(d, 0) @ dag(ket(d, 0)) for i in range(1, d): Z = Z + w**i * ket(d, i) @ dag(ket(d, i)) return Z
def trace_distance_pure_states(psi, phi): ''' Computes the squared trace distance between two pure states psi and phi, i.e., || |psi><psi|-|phi><phi| ||_1^2 ''' if psi.shape[1] == 1: psi = psi @ dag(psi) if phi.shape[1] == 1: phi = phi @ dag(phi) return 1 - Tr(psi * phi)
def RandomStateVector(dim, rank=None): ''' Generates a random pure state. For multipartite states, dim should be a list of dimensions for each subsystem. In this case, the rank variable is for the Schmidt rank. To specify the Schmidt rank, there has to be a bipartition of the systems, so that dim has only two elements. ''' if rank == None: if type(dim) == list: dim = np.prod(dim) # Generate the real and imaginary parts of the components using numbers # sampled from the standard normal distribution (normal distribution with # mean zero and variance 1). psi = dag( np.array([np.random.randn(dim)]) + 1j * np.array([np.random.randn(dim)])) psi = psi / norm(psi) return psi else: dimA = dim[0] dimB = dim[1] if rank == None: rank = max([dimA, dimB]) else: k = rank psi_k = MaxEnt_state(k, density_matrix=False, normalized=False) a = dag( np.array([np.random.rand(dimA * k)]) + 1j * np.array([np.random.rand(dimA * k)])) b = dag( np.array([np.random.rand(dimB * k)]) + 1j * np.array([np.random.rand(dimB * k)])) psi_init = syspermute(tensor(a, b), [1, 3, 2, 4], [k, dimA, k, dimB]) psi = tensor(dag(psi_k), eye(dimA * dimB)) @ psi_init psi = psi / norm(psi) return psi
def nQudit_quadratures(d,n): ''' Returns the list of n-qudit "quadrature" operators, which are defined as (for two qudits) S[0]=X(0) ⊗ Id S[1]=Z(0) ⊗ Id S[2]=Id ⊗ X(0) S[3]=Id ⊗ Z(0) In general, for n qubits: S[0]=X(0) ⊗ Id ⊗ ... ⊗ Id S[1]=Z(0) ⊗ Id ⊗ ... ⊗ Id S[2]=Id ⊗ X(0) ⊗ ... ⊗ Id S[3]=Id ⊗ Z(0) ⊗ ... ⊗ Id . . . S[2n-2]=Id ⊗ Id ⊗ ... ⊗ X(0) S[2n-1]=Id ⊗ Id ⊗ ... ⊗ Z(0) ''' S={} count=0 for i in range(1,2*n+1,2): v=list(np.array(dag(ket(n,count)),dtype=np.int).flatten()) S[i]=generate_nQudit_X(d,v) S[i+1]=generate_nQudit_Z(d,v) count+=1 return S
def apply_teleportation_channel(rho,dA=2,dR1=2,dR2=2,dB=2): ''' Applies the d-dimensional teleportation channel to the four-qudit state rho_{AR1R2B}. The channel measures R1 and R2 in the d-dimensional Bell basis and, based on the outcome, applies a 'correction operation' to B. So the output of the channel consists only of the systems A and B. We obtain quantum teleportation by letting rho_{AR1R2B} = psi_{R1} ⊗ Phi_{R2B}^+, so that dA=1. This simulates teleportation of the state psi in the system R1 to the system B. We obtain entanglement swapping by letting rho_{AR1R2B} = Phi_{AR1}^+ ⊗ Phi_{R2B}^+. The result of the channel is then Phi_{AB}^+ ''' X=[matrix_power(discrete_Weyl_X(dB),x) for x in range(dB)] Z=[matrix_power(discrete_Weyl_Z(dB),z) for z in range(dB)] rho_out=np.sum([tensor(eye(dA),dag(Bell_state(dR1,z,x)),Z[z]@X[x])@rho@tensor(eye(dA),Bell_state(dR1,z,x),dag(X[x])@dag(Z[z])) for z in range(dB) for x in range(dB)],0) return rho_out
def partial_trace(X,sys,dim): ''' sys is a list of systems over which to take the partial trace (i.e., the systems to discard). Example: If rho_AB is a bipartite state with dimA the dimension of system A and dimB the dimension of system B, then partial_trace(rho_AB,[2],[dimA,dimB]) gives the density matrix on system A, i.e., rho_A:=partial_trace[rho_AB]. Similarly, partial_trace(rho_AB,[1],[dimA,dimB]) discards the first subsystem, returning the density matrix of system B. If rho_ABC is a tripartite state, then, e.g., partial_trace(rho_ABC,[1,3],[dimA,dimB,dimC]) discards the first and third subsystems, so that we obtain the density matrix for system B. ''' if isinstance(X,cvxpy.Variable): X=cvxpy_to_numpy(X) X_out=partial_trace(X,sys,dim) return numpy_to_cvxpy(X_out) if not sys: # If sys is empty, just return the original operator return X elif len(sys)==len(dim): # If tracing over all systems return Tr(X) else: if X.shape[1]==1: X=X@dag(X) num_sys=len(dim) total_sys=range(1,num_sys+1) dims_sys=[dim[s-1] for s in sys] # Dimensions of the system to be traced over dims_keep=[dim[s-1] for s in list(set(total_sys)-set(sys))] dim_sys=np.product(dims_sys) dim_keep=np.product(dims_keep) perm=sys+list(set(total_sys)-set(sys)) X=syspermute(X,perm,dim) X=np.array(X) dim=[dim_sys]+dims_keep X_reshape=np.reshape(X,dim+dim) X_reshape=np.sum(np.diagonal(X_reshape,axis1=0,axis2=len(dim)),axis=-1) X=np.reshape(X_reshape,(dim_keep,dim_keep)) return X
def apply_teleportation_chain_channel(rho, n, dA=2, dR=2, dB=2): ''' Applies the teleportation chain channel to the state rho, which is of the form rho_{A R11 R12 R21 R22 ... Rn1 Rn2 B}. The channel is defined by performing a d-dimensional Bell basis measurement independently on the system pairs Ri1 and Ri2, for 1 <= i <= n; based on the outcome, a 'correction operation' is applied to B. The system pairs Ri1 and Ri2 can be thought of as 'repeaters'. Note that n>=1. For n=1, we get the same channel as in apply_teleportation_channel(). We obtain teleportation by letting dA=1 and letting rho_{A R11 R12 R21 R22 ... Rn1 Rn2 B} = psi_{R11} ⊗ Phi_{R12 R21}^+ ⊗ ... ⊗ Phi_{Rn2 B}^+, so that we have teleportation of the state psi in the system R11 to the system B. We obtain a chain of entanglement swaps by letting rho_{A R11 R12 R21 R22 ... Rn1 Rn2 B} = Phi_{A R11}^+ ⊗ Phi_{R12 R21}^+ ⊗ ... ⊗ Phi_{Rn2 B}^+. ''' indices = list(itertools.product(*[range(dB)] * n)) rho_out = np.array(np.zeros((dA * dB, dA * dB), dtype=complex)) for z_indices in indices: for x_indices in indices: Bell_zx = Bell_state(dB, z_indices[0], x_indices[0]) for j in range(1, n): Bell_zx = tensor(Bell_zx, Bell_state(dB, z_indices[j], x_indices[j])) z_sum = np.mod(sum(z_indices), dB) x_sum = np.mod(sum(x_indices), dB) W_zx = matrix_power(discrete_Weyl_Z(dB), z_sum) @ matrix_power( discrete_Weyl_X(dB), x_sum) rho_out = rho_out + tensor(eye(dA), dag( Bell_zx), W_zx) @ rho @ tensor(eye(dA), Bell_zx, dag(W_zx)) return rho_out
def entanglement_distillation(rho1, rho2, outcome=1, twirl_after=False, normalize=False): ''' Applies a particular entanglement distillation channel to the two two-qubit states rho1 and rho2. [PRL 76, 722 (1996)] The channel is probabilistic. If the variable outcome=1, then the function returns the two-qubit state conditioned on the success of the distillation protocol. ''' CNOT = CNOT_ij(1, 2, 2) proj0 = ket(2, 0) @ dag(ket(2, 0)) proj1 = ket(2, 1) @ dag(ket(2, 1)) P0 = tensor(eye(2), proj0, eye(2), proj0) P1 = tensor(eye(2), proj1, eye(2), proj1) P2 = eye(16) - P0 - P1 C = tensor(CNOT, CNOT) K0 = P0 * C K1 = P1 * C K2 = P2 * C rho_in = syspermute(tensor(rho1, rho2), [1, 3, 2, 4], [2, 2, 2, 2]) # rho_in==rho_{A1A2B1B2} if outcome == 1: # rho_out is unnormalized. The trace of rho_out is equal to the success probability. rho_out = partial_trace(K0 @ rho_in @ dag(K0) + K1 @ rho_in @ dag(K1), [2, 4], [2, 2, 2, 2]) if twirl_after: rho_out = isotropic_twirl_state(rho_out, 2) if normalize: rho_out = rho_out / Tr(rho_out) elif outcome == 0: # rho_out is unnormalized. The trace of rho_out is equal to the failure probability. rho_out = partial_trace(K2 @ rho_in @ dag(K2), [2, 4], [2, 2, 2, 2]) if normalize: rho_out = rho_out / Tr(rho_out) return rho_out
def objfunc(x): Re = np.array(x[0:dim**3]) Im = np.array(x[dim**3:]) psi = np.array([Re + 1j * Im]).T psi = psi / norm(psi) p = [] S = [] for j in range(dim**2): R = tensor(dag(ket(dim**2, j)), eye(dim)) @ ( psi @ dag(psi)) @ tensor(ket(dim**2, j), eye(dim)) p.append(Tr(R)) rho = R / Tr(R) rho_out = apply_channel(K, rho) S.append(rho_out) return -np.real(Holevo_inf_ensemble(p, S))
def objfunc(x): Re = np.array(x[0:dim]) Im = np.array(x[dim:]) psi = np.array([Re + 1j * Im]).T psi = psi / norm(psi) rho = psi @ dag(psi) rho_out = apply_channel(K, rho) return entropy(rho_out)
def K(j, x): # j is between 1 and n, denoting the pair of R systems. x is either 0 or 1. # For each j, the qubit indices are 2*j and 2*j+1 for the pair Rj1 and Rj2 Mx = tensor(eye(2), eye(2**(2 * j - 2)), eye(2), ket(2, x) @ dag(ket(2, x)), eye(2**(2 * (n - j))), eye(2)) C = CNOT_ij(2 * j, 2 * j + 1, 2 * n + 2) X = 1j * Rx_i(2 * j + 2, np.pi, 2 * n + 2) return Mx @ C @ matrix_power(X, x)
def avg_fidelity_qubit(K): ''' K is the set of Kraus operators for the (qubit to qubit) channel whose average fidelity is to be found. ''' ket0 = ket(2, 0) ket1 = ket(2, 1) ket_plus = (1. / np.sqrt(2)) * (ket0 + ket1) ket_minus = (1. / np.sqrt(2)) * (ket0 - ket1) ket_plusi = (1. / np.sqrt(2)) * (ket0 + 1j * ket1) ket_minusi = (1. / np.sqrt(2)) * (ket0 - 1j * ket1) states = [ket0, ket1, ket_plus, ket_minus, ket_plusi, ket_minusi] F = 0 for state in states: F += np.real( Tr((state @ dag(state)) * apply_channel(K, state @ dag(state)))) return (1. / 6.) * F
def unitary_distance(U, V): ''' Checks whether two unitaries U and V are the same (taking into account global phase) by using the distance measure: 1-(1/d)*|Tr[UV^†]|, where d is the dimension of the space on which the unitaries act. U and V are the same if and only if this is equal to zero; otherwise, it is greater than zero. ''' d = U.shape[0] return 1 - (1 / d) * np.abs(Tr(U @ dag(V)))
def GHZ_state(dim, n, density_matrix=True): ''' Generates the n-party GHZ state in dim-dimensions for each party, which is defined as |GHZ_n> = (1/sqrt(dim))*(|0,0,...,0> + |1,1,...,1> + ... + |d-1,d-1,...,d-1>) If density_matrix=True, then the function returns the state as a density matrix. ''' GHZ = (1 / np.sqrt(dim)) * np.sum([ket(dim, [i] * n) for i in range(dim)], 0) if density_matrix: return GHZ @ dag(GHZ) else: return GHZ
def apply_ent_swap_GHZ_chain_channel(rho, n): ''' Applies the channel that takes n+1 copies of a maximally entangled state and outputs a (n+2)-party GHZ state. The input state rho is of the form rho_{A R11 R12 R21 R22 ... Rn1 Rn2 B} A CNOT is applies to each pair Rj1 Rj2. Then, the qubits Rj2 are measured in the standard basis. Conditioned on these outcomes, a correction operation is applied at B. Currently only works for qubits. For n=1, we get the same thing as apply_ent_swap_GHZ_channel(). ''' def K(j, x): # j is between 1 and n, denoting the pair of R systems. x is either 0 or 1. # For each j, the qubit indices are 2*j and 2*j+1 for the pair Rj1 and Rj2 Mx = tensor(eye(2), eye(2**(2 * j - 2)), eye(2), ket(2, x) @ dag(ket(2, x)), eye(2**(2 * (n - j))), eye(2)) C = CNOT_ij(2 * j, 2 * j + 1, 2 * n + 2) X = 1j * Rx_i(2 * j + 2, np.pi, 2 * n + 2) return Mx @ C @ matrix_power(X, x) indices = list(itertools.product(*[range(2)] * n)) rho_out = np.array( np.zeros((2**(2 * n + 2), 2**(2 * n + 2)), dtype=complex)) for index in indices: index = list(index) L = K(1, index[0]) for j in range(2, n + 1): L = K(j, index[j - 1]) @ L rho_out = rho_out + L @ rho @ dag(L) rho_out = partial_trace(rho_out, [2 * j + 1 for j in range(1, n + 1)], [2] * (2 * n + 2)) return rho_out
def nQudit_Weyl_coeff(X, d, n): ''' Generates the coefficients of the operator X acting on n qudit systems. ''' C = {} S = list(itertools.product(*[range(0, d)] * n)) for s in S: s = list(s) for t in S: t = list(t) G = generate_nQudit_X(d, s) @ generate_nQudit_Z(d, t) C[(str(s), str(t))] = np.around(Tr(dag(X) @ G), 10) return C
def diamond_norm(J, dimA, dimB, display=False): ''' Computes the diamond norm of a superoperator with Choi representation J. dimA is the dimension of the input space of the channel, and dimB is the dimension of the output space. The form of the SDP used comes from Theorem 3.1 of: 'Simpler semidefinite programs for completely bounded norms', Chicago Journal of Theoretical Computer Science 2013, by John Watrous ''' ''' The Choi representation J in the above paper is defined using a different convention: J=(N\otimes I)(|Phi^+><Phi^+|). In other words, the channel N acts on the first half of the maximally- entangled state, while the convention used throughout this code stack is J=(I\otimes N)(|Phi^+><Phi^+|). We thus use syspermute to convert to the form used in the aforementioned paper. ''' J = syspermute(J, [2, 1], [dimA, dimB]) X = cvx.Variable((dimA * dimB, dimA * dimB), hermitian=False) rho0 = cvx.Variable((dimA, dimA), PSD=True) rho1 = cvx.Variable((dimA, dimA), PSD=True) M = cvx.bmat([[cvx.kron(eye(dimB), rho0), X], [X.H, cvx.kron(eye(dimB), rho1)]]) c = [] c += [M >> 0, cvx.trace(rho0) == 1, cvx.trace(rho1) == 1] obj = cvx.Maximize((1 / 2) * cvx.real(cvx.trace(dag(J) @ X)) + (1 / 2) * cvx.real(cvx.trace(J @ X.H))) prob = cvx.Problem(obj, constraints=c) prob.solve(verbose=display, eps=1e-7) return prob.value
def Bell_state(d, z, x, density_matrix=False): ''' Generates a d-dimensional Bell state with 0 <= z,x <= d-1. These are defined as |Phi_{z,x}> = (Z(z)X(x) ⊗ I)|Phi^+> ''' Bell = MaxEnt_state(d, density_matrix=density_matrix) W_zx = matrix_power(discrete_Weyl_Z(d), z) @ matrix_power( discrete_Weyl_X(d), x) if density_matrix: out = tensor(W_zx, eye(d)) @ Bell @ tensor(dag(W_zx), eye(d)) return out else: out = tensor(W_zx, eye(d)) @ Bell return out
def permute_tensor_factors(perm, dims): ''' Generates the permutation operator that permutes the tensor factors according to the given permutation. perm is a list containing the desired order, and dim is a list of the dimensions of all tensor factors. ''' K = generate_all_kets(dims) dim = np.prod(dims) W = np.zeros((dim, dim), dtype=complex) for ket in K: W = W + syspermute(ket, perm, dims) @ dag(ket) return W
def SWAP(sys, dim): ''' Generates a swap matrix between the pair of systems in sys. dim is a list of the dimensions of the subsystems. For example, SWAP([1,2],[2,2]) generates the two-qubit swap matrix. ''' dim_total = np.product(dim) n = len(dim) sys_rest = list(np.setdiff1d(range(1, n + 1), sys)) perm = sys + sys_rest p = {} for i in range(1, n + 1): p[i] = perm[i - 1] p2 = {v: k for k, v in p.items()} perm_rearrange = list(p2.values()) dim1 = dim[sys[0] - 1] # Dimension of the first subsystem to be swapped dim2 = dim[sys[1] - 1] # Dimension of the second subsystem to be swapped dim_rest = int(float(dim_total) / float(dim1 * dim2)) G1 = np.matrix(np.sum([ket(dim1, [i, i]) for i in range(dim1)], 0)) G2 = np.matrix(np.sum([ket(dim2, [i, i]) for i in range(dim2)], 0)) G = G1 @ dag(G2) S = partial_transpose(G, [2], [(dim1, dim2), (dim1, dim2)]) P = tensor(S, eye(dim_rest)) p_alt = list(np.array(list(p.values())) - 1) P = syspermute(P, perm_rearrange, list(np.array(dim)[p_alt])) return P