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 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 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 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 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 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 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 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 generate_all_kets(dims): ''' Generates the tensor-product orthonormal basis corresponding to vector spaces with dimensions in the list dims. ------------------------ Example: generate_all_kets([2,3]) returns a list containing |0,0> |0,1> |0,2> |1,0> |1,1> |1,2> ''' dims_set=[range(d) for d in dims] L=list(itertools.product(*dims_set)) K=[] for l in L: K.append(ket(dims,l)) return K
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 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 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 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 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 generate_state_2design(C,n,display=False): ''' Takes the n-qubit Clifford gates provided in C and returns a corresponding state 2-design. This uses the fact that the Clifford gates (for any n) form a unitary 2-design, and that any unitary t-design can be used to construct a state t-design. ''' def in_list(L,elem): ''' Checks if the given pure state elem is in the list L. ''' x=0 for l in L: if np.around(trace_distance_pure_states(l,elem),10)==0: x=1 break return x S=[ket(2**n,0)] for c in C: s_test=c@ket(2**n,0) if not in_list(S,s_test): S.append(s_test) if display: print(len(S)) return S
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_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 nQubit_quadratures(n): ''' Returns the list of n-qubit "quadrature" operators, which are defined as (for two qubits) S[0]=Sx \otimes Id S[1]=Sz \otimes Id S[2]=Id \otimes Sx S[3]=Id \otimes Sz In general, for n qubits: S[0]=Sx \otimes Id \otimes ... \otimes Id S[1]=Sz \otimes Id \otimes ... \otimes Id S[2]=Id \otimes Sx \otimes ... \otimes Id S[3]=Id \otimes Sz \otimes ... \otimes Id . . . S[2n-2]=Id \otimes Id \otimes ... \otimes Sx S[2n-1]=Id\otimes Id \otimes ... \otimes Sz ''' S = {} #Sx=np.matrix([[0,1],[1,0]]) #Sz=np.matrix([[1,0],[0,-1]]) 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_nQubit_Pauli_X(v) S[i + 1] = generate_nQubit_Pauli_Z(v) count += 1 return S
def su_generators(d): ''' Generates the basis (aka generators) of the Lie algebra su(d) corresponding to the Lie group SU(d). The basis has d^2-1 elements. All of the generators are traceless and Hermitian. After adding the identity matrix, they form an orthogonal basis for all dxd matrices. The orthogonality condition is Tr[S_i*S_j]=d*delta_{i,j} (This is a particular convention we use here; there are other conventions.) For d=2, we get the Pauli matrices. ''' S=[] S.append(eye(d)) for l in range(d): for k in range(l): S.append(np.sqrt(d/2)*(ket(d,k)@dag(ket(d,l))+ket(d,l)@dag(ket(d,k)))) S.append(np.sqrt(d/2)*(-1j*ket(d,k)@dag(ket(d,l))+1j*ket(d,l)@dag(ket(d,k)))) for k in range(1,d): X=0 for j in range(k): X+=ket(d,j)@dag(ket(d,j)) S.append(np.sqrt(d/(k*(k+1)))*(X-k*ket(d,k)@dag(ket(d,k)))) return S