Пример #1
0
def hartree_fock_state_jellium(grid,
                               n_electrons,
                               spinless=True,
                               plane_wave=False):
    """Give the Hartree-Fock state of jellium.

    Args:
        grid (Grid): The discretization to use.
        n_electrons (int): Number of electrons in the system.
        spinless (bool): Whether to use the spinless model or not.
        plane_wave (bool): Whether to return the Hartree-Fock state in
                           the plane wave (True) or dual basis (False).

    Notes:
        The jellium model is built up by filling the lowest-energy
        single-particle states in the plane-wave Hamiltonian until
        n_electrons states are filled.
    """
    from openfermion.hamiltonians import plane_wave_kinetic
    # Get the jellium Hamiltonian in the plane wave basis.
    # For determining the Hartree-Fock state in the PW basis, only the
    # kinetic energy terms matter.
    hamiltonian = plane_wave_kinetic(grid, spinless=spinless)
    hamiltonian = normal_ordered(hamiltonian)
    hamiltonian.compress()

    # The number of occupied single-particle states is the number of electrons.
    # Those states with the lowest single-particle energies are occupied first.
    occupied_states = lowest_single_particle_energy_states(
        hamiltonian, n_electrons)
    occupied_states = numpy.array(occupied_states)

    if plane_wave:
        # In the plane wave basis the HF state is a single determinant.
        hartree_fock_state_index = numpy.sum(2**occupied_states)
        hartree_fock_state = csr_matrix(
            ([1.0], ([hartree_fock_state_index], [0])),
            shape=(2**count_qubits(hamiltonian), 1))

    else:
        # Inverse Fourier transform the creation operators for the state to get
        # to the dual basis state, then use that to get the dual basis state.
        hartree_fock_state_creation_operator = FermionOperator.identity()
        for state in occupied_states[::-1]:
            hartree_fock_state_creation_operator *= (FermionOperator(
                ((int(state), 1), )))
        dual_basis_hf_creation_operator = inverse_fourier_transform(
            hartree_fock_state_creation_operator, grid, spinless)

        dual_basis_hf_creation = normal_ordered(
            dual_basis_hf_creation_operator)

        # Initialize the HF state as a sparse matrix.
        hartree_fock_state = dok_matrix((2**count_qubits(hamiltonian), 1),
                                        dtype=complex)

        # Populate the elements of the HF state in the dual basis.
        for term in dual_basis_hf_creation.terms:
            index = 0
            for operator in term:
                index += 2**operator[0]
            hartree_fock_state[index, 0] = dual_basis_hf_creation.terms[term]
        hartree_fock_state = hartree_fock_state.tocsr()

    return hartree_fock_state
Пример #2
0
 def test_quad_offsite_reversed(self):
     op = QuadOperator(((3, 'q'), (2, 'p')))
     expected = QuadOperator(((2, 'p'), (3, 'q')))
     self.assertTrue(expected == normal_ordered(op))
def simulation_ordered_grouped_low_depth_terms_with_info(
        hamiltonian, input_ordering=None, external_potential_at_end=False):
    """Give terms from the dual basis Hamiltonian in simulated order.

    Uses the simulation ordering, grouping terms into hopping
    (i^ j + j^ i) and number (i^j^ i j + c_i i^ i + c_j j^ j) operators.
    Pre-computes term information (indices each operator acts on, as
    well as whether each operator is a hopping operator.

    Args:
        hamiltonian (FermionOperator): The Hamiltonian.
        input_ordering (list): The initial Jordan-Wigner canonical order.
                               If no input ordering is specified, defaults to
                               [0..n_qubits] where n_qubits is the number of
                               qubits in the Hamiltonian.
        external_potential_at_end (bool): Whether to include the rotations from
            the external potential at the end of the Trotter step, or
            intersperse them throughout it.

    Returns:
        A 3-tuple of terms from the Hamiltonian in order of simulation,
        the indices they act on, and whether they are hopping operators
        (both also in the same order).

    Notes:
        Follows the "stagger"-based simulation order discussed in Kivlichan
        et al., "Quantum Simulation of Electronic Structure with Linear
        Depth and Connectivity", arxiv:1711.04789; as such, the only
        permitted types of terms are hopping (i^ j + j^ i) and potential
        terms which are products of at most two number operators.
    """
    n_qubits = count_qubits(hamiltonian)
    hamiltonian = normal_ordered(hamiltonian)

    ordered_terms = []
    ordered_indices = []
    ordered_is_hopping_operator = []

    # If no input mode ordering is specified, default to range(n_qubits).
    try:
        input_ordering = list(input_ordering)
    except TypeError:
        input_ordering = list(range(n_qubits))

    # Half a second-order Trotter step reverses the input ordering: this tells
    # us how much we need to include in the ordered list of terms.
    final_ordering = list(reversed(input_ordering))

    # Follow odd-even transposition sort. In alternating steps, swap each even
    # qubits with the odd qubit to its right, and in the next step swap each
    # the odd qubits with the even qubit to its right. Do this until the input
    # ordering has been reversed.
    parity = 0
    while input_ordering != final_ordering:
        results = stagger_with_info(
            hamiltonian, input_ordering, parity,
            external_potential_at_end)
        terms_in_layer, indices_in_layer, is_hopping_operator_in_layer = (
            results)

        ordered_terms.extend(terms_in_layer)
        ordered_indices.extend(indices_in_layer)
        ordered_is_hopping_operator.extend(is_hopping_operator_in_layer)

        # Alternate even and odd steps of the reversal procedure.
        parity = 1 - parity

    # If all the rotations from the external potential are in the final layer,
    # i.e. we don't intersperse them throughout the Trotter step.
    if external_potential_at_end:
        terms_in_final_layer = []
        indices_in_final_layer = []
        is_hopping_operator_in_final_layer = []

        for qubit in range(n_qubits):
            coeff = hamiltonian.terms.get(((qubit, 1), (qubit, 0)), 0.0)
            if coeff:
                terms_in_final_layer.append(
                    FermionOperator(((qubit, 1), (qubit, 0)), coeff))
                indices_in_final_layer.append(set((qubit,)))
                is_hopping_operator_in_final_layer.append(False)

        ordered_terms.extend(terms_in_final_layer)
        ordered_indices.extend(indices_in_final_layer)
        ordered_is_hopping_operator.extend(is_hopping_operator_in_final_layer)

    return (ordered_terms, ordered_indices, ordered_is_hopping_operator)
Пример #4
0
 def test_fermion_double_create_separated(self):
     op = FermionOperator(((3, 1), (2, 0), (3, 1)))
     expected = FermionOperator((), 0.0)
     self.assertTrue(expected == normal_ordered(op))
Пример #5
0
 def test_quad_two_term(self):
     op_b = QuadOperator('p0 q0 p3', 88.)
     normal_ordered_b = normal_ordered(op_b, hbar=2)
     expected = QuadOperator('p3', -88.*2j) + QuadOperator('q0 p0 p3', 88.0)
     self.assertTrue(normal_ordered_b == expected)
Пример #6
0
 def test_fermion_number(self):
     number_op2 = FermionOperator(((2, 1), (2, 0)))
     self.assertTrue(number_op2 == normal_ordered(number_op2))
Пример #7
0
 def test_fermion_offsite(self):
     op = FermionOperator(((3, 1), (2, 0)))
     self.assertTrue(op == normal_ordered(op))
Пример #8
0
def get_number_preserving_sparse_operator(fermion_op,
                                          num_qubits,
                                          num_electrons,
                                          spin_preserving=False,
                                          reference_determinant=None,
                                          excitation_level=None):
    """Initialize a Scipy sparse matrix in a specific symmetry sector.

    This method initializes a Scipy sparse matrix from a FermionOperator,
    explicitly working in a particular particle number sector. Optionally, it
    can also restrict the space to contain only states with a particular Sz.

    Finally, the Hilbert space can also be restricted to only those states
    which are reachable by excitations up to a fixed rank from an initial
    reference determinant.

    Args:
        fermion_op(FermionOperator): An instance of the FermionOperator class.
            It should not contain terms which do not preserve particle number.
            If spin_preserving is set to True it should also not contain terms
            which do not preserve the Sz (it is assumed that the ordering of
            the indices goes alpha, beta, alpha, beta, ...).
        num_qubits(int): The total number of qubits / spin-orbitals in the
            system.
        num_electrons(int): The number of particles in the desired Hilbert
            space.
        spin_preserving(bool): Whether or not the constructed operator should
            be defined in a space which has support only on states with the
            same Sz value as the reference_determinant.
        reference_determinant(list(bool)): A list, whose length is equal to
            num_qubits, which specifies which orbitals should be occupied in
            the reference state. If spin_preserving is set to True then the Sz
            value of this reference state determines the Sz value of the
            symmetry sector in which the generated operator acts. If a value
            for excitation_level is provided then the excitations are generated
            with respect to the reference state. In any case, the ordering of
            the states in the matrix representation of the operator depends on
            reference_determinant and the state corresponding to
            reference_determinant is the vector [1.0, 0.0, 0.0 ... 0.0]. Can be
            set to None in order to take the first num_electrons orbitals to be
            the occupied orbitals.
        excitation_level(int): The number of excitations from the reference
            state which should be included in the generated operator's matrix
            representation. Can be set to None to include all levels of
            excitation.

    Returns:
        sparse_op(scipy.sparse.csc_matrix): A sparse matrix representation of
            fermion_op in the basis set by the arguments.
    """

    # We use the Hartree-Fock determinant as a reference if none is provided.
    if reference_determinant is None:
        reference_determinant = numpy.array(
            [i < num_electrons for i in range(num_qubits)])
    else:
        reference_determinant = numpy.asarray(reference_determinant)

    if excitation_level is None:
        excitation_level = num_electrons

    state_array = numpy.asarray(
        list(
            _iterate_basis_(reference_determinant, excitation_level,
                            spin_preserving)))
    # Create a 1d array with each determinant encoded as an integer for sorting purposes.
    int_state_array = state_array.dot(
        1 << numpy.arange(state_array.shape[1])[::-1])
    sorting_indices = numpy.argsort(int_state_array)

    space_size = state_array.shape[0]

    fermion_op = normal_ordered(fermion_op)

    sparse_op = scipy.sparse.csc_matrix((space_size, space_size), dtype=float)

    for term, coefficient in fermion_op.terms.items():
        if len(term) == 0:
            constant = coefficient * scipy.sparse.identity(
                space_size, dtype=float, format='csc')

            sparse_op += constant

        else:
            term_op = _build_term_op_(term, state_array, int_state_array,
                                      sorting_indices)

            sparse_op += coefficient * term_op

    return sparse_op
Пример #9
0
def get_interaction_operator(fermion_operator, n_qubits=None):
    r"""Convert a 2-body fermionic operator to InteractionOperator.

    This function should only be called on fermionic operators which
    consist of only a_p^\dagger a_q and a_p^\dagger a_q^\dagger a_r a_s
    terms. The one-body terms are stored in a matrix, one_body[p, q], and
    the two-body terms are stored in a tensor, two_body[p, q, r, s].

    Returns:
       interaction_operator: An instance of the InteractionOperator class.

    Raises:
        TypeError: Input must be a FermionOperator.
        TypeError: FermionOperator does not map to InteractionOperator.

    Warning:
        Even assuming that each creation or annihilation operator appears
        at most a constant number of times in the original operator, the
        runtime of this method is exponential in the number of qubits.
    """
    if not isinstance(fermion_operator, FermionOperator):
        raise TypeError('Input must be a FermionOperator.')

    if n_qubits is None:
        n_qubits = count_qubits(fermion_operator)
    if n_qubits < count_qubits(fermion_operator):
        raise ValueError('Invalid number of qubits specified.')

    # Normal order the terms and initialize.
    fermion_operator = normal_ordered(fermion_operator)
    constant = 0.
    one_body = numpy.zeros((n_qubits, n_qubits), complex)
    two_body = numpy.zeros((n_qubits, n_qubits, n_qubits, n_qubits), complex)

    # Loop through terms and assign to matrix.
    for term in fermion_operator.terms:
        coefficient = fermion_operator.terms[term]
        # Ignore this term if the coefficient is zero
        if abs(coefficient) < EQ_TOLERANCE:
            continue

        # Handle constant shift.
        if len(term) == 0:
            constant = coefficient

        elif len(term) == 2:
            # Handle one-body terms.
            if [operator[1] for operator in term] == [1, 0]:
                p, q = [operator[0] for operator in term]
                one_body[p, q] = coefficient
            else:
                raise InteractionOperatorError('FermionOperator does not map '
                                               'to InteractionOperator.')

        elif len(term) == 4:
            # Handle two-body terms.
            if [operator[1] for operator in term] == [1, 1, 0, 0]:
                p, q, r, s = [operator[0] for operator in term]
                two_body[p, q, r, s] = coefficient
            else:
                raise InteractionOperatorError('FermionOperator does not map '
                                               'to InteractionOperator.')

        else:
            # Handle non-molecular Hamiltonian.
            raise InteractionOperatorError('FermionOperator does not map '
                                           'to InteractionOperator.')

    # Form InteractionOperator and return.
    interaction_operator = InteractionOperator(constant, one_body, two_body)
    return interaction_operator
Пример #10
0
def get_quadratic_hamiltonian(fermion_operator,
                              chemical_potential=0.,
                              n_qubits=None,
                              ignore_incompatible_terms=False):
    r"""Convert a quadratic fermionic operator to QuadraticHamiltonian.

    Args:
        fermion_operator(FermionOperator): The operator to convert.
        chemical_potential(float): A chemical potential to include in
            the returned operator
        n_qubits(int): Optionally specify the total number of qubits in the
            system
        ignore_incompatible_terms(bool): This flag determines the behavior
            of this method when it encounters terms that are not quadratic
            that is, terms that are not of the form a^\dagger_p a_q.
            If set to True, this method will simply ignore those terms.
            If False, then this method will raise an error if it encounters
            such a term. The default setting is False.

    Returns:
       quadratic_hamiltonian: An instance of the QuadraticHamiltonian class.

    Raises:
        TypeError: Input must be a FermionOperator.
        TypeError: FermionOperator does not map to QuadraticHamiltonian.

    Warning:
        Even assuming that each creation or annihilation operator appears
        at most a constant number of times in the original operator, the
        runtime of this method is exponential in the number of qubits.
    """
    if not isinstance(fermion_operator, FermionOperator):
        raise TypeError('Input must be a FermionOperator.')

    if n_qubits is None:
        n_qubits = count_qubits(fermion_operator)
    if n_qubits < count_qubits(fermion_operator):
        raise ValueError('Invalid number of qubits specified.')

    # Normal order the terms and initialize.
    fermion_operator = normal_ordered(fermion_operator)
    constant = 0.
    combined_hermitian_part = numpy.zeros((n_qubits, n_qubits), complex)
    antisymmetric_part = numpy.zeros((n_qubits, n_qubits), complex)

    # Loop through terms and assign to matrix.
    for term in fermion_operator.terms:
        coefficient = fermion_operator.terms[term]
        # Ignore this term if the coefficient is zero
        if abs(coefficient) < EQ_TOLERANCE:
            continue

        if len(term) == 0:
            # Constant term
            constant = coefficient
        elif len(term) == 2:
            ladder_type = [operator[1] for operator in term]
            p, q = [operator[0] for operator in term]

            if ladder_type == [1, 0]:
                combined_hermitian_part[p, q] = coefficient
            elif ladder_type == [1, 1]:
                # Need to check that the corresponding [0, 0] term is present
                conjugate_term = ((p, 0), (q, 0))
                if conjugate_term not in fermion_operator.terms:
                    raise QuadraticHamiltonianError(
                        'FermionOperator does not map '
                        'to QuadraticHamiltonian (not Hermitian).')
                else:
                    matching_coefficient = -fermion_operator.terms[
                        conjugate_term].conjugate()
                    discrepancy = abs(coefficient - matching_coefficient)
                    if discrepancy > EQ_TOLERANCE:
                        raise QuadraticHamiltonianError(
                            'FermionOperator does not map '
                            'to QuadraticHamiltonian (not Hermitian).')
                antisymmetric_part[p, q] += .5 * coefficient
                antisymmetric_part[q, p] -= .5 * coefficient
            else:
                # ladder_type == [0, 0]
                # Need to check that the corresponding [1, 1] term is present
                conjugate_term = ((p, 1), (q, 1))
                if conjugate_term not in fermion_operator.terms:
                    raise QuadraticHamiltonianError(
                        'FermionOperator does not map '
                        'to QuadraticHamiltonian (not Hermitian).')
                else:
                    matching_coefficient = -fermion_operator.terms[
                        conjugate_term].conjugate()
                    discrepancy = abs(coefficient - matching_coefficient)
                    if discrepancy > EQ_TOLERANCE:
                        raise QuadraticHamiltonianError(
                            'FermionOperator does not map '
                            'to QuadraticHamiltonian (not Hermitian).')
                antisymmetric_part[p, q] -= .5 * coefficient.conjugate()
                antisymmetric_part[q, p] += .5 * coefficient.conjugate()
        elif not ignore_incompatible_terms:
            # Operator contains non-quadratic terms
            raise QuadraticHamiltonianError('FermionOperator does not map '
                                            'to QuadraticHamiltonian '
                                            '(contains non-quadratic terms).')

    # Compute Hermitian part
    hermitian_part = (combined_hermitian_part +
                      chemical_potential * numpy.eye(n_qubits))

    # Check that the operator is Hermitian
    if not is_hermitian(hermitian_part):
        raise QuadraticHamiltonianError(
            'FermionOperator does not map '
            'to QuadraticHamiltonian (not Hermitian).')

    # Form QuadraticHamiltonian and return.
    discrepancy = numpy.max(numpy.abs(antisymmetric_part))
    if discrepancy < EQ_TOLERANCE:
        # Hamiltonian conserves particle number
        quadratic_hamiltonian = QuadraticHamiltonian(
            hermitian_part,
            constant=constant,
            chemical_potential=chemical_potential)
    else:
        # Hamiltonian does not conserve particle number
        quadratic_hamiltonian = QuadraticHamiltonian(hermitian_part,
                                                     antisymmetric_part,
                                                     constant,
                                                     chemical_potential)

    return quadratic_hamiltonian
Пример #11
0
def get_diagonal_coulomb_hamiltonian(fermion_operator,
                                     n_qubits=None,
                                     ignore_incompatible_terms=False):
    r"""Convert a FermionOperator to a DiagonalCoulombHamiltonian.

    Args:
        fermion_operator(FermionOperator): The operator to convert.
        n_qubits(int): Optionally specify the total number of qubits in the
            system
        ignore_incompatible_terms(bool): This flag determines the behavior
            of this method when it encounters terms that are not represented
            by the DiagonalCoulombHamiltonian class, namely, terms that are
            not quadratic and not quartic of the form
            a^\dagger_p a_p a^\dagger_q a_q. If set to True, this method will
            simply ignore those terms. If False, then this method will raise
            an error if it encounters such a term. The default setting is False.
    """
    if not isinstance(fermion_operator, FermionOperator):
        raise TypeError('Input must be a FermionOperator.')

    if n_qubits is None:
        n_qubits = count_qubits(fermion_operator)
    if n_qubits < count_qubits(fermion_operator):
        raise ValueError('Invalid number of qubits specified.')

    fermion_operator = normal_ordered(fermion_operator)
    constant = 0.
    one_body = numpy.zeros((n_qubits, n_qubits), complex)
    two_body = numpy.zeros((n_qubits, n_qubits), float)

    for term, coefficient in fermion_operator.terms.items():
        # Ignore this term if the coefficient is zero
        if abs(coefficient) < EQ_TOLERANCE:
            continue

        if len(term) == 0:
            constant = coefficient
        else:
            actions = [operator[1] for operator in term]
            if actions == [1, 0]:
                p, q = [operator[0] for operator in term]
                one_body[p, q] = coefficient
            elif actions == [1, 1, 0, 0]:
                p, q, r, s = [operator[0] for operator in term]
                if p == r and q == s:
                    if abs(numpy.imag(coefficient)) > EQ_TOLERANCE:
                        raise ValueError(
                            'FermionOperator does not map to '
                            'DiagonalCoulombHamiltonian (not Hermitian).')
                    coefficient = numpy.real(coefficient)
                    two_body[p, q] = -.5 * coefficient
                    two_body[q, p] = -.5 * coefficient
                elif not ignore_incompatible_terms:
                    raise ValueError('FermionOperator does not map to '
                                     'DiagonalCoulombHamiltonian '
                                     '(contains terms with indices '
                                     '{}).'.format((p, q, r, s)))
            elif not ignore_incompatible_terms:
                raise ValueError('FermionOperator does not map to '
                                 'DiagonalCoulombHamiltonian (contains terms '
                                 'with action {}.'.format(tuple(actions)))

    # Check that the operator is Hermitian
    if not is_hermitian(one_body):
        raise ValueError(
            'FermionOperator does not map to DiagonalCoulombHamiltonian '
            '(not Hermitian).')

    return DiagonalCoulombHamiltonian(one_body, two_body, constant)
Пример #12
0
    def test_ucc_h2_singlet(self):
        geometry = [('H', (0., 0., 0.)), ('H', (0., 0., 0.7414))]
        basis = 'sto-3g'
        multiplicity = 1
        filename = os.path.join(THIS_DIRECTORY, 'data',
                                'H2_sto-3g_singlet_0.7414')
        self.molecule = MolecularData(
            geometry, basis, multiplicity, filename=filename)
        self.molecule.load()

        # Get molecular Hamiltonian.
        self.molecular_hamiltonian = self.molecule.get_molecular_hamiltonian()

        # Get FCI RDM.
        self.fci_rdm = self.molecule.get_molecular_rdm(use_fci=1)

        # Get explicit coefficients.
        self.nuclear_repulsion = self.molecular_hamiltonian.constant
        self.one_body = self.molecular_hamiltonian.one_body_tensor
        self.two_body = self.molecular_hamiltonian.two_body_tensor

        # Get fermion Hamiltonian.
        self.fermion_hamiltonian = normal_ordered(get_fermion_operator(
            self.molecular_hamiltonian))

        # Get qubit Hamiltonian.
        self.qubit_hamiltonian = jordan_wigner(self.fermion_hamiltonian)

        # Get the sparse matrix.
        self.hamiltonian_matrix = get_sparse_operator(
            self.molecular_hamiltonian)
        # Test UCCSD for accuracy against FCI using loaded t amplitudes.
        ucc_operator = uccsd_generator(
            self.molecule.ccsd_single_amps,
            self.molecule.ccsd_double_amps)

        hf_state = jw_hartree_fock_state(
            self.molecule.n_electrons, count_qubits(self.qubit_hamiltonian))
        uccsd_sparse = jordan_wigner_sparse(ucc_operator)
        uccsd_state = scipy.sparse.linalg.expm_multiply(uccsd_sparse,
                                                        hf_state)
        expected_uccsd_energy = expectation(self.hamiltonian_matrix,
                                            uccsd_state)
        self.assertAlmostEqual(expected_uccsd_energy, self.molecule.fci_energy,
                               places=4)
        print("UCCSD ENERGY: {}".format(expected_uccsd_energy))

        # Test CCSD singlet for precise match against FCI using loaded t
        # amplitudes
        packed_amplitudes = uccsd_singlet_get_packed_amplitudes(
            self.molecule.ccsd_single_amps,
            self.molecule.ccsd_double_amps,
            self.molecule.n_qubits,
            self.molecule.n_electrons)
        ccsd_operator = uccsd_singlet_generator(
            packed_amplitudes,
            self.molecule.n_qubits,
            self.molecule.n_electrons,
            anti_hermitian=False)

        ccsd_sparse_r = jordan_wigner_sparse(ccsd_operator)
        ccsd_sparse_l = jordan_wigner_sparse(
            -hermitian_conjugated(ccsd_operator))

        ccsd_state_r = scipy.sparse.linalg.expm_multiply(ccsd_sparse_r,
                                                         hf_state)
        ccsd_state_l = scipy.sparse.linalg.expm_multiply(ccsd_sparse_l,
                                                         hf_state)
        expected_ccsd_energy = ccsd_state_l.conjugate().dot(
            self.hamiltonian_matrix.dot(ccsd_state_r))
        self.assertAlmostEqual(expected_ccsd_energy, self.molecule.fci_energy)
Пример #13
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')

        self.term_es = {}
        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)
                self.compile_tomo_expts()
            for experiment in self.experiment_list:
                term_es = experiment.run_experiment(self.qc, packed_amps)
                self.term_es.update(term_es)

            for term in self.pauli_list:
                key = term.operations_as_set()
                if len(key) > 0:
                    E += term.coefficient * self.term_es[key]
            E += self.offset
            # add the offset energy to avoid doing superfluous
            # tomography over the identity operator.
        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')

        # energy should be real
        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
Пример #14
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, a PyQVM.
        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')

        # 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 or PyQVM 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 = []

        # 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

        # initialize offset for pauli terms with identity.
        # this serves to avoid doing simulation for the identity
        # operator, which always yields unity times the coefficient
        # due to wavefunction normalization.
        self.set_offset()

        # 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()

        # prepare tomography experiment if necessary
        if self.tomography:
            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')
Пример #15
0
 def test_boson_multi(self):
     op = BosonOperator(((2, 0), (1, 1), (2, 1)))
     expected = (BosonOperator(((2, 1), (1, 1), (2, 0))) +
                 BosonOperator(((1, 1),)))
     self.assertTrue(expected == normal_ordered(op))
Пример #16
0
    def test_ccr_offsite_even_cc(self):
        c2 = FermionOperator(((2, 1), ))
        c4 = FermionOperator(((4, 1), ))
        self.assertTrue(normal_ordered(c2 * c4) == normal_ordered(-c4 * c2))

        self.assertTrue(jordan_wigner(c2 * c4) == jordan_wigner(-c4 * c2))
Пример #17
0
 def test_fermion_two_term(self):
     op_b = FermionOperator(((2, 0), (4, 0), (2, 1)), -88.)
     normal_ordered_b = normal_ordered(op_b)
     expected = (FermionOperator(((4, 0),), 88.) +
                 FermionOperator(((2, 1), (4, 0), (2, 0)), 88.))
     self.assertTrue(normal_ordered_b == expected)
Пример #18
0
    def test_ccr_offsite_odd_cc(self):
        c1 = FermionOperator(((1, 1), ))
        c4 = FermionOperator(((4, 1), ))
        self.assertTrue(normal_ordered(c1 * c4) == normal_ordered(-c4 * c1))

        self.assertTrue(jordan_wigner(c1 * c4) == jordan_wigner(-c4 * c1))
Пример #19
0
 def test_fermion_number_reversed(self):
     n_term_rev2 = FermionOperator(((2, 0), (2, 1)))
     number_op2 = number_operator(3, 2)
     expected = FermionOperator(()) - number_op2
     self.assertTrue(normal_ordered(n_term_rev2) == expected)
Пример #20
0
    def test_ccr_offsite_even_aa(self):
        a2 = FermionOperator(((2, 0), ))
        a4 = FermionOperator(((4, 0), ))
        self.assertTrue(normal_ordered(a2 * a4) == normal_ordered(-a4 * a2))

        self.assertTrue(jordan_wigner(a2 * a4) == jordan_wigner(-a4 * a2))
Пример #21
0
 def test_fermion_offsite_reversed(self):
     op = FermionOperator(((3, 0), (2, 1)))
     expected = -FermionOperator(((2, 1), (3, 0)))
     self.assertTrue(expected == normal_ordered(op))
Пример #22
0
    def test_ccr_offsite_odd_aa(self):
        a1 = FermionOperator(((1, 0), ))
        a4 = FermionOperator(((4, 0), ))
        self.assertTrue(normal_ordered(a1 * a4) == normal_ordered(-a4 * a1))

        self.assertTrue(jordan_wigner(a1 * a4) == jordan_wigner(-a4 * a1))
Пример #23
0
 def test_fermion_multi(self):
     op = FermionOperator(((2, 0), (1, 1), (2, 1)))
     expected = (-FermionOperator(((2, 1), (1, 1), (2, 0))) -
                 FermionOperator(((1, 1),)))
     self.assertTrue(expected == normal_ordered(op))
Пример #24
0
 def test_commutator_hopping_with_single_number(self):
     com = commutator(FermionOperator('1^ 2', 1j), FermionOperator('1^ 1'))
     com = normal_ordered(com)
     self.assertEqual(com, -FermionOperator('1^ 2') * 1j)
Пример #25
0
 def test_quad_offsite(self):
     op = QuadOperator(((3, 'p'), (2, 'q')))
     self.assertTrue(op == normal_ordered(op))
Пример #26
0
 def test_commutator_hopping_with_double_number_two_intersections(self):
     com = commutator(FermionOperator('2^ 3'), FermionOperator('3^ 2^ 3 2'))
     com = normal_ordered(com)
     self.assertEqual(com, FermionOperator.zero())
Пример #27
0
 def test_exceptions(self):
     with self.assertRaises(TypeError):
         _ = normal_ordered(1)
Пример #28
0
 def test_boson_number_reversed(self):
     n_term_rev2 = BosonOperator(((2, 0), (2, 1)))
     number_op2 = number_operator(3, 2, parity=1)
     expected = BosonOperator(()) + number_op2
     self.assertTrue(normal_ordered(n_term_rev2) == expected)
Пример #29
0
 def test_boson_single_term(self):
     op = BosonOperator('4 3 2 1') + BosonOperator('3 2')
     self.assertTrue(op == normal_ordered(op))
def simulation_ordered_grouped_hubbard_terms_with_info(hubbard_hamiltonian):
    """Give terms from the Fermi-Hubbard Hamiltonian in simulated order.

    Uses the simulation ordering, grouping terms into hopping
    (i^ j + j^ i) and on-site potential (i^j^ i j) operators.
    Pre-computes term information (indices each operator acts on, as
    well as whether each operator is a hopping operator).

    Args:
        hubbard_hamiltonian (FermionOperator): The Hamiltonian.
        original_ordering (list): The initial Jordan-Wigner canonical order.

    Returns:
        A 3-tuple of terms from the Hubbard Hamiltonian in order of
        simulation, the indices they act on, and whether they are hopping
        operators (both also in the same order).

    Notes:
        Assumes that the Hubbard model has spin and is on a 2D square
        aperiodic lattice. Uses the "stagger"-based Trotter step for the
        Hubbard model detailed in Kivlichan et al., "Quantum Simulation
        of Electronic Structure with Linear Depth and Connectivity",
        arxiv:1711.04789.
    """
    hamiltonian = normal_ordered(hubbard_hamiltonian)
    n_qubits = count_qubits(hamiltonian)
    side_length = int(numpy.sqrt(n_qubits / 2.0))

    ordered_terms = []
    ordered_indices = []
    ordered_is_hopping_operator = []

    original_ordering = list(range(n_qubits))
    for i in range(0, n_qubits - side_length, 2 * side_length):
        for j in range(2 * bool(i % (4 * side_length)), 2 * side_length, 4):
            original_ordering[i + j], original_ordering[i + j + 1] = (
                original_ordering[i + j + 1], original_ordering[i + j])

    input_ordering = list(original_ordering)

    # Follow odd-even transposition sort. In alternating steps, swap each even
    # qubits with the odd qubit to its right, and in the next step swap each
    # the odd qubits with the even qubit to its right. Do this until the input
    # ordering has been reversed.
    parities = [False, True] * int(side_length / 2 + 1)  # int floors here
    for parity in parities:
        results = stagger_with_info(hubbard_hamiltonian, input_ordering,
                                    parity)
        terms_in_step, indices_in_step, is_hopping_operator_in_step = results

        ordered_terms.extend(terms_in_step)
        ordered_indices.extend(indices_in_step)
        ordered_is_hopping_operator.extend(is_hopping_operator_in_step)

    input_ordering = list(original_ordering)

    parities = [True] + [False, True] * int(side_length / 2)  # int floors here
    for parity in parities:
        results = stagger_with_info(hubbard_hamiltonian, input_ordering,
                                    parity)
        terms_in_step, indices_in_step, is_hopping_operator_in_step = results

        ordered_terms.extend(terms_in_step)
        ordered_indices.extend(indices_in_step)
        ordered_is_hopping_operator.extend(is_hopping_operator_in_step)

    return (ordered_terms, ordered_indices, ordered_is_hopping_operator)