Skip to content

Ground-State Energy with VQE

This example finds the ground-state energy of a small model with the variational quantum eigensolver (VQE). The model is the Fermi-Hubbard dimer: two sites, two electrons, a hopping term t and an on-site repulsion u. Its ground-state energy is known in closed form, so the result can be checked exactly.

The example runs on provider="numpy", which is the default of VQE, and it uses only the fermionic tools built into the library, so it does not need openfermion or pyscf. The whole page runs in under two seconds. See the VQE chapter for the algorithm, and the Quantum Chemistry guide for the interface.

The model

import numpy as np
from eigenkit.chemistry import (
    VQE,
    exact_ground_state_energy,
    fermi_hubbard_dimer,
    fermi_hubbard_dimer_ground_energy,
    jordan_wigner,
    uccsd_ansatz,
)

T, U, N_QUBITS = 1.0, 2.0, 4

fermionic = fermi_hubbard_dimer(t=T, u=U)
analytic = fermi_hubbard_dimer_ground_energy(t=T, u=U)

print("operator type  :", type(fermionic).__name__)
print("analytic ground:", analytic)
operator type  : FermionOperator
analytic ground: -1.2360679774997898

The model has four spin-orbitals (two sites, each with two spins), so it needs four qubits after the mapping.

Mapping fermions to qubits

jordan_wigner() returns two values: the qubit Observable, and a scalar offset that holds the identity part of the Hamiltonian.

hamiltonian, offset = jordan_wigner(fermionic)

print("pauli terms:", len(hamiltonian.terms))
print("offset     :", offset)
for term in hamiltonian.terms[:4]:
    print("   ", term.coeff, term.paulis)
pauli terms: 10
offset     : 1.0
    -0.5 (('Y', 0), ('Z', 1), ('Y', 2))
    -0.5 (('X', 0), ('Z', 1), ('X', 2))
    -0.5 (('Y', 1), ('Z', 2), ('Y', 3))
    -0.5 (('X', 1), ('Z', 2), ('X', 3))

The first four terms are the hopping terms. Each one moves an electron from one site to the other, and the Z between the two ends gives the sign that fermions need. The Z and ZZ terms that follow are the on-site repulsion.

The offset must be included in every energy calculation. Here it is exactly 1.0, and if you leave it out, every result is shifted by this amount.

without = exact_ground_state_energy(hamiltonian, N_QUBITS)
with_off = exact_ground_state_energy(hamiltonian, N_QUBITS, offset)

print("without the offset:", without)
print("with the offset   :", with_off)
print("analytic          :", analytic)
without the offset: -2.23606797749979
with the offset   : -1.2360679774997898
analytic          : -1.2360679774997898

exact_ground_state_energy() diagonalises the Hamiltonian directly. At four qubits, this is a 16 × 16 matrix.

Running VQE

result = VQE(layers=2, random_state=0).solve(hamiltonian, N_QUBITS, offset=offset)

print("energy        ", result.energy)
print("error vs exact", abs(result.energy - analytic))
print("n_iterations  ", result.n_iterations)
print("success       ", result.success)
print("n parameters  ", len(result.optimal_parameters))
energy         -0.9999999991069795
error vs exact 0.23606797839281035
n_iterations   20
success        True
n parameters   20

success only reports that the classical optimiser converged. Here it converged to -1.0000, which is 0.236 above the ground-state energy. -1.0 is itself an eigenvalue of this Hamiltonian. It is the lowest energy with one electron, and the default hardware-efficient ansatz with two layers cannot reach the lower two-electron state.

Depth of the default ansatz

layers sets the depth of the hardware-efficient ansatz that VQE builds when no ansatz is given.

print("layers  params  energy          error")
for layers in (1, 2, 3, 4):
    r = VQE(layers=layers, random_state=0).solve(hamiltonian, N_QUBITS, offset=offset)
    print(
        f"{layers:6d}  {len(r.optimal_parameters):6d}  {r.energy:.10f}  {abs(r.energy - analytic):.3e}"
    )
layers  params  energy          error
     1      12  -0.8535533903  3.825e-01
     2      20  -0.9999999991  2.361e-01
     3      28  -0.9999999893  2.361e-01
     4      36  -1.2360679394  3.806e-08

Two and three layers both stop at -1.0. Four layers, with 36 parameters, reach the ground-state energy with an error of 3.8e-8.

A chemistry-aware ansatz

uccsd_ansatz() builds the unitary coupled cluster circuit with single and double excitations (UCCSD), approximated by Trotterisation, on the Hartree-Fock reference state for a given number of electrons. reps is the number of Trotter repetitions of the product of excitations.

print("reps  params  energy          error")
for reps in (1, 2, 3):
    ansatz = uccsd_ansatz(n_qubits=N_QUBITS, n_electrons=2, reps=reps)
    r = VQE(random_state=0).solve(hamiltonian, N_QUBITS, offset=offset, ansatz=ansatz)
    print(
        f"{reps:4d}  {len(r.optimal_parameters):6d}  {r.energy:.10f}  {abs(r.energy - analytic):.3e}"
    )
reps  params  energy          error
   1       5  -0.9999999971  2.361e-01
   2      10  -1.2360679758  1.657e-09
   3      15  -1.2360679775  2.675e-12

One repetition stops at -1.0, the same value where the shallow hardware-efficient circuits stopped. The first-order Trotter error is the limit here. Two repetitions, with 10 parameters, give an error of 1.7e-9, compared with 3.8e-8 for the hardware-efficient ansatz with 36 parameters. Three repetitions reach an error of 2.7e-12.

Next

  • MaxCut with QAOA shows the other variational solver, the Quantum Approximate Optimization Algorithm (QAOA), on a combinatorial problem.
  • The VQE chapter explains the ansätze and the mappings.
  • The Quantum Chemistry guide covers Molecule, bravyi_kitaev() and the rest of the package. Real molecules need the chemistry extra.