Tangelo hands-on: VQE framework

Before you jump in

This hands-on notebook complements existing tutorials, documentation and the developer notes available in the Tangelo GitHub repositories, which present content in much more depth.

You will come across code cells that require you to change code or fill in the blanks in order to achieve a desired outcome. There may be many ways to solve these simple exercises, and you are encouraged to explore.


Getting started

Please have a look at the landing page of this repository for guidance about how to deploy these notebooks and get started easily.

In order to complete this hands-on tutorial, we recommend you use the latest version of Tangelo. If you encounter errors related to missing Python packages (classical chemistry backend, quantum circuit simulator…), you can install them on-the-fly by typing !pip install <package-name> in a new code cell, and then restart the Jupyter notebook kernel.

# If Tangelo is not found in your current environment, this cell installs all dependencies required for this hands-on
try:
    import tangelo
except ModuleNotFoundError:
    !pip install git+https://github.com/goodchemistryco/Tangelo.git@develop  --quiet
    !pip install pyscf
    !pip install qulacs qiskit qiskit-aer

For this hands-on, we recommend you consider the following resources: - the VQE tutorial notebook - Optional: Other tutorials tagged with VQA. VQE can be used to compute excited states, or take as input a user-defined ansatz or Hamtiltonian. Moreover, there are quite a few variants of VQE available in Tangelo (ADAPT, etc). - Optional: Introduction notebook on the basics of modelling a chemical system (Second Quantized molecule, Hartree-Fock, FCI, CCSD, active space selection …)


Hands-on

The use cases for this hands-on will be small molecular systems. In-depth understanding of the chemistry behind second quantization, Hartree-Fock, active space selection and classical solvers is nice to have, but not necessary to go throuygh these hands-on. Feel free to have a look at the list of resources recommended in the first section of this hands-on if you would like to know more about these topics.

We’ll start with the iconic second quantized \(H_2\) molecule in sto-3g basis as an initial use case for this hands-on. We’ll quickly ramp up in higher basis sets and also play with \(H_2O\).

The cell below defines the molecule explicitly, specifying a geometry, charge, spin and basis set. Under the hood, a classical chemistry library (pyscf, psi4…) is used to create this object and compute its mean-field, which will enable more advanced calculations.

# Define H2 molecule in minimal basis set
from tangelo import SecondQuantizedMolecule

xyz_H2 = [("H", (0., 0., 0.)), ("H", (0., 0., 0.7414))]
mol_H2_sto3g = SecondQuantizedMolecule(xyz_H2, q=0, spin=0, basis="sto-3g")

mol = mol_H2_sto3g

Before we delve into quantum algorithms implementing variational approaches, let’s use classical solvers such as FCI and CCSD to compute references results for the ground state energy of our molecule. These classical solvers can be used to benchmark the accuracy of our variational algorithms later in this hands-on. The results are computed in Hartrees, and in practice an accuracy of 3 digits of precision (e.g about 1 mHa) is considered satisfying.

from tangelo.algorithms import FCISolver, CCSDSolver

# The FCI solver is the reference for chemists, but computational cost scales badly.
fci_solver = FCISolver(mol)
fci_energy = fci_solver.simulate()
print(f'FCI  energy for H2 ={fci_energy}')

# The CCSD solver has better scaling, but is less accurate.
ccsd_solver = CCSDSolver(mol)
ccsd_energy = ccsd_solver.simulate()
print(f'CCSD energy for H2 ={ccsd_energy}')
FCI  energy for H2 =-1.1372701746609042
CCSD energy for H2 =-1.1372701746673048

Level 1: VQE basics

The variational solvers in Tangelo currently work in 3 steps: - creation of solver object, which takes options (molecule, ansatz, backend, etc) as input - construction of the underlying objects (quantum circuits, initial parameters, …) - simulation of the algorithm

Q: Can you use the VQESolver class to compute the ground state energy of our \(H_2\) molecule? Use the default vqe_options below. You should find a result of about -1.137270168 Ha.

from tangelo.algorithms import VQESolver

vqe_options = {"molecule": mol}

# INSERT CODE HERE

# Print the optimal energy found by VQE and how far it is from FCI
print(f'VQE energy = {opt_energy} (difference with FCI = {abs(opt_energy - fci_energy)})')

A lot has happened under the hood, let’s highlight a few of them:

  • the molecule has been mapped into qubits using a qubit mapping
  • an ansatz circuit with variational parameters has been built
  • a backend (here a simulator) was to simulate quantum circuits
  • a classical optimizer has been chosen to drive the optimization of variational parameters

The cell below use the vars function in python which prints the attributes of an objects, and pprint to print things nicely. If we use that on your vqe_solver object, we should be able to understand what has happened in the previous cell better.

Q: Using the output of the cell below, can you find what qubit mapping, ansatz, backend and classical optimizer were used in your previous simulation ?

from pprint import pprint
pprint(vars(vqe_solver))
{'ansatz': <tangelo.toolboxes.ansatz_generator.uccsd.UCCSD object at 0x7fe03f9bee30>,
 'ansatz_options': {},
 'backend': <tangelo.linq.target.target_qulacs.QulacsSimulator object at 0x7fe04d857a60>,
 'backend_options': {'n_shots': None, 'noise_model': None, 'target': None},
 'builtin_ansatze': {<BuiltInAnsatze.UCCSD: <class 'tangelo.toolboxes.ansatz_generator.uccsd.UCCSD'>>,
                     <BuiltInAnsatze.pUCCD: <class 'tangelo.toolboxes.ansatz_generator.puccd.pUCCD'>>,
                     <BuiltInAnsatze.ILC: <class 'tangelo.toolboxes.ansatz_generator.ilc.ILC'>>,
                     <BuiltInAnsatze.UCCGD: <class 'tangelo.toolboxes.ansatz_generator.uccgd.UCCGD'>>,
                     <BuiltInAnsatze.VSQS: <class 'tangelo.toolboxes.ansatz_generator.vsqs.VSQS'>>,
                     <BuiltInAnsatze.QCC: <class 'tangelo.toolboxes.ansatz_generator.qcc.QCC'>>,
                     <BuiltInAnsatze.QMF: <class 'tangelo.toolboxes.ansatz_generator.qmf.QMF'>>,
                     <BuiltInAnsatze.UpCCGSD: <class 'tangelo.toolboxes.ansatz_generator.upccgsd.UpCCGSD'>>,
                     <BuiltInAnsatze.HEA: <class 'tangelo.toolboxes.ansatz_generator.hea.HEA'>>,
                     <BuiltInAnsatze.UCC3: <tangelo.toolboxes.ansatz_generator.rucc.RUCC object at 0x7fe09086fbe0>>,
                     <BuiltInAnsatze.UCC1: <tangelo.toolboxes.ansatz_generator.rucc.RUCC object at 0x7fe09086e620>>},
 'deflation_circuits': [],
 'deflation_coeff': 1,
 'initial_var_params': [2e-05, 0.03632537110234506],
 'molecule': SecondQuantizedMolecule(xyz=[('H', (0.0, 0.0, 0.0)),
                                          ('H',
                                           (0.0, 0.0, 0.7413999999999998))],
                                     q=0,
                                     spin=0,
                                     solver=<tangelo.toolboxes.molecular_computation.integral_solver_pyscf.IntegralSolverPySCF object at 0x7fe09086f4f0>,
                                     n_atoms=2,
                                     n_electrons=2,
                                     basis='sto-3g',
                                     ecp={},
                                     symmetry=False,
                                     uhf=False,
                                     mf_energy=-1.1166843870853411,
                                     mo_energies=array([-0.57797481,  0.66969867]),
                                     mo_occ=array([2., 0.]),
                                     n_mos=2,
                                     n_sos=4,
                                     active_occupied=[0],
                                     frozen_occupied=[],
                                     active_virtual=[1],
                                     frozen_virtual=[]),
 'optimal_circuit': <tangelo.linq.circuit.Circuit object at 0x7fe03fa00a90>,
 'optimal_energy': -1.1372701683419073,
 'optimal_var_params': array([-5.44105775e-05,  5.65245079e-02]),
 'optimizer': <bound method VQESolver._default_optimizer of <tangelo.algorithms.variational.vqe_solver.VQESolver object at 0x7fe03fa02650>>,
 'penalty_terms': None,
 'projective_circuit': None,
 'qubit_hamiltonian': (-0.0988639693354576+0j) [] +
(-0.04532220205287404+0j) [X0 X1 Y2 Y3] +
(0.04532220205287404+0j) [X0 Y1 Y2 X3] +
(0.04532220205287404+0j) [Y0 X1 X2 Y3] +
(-0.04532220205287404+0j) [Y0 Y1 X2 X3] +
(0.1711977490343296+0j) [Z0] +
(0.16862219158920955+0j) [Z0 Z1] +
(0.12054482205301811+0j) [Z0 Z2] +
(0.16586702410589216+0j) [Z0 Z3] +
(0.17119774903432963+0j) [Z1] +
(0.16586702410589216+0j) [Z1 Z2] +
(0.12054482205301811+0j) [Z1 Z3] +
(-0.22278593040418523+0j) [Z2] +
(0.17434844185575676+0j) [Z2 Z3] +
(-0.22278593040418523+0j) [Z3],
 'qubit_mapping': 'jw',
 'ref_state': None,
 'reference_circuit': <tangelo.linq.circuit.Circuit object at 0x7fe03fa028f0>,
 'simulate_options': {},
 'up_then_down': False,
 'verbose': False}

Q: Now that you’ve taken a peek inside the vqe_solver object, can you print: - the optimal parameters values found by VQE ? - the optimal circuit ?

# INSERT CODE HERE

Q: The simulate method runs the entire VQE algorithm, which performed many energy estimations along the way, driven by the classical optimizer. Can you instead perform a single energy estimation, using vqe_solver ? If you pass the optimal parameters as input, you should see that the output is the optimal energy.

# INSERT CODE HERE

Level 2: Impact of some of the options in VQE

A number of options are available in the VQE framework. In this hands-on, we only graze the surface but some of the additional resources at the beginning of this notebook are more comprehensive. Let’s see how some of choices impact the calculations. Some of these options require more information from the user, as they may be chemically-inspired. Others may work right away. Some combination of options may be incompatible. Tangelo is doing its best to return appropriate error messages when things don’t work out.

2a) Qubit mappings

Our VQE framework supports a number of qubit mappings, such as Jordan-Wigner ('jw'), Bravyi-Kitaev ('bk') or Symmetry-Conserving Bravyi-Kitaev ('bk'). But there’s more to life than these ! Tangelo also supports more uncommon ones, such as JKMN, Hardcore Boson (HCB), combinational mapping, and probably more in the future.

VQESolver provides a rather high-level interface, but all the building blocks used underneath are available to you ! You can check out the qubit_mapping toolbox in Tangelo if you’d like to learn more. The fermion_to_qubit_mapping function and the individual mapping functions will be helpful if you decide to explore this topic more in depth, or try to build custom Hamiltonians.

Q: Can you instantiate VQESolver objects using a few of these different qubit mappings, and see how they impact the complexity of the algorithm using the get_resources method and printing the qubit_hamiltonian?

for qb_mapping in ["JW", "SCBK", "JKMN"]:

    # INSERT CODE HERE
    vqe_options = 

    vqe_solver = VQESolver(vqe_options)
    vqe_solver.build()
    opt_energy = vqe_solver.simulate()

    # INSERT PRINT INSTRUCTIONS HERE

2b) Compute backends

Algorithms in Tangelo are backend-agnostic. That means that you do not have to rewrite your code if you want to run it on a different simulator or quantum device, switching between them is rather effortless. There’s a lot of quantum simulators out there, with different tradeoffs between speed, accuracy, maximal circuit size or ability to simulate noise.

Let’s revisit \(H_2\) in a bigger basis set (this means a more accurate representation, but more intensive calculations), and see what impact the choice of backend has on our experience.

# Tangelo has some built-in molecules that are often reused in tests and tutorials.
from tangelo.molecule_library import mol_H2_321g

mol = mol_H2_321g

We’ll use FCI to get the exact value of the ground state of this molecule, for reference:

fci_energy = FCISolver(mol).simulate()
print(fci_energy)
-1.1478303189235124
# Before we attempt to simulate anything: let's get an idea of the compute requirements
# We'd rather not try to simulate something that we think is not worth it.

vqe_options = {"molecule": mol}
vqe_solver = VQESolver(vqe_options)
vqe_solver.build()
print(vqe_solver.get_resources())
{'qubit_hamiltonian_terms': 185, 'circuit_width': 8, 'circuit_depth': 586, 'circuit_2qubit_gates': 432, 'circuit_var_gates': 52, 'vqe_variational_parameters': 9}

Q: Can you fill in the cell below to run the same VQE approach on this molecule, with the different backends below ? What do you observe ?

This cell may take a few minutes to run, depending on the machine used. Feel free to continue reading, interrupt the execution of the cell, or revert the molecule to our smaller original use case mol_H2_sto3g.

import time

for b in ["qulacs", "cirq", "qiskit"]:

    # INSERT CODE HERE
    vqe_options = 

    # Start timer
    t_start = time.time()

    # Run VQE algorithm
    vqe_solver = VQESolver(vqe_options)
    vqe_solver.build()
    opt_energy = vqe_solver.simulate()

    # Stop timer, print elapsed time
    t_stop = time.time()
    print(f"{b:10} :: {t_stop - t_start:6.3f} s \t (energy = {opt_energy})")
qulacs     ::  2.059 s   (energy = -1.1478302233980724)
cirq       :: 69.248 s   (energy = -1.1478302233982864)
qiskit     :: 246.155 s      (energy = -1.147830223398106)

Optional boss fight: \(H_2O\) sto-3g

Boss fight, boss fight !

H2O

\(H_2O\) is a humble molecule, but already quite a challenge with VQE. Assume you want to compute the ground state energy of \(H_2O\) in the sto-3g basis set using VQE. Use everything at your disposal here, and don’t be afraid to explore (but maybe use get_resources before blindly jumping into simulate).

It’s still pretty easy for FCI on a laptop though, and we can get a reference value:

from tangelo.molecule_library import mol_H2O_sto3g
mol = mol_H2O_sto3g

fci_H2O_energy = FCISolver(mol_H2O_sto3g).simulate()
print(f"H2O FCI energy = {fci_H2O_energy}")
H2O FCI energy = -75.01277542934052

On your machine, VQE may take a while. But it is possible for a laptop to handle it in a few minutes or less. It is already valuable for you to explore ideas without performing any simulation: what “levers” do you have to shorten time-to-solution, improve accuracy and reduce resource requirements ?

Q: Can you compute the ground state energy of this molecule ? - How close can you get to FCI reference results ? - How much can you reduce computational requirements ? - How low can you bring your time-to-solution ?

# CHANGE CODE AND EXPLORE

b = "qulacs"
backend_options = {"target":'qulacs'}
vqe_options = {"molecule": mol, "qubit_mapping": 'scbk', "backend_options": backend_options, "verbose": True}

# --------------------

# Start timer
t_start = time.time()

# Run VQE algorithm
vqe_solver = VQESolver(vqe_options)
vqe_solver.build()
print(vqe_solver.get_resources())
opt_energy = vqe_solver.simulate()

# Stop timer, print elapsed time
t_stop = time.time()
print(f"{b:10} :: {t_stop - t_start:3.3f} s \t (energy = {opt_energy})")

Final words

After this hands-on, you now understand how to use the VQE algorithm and its options. It is a very flexible NISQ algorithm which can yield approaches with very differing accuracies and compute requirements depending on the use cases. A lot of research is being done on the topic, in order to apply this to current quantum computers.

Do not hesitate to explore the resources mentioned at the beginning of this notebook at your own pace to learn more about the topics discussed here. There is so much more we can do with variational algortihms.