Hydrogen Molecule Geometry with VQE in PennyLane on Braket
Overview
# --- Setup cell added by QCR (not part of the original tutorial) ---
# Source: amazon-braket/amazon-braket-examples @ 0c0818f315479aab9deebed7e7ed7533ac581923, Apache License 2.0.
# Installs the example's dependencies. If a later cell still reports a missing
# package, restart the runtime/kernel and run again from the top.
%pip install -q amazon-braket-sdk==1.117.3 pennylane==0.45.0 amazon-braket-pennylane-plugin==1.34.1 openfermionpyscf==0.5 matplotlib
Hydrogen Molecule geometry with VQE
This tutorial will show you how to solve an important problem for quantum chemistry using PennyLane on Amazon Braket: finding the ground-state energy of a molecule. The problem can be tackled using near-term quantum hardware by implementing the variational quantum eigensolver (VQE) algorithm. You can find further details on quantum chemistry and VQE in both the Braket VQE notebook and PennyLane tutorials.
From quantum chemistry to quantum circuits
Our first step is to convert our chemistry problem into something that can be tackled with a quantum computer. To do this, we will use PennyLane's qchem package. If running on a local machine, the qchem package must be installed separately by following these instructions.
import pennylane as qml
from pennylane import numpy as np
from pennylane import qchemThe input chemistry data is often provided in the form of a geometry file containing details about the molecule. Here, we consider the hydrogen molecule qchem package.
symbols, coordinates = qchem.read_structure("hydrogen_molecule/h2.xyz")
h, qubits = qchem.molecular_hamiltonian(symbols, coordinates, name="h2")
print(h)(-0.24274280046588792) [Z2] + (-0.24274280046588792) [Z3] + (-0.04207898539364302) [I0] + (0.17771287502681438) [Z0] + (0.1777128750268144) [Z1] + (0.12293305045316086) [Z0 Z2] + (0.12293305045316086) [Z1 Z3] + (0.16768319431887935) [Z0 Z3] + (0.16768319431887935) [Z1 Z2] + (0.1705973836507714) [Z0 Z1] + (0.1762764072240811) [Z2 Z3] + (-0.044750143865718496) [Y0 Y1 X2 X3] + (-0.044750143865718496) [X0 X1 Y2 Y3] + (0.044750143865718496) [Y0 X1 X2 Y3] + (0.044750143865718496) [X0 Y1 Y2 X3]
In the VQE algorithm, we compute the energy of the
In this tutorial, we also want to compute the total spin. To that aim, we use the qchem package to build the total-spin operator
electrons = 2 # Molecular hydrogen has two electrons
S2 = qchem.spin2(electrons, qubits)
print(S2)((0.375+0j)) [Z0] + ((0.375+0j)) [Z1] + ((0.375+0j)) [Z2] + ((0.375+0j)) [Z3] + ((0.75+0j)) [I0] + ((-0.375+0j)) [Z0 Z1] + ((-0.375+0j)) [Z2 Z3] + ((-0.125+0j)) [Z0 Z3] + ((-0.125+0j)) [Z1 Z2] + ((0.125+0j)) [Z0 Z2] + ((0.125+0j)) [Z1 Z3] + ((-0.125+0j)) [Y0 X1 X2 Y3] + ((-0.125+0j)) [X0 Y1 Y2 X3] + ((0.125+0j)) [Y0 X1 Y2 X3] + ((0.125+0j)) [Y0 Y1 X2 X3] + ((0.125+0j)) [Y0 Y1 Y2 Y3] + ((0.125+0j)) [X0 X1 X2 X3] + ((0.125+0j)) [X0 X1 Y2 Y3] + ((0.125+0j)) [X0 Y1 X2 Y3]
Defining an ansatz circuit
We now set up the ansatz circuit that will be trained to prepare the ground state of the Hamiltonian.
This tutorial uses a chemically-inspired circuit, the AllSinglesDoubles ansatz of Delgado et al. (2020). To use this, we must define some additional inputs from quantum chemistry.
# Hartree-Fock state
hf_state = qchem.hf_state(electrons, qubits)
# generate single- and double-excitations
singles, doubles = qchem.excitations(electrons, qubits)Our ansatz circuit is then simple to define:
def circuit(params, wires):
qml.templates.AllSinglesDoubles(params, wires, hf_state, singles, doubles)Note that an output measurement has not yet been defined! This is the next step.
Measuring the energy and total spin
Now we instantiate a device to run our circuits; we will use the Braket local simulator:
dev = qml.device("lightning.qubit", wires=qubits)We discussed earlier that we want to minimize the expectation value of the qubit Hamiltonian, corresponding to the energy of
wires = dev.wires.tolist()
@qml.qnode(dev)
def energy_expval(params):
circuit(params, wires)
return qml.expval(h)
@qml.qnode(dev)
def S2_expval(params):
circuit(params, wires)
return qml.expval(S2)Notice that dev was created without a shots argument. This means it will calculate the exact expectation value of the Hamiltonian, and can do so in a single evaluation.
Let's now initialize some random values and evaluate the energy and spin. The total spin
def spin(params):
return -0.5 + np.sqrt(1 / 4 + S2_expval(params))np.random.seed(1967)
params = np.random.normal(0, np.pi, len(singles) + len(doubles))The energy and total spin are then
print("Energy:", float(energy_expval(params)))
print("Spin: ", float(spin(params)))Energy: -0.2730496738441154 Spin: 0.11000908988780544
/home/drbeach/miniconda3/envs/decorator/lib/python3.10/site-packages/pennylane_lightning/core/_serialize.py:133: ComplexWarning: Casting complex values to real discards the imaginary part coeffs = np.array(unwrap(observable.coeffs)).astype(self.rtype)
Since we have picked random parameters, the measured energy does not correspond to the ground state energy and the prepared state is not an eigenstate of the total-spin operator. We must now train the parameters to find the minimum energy.
Minimizing the energy
The energy can be minimized by choosing an optimizer and running the standard optimization loop:
# Lets choose RMSPropOptimizer.
# Other alternatives: GradientDescentOptimizer, AdagradOptimizer, AdamOptimizer, ...
opt = qml.RMSPropOptimizer(stepsize=0.2)iterations = 7import time
from braket.jobs.metrics import log_metric
def run_vqe(energy_expval, spin, opt, initial_params, iterations):
energies = []
spins = []
params = initial_params
start = time.time()
for i in range(iterations):
params = opt.step(energy_expval, params)
e = energy_expval(params)
s = spin(params)
energies.append(e)
spins.append(s)
log_metric(metric_name="energy", value=e, iteration_number=i)
print(f"Completed iteration {i + 1}")
print("Energy:", e)
print("Total spin:", s)
print("----------------")
print(f"Optimized energy: {e} Ha")
print(f"Corresponding total spin: {s}")
print(f"Elapsed: {time.time() - start} s")
return energies, spinsenergies, spins = run_vqe(energy_expval, spin, opt, params, iterations)/home/drbeach/miniconda3/envs/decorator/lib/python3.10/site-packages/pennylane_lightning/core/_serialize.py:133: ComplexWarning: Casting complex values to real discards the imaginary part coeffs = np.array(unwrap(observable.coeffs)).astype(self.rtype)
Metrics - timestamp=1697139811.2718408; energy=-0.5602851198326602; iteration_number=0; Completed iteration 1 Energy: -0.5602851198326602 Total spin: 0.13287789321454435 ---------------- Metrics - timestamp=1697139811.3152246; energy=-0.7773309017116151; iteration_number=1; Completed iteration 2 Energy: -0.7773309017116151 Total spin: 0.09488198120583491 ---------------- Metrics - timestamp=1697139811.3421786; energy=-1.0083677187505569; iteration_number=2; Completed iteration 3 Energy: -1.0083677187505569 Total spin: 0.04940760029256386 ---------------- Metrics - timestamp=1697139811.369656; energy=-1.1060574430621748; iteration_number=3; Completed iteration 4 Energy: -1.1060574430621748 Total spin: 0.01902625475224251 ---------------- Metrics - timestamp=1697139811.3969185; energy=-1.1301112509281241; iteration_number=4; Completed iteration 5 Energy: -1.1301112509281241 Total spin: 0.0056347819636390906 ---------------- Metrics - timestamp=1697139811.424453; energy=-1.1351064397742514; iteration_number=5; Completed iteration 6 Energy: -1.1351064397742514 Total spin: 0.0012625902560892133 ---------------- Metrics - timestamp=1697139811.4511507; energy=-1.136021579371058; iteration_number=6; Completed iteration 7 Energy: -1.136021579371058 Total spin: 0.00022566866798245933 ---------------- Metrics - timestamp=1697139811.4915578; energy=-1.1361670884772965; iteration_number=7; Completed iteration 8 Energy: -1.1361670884772965 Total spin: 3.215466396355726e-05 ---------------- Metrics - timestamp=1697139811.5190527; energy=-1.1361867905470242; iteration_number=8; Completed iteration 9 Energy: -1.1361867905470242 Total spin: 3.626608394258213e-06 ---------------- Metrics - timestamp=1697139811.5578496; energy=-1.1361890094090557; iteration_number=9; Completed iteration 10 Energy: -1.1361890094090557 Total spin: 3.158279299197986e-07 ---------------- Optimized energy: -1.1361890094090557 Ha Corresponding total spin: 3.158279299197986e-07 Elapsed: 0.35576891899108887 s
The exact value for the ground state energy of molecular hydrogen has been theoretically calculated as -1.136189454088 Hartrees (Ha). Notice that the optimized energy is off by less than
Let's visualize how the two quantities evolved during optimization:
import matplotlib.pyplot as plt
theory_energy = -1.136189454088
theory_spin = 0
plt.hlines(theory_energy, 0, iterations, colors="black")
plt.plot(energies, "o-")
plt.xlabel("Steps")
plt.ylabel("Energy")
axs = plt.gca()We have learned how to efficiently find the ground state energy of a molecule using the PennyLane/Braket pipeline!
Scaling up with hybrid jobs
Next, we scale up the experiment using Amazon Braket Hybrid Jobs.
You can do this by annotating your code with the @hybrid_job decorator. We set device="local:pennylane/lightning" since we are using the lightning.qubit simulator.
We also change the classical instance to a larger instance called "ml.c5.xlarge".
from braket.jobs import InstanceConfig, hybrid_job
from braket.tracking import Tracker
large_instance = InstanceConfig(instanceType="ml.c5.xlarge")
@hybrid_job(
device="local:pennylane/lightning",
instance_config=large_instance,
data_format="pickle",
)
def run_large_vqe(iterations):
task_tracker = Tracker().start() # track Braket quantum tasks costs
dev = qml.device("lightning.qubit", wires=qubits)
params = np.random.normal(0, np.pi, len(singles) + len(doubles))
@qml.qnode(dev)
def energy_expval(params):
circuit(params, wires)
return qml.expval(h)
@qml.qnode(dev)
def S2_expval(params):
circuit(params, wires)
return qml.expval(S2)
def spin(params):
return -0.5 + np.sqrt(1 / 4 + S2_expval(params))
energies, spins = run_vqe(energy_expval, spin, opt, params, iterations)
return {
"energies": energies,
"spins": spins,
"braket_tasks_cost": task_tracker.qpu_tasks_cost() + task_tracker.simulator_tasks_cost(),
}In the following cell, we create the hybrid job by calling the function normally. Note that the function returns the handle to the hybrid job which runs asynchronously. Once the hybrid job has completed, we retrieve the results.
job = run_large_vqe(iterations=15)
print(job)AwsQuantumJob('arn':'arn:aws:braket:us-west-1:961591465522:job/run-large-vqe-1697139811768')
You retrieve the results when the algorithm is complete with
%%time
result = job.result(allow_pickle=True)
print(result){'energies': [array(-0.55187225), array(-0.70005221), array(-0.89729232), array(-1.03687194), array(-1.09926479), array(-1.1235433), array(-1.13217906), array(-1.1350177), array(-1.13587664), array(-1.13611398), array(-1.13617316), array(-1.13618628), array(-1.1361888), array(-1.1361891), array(-1.13618883), array(-1.13618784), array(-1.13618452), array(-1.13617163), array(-1.13611418), array(-1.13582078), array(-1.13416153), array(-1.12555889), array(-1.10835438), array(-1.11654572), array(-1.12851297), array(-1.13326765), array(-1.13489124), array(-1.13548314), array(-1.13571512), array(-1.13579802), array(-1.13579607), array(-1.13571334), array(-1.13550525), array(-1.13504536), array(-1.13403895), array(-1.1319386), array(-1.12839979), array(-1.12513719), array(-1.12533626), array(-1.12823079), array(-1.1310321), array(-1.13281667), array(-1.13379207), array(-1.13427242), array(-1.13444455), array(-1.13438512), array(-1.13409575), array(-1.13352525), array(-1.13260457), array(-1.13134793), array(-1.13003231), array(-1.12921109), array(-1.1288403), array(-1.11658034), array(-1.11047216), array(-1.12219718), array(-1.10627973), array(-1.1236975), array(-1.13135412), array(-1.13243005), array(-1.13244886), array(-1.13202399), array(-1.13149092), array(-1.13098935), array(-1.13070568), array(-1.13068291), array(-1.13087401), array(-1.13099708), array(-1.13071549), array(-1.12901891), array(-1.1259372), array(-1.12274542), array(-1.12239518), array(-1.12201351), array(-1.12577647), array(-1.12791115), array(-1.12943676), array(-1.12956896), array(-1.12972941), array(-1.12965116), array(-1.12982715), array(-1.12971169), array(-1.12954916), array(-1.12859317), array(-1.12765628), array(-1.12607814), array(-1.12582959), array(-1.12546759), array(-1.12678979), array(-1.12732392), array(-1.12836158), array(-1.12843569), array(-1.1288221), array(-1.12868107), array(-1.12889865), array(-1.12861641), array(-1.12861102), array(-1.12794607), array(-1.12775452), array(-1.1269936)], 'spins': [0.15969152963337319, 0.14300644899470527, 0.10138139829601989, 0.05841186821393585, 0.029397735445742645, 0.012683239937044788, 0.004709419412889271, 0.0015356565172549574, 0.0004406833589031267, 0.00011150185434460891, 2.4696881266605963e-05, 4.754753258340294e-06, 7.841747289294432e-07, 1.0911400150082073e-07, 1.2487133216332325e-08, 1.1431044999454798e-09, 7.994138684352947e-11, 4.0460967909439205e-12, 1.354472090042691e-13, 2.55351295663786e-15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.1102230246251565e-16, 1.2212453270876722e-15, 1.2989609388114332e-14, 2.1038726316646716e-13, 2.903122187092322e-12, 6.226463789005265e-11, 1.0713994136324345e-09, 3.111918500664501e-08, 6.453371255155105e-07, 2.5295425650662118e-05, 0.0006400722119676017, 0.022025029044442035, 0.023385561885357342, 0.00014311106120790118, 0.021705432372581934, 0.008924076514024493, 0.0011652488654905202, 0.0002831588044178712, 4.200450060132255e-05, 9.251351257621998e-06, 1.8141004576310849e-07, 1.6834871601201229e-06, 7.732131861448721e-06, 3.198347546606861e-05, 7.046684828326821e-05, 0.00026530689107850947, 0.0006129610537283225, 0.002087209960787595, 0.00290374071321875, 0.0020472894698817523, 2.360806709422736e-05, 0.0017449222730436809, 0.0014809373925386282, 0.0009302646937726644, 0.00023724624727050614, 8.150100063286647e-05, 4.984330999269204e-06, 4.925328396909734e-06, 3.6265129713664024e-05, 0.00013657677273337665, 0.00022008908595805288, 0.00044302657230532727, 0.0003533976575068598, 0.00020351535710572133, 5.43610939485184e-07, 0.00011382311021634894, 0.00019422317002082412, 0.00021497283602989192, 8.70192302054873e-05, 4.1234804362910715e-05, 4.038649503712577e-06, 1.124619311898556e-06, 1.3053006908458897e-05, 4.337371918849975e-05, 5.271085643610007e-05, 7.287264124733461e-05, 3.572624691072779e-05, 1.0876786015745488e-05], 'braket_tasks_cost': Decimal('0')}
CPU times: user 118 ms, sys: 8.17 ms, total: 126 ms
Wall time: 1min 55s
Now we plot the results from the hybrid job
import matplotlib.pyplot as plt
theory_energy = -1.136189454088
theory_spin = 0
plt.hlines(theory_energy, 0, iterations, colors="black")
plt.plot(result["energies"], "o-")
plt.xlabel("Steps")
plt.ylabel("Energy")Text(0, 0.5, 'Energy')
Running VQE with shots>0
In the above example, we computed exact expectation values of our Hamiltonians, and were able to do so with a single evaluation each. This relies on the simulator having access to the exact state, and isn't possible if the device estimates the expectation with shots, like in the case of a QPU.
Suppose we want to estimate the expectation value of the electronic Hamiltonian h with a finite number of shots. This Hamiltonian is composed of 15 individual observables that are tensor products of Pauli operators:
print("Number of Pauli terms in h:", len(h.operands))Number of Pauli terms in h: 15
A straightforward approach to measuring the expectation value would be to run the circuit 15 times, each time measuring one of the Pauli terms that form part of the Hamiltonian h. However, we can be more efficient. The Pauli terms can be separated into groups (see PennyLane's grouping module) that share a basis, and thus can be measured concurrently on a single circuit. Elements of each group are known as qubit-wise commuting observables. The Hamiltonian h can be split into 5 groups:
original_coeffs, operands = h.terms()
groups, coeffs = qml.pauli.group_observables(operands, original_coeffs)
print("Number of qubit-wise commuting groups:", len(groups))Number of qubit-wise commuting groups: 5
Practically, this means that instead of executing 15 separate circuits, we just need to execute 5. This saving can become even more pronounced as the number of Pauli terms in the Hamiltonian increases. For example, switching to a larger molecule or a different chemical basis set can increase both the number of qubits and the number of terms.
Fortunately, the PennyLane/Braket pipeline has builtin support for pre-grouping the observables in a Hamiltonian to minimize the number of device executions, saving both runtime and simulation fees when using remote devices.

To take advantage of grouping, we must instruct PennyLane to break up each Hamiltonian into qubit-wise commuting groups. We do this by computing the groups and storing them with the Hamiltonian; the device will use this information when to create the circuits to run.
h.compute_grouping()
S2.compute_grouping()Next, we instantiate the device, but this time with nonzero shots:
dev = qml.device("braket.local.qubit", wires=qubits, shots=5000)And redefine our cost functions
wires = dev.wires.tolist()
@qml.qnode(dev)
def energy_expval(params):
circuit(params, wires)
return qml.expval(h)
@qml.qnode(dev)
def S2_expval(params): # noqa: F811
circuit(params, wires)
return qml.expval(S2)
def spin(params):
return -0.5 + np.sqrt(1 / 4 + S2_expval(params))Finally, we run the VQE experiment and get our expectation estimates:
energies, spins = run_vqe(energy_expval, spin, opt, params, iterations)Metrics - timestamp=1697139937.3019145; energy=-0.5412825955158931; iteration_number=0; Completed iteration 1 Energy: -0.5412825955158931 Total spin: (0.15326105042318272+0j) ---------------- Metrics - timestamp=1697139944.412684; energy=-0.6936120312135796; iteration_number=1; Completed iteration 2 Energy: -0.6936120312135796 Total spin: (0.1396092557178953+0j) ---------------- Metrics - timestamp=1697139951.4129083; energy=-0.8906199472711986; iteration_number=2; Completed iteration 3 Energy: -0.8906199472711986 Total spin: (0.09703433737097567+0j) ---------------- Metrics - timestamp=1697139958.4950173; energy=-1.0433105151800557; iteration_number=3; Completed iteration 4 Energy: -1.0433105151800557 Total spin: (0.05785302723925401+0j) ---------------- Metrics - timestamp=1697139965.4518325; energy=-1.0992473165501542; iteration_number=4; Completed iteration 5 Energy: -1.0992473165501542 Total spin: (0.024499761677734266+0j) ---------------- Metrics - timestamp=1697139972.4262433; energy=-1.1281011541233785; iteration_number=5; Completed iteration 6 Energy: -1.1281011541233785 Total spin: (0.012347538297979854+0j) ---------------- Metrics - timestamp=1697139979.4071403; energy=-1.132303797965168; iteration_number=6; Completed iteration 7 Energy: -1.132303797965168 Total spin: (-0.007658654996353209+0j) ---------------- Metrics - timestamp=1697139986.409697; energy=-1.1349500459612611; iteration_number=7; Completed iteration 8 Energy: -1.1349500459612611 Total spin: (0.0019462122578475238+0j) ---------------- Metrics - timestamp=1697139993.3897896; energy=-1.1356074477133202; iteration_number=8; Completed iteration 9 Energy: -1.1356074477133202 Total spin: (0.0023445033042561736+0j) ---------------- Metrics - timestamp=1697140000.283897; energy=-1.1373271109993635; iteration_number=9; Completed iteration 10 Energy: -1.1373271109993635 Total spin: (-0.0016527315214822647+0j) ---------------- Optimized energy: -1.1373271109993635 Ha Corresponding total spin: (-0.0016527315214822647+0j) Elapsed: 69.98771715164185 s
We see that we get pretty close to the values found through exact calculation! In this experiment, we used the local simulator, but the same steps apply for QPUs, or any simulator that uses a nonzero number of shots!
hydrogen_molecule folder contains additional molecular structure files for different atomic separations of molecular hydrogen. Pick one of the separations and find the ground state energy. How does the ground state energy change with atomic separation?
job_cost = job.result(allow_pickle=True)["braket_tasks_cost"]
print(
"Note: Charges shown are estimates based on your Amazon Braket simulator and quantum processing unit (QPU) task usage. Estimated charges shown may differ from your actual charges. Estimated charges do not factor in any discounts or credits, and you may experience additional charges based on your use of other services such as Amazon Elastic Compute Cloud (Amazon EC2).",
)
print(f"Estimated cost to run this example: {job_cost:.3f} USD")Note: Charges shown are estimates based on your Amazon Braket simulator and quantum processing unit (QPU) task usage. Estimated charges shown may differ from your actual charges. Estimated charges do not factor in any discounts or credits, and you may experience additional charges based on your use of other services such as Amazon Elastic Compute Cloud (Amazon EC2). Estimated cost to run this example: 0.000 USD
This entry was created automatically from publicly available records. QCR links to public sources and only stores repository content where the license permits redistribution.
Versions
Cite all versions? Use the base QCR ID to always reference the latest version of this entry.
Join the Discussion
Comments (0)
No comments yet. Be the first to share your thoughts!