def proj_gen(outcome): """ Generates the measurement projector for specified Fock state. """ # input parsing modes = len(outcome) photons = sum(outcome) # dimension dim = dim = comb(modes + photons - 1, photons, exact=True) # compute number states nstates = number_states(modes, photons) # basis set for complete space basis = np.eye(dim) # output projector proj = np.zeros([ dim, ]) # find basis element for i in range(dim): if (outcome == nstates[i, :]).all(): proj = proj + basis[:, i] proj = proj.reshape(dim, 1) return np.kron(proj, dagger(proj))
def M_apply(M, rhob): """ Applies a map M to an input batch of states """ for i in range(np.shape(rhob)[0]): rho = np.asarray(rhob[i, :, :]) rhob[i, :, :] *= 0 for j in range(np.shape(M)[0]): rho += M[j, :, :] @ rho @ dagger(M[j, :, :]) rhob[i, :, :] = rho return rhob
def noon_gen(modes, photons, N=None): """ Generates an NOON state on the last two modes """ # basic input value check if N is None: N = photons else: if N > photons: raise ValueError( "NOON state {} cannot be generated with {} photons".format( N, photons)) if modes <= 2 and N != photons: raise ValueError( "Mode number must be greater than two to support ancilla photons" ) # prelims nstates = number_states(modes, photons) dim = comb(modes + photons - 1, photons, exact=True) basis = np.eye(dim) proj = np.zeros([ dim, ]) # basis states |1001> and |0110> if modes > 2: # gross s1 = [0] * (modes - 2) s2 = [0] * (modes - 2) s1[0] = photons - N s2[0] = photons - N s1.append(N) s1.append(0) s2.append(0) s2.append(N) s1 = np.asarray(s1) s2 = np.asarray(s2) else: s1 = np.asarray([N, 0]) s2 = np.asarray([0, N]) # this is bad even for me for i in range(len(nstates)): if (s1 == nstates[i, :]).all(): proj = proj + basis[:, i] if (s2 == nstates[i, :]).all(): proj = proj + np.exp(1j * np.pi * N) * basis[:, i] proj = proj.reshape(dim, 1) return np.kron(proj, dagger(proj)) / 2
def call(self, input_states): """ Method that applies memristor element to encoded quantum state. Takes as input a single quantum state sequence in density operator form """ # some basic data checking to catch annoying to track errors if len(input_states) != self.tlen: print("Dimension mismatch: Specified time series length does not match data length {}!={}".format(self.tlen, len(input_states))) # iteratively apply memristor for each time interval and produce next output state in sequence for i in range(self.tlen): # get input for time step time_input = np.squeeze(input_states[i,:,:]) # get unitary for this time step unitary = self.unitary_array[i,:,:] # apply unitary to density operator output_state = unitary @ time_input @ dagger(unitary) # apply unitary transform as left hand operator #left = np.einsum('ij,jl->il', unitary, time_input)u # right hand operator #out = np.einsum('il,lj->ij', left, dagger(unitary)) # apply projection operators to input state #compute effect of measurement projector on modes specified by photonic projectors left = np.einsum('ijk,kl->ijl', self.proj, output_state) # can skip adjoint calculation since projectors are all real diagonals (does that seem right?) right = np.einsum('ijl,ilk->ijk', left, self.proj) #TODO # collapse projector outcome sum into single array, taking of batch broadcasting rules out = np.sum(right, axis=0) # add state to output tensor input_states[i,:,:] = out if i+1<self.tlen: # compute the average number of photons in measurement mode self.mem_states[i+1] = np.real(np.sum([np.trace(out @ self.proj[j,:,:])*self.proj_photon[j] for j in range(len(self.proj))])) # now normalise and scale to total number of photons self.mem_states[i+1] *= 2*np.pi/self.photons # build next unitary operator in chain self.update(i+1) #print(self.mem_states[-1]) # return (probably) coherent output state return input_states
def call(self, input_states): """This is where the layer's logic lives. Arguments: inputs: Input tensor, or list/tuple of input tensors. **kwargs: Additional keyword arguments. Returns: A tensor or list/tuple of tensors. """ # apply unitary channel as left operator input_left = np.einsum('ij,bkjl->bkil', self.unitary, input_states) # now as right hand operator and return input_states = np.einsum('bkil,lj->bkij', input_left, dagger(self.unitary)) return input_states
def eigen_encode_map(data, modes, photons, rand_encoding=False, density=True): """ Takes as input an NxMxW data array and applies a quantum encoding in given dimension """ # get shape of array and package as instance, input dimension and time series length N,in_dim,tlen = np.shape(data) # calculate system dimension dim = comb(modes+photons-1, photons, exact=True) # generate state encodings - assume modes and photons are enough encodings = basis_generator(dim) # perform in-place shuffle of encoding states if requested if rand_encoding: np.random.shuffle(encodings) # assert encoding is sufficent assuming binary data assert 2**in_dim<=len(encodings), "Woah hey woah, your system size is not sufficent to encode all states: {}<{}".format(len(encodings), 2**in_dim) # preallocate output encoding array as vector state or density operator if density: data_encode = np.zeros((N,tlen,dim,dim), dtype=np.complex64) else: data_encode = np.zeros((N,tlen,dim), dtype=np.complex64) # no doubt smarter way of computing this but it doesn't matter for the problem sizes we consider for i in range(N): if i % 100==0: print("{}/{} images encoded in quantum state space".format(i,N), end="\r") for j in range(tlen): # get classical state classical_state = data[i,:,j] # compute index of state to map it to ind = int(''.join(str(s) for s in classical_state), 2) # map classical binary data to a quantum state if density: state = encodings[ind,:].reshape([-1,1]) data_encode[i,j,:,:] = np.kron(state, dagger(state)) else: data_encode[i,j,:] = encodings[ind,:] print() return data_encode
def _amplitude_map(self, data, levels=0,): """ Generates a standard amplitude encoding scheme. Hard to prepare in practice but acceptable for prototyping. """ # get shape of array and package as instance, input dimension and time series length N,in_dim,tlen = np.shape(data) # check whether to apply any kind of granulation effect if levels>0: print("Parse number set: applying logistic scaling and granulation") data = self._granulate(data, levels=levels) # preallocate output encoding array as vector state or density operator if self.density: data_encode = np.zeros((N,tlen,self.dim,self.dim), dtype=np.complex64) else: data_encode = np.zeros((N,tlen,self.dim), dtype=np.complex64) # generate computational basis elements basis = np.eye(self.dim, dtype=np.complex128) # convert each data element - likely a smart way to vectorise it but its not worth it here for i in range(N): if i % 100==0: print("{}/{} images encoded in quantum state space using amplitude map scheme".format(i,N), end="\r") # iterate over each column for t in range(tlen): # construct a new state vector state = np.zeros((self.dim,), dtype=np.complex128) # phase encoding to random states state[self.vector_map] = np.exp(1j*data[i,:,t]*2*np.pi) # normalise state vector state /= np.linalg.norm(state, 2) # assign to output set if self.density: state = state.reshape([-1,1]) data_encode[i,t,:,:] = np.kron(state, dagger(state)) else: data_encode[i,t,:] = state print() return data_encode
def Udata_gen(U, num=1000): """ Generates some input/output data for unitary evolution """ # compute dimension of system dim = np.size(U, 1) # generate randomly sampled pure input states psi = haar_sample(dim=dim, num=num, pure=True, operator=True) # preallocate unitary output states phi = np.zeros_like(psi) # compute output states phi subject to U*psi*U' for i in range(num): phi[i, :, :] = U @ psi[i, :, :] @ dagger(U) psi = tf.convert_to_tensor(psi, dtype=tf.complex64, name='psi') phi = tf.convert_to_tensor(phi, dtype=tf.complex64, name='phi') return psi, phi
def ent_gen(dim, vec=False): """ Generates a maximally entangled bi-partite system each of dimension dim """ # pre allocate entangled state array ent = np.zeros((dim**2,1),dtype=np.complex128) # iterate over each basis element for i in range(dim): # generate computaional basis element comput_el = np.zeros((dim, 1), dtype=np.complex128) # assign value comput_el[i] = 1.0 # add to state ent += np.kron(comput_el, comput_el) if vec: return ent else: return np.kron(ent, dagger(ent))/dim
def bell_gen(modes, photons, bell=1): """ Generates a bell state of two photons using the last 4 modes """ assert modes >= 4, "Need at least 4 modes for Bell state generation" assert photons >= 2, "Need at least two photons for Bell state generation" # number of ancilla photons aphotons = photons - 2 # number of ancilla modes amodes = modes - 4 if amodes == 0 and photons > 2: aphotons = 0 photons = 2 print( "Warning: Must have ancilla modes for ancilla photons, truncating") # prelims nstates = number_states(modes, photons) # dimension of space dim = comb(modes + photons - 1, photons, exact=True) # basis set for complete space basis = np.eye(dim) # output projector proj = np.zeros([ dim, ]) # ancilla output state if amodes > 0: aout = [0] * amodes aout[0] = aphotons else: aout = [] # generate psi- if bell == 2: # basis states |1001> and -|0110> s1 = aout + [1, 0, 0, 1] s2 = aout + [0, 1, 1, 0] # this is bad even for me for i in range(dim): if (s1 == nstates[i, :]).all(): proj = proj + basis[:, i] if (s2 == nstates[i, :]).all(): proj = proj - basis[:, i] # generate phi+ elif bell == 3: # states |1010> and |0101> s1 = aout + [1, 0, 1, 0] s2 = aout + [0, 1, 0, 1] for i in range(dim): if (s1 == nstates[i, :]).all(): proj = proj + basis[:, i] if (s2 == nstates[i, :]).all(): proj = proj + basis[:, i] # generate phi- elif bell == 4: # states |1010> and -|0101> s1 = aout + [1, 0, 1, 0] s2 = aout + [0, 1, 0, 1] for i in range(dim): if (s1 == nstates[i, :]).all(): proj = proj + basis[:, i] if (s2 == nstates[i, :]).all(): proj = proj - basis[:, i] # default to psi+ else: # basis states |1001> and |0110> s1 = aout + [1, 0, 0, 1] s2 = aout + [0, 1, 1, 0] for i in range(dim): if (s1 == nstates[i, :]).all(): proj = proj + basis[:, i] if (s2 == nstates[i, :]).all(): proj = proj + basis[:, i] proj = proj.reshape(dim, 1) return np.kron(proj, dagger(proj)) / 2
def bellgen(num=1): """ Generates an optical bell state on 4 modes """ # prelims nstates = number_states(4, 2) dim = comb(4 + 2 - 1, 2, exact=True) basis = np.eye(dim) proj = np.zeros([ dim, ]) # generate psi- if num == 2: # basis states |1001> and -|0110> s1 = np.asarray([1, 0, 0, 1]) s2 = np.asarray([0, 1, 1, 0]) # this is bad even for me for i in range(len(nstates)): if (s1 == nstates[i, :]).all(): proj = proj + basis[:, i] if (s2 == nstates[i, :]).all(): proj = proj - basis[:, i] # generate phi+ elif num == 3: # states |1010> and |0101> s1 = np.asarray([1, 0, 1, 0]) s2 = np.asarray([0, 1, 0, 1]) for i in range(len(nstates)): if (s1 == nstates[i, :]).all(): proj = proj + basis[:, i] if (s2 == nstates[i, :]).all(): proj = proj + basis[:, i] # generate phi- elif num == 4: # states |1010> and -|0101> s1 = np.asarray([1, 0, 1, 0]) s2 = np.asarray([0, 1, 0, 1]) for i in range(len(nstates)): if (s1 == nstates[i, :]).all(): proj = proj + basis[:, i] if (s2 == nstates[i, :]).all(): proj = proj - basis[:, i] # default to psi+ else: # basis states |1001> and |0110> s1 = np.asarray([1, 0, 0, 1]) s2 = np.asarray([0, 1, 1, 0]) for i in range(len(nstates)): if (s1 == nstates[i, :]).all(): proj = proj + basis[:, i] if (s2 == nstates[i, :]).all(): proj = proj + basis[:, i] proj = proj.reshape(dim, 1) return np.kron(proj, dagger(proj)) / 2
def entanglement_gen(dim, num=100, partition=0.5, embed_dim=None): """ Generates a set of training and testing data for bipartite entanglement witnesses. dim gives the dimension of the subsystem and embed dim places the generated states into larger Hilbert space """ # generate entangled state ent_state = ent_gen(dim) # generate base seperable state sep_state = np.ones((dim**2, dim**2),dtype=np.complex128)/dim**2 # check if embedding must occur if embed_dim is not None: assert embed_dim>=dim**2, "embedded dimension is of insufficient size" data_dim = embed_dim else: data_dim = dim**2 # initialise data constructs ent_states = np.zeros((num,1, data_dim, data_dim), dtype=np.complex128) sep_states = np.zeros((num,1, data_dim, data_dim), dtype=np.complex128) # iterate over it all for i in range(num): # generate random local unitaries #U_ent = np.kron(np.squeeze(random_U(dim=dim, num=1)), np.squeeze(random_U(dim=dim, num=1))) U_sep = np.kron(np.squeeze(random_U(dim=dim, num=1)), np.squeeze(random_U(dim=dim, num=1))) # apply to entangled state and add to collection ent_states[i,0,:dim**2,:dim**2] = np.squeeze(haar_sample(dim=dim**2,num=1)) #U_ent @ ent_state @ dagger(U_ent) # apply seperable state and add to collection sep_states[i,0,:dim**2,:dim**2] = U_sep @ sep_state @ dagger(U_sep) # generate labels sep_labels = np.zeros((num, 1), dtype=int) ent_labels = np.ones((num, 1), dtype=int) # concatenate everything entangled_data = np.concatenate([sep_states, ent_states], axis=0) entangled_labels = np.concatenate([sep_labels, ent_labels], axis=0) # shuffle everything using repeateable rng state seed_state = np.random.get_state() np.random.shuffle(entangled_data) np.random.set_state(seed_state) np.random.shuffle(entangled_labels) # apply partition partition_index = int(partition*len(entangled_data)) # training data train_data = entangled_data[:partition_index] train_label = entangled_labels[:partition_index] # testing data test_data = entangled_data[partition_index:] test_label = entangled_labels[partition_index:] # return it all return train_data,train_label,test_data,test_label
def _milano_map(self, data, parse=False, masks=None, input_state=None, base=[0,2*np.pi/3,4*np.pi/3]): """ A highly specific encoding scheme using Milano optical chip - use eigenstate or amplitude encoding unless you know what you are doing! This encoding assumes a constant input state and then configures an optical map that """ # get shape of array and package as instance, input dimension and time series length N,in_dim,tlen = np.shape(data) # granulate data to be accepted to our encoding scheme if parse: print("Parse flag set: applying logistic scaling and granulation") data = self._granulate(data, levels=len(base)) # generate initial input state if not defined (assumes mode and photon numbers) if input_state is None: # 12 modes, 3 photons nstate = [0,0,1,0,0,0,1,0,0,0,1,0] # make use of qfunk optical library input_state = qop.optical_state_gen(m_num=self.modes, p_num=self.photons, nstate=nstate, density=False) # generate topology if not set if self.topology is None: self.topology = self._milano_topology_gen() # generate a set of masks if none are given if masks is None: masks = mask_generator(in_dim, 3) # initialise a dictionary so generated gates don't have to be rebuilt gate_collector = {} # preallocate output encoding array as vector state or density operator if self.density: data_encode = np.empty((N,tlen,self.dim,self.dim), dtype=np.complex128) else: data_encode = np.empty((N,tlen,self.dim), dtype=np.complex128) # iterate over each and every input image for i in range(N): if i % 100==0: print("{}/{} images encoded in quantum state space using Milano scheme".format(i,N), end="\r") # iterate over each column for t in range(tlen): # get the classical data to be encoded classical_state = list(data[i,:,t]) # convert to string description state_string = ''.join(str(s) for s in classical_state) # check if we have already generated the resultant gate if state_string in gate_collector: U = gate_collector[state_string] else: # compute trinary mapping phase_shifters = [base[el] for el in classical_state] # apply weighted mask for each pre-encoder MZI for mask in masks: # compute maximum value possible for mask max_val = np.sum(mask*(len(base)-1)) # compute unscaled phase value for pre-encoder phase_val = (base[-1]-base[0])*mask @ classical_state # add to end of phase shifters and rescale with range phase_shifters.append(phase_val/max_val) # now iterate over topology to generate encoder # compute unitary from values #T(self.targets[0], self.targets[1], theta, theta, self.modes) U = np.eye(self.dim) # save computed unitary for repeated use gate_collector[state_string] = U # apply computed unitary to input state state = U @ input_state if self.density: data_encode[i,t,:,:] = np.kron(state, dagger(state)) else: data_encode[i,t,:] = state return data_encode