def uccsd_singlet_evolution(packed_amplitudes, n_qubits, n_electrons, fermion_transform=jordan_wigner): """Create a ProjectQ evolution operator for a UCCSD singlet circuit Args: packed_amplitudes(ndarray): Compact array storing the unique single and double excitation amplitudes for a singlet UCCSD operator. The ordering lists unique single excitations before double excitations. n_qubits(int): Number of spin-orbitals used to represent the system, which also corresponds to number of qubits in a non-compact map. n_electrons(int): Number of electrons in the physical system fermion_transform(openfermion.transform): The transformation that defines the mapping from Fermions to QubitOperator. Returns: evoution_operator(TimeEvolution): The unitary operator that constructs the UCCSD singlet state. """ # Build UCCSD generator fermion_generator = uccsd_singlet_generator(packed_amplitudes, n_qubits, n_electrons) evolution_operator = uccsd_evolution(fermion_generator, fermion_transform) return evolution_operator
def build_singlet_uccsd_circuit(parameters, n_mo, n_electrons, transformation): """Constructs the circuit for a singlet UCCSD ansatz with HF initial state. Args: parameters (numpy.ndarray): Vector of coupled-cluster amplitudes n_mo (int): number of molecular orbitals n_electrons (int): number of electrons Returns: qprogram (pyquil.quil.Program): a program for simulating the ansatz """ qprogram = Program() # Set initial state with correct number of electrons for i in range(n_electrons): qubit_index = n_electrons-i-1 qprogram += X(qubit_index) # Build UCCSD generator fermion_generator = uccsd_singlet_generator(parameters, 2*n_mo, n_electrons, anti_hermitian=True) evolution_operator = exponentiate_fermion_operator(fermion_generator, transformation) qprogram += evolution_operator return Circuit(qprogram)
def uccsd_ansatz_circuit(packed_amplitudes, n_orbitals, n_electrons, cq=None): # TODO apply logical re-ordering to Fermionic non-parametric way too! """ This function returns a UCCSD variational ansatz with hard-coded gate angles. The number of orbitals specifies the number of qubits, the number of electrons specifies the initial HF reference state which is assumed was prepared. The packed_amplitudes input defines which gate angles to apply for each CC operation. The list cq is an optional list which specifies the qubit label ordering which is to be assumed. :param list() packed_amplitudes: amplitudes t_ij and t_ijkl of the T_1 and T_2 operators of the UCCSD ansatz :param int n_orbitals: number of *spatial* orbitals :param int n_electrons: number of electrons considered :param list() cq: list of qubit label order :return: circuit which prepares the UCCSD variational ansatz :rtype: Program """ # Fermionic UCCSD uccsd_propagator = normal_ordered( uccsd_singlet_generator(packed_amplitudes, 2 * n_orbitals, n_electrons)) qubit_operator = jordan_wigner(uccsd_propagator) # convert the fermionic propagator to a Pauli spin basis via JW, then convert to a Pyquil compatible PauliSum pyquilpauli = qubitop_to_pyquilpauli(qubit_operator) # re-order logical stuff if necessary if cq is not None: pauli_list = [PauliTerm("I", 0, 0.0)] for term in pyquilpauli.terms: new_term = term # if the QPU has a custom lattice labeling, supplied by user through a list cq, reorder the Pauli labels. if cq is not None: new_term = term.coefficient for pauli in term: new_index = cq[pauli[0]] op = pauli[1] new_term = new_term * PauliTerm(op=op, index=new_index) pauli_list.append(new_term) pyquilpauli = PauliSum(pauli_list) # create a pyquil program which performs the ansatz state preparation with angles unpacked from packed_amplitudes ansatz_prog = Program() # add each term as successive exponentials (1 single Trotter step, not taking into account commutation relations!) for commuting_set in commuting_sets(simplify_pauli_sum(pyquilpauli)): ansatz_prog += exponentiate_commuting_pauli_sum( -1j * PauliSum(commuting_set))(-1.0) return ansatz_prog
def objective_function(self, amps=None): """ This function returns the Hamiltonian expectation value over the final circuit output state. If argument packed_amps is given, the circuit will run with those parameters. Otherwise, the initial angles will be used. :param [list(), numpy.ndarray] amps: list of circuit angles to run the objective function over. :return: energy estimate :rtype: float """ E = 0 t = time.time() if amps is None: packed_amps = self.initial_packed_amps elif isinstance(amps, np.ndarray): packed_amps = amps.tolist() elif isinstance(amps, list): packed_amps = amps else: raise TypeError('Please supply the circuit parameters' ' as a list or np.ndarray') if self.tomography: if (not self.parametric_way) and (self.strategy == 'UCCSD'): # modify hard-coded type ansatz circuit based # on packed_amps angles self.ansatz = uccsd_ansatz_circuit( packed_amps, self.molecule.n_orbitals, self.molecule.n_electrons, cq=self.custom_qubits) if self.experiment_list is None or not self.parametric_way: self.compile_tomo_expts() self.run_experiments(packed_amps) for term in self.pauli_list: key = term.operations_as_set() if len(key) > 0: E += term.coefficient*self.term_es[key] else: E += term.coefficient elif self.method == 'WFS': # In the direct WFS method without tomography, # direct access to wavefunction is allowed and expectation # value is exact each run. if self.parametric_way: E += WavefunctionSimulator().expectation( self.ref_state+self.ansatz, self.pauli_sum, {'theta': packed_amps}).real # attach parametric angles here else: if packed_amps is not None: # modify hard-coded type ansatz circuit # based on packed_amps angles self.ansatz = uccsd_ansatz_circuit( packed_amps, self.molecule.n_orbitals, self.molecule.n_electrons, cq=self.custom_qubits) E += WavefunctionSimulator().expectation( self.ref_state+self.ansatz, self.pauli_sum).real elif self.method == 'Numpy': if self.parametric_way: raise ValueError('NumpyWavefunctionSimulator() backend' ' does not yet support parametric programs.') else: if packed_amps is not None: self.ansatz = uccsd_ansatz_circuit( packed_amps, self.molecule.n_orbitals, self.molecule.n_electrons, cq=self.custom_qubits) E += NumpyWavefunctionSimulator(n_qubits=self.n_qubits).\ do_program(self.ref_state+self.ansatz).\ expectation(self.pauli_sum).real elif self.method == 'linalg': # check if molecule has data sufficient to construct UCCSD ansatz # and propagate starting from HF state if self.molecule is not None: propagator = normal_ordered( uccsd_singlet_generator( packed_amps, 2 * self.molecule.n_orbitals, self.molecule.n_electrons, anti_hermitian=True)) qubit_propagator_matrix = get_sparse_operator( propagator, n_qubits=self.n_qubits) uccsd_state = expm_multiply(qubit_propagator_matrix, self.initial_psi) expected_uccsd_energy = expectation( self.hamiltonian_matrix, uccsd_state).real E += expected_uccsd_energy else: # apparently no molecule was supplied; # attempt to just propagate the ansatz from user-specified # initial state, using a circuit unitary # if supplied by the user, otherwise the initial state itself, # and then estimate over <H> if self.initial_psi is None: raise ValueError('Warning: no initial wavefunction set.' ' Please set using ' 'VQEexperiment().set_initial_state()') # attempt to propagate with a circuit unitary if self.circuit_unitary is None: psi = self.initial_psi else: psi = expm_multiply(self.circuit_unitary, self.initial_psi) E += expectation(self.hamiltonian_matrix, psi).real else: raise ValueError('Impossible method: please choose from method' ' = {WFS, Numpy, linalg} if Tomography is set' ' to False, or choose from method = ' '{QC, WFS, Numpy, linalg} if tomography is set to True') E = E.real if self.verbose: self.it_num += 1 print('black-box function call #' + str(self.it_num)) print('Energy estimate is now: ' + str(E)) print('at angles: ', packed_amps) print('and this took ' + '{0:.3f}'.format(time.time()-t) + \ ' seconds to evaluate') self.history.append(E) return E
def simulate(self, amplitudes): """Perform the simulation for the molecule. Args: amplitudes (list): The initial amplitudes (float64). Returns: float64: The total energy (energy). Raise: ValueError: If the dimension of the amplitude list is incorrect. """ if len(amplitudes) != self.amplitude_dimension: raise ValueError("Incorrect dimension for amplitude list.") #Anti-hermitian operator and its qubit form generator = uccsd_singlet_generator(amplitudes, self.of_mole.n_qubits, self.of_mole.n_electrons) jw_generator = jordan_wigner(generator) pyquil_generator = qubitop_to_pyquilpauli(jw_generator) p = Program(Pragma('INITIAL_REWIRING', ['"GREEDY"'])) # Set initial wavefunction (Hartree-Fock) for i in range(self.of_mole.n_electrons): p.inst(X(i)) # Trotterization (unitary for UCCSD state preparation) for term in pyquil_generator.terms: term.coefficient = np.imag(term.coefficient) p += exponentiate(term) p.wrap_in_numshots_loop(self.backend_options["n_shots"]) # Do not simulate if no operator was passed if len(self.qubit_hamiltonian.terms) == 0: return 0. else: # Run computation using the right backend if isinstance(self.backend_options["backend"], WavefunctionSimulator): energy = self.backend_options["backend"].expectation( prep_prog=p, pauli_terms=self.forest_qubit_hamiltonian) else: # Set up experiment, each setting corresponds to a particular measurement basis settings = [ ExperimentSetting(in_state=TensorProductState(), out_operator=forest_term) for forest_term in self.forest_qubit_hamiltonian.terms ] experiment = TomographyExperiment(settings=settings, program=p) print(experiment, "\n") results = self.backend_options["backend"].experiment( experiment) energy = 0. coefficients = [ forest_term.coefficient for forest_term in self.forest_qubit_hamiltonian.terms ] for i in range(len(results)): energy += results[i].expectation * coefficients[i] energy = np.real(energy) # Save the amplitudes so we have the optimal ones for RDM calculation self.optimized_amplitudes = amplitudes return energy