Example #1
0
def test_uccsd_ansatz_circuit(sample_molecule, h2_programs):

    n_spatial_orbitals = sample_molecule.n_orbitals
    n_occupied = int(np_ceil(sample_molecule.n_electrons / 2))
    n_virtual = n_spatial_orbitals - n_occupied
    n_single_amplitudes = n_occupied * n_virtual
    # make a mock-packed_amplitudes of length (2 * n_single_amplitudes)
    packed_amplitudes = [1]*(2 * n_single_amplitudes + int(comb(n_single_amplitudes, 2)))

    ansatz = uccsd_ansatz_circuit(packed_amplitudes, sample_molecule.n_orbitals,
                                  sample_molecule.n_electrons)

    assert isinstance(ansatz, Program)
    assert ansatz.out() == h2_programs.out()
Example #2
0
    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
Example #3
0
    def __init__(self, qc: Union[QuantumComputer, None] = None,
            hamiltonian: Union[PauliSum, List[PauliTerm], None] = None,
            molecule: MolecularData = None,
            method: str = 'Numpy',
            strategy: str = 'UCCSD',
            optimizer: str = 'BFGS',
            maxiter: int = 100000,
            shotN: int = 10000,
            active_reset: bool = True,
            tomography: bool = False,
            verbose: bool = False,
            parametric: bool = False,
            custom_qubits = None):
        """

        VQE experiment class.
        Initialize an instance of this class to prepare a VQE experiment.
        One may instantiate this class either based on
        an OpenFermion MolecularData object
        (containing a chemistry problem Hamiltonian)
        or manually suggest a Hamiltonian.

        The VQE can run circuits on different virtual or real backends:
        currently, we support the Rigetti QPU backend, locally running QVM,
        a WavefunctionSimulator, a NumpyWavefunctionSimulator.
        Alternatively, one may run the VQE ansatz unitary directly
        (not decomposed as a circuit)
        via direct exponentiation of the unitary ansatz,
        with the 'linalg' method.
        The different backends do not all support parametric gates (yet),
        and the user can specify whether or not to use it.

        Currently, we support two built-in ansatz strategies
        and the option of setting your own ansatz circuit.
        The built-in UCCSD and HF strategies are based on data
        from MolecularData object and thus require one.
        For finding the groundstate of a custom Hamiltonian,
        it is required to manually set an ansatz strategy.

        Currently, the only classical optimizer for the VQE
        is the scipy.optimize.minimize module.
        This may be straightforwardly extended in future releases,
        contributions are welcome.
        This class can be initialized with any algorithm in the scipy class,
        and the max number of iterations can be specified.

        For some QuantumComputer objects,
        the qubit lattice is not numbered 0..N-1
        but has architecture-specific logical labels.
        These need to be manually read from the lattice topology
        and specified in the list custom_qubits.
        On the physical hardware QPU,
        actively resetting the qubits is supported
        to speed up the repetition time of VQE.

        To debug and during development, set verbose=True
        to print output details to the console.

        :param [QuantumComputer(),None] qc:  object
        :param [PauliSum, list(PauliTerm)] hamiltonian:
                Hamiltonian which one would like to simulate
        :param MolecularData molecule: OpenFermion Molecule data object.
                If this is given, the VQE module assumes a
                chemistry experiment using OpenFermion
        :param str method: string describing the Backend solver method.
                current options: {Numpy, WFS, linalg, QC}
        :param str strategy: string describing circuit VQE strategy.
                current options: {UCCSD, HF, custom_program}
        :param str optimizer: classical optimization algorithm,
                choose from scipy.optimize.minimize options
        :param int maxiter: max number of iterations
        :param int shotN: number of shots in the Tomography experiments
        :param bool active_reset:  whether or not to actively reset the qubits
        :param bool tomography: set to False for access to full wavefunction,
                set to True for just sampling from it
        :param bool verbose: set to True for verbose output to the console,
                for all methods in this class
        :param bool parametric: set to True to use parametric gate compilation,
                False to compile a new circuit for every iteration
        :param list() custom_qubits: list of qubits, i.e. [7,0,1,2] ordering
                the qubit IDs as they appear on the QPU
                lattice of the QuantumComputer() object.

        """

        if isinstance(hamiltonian, PauliSum):
            if molecule is not None:
                raise TypeError('Please supply either a Hamiltonian object'
                        ' or a Molecule object, but not both.')
            # Hamiltonian as a PauliSum, extracted to give a list instead
            self.pauli_list = hamiltonian.terms
            self.n_qubits = self.get_qubit_req()
            # assumes 0-(N-1) ordering and every pauli index is in use
        elif isinstance(hamiltonian, List):
            if molecule is not None:
                raise TypeError('Please supply either a Hamiltonian object'
                        ' or a Molecule object, but not both.')
            if len(hamiltonian) > 0:
                if all([isinstance(term, PauliTerm) for term in hamiltonian]):
                    self.pauli_list = hamiltonian
                    self.n_qubits = self.get_qubit_req()
                else:
                    raise TypeError('Hamiltonian as a list must '
                            'contain only PauliTerm objects')
            else:
                print('Warning, empty hamiltonian passed, '
                        'assuming identity Hamiltonian = 1')
                self.pauli_list = [ID()]
                # this is allowed in principle,
                # but won't make a lot of sense to use.
        elif hamiltonian is None:
            if molecule is None:
                raise TypeError('either feed a MolecularData object '
                        'or a PyQuil Hamiltonian to this class')
            else:
                self.H = normal_ordered(get_fermion_operator(
                        molecule.get_molecular_hamiltonian()))
                # store Fermionic
                # Hamiltonian in FermionOperator() instance
                self.qubitop = jordan_wigner(self.H)
                # Apply jordan_wigner transformation and store
                self.n_qubits = 2 * molecule.n_orbitals
                self.pauli_list = qubitop_to_pyquilpauli(self.qubitop).terms
        else:
            raise TypeError('hamiltonian must be a PauliSum '
                    'or list of PauliTerms')

        # abstract QC. can refer to a qvm or qpu.
        # QC architecture and available gates decide the compilation of the
        # programs!
        if isinstance(qc, QuantumComputer):
            self.qc = qc
        elif qc is None:
            self.qc = None
        else:
            raise TypeError('qc must be a QuantumComputer object.'
                    ' If you do not use a QC backend, omit, or supply '
                    'qc=None')

        # number of shots in a tomography experiment
        if isinstance(shotN, int):
            self.shotN = shotN
        elif isinstance(shotN, float):
            self.shotN = int(shotN)
        else:
            raise TypeError('shotN must be an integer or float')
        print(f"shots = {self.shotN}")

        # simulation method. Choose from
        methodoptions = ['WFS', 'linalg', 'QC', 'Numpy']
        if method in methodoptions:
            self.method = method
        else:
            raise ValueError('choose a method from the following list: '\
                    + str(methodoptions) +
                    '. If a QPU, QVM is passed to qc, select QC.')

        # circuit strategy. choose from UCCSD, HF, custom_program
        strategyoptions = ['UCCSD', 'HF', 'custom_program']
        if strategy in strategyoptions:
            if (strategy in ['UCCSD', 'HF']) and molecule is None:
                raise ValueError('Strategy selected, UCCSD or HF, '
                        'requires a MolecularData object from PySCF as input.')
            self.strategy = strategy
        else:
            raise ValueError('choose a circuit strategy from the'
                    ' following list: ' + str(strategyoptions))

        # classical optimizer
        classical_options = ['Nelder-Mead', 'Powell', 'CG', 'BFGS',
                'Newton-CG', 'L-BFGS-B ', 'TNC', 'COBYLA',
                'SLSQP', 'trust-constr', 'dogleg', 'trust-ncg',
                'trust-exact', 'trust-krylov']
        if optimizer not in classical_options:
            raise ValueError('choose a classical optimizer from'
                    ' the following list: ' + str(classical_options))
        else:
            self.optimizer = optimizer

        # store the optimizer historical values
        self.history = []

        # chemistry files. must be properly formatted
        # in order to use a UCCSD ansatz (see MolecularData)
        self.molecule = molecule

        # whether or not the qubits should be actively reset.
        # False will make the hardware wait for 3 coherence lengths
        # to go back to |0>
        self.active_reset = active_reset

        # max number of iterations for the classical optimizer
        self.maxiter = maxiter

        # vqe results, stores output of scipy.optimize.minimize,
        # a OptimizeResult object. initialize to None
        self.res = None

        # list of grouped experiments (only relevant to tomography)
        self.experiment_list = None

        # whether to print debugging data to console
        self.verbose = verbose

        # real QPU has a custom qubit labeling
        self.custom_qubits = custom_qubits

        # i'th function call
        self.it_num = 0

        # whether to perform parametric method
        self.parametric_way = parametric

        # whether to do tomography or just calculate the wavefunction
        self.tomography = tomography

        # set empty circuit unitary.
        # This is used for the direct linear algebraic methods.
        self.circuit_unitary = None

        if strategy not in ['UCCSD', 'HF', 'custom_program']:
            raise ValueError('please select a strategy from UCCSD,'
                    ' HF, custom_program or modify this class with your '
                    'own options')

        if strategy == 'UCCSD':
            # load UCCSD initial amps from the CCSD amps
            # in the MolecularData() object
            amps = uccsd_singlet_get_packed_amplitudes(
                    self.molecule.ccsd_single_amps,
                    self.molecule.ccsd_double_amps,
                    n_qubits=self.molecule.n_orbitals * 2,
                    n_electrons=self.molecule.n_electrons)
            self.initial_packed_amps = amps
        else:
            # allocate empty initial angles for the circuit. modify later.
            self.initial_packed_amps = []

        if (strategy == 'UCCSD') and (method != 'linalg'):
            # UCCSD circuit strategy preparations
            self.ref_state = ref_state_preparation_circuit(
                    molecule,
                    ref_type='HF',
                    cq=self.custom_qubits)

            if self.parametric_way:
                # in the parametric_way,
                # the circuit is built with free parameters
                self.ansatz = uccsd_ansatz_circuit_parametric(
                        self.molecule.n_orbitals,
                        self.molecule.n_electrons,
                        cq=self.custom_qubits)
            else:
                # in the non-parametric_way,
                # the circuit has hard-coded angles for the gates.
                self.ansatz = uccsd_ansatz_circuit(
                        self.initial_packed_amps,
                        self.molecule.n_orbitals,
                        self.molecule.n_electrons,
                        cq=self.custom_qubits)
        elif strategy == 'HF':
            self.ref_state = ref_state_preparation_circuit(
                    self.molecule,
                    ref_type='HF',
                    cq=self.custom_qubits)
            self.ansatz = Program()
        elif strategy == 'custom_program':
            self.parametric_way = True
            self.ref_state = Program()
            self.ansatz = Program()

        if self.tomography:
            self.term_es = {}
            if self.method == 'linalg':
                raise NotImplementedError('Tomography is not'
                        ' yet implemented for the linalg method.')
        else:
            # avoid having to re-calculate the PauliSum object each time,
            # store it.
            self.pauli_sum = PauliSum(self.pauli_list)

        # perform miscellaneous method-specific preparations
        if self.method == 'QC':
            if qc is None:
                raise ValueError('Method is QC, please supply a valid '
                        'QuantumComputer() object to the qc variable.')
        elif self.method == 'WFS':
            if (self.qc is not None) or (self.custom_qubits is not None):
                raise ValueError('The WFS method is not intended to be used'
                        ' with a custom qubit lattice'
                        ' or QuantumComputer object.')
        elif self.method == 'Numpy':
            if self.parametric_way:
                raise ValueError('NumpyWavefunctionSimulator() backend'
                        ' does not yet support parametric programs.')
            if (self.qc is not None) or (self.custom_qubits is not None):
                raise ValueError('NumpyWavefunctionSimulator() backend is'
                        ' not intended to be used with a '
                        'QuantumComputer() object or custom lattice. '
                        'Consider using PyQVM instead')
        elif self.method == 'linalg':
            if molecule is not None:
                # sparse initial state vector from the MolecularData() object
                self.initial_psi = jw_hartree_fock_state(
                        self.molecule.n_electrons,
                        2*self.molecule.n_orbitals)
                # sparse operator from the MolecularData() object
                self.hamiltonian_matrix = get_sparse_operator(
                        self.H,
                        n_qubits=self.n_qubits)
            else:
                self.hamiltonian_matrix = get_sparse_operator(
                        pyquilpauli_to_qubitop(PauliSum(self.pauli_list)))
                self.initial_psi = None
                print('Please supply VQE initial state with method'
                        ' VQEexperiment().set_initial_state()')
        else:
            raise ValueError('unknown method: please choose from method ='
                    ' {linalg, WFS, tomography} for direct linear '
                    'algebra, pyquil WavefunctionSimulator, '
                    'or doing Tomography, respectively')