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 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 generate_nQubit_Pauli(indices): ''' Generates a tensor product of Pauli operators for n qubits. indices is a list of indices i specifying the Pauli operator for each tensor factor. i=0 is the identity, i=1 is sigma_x, i=2 is sigma_y, and i=3 is sigma_z. ''' Id = eye(2) Sx = np.array([[0, 1], [1, 0]]) Sy = np.array([[0, -1j], [1j, 0]]) Sz = np.array([[1, 0], [0, -1]]) out = 1 for index in indices: if index == 0: out = tensor(out, Id) elif index == 1: out = tensor(out, Sx) elif index == 2: out = tensor(out, Sy) elif index == 3: out = tensor(out, Sz) return out
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 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 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 post_teleportation_chain_fidelity(rho, n, dA=2): ''' Calculates the fidelity of the output state of the teleportation chain channel with respect to the maximally entangled state on AB. The input state rho is of the form rho_{A R11 R12 R21 R22 ... Rn1 Rn2 B}. We assume that A, B, and all R systems have the same dimension. ''' f = 0 indices = list(itertools.product(*[range(dA)] * n)) for z_indices in indices: for x_indices in indices: z_sum = np.mod(sum(z_indices), dA) x_sum = np.mod(sum(x_indices), dA) Bell_tot = Bell_state(dA, z_sum, x_sum, density_matrix=True) for j in range(n): Bell_tot = tensor( Bell_tot, Bell_state(dA, z_indices[j], x_indices[j], density_matrix=True)) f += fidelity(rho, Bell_tot) return f
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 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 post_graph_state_dist_fidelity(A_G,n,rho): ''' Finds the fidelity of the output state of the graph state distribution channel with respect to the graph state |G>, where A_G is the adjacency matrix of the graph G and n is the number of vertices of G. ''' X_n=list(itertools.product(*[range(2)]*n)) f=0 for x_n in X_n: x_n=np.array([x_n]).T # Turn x_n into a column vector matrix z_n=A_G*x_n z_n=np.mod(z_n,2) Bell=Bell_state(2,z_n[0,0],x_n[0,0],density_matrix=True) for k in range(1,n): Bell=tensor(Bell,Bell_state(2,z_n[k,0],x_n[k,0],density_matrix=True)) Bell=syspermute(Bell,list(range(1,2*n,2))+list(range(2,2*n+1,2)),2*np.ones(2*n,dtype=int)) f+=fidelity(rho,Bell) return f
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 S_i(i, n): ''' Generates the matrix for the S gate applied to the ith qubit. n is the total number of qubits. The S gate is defined as: S:=[[1 0], [0 1j]] It is one of the generators of the Clifford group. ''' dims = 2 * np.ones(n) dims = dims.astype(int) indices = np.linspace(1, n, n) indices_diff = np.setdiff1d(indices, i) perm_arrange = np.append(np.array([i]), 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) S = np.array([[1, 0], [0, 1j]]) out_temp = tensor(S, [eye(2), n - 1]) out = syspermute(out_temp, perm_rearrange, dims) return out
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 post_ent_swap_GHZ_fidelity(rho): ''' Finds the fidelity of the output state of the apply_ent_swap_GHZ_channel() function with respect to the three-party GHZ state. ''' Phi = [Bell_state(2, z, 0, density_matrix=True) for z in range(2)] return sum([fidelity(tensor(Phi[z], Phi[z]), rho) for z in range(2)])
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 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 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 mutual_information(rhoAB, dimA, dimB): ''' Computes the mutual information of the bipartite state rhoAB, defined as I(A;B)_rho=D(rhoAB||rhoA\otimes rhoB) ''' rhoA = partial_trace(rhoAB, [2], [dimA, dimB]) rhoB = partial_trace(rhoAB, [1], [dimA, dimB]) return relative_entropy(rhoAB, tensor(rhoA, rhoB))
def generate_nQubit_Pauli_X(indices): ''' Generates a tensor product of Pauli-X operators for n qubits. indices is a list of bits. ''' Id = eye(2) Sx = np.array([[0, 1], [1, 0]]) out = 1 for index in indices: if index == 0: out = tensor(out, Id) elif index == 1: out = tensor(out, Sx) else: return ('Error: Indices must be bits, either 0 or 1!') return out
def apply_ent_swap_GHZ_channel(rho): ''' Applies the channel that takes two copies of a maximally entangled state and outputs a three-party GHZ state. The input state rho is of the form rho_{A R1 R2 B}. A CNOT is applied to R1 and R2, followed by a measurement in the standard basis on R2, followed by a correction operation on B based on the outcome of the measurement. Currently only works for qubits. ''' C=CNOT_ij(2,3,4) X=[matrix_power(discrete_Weyl_X(2),x) for x in range(2)] rho_out=np.sum([tensor(eye(4),dag(ket(2,x)),eye(2))@C@tensor(eye(8),X[x])@rho@tensor(eye(8),X[x])@dag(C)@tensor(eye(4),ket(2,x),eye(2)) for x in range(2)],0) return rho_out
def Natural_representation(K): ''' Calculates the natural representation of the channel (in the standard basis) given by the Kraus operators in K. In terms of the Kraus operators, the natural representation of the channel in the standard basis is given by N=sum_i K_i ⊗ conj(K_i), where the sum is over the Kraus operators K_i in K. ''' return np.sum([tensor(k,np.conjugate(k)) for k in K],0)
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 post_teleportation_fidelity(rho, dA=2): ''' Calculates the fidelity of the output state of the teleportation channel with respect to the maximally entangled state on AB. The input state rho is of the form rho_{AR1R2B}. We assume that A, R1, R2, B all have the same dimension. ''' return sum([ fidelity( rho, tensor(Bell_state(dA, z, x, density_matrix=True), Bell_state(dA, z, x, density_matrix=True))) for z in range(dA) for x in range(dA) ])
def apply_graph_state_dist_channel(A_G,n,rho): ''' Applies the graph state distribution channel to the 2n-partite state rho, where n is the number of vertices in the graph G with adjacency matrix A_G (binary symmetric matrix). rho is a state of the form rho_{A_1...A_n R_1...R_n} The local graph state operations and measurements are applied to the qubits R_1,...,R_n, and the correction operations are applied to A_1,...,A_n. When rho is a state of the form Phi_{A_1 R_1}^+ ⊗ Phi_{A_2 R_2}^+ ⊗ ... ⊗ Phi_{A_n R_n}^+, then the output state on A_1,...,A_n is the graph state |G>. ''' indices=list(itertools.product(*[range(2)]*n)) H=(1/np.sqrt(2))*np.array([[1,1],[1,-1]]) Hn=tensor([H,n]) ket_G=graph_state(A_G,n) rho_out=np.array(np.zeros((2**n,2**n),dtype=complex)) for index in indices: Zx=generate_nQubit_Pauli_Z(index) Gx=Zx*ket_G rho_out=rho_out+tensor(Zx,dag(Gx))@rho@tensor(Zx,Gx) return rho_out
def Petz_Renyi_mut_inf_state(rhoAB, dimA, dimB, alpha, opt=True): ''' Computes the Petz-Renyi mutual information of the bipartite state rhoAB for 0<=alpha<=1. TO DO: Figure out how to do the computation with optimization over sigmaB. ''' rhoA = partial_trace(rhoAB, [2], [dimA, dimB]) rhoB = partial_trace(rhoAB, [1], [dimA, dimB]) if opt == False: return Petz_Renyi_rel_ent(rhoAB, tensor(rhoA, rhoB), alpha) else: return None
def generate_nQudit_X(d,indices): ''' Generates a tensor product of discrete Weyl-X operators. indices is a list of dits (i.e., each element of the list is a number between 0 and d-1). ''' X=discrete_Weyl_X(d) out=1 for index in indices: out=tensor(out,matrix_power(X,index)) return out
def depolarizing_channel_n_uses(p,n,rho,m): ''' Generates the output state corresponding to the depolarizing channel applied to each one of n systems in the joint state rho. p is the depolarizing probability as defined in the function "depolarizing_channel" above. If rho contains m>n systems, then the first m-n systems are left alone. ''' dims=2*np.ones(m).astype(int) rho_out=np.zeros((2**m,2**m)) for k in range(n+1): indices=list(itertools.combinations(range(1,n+1),k)) #print k,indices for index in indices: index=list(index) index=np.array(index)+(m-n) index=list(index.astype(int)) index_diff=np.setdiff1d(range(1,m+1),index) perm_arrange=np.append(index,index_diff).astype(int) perm_rearrange=np.zeros(m) for i in range(m): perm_rearrange[i]=np.argwhere(perm_arrange==i+1)[0][0]+1 perm_rearrange=perm_rearrange.astype(int) mix=matrix_power(eye(2**k)/2,k) rho_part=partial_trace(rho,index,dims) rho_out=rho_out+(4*p/3.)**k*(1-(4*p/3.))**(n-k)*syspermute(tensor(mix,rho_part),perm_rearrange,dims) return rho_out
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
def H_i(i, n): ''' Generates the matrix for the Hadamard gate applied to the ith qubit. n is the total number of qubits. ''' dims = 2 * np.ones(n) dims = dims.astype(int) indices = np.linspace(1, n, n) indices_diff = np.setdiff1d(indices, i) perm_arrange = np.append(np.array([i]), 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) H = (1 / np.sqrt(2)) * np.array([[1, 1], [1, -1]]) out_temp = tensor(H, [eye(2), n - 1]) out = syspermute(out_temp, perm_rearrange, dims) return out
def n_channel_uses(K, n): ''' Given the Kraus operators K of a channel, this function generates the Kraus operators corresponding to the n-fold tensor power of the channel. dimA is the dimension of the input space, and dimB the dimension of the output space. ''' r = len(K) # Number of Kraus operators combs = list(itertools.product(*[range(r)] * n)) K_n = [] for comb in combs: #print comb tmp = 1 for i in range(n): tmp = tensor(tmp, K[comb[i]]) K_n.append(tmp) return K_n