Applications Built on XACC

XACC is designed to serve as a core framework for the development of hybrid quantum-classical applications. Its extensible interfaces can be implemented to provide application-specific problems, compilers, and backend accelerators. Here we detail specific applications leveraging XACC.

XACC-VQE

The variational quantum eigensolver algorithm provides (VQE) a hybrid quantum-classical approach for computing the lowest eigenvalue of a Hamiltonian by positing some parameterized circuit ansatz, and classically optimizing those parameters to find that optimal, lowest eigenvalue.

Often times these Hamiltonians are expressed as second quantized fermionic Hamiltonians that require some additional compile steps to map to a spin Hamiltonian amenable for use with quantum computation.

The XACC-VQE application builds off of XACC extensible interfaces to provide a compilation and execution workflow for high-level problems leveraging the VQE algorithm. It enables the expression of XACC quantum kernels that model high-level second-quantized fermionic Hamiltonians and are compiled to the XACC gate model intermediate representation. It provides C++ and Python APIs for executing this algorithm, as well as a command line utility that takes input information on the Hamiltonian and the circuit ansatz to execute the VQE algorithm.

Since it builds off of the XACC framework, XACC-VQE provides a way for users to program, compile, and execute VQE in a manner that is independent of the underlying gate model QPU. Therefore running VQE on a set of different quantum computers can be accomplished via switching a string (‘ibm’ -> ‘rigetti’).

Architecture Overview

The core abstraction for the XACC-VQE application is the concept of a VQETask, which is an interface representing some VQE-like action the user would like to accomplish. Common VQETask implementations are the VQEMinimizeTask and the ComputeEnergyVQETask which run the full VQE algorithm or just compute the energy at a given parameter configuration, respectively. The VQETask leverages the XACC CppMicroServices infrastructure and therefore is an extensible plugin.

The VQETask delegates to the ComputeEnergyVQETask leveraging it throughout the variational loop. Furthermore, it delegates to an interface called VQEBackend which abstracts the classical optimizer to be used as part of the VQE parameter search. This is also an extensible interface plugged into XACC, and implementations exist for Nelder-Mead, Bayesian Optimization, Conjugate Gradient, Particle Swarm, etc. (its extensible, so more to come in the future).

XACC-VQE provides an xacc::Compiler implementation called the FermionCompiler which takes XACC quantum kernel source strings representing a fermionic second-quantized Hamiltonian and compiles it to the gate model XACC IR. To do so, it delegates to an extensible XACC IRTransformation that represents Jordan-Wigner, Bravyi-Kitaev, or any other fermion-to-spin transformation (JW, BK are currently implemented). The FermionCompiler reads quantum kernels with the following language syntax

__qpu__ kernel(AcceleratorBuffer b) {
   1.0 1 1 0 0 // adag_1 a_0
   2.0 2 1 3 1 1 0 0 0 // adag_2 adag_3 a_1 a_0
   ...
}

Each line in this language represents a term of the Hamiltonian. The first value is the term coefficient, followed by (site, creation/annhilation (1/0)) pairs.

The XACC-VQE C++ and Python API expose a class called the PauliOperator that models a spin Hamiltonian (terms made up of I, X, Y, Z on various qubit sites, with an associated coefficient). Instantiation of these types in Python is accomplished in the following way

ham = PauliOperator(5.906709445) + \
     PauliOperator({0:'X',1:'X'}, -2.1433) + \
     PauliOperator({0:'Y',1:'Y'}, -2.1433) + \
     PauliOperator({0:'Z'}, .21829) + \
     PauliOperator({1:'Z'}, -6.125)

Note the PauliOperator respects the usual algebraic manipulations on these types of Hamiltonians and auto-simplifies itself during the process. Its constructor takes a map of site:Pauli key-value pairs, and the term coefficient.

XACC-VQE leverages the extensible IRGenerator interface to auto-generate problem-specific VQE circuit ansatz’s. Currently implemented are the UCCSD (unitary coupled cluster singlet doublet) and HWE (hardware efficient) IRGenerators.

Installation

With XACC installed, run the following to build XACC-VQE

$ git clone --recursive https://github.com/ornl-qci/xacc-vqe
$ cd xacc-vqe && mkdir build && cd build
$ cmake .. -DXACC_DIR=~/.xacc -DPYTHON_INCLUDE_DIR=/usr/include/python3.5 (or wherever XACC and Python are installed)
$ make install

This will build and install all XACC-VQE provided plugins as well as the xacc-vqe command line utility and the xaccvqe Python module. xacc-vqe will be installed to $XACC_DIR/bin.

If you installed XACC via pip, then you can run

$ python -m pip install xacc-vqe (with --user if you used that flag for your xacc install)

xacc-vqe Command Line

Suppose we have a file h2-sto3g.hpp with the following XACC quantum kernel

__qpu__ kernel() {
   0.7080240949826064
   -1.248846801817026 0 1 0 0
   -1.248846801817026 1 1 1 0
   -0.4796778151607899 2 1 2 0
   -0.4796778151607899 3 1 3 0
   0.33667197218932576 0 1 1 1 1 0 0 0
   0.0908126658307406 0 1 1 1 3 0 2 0
   0.09081266583074038 0 1 2 1 0 0 2 0
   0.331213646878486 0 1 2 1 2 0 0 0
   0.09081266583074038 0 1 3 1 1 0 2 0
   0.331213646878486 0 1 3 1 3 0 0 0
   0.33667197218932576 1 1 0 1 0 0 1 0
   0.0908126658307406 1 1 0 1 2 0 3 0
   0.09081266583074038 1 1 2 1 0 0 3 0
   0.331213646878486 1 1 2 1 2 0 1 0
   0.09081266583074038 1 1 3 1 1 0 3 0
   0.331213646878486 1 1 3 1 3 0 1 0
   0.331213646878486 2 1 0 1 0 0 2 0
   0.09081266583074052 2 1 0 1 2 0 0 0
   0.331213646878486 2 1 1 1 1 0 2 0
   0.09081266583074052 2 1 1 1 3 0 0 0
   0.09081266583074048 2 1 3 1 1 0 0 0
   0.34814578469185886 2 1 3 1 3 0 2 0
   0.331213646878486 3 1 0 1 0 0 3 0
   0.09081266583074052 3 1 0 1 2 0 1 0
   0.331213646878486 3 1 1 1 1 0 3 0
   0.09081266583074052 3 1 1 1 3 0 1 0
   0.09081266583074048 3 1 2 1 0 0 1 0
   0.34814578469185886 3 1 2 1 2 0 3 0
}

and we would like to use the UCCSD ansatz to compute the ground state energy of this corresponding Hamiltonian. One could run the following

$ xacc-vqe -f h2-sto3g.hpp -t vqe --n-electrons 2

To run the compute-energy task, one could

$ xacc-vqe -f h2-sto3g.hpp -t compute-energy --n-electrons 2 --vqe-parameters "0.0,-.05"

To run with a custom ansatz written as an XACC quantum kernel in a file ansatz.hpp,

$ xacc-vqe -f h2-sto3g.hpp -a ansatz.hpp -t vqe --n-electrons 2

The previous examples will run by default on the TNQVM Accelerator (if installed). To switch just pass --accelerator ACCELERATORNAME to the command line arguments.

Python API

XACC-VQE exposes a Python API to enable ease of scripting for the VQE algorithm targeting available quantum computers. In essence, the Python API exposes two functions: compile and execute. compile takes as input a high-level fermionic Hamiltonian represented as a XACC quantum kernel string (just like the one above for H2) and runs the appropriate FermionCompiler to map it to a PauliOperator instance. Imagine we had the above quantum kernel as a string in Python, h2Src

import xaccvqe as vqe
pauliOp = vqe.compile(h2Src)
print(pauliOp)

would produce the following PauliOperator output

(0.174073,0) Z2 Z3 + (0.1202,0) Z1 Z3 + (0.165607,0) Z1 Z2 + (0.165607,0) Z0 Z3 +
(0.1202,0) Z0 Z2 + (-0.0454063,0) Y0 Y1 X2 X3 + (-0.220041,0) Z3 + (-0.106477,0) +
(0.17028,0) Z0 + (-0.220041,0) Z2 + (0.17028,0) Z1 + (-0.0454063,0) X0 X1 Y2 Y3 +
(0.0454063,0) X0 Y1 Y2 X3 + (0.168336,0) Z0 Z1 + (0.0454063,0) Y0 X1 X2 Y3

XACC-VQE integrates nicely with OpenFermion. As such this compile API function can also take FermionOperators as input and compile them to PauliOperators. If one has an OpenFermion QubitOperator, xaccvqe also provides a QubitOperator2XACC function that maps the QubitOperator to a PauliOperator.

The execute method takes 3 arguments. The first is a representation of the Hamiltonian provided as a PauliOperator or a FermionOperator. The second is the AcceleratorBuffer to operate on (the user keeps this reference until after execution in order to retrieve the results of the computation). And the final argument is a Python **kwargs key-value dictionary of parameters that direct the overall compilation and execution workflow. This dictionary should detail the 'task', 'ansatz', 'vqe-params', and 'accelerator' keys.

Here are some examples of using the compile and execute API. This will run the H2 example, with the default ansatz of UCCSD.

import xacc
import xaccvqe as vqe
xacc.Initialize()

# See above kernel for full definition
h2Src = '__qpu__ h2(AcceleratorBuffer ab) {...}'

pauliOp = vqe.compile(h2Src)

qpu = xacc.getAccelerator('ibm')
qubits = qpu.createBuffer('q',4)

vqe.execute(pauliOp, qubits, **{'task':'vqe', 'accelerator':qpu.name()})

print(qubits.getInformation('vqe-energy'))
print(qubits.getInformation('vqe-angles'))

xacc.Finalize()

Running with a custom ansatz can be seen in the below example (ansatz expressed as xacc quantum kernel written in Quil)

import xacc, xaccvqe as vqe
from xaccvqe import PauliOperator

xacc.Initialize(['--compiler','quil'])

qpu = xacc.getAccelerator('rigetti')
qubits = qpu.createBuffer('q',2)

ham = PauliOperator(5.906709445) + \
     PauliOperator({0:'X',1:'X'}, -2.1433) + \
     PauliOperator({0:'Y',1:'Y'}, -2.1433) + \
     PauliOperator({0:'Z'}, .21829) + \
     PauliOperator({1:'Z'}, -6.125)

src = """__qpu__ f(AcceleratorBuffer b, double t0) {
           X 0
           RY(t0) 1
           CNOT 1 0
        }"""
ansatz = xacc.compileKernel(qpu, src)

vqe.execute(pauliOp, qubits, **{'task':'vqe', 'ansatz':ansatz, 'accelerator':qpu.name()})

print(qubits.getInformation('vqe-energy'))
print(qubits.getInformation('vqe-angles'))

xacc.Finalize()

Python JIT VQE Decorator

The easiest way to run the VQE algorithm with XACC-VQE is to leverage the xacc.qpu decorator. Using the algo decorator argument, it is possible to specify that the function will run VQE with the given ansatz ciruit and parameters. This gives users an opportunity to define the VQE ansatz as a single Python function, and through this algorithmic decoration, execute the VQE algorithm according to the other decorator arguments. Let’s look at an example

import xacc
import xaccvqe
from xaccvqe import PauliOperator

xacc.Initialize()

tnqvm = xacc.getAccelerator('tnqvm')
buffer = tnqvm.createBuffer('q', 2)

ham = PauliOperator(5.906709445) + \
     PauliOperator({0:'X',1:'X'}, -2.1433) + \
     PauliOperator({0:'Y',1:'Y'}, -2.1433) + \
     PauliOperator({0:'Z'}, .21829) + \
     PauliOperator({1:'Z'}, -6.125)

@xacc.qpu(algo='vqe', accelerator=tnqvm, observable=ham)
def ansatz(buffer, t0):
   X(0)
   Ry(t0, 1)
   CNOT(1, 0)

# Run VQE with given ansatz kernel
initAngle = .5

ansatz(buffer, initAngle)

print('Energy = ', buffer.getInformation('vqe-energy'))
print('Opt Angles = ', buffer.getInformation('vqe-angles'))

After running the VQE algorithm with the above example script, the AcceleratorBuffer now stores the information regarding the results of the executions. The XACC Python API enables the user to easily access this information stored in the AcceleratorBuffer and its children.

{
 "AcceleratorBuffer": {
     "name": "q",
     "size": 2,
     "Information": {
         "ansatz-qasm": "X q0\nRy(0.594531) q1\nCNOT q1,q0\n",
         "circuit-depth": 2,
         "vqe-angles": [
             0.5945312500000002
         ],
         "vqe-energy": -1.7491552234943558,
         "vqe-nQPU-calls": 0
     },
     "Measurements": {},
     "Children": [
         {
             "name": "Z1",
             "Information": {
                 "coefficient": -6.125,
                 "exp-val-z": 0.8775825618903725,
                 "kernel": "Z1",
                 "parameters": [
                     0.5
                 ]
             },

     ...
                 {
             "name": "X0X1",
             "Information": {
                 "coefficient": -2.1433,
                 "exp-val-z": 0.5601204983247997,
                 "kernel": "X0X1",
                 "parameters": [
                     0.5945312500000002
                 ]
             },
             "Measurements": {}
         }
     ]
 }
...

For example, if the user wanted to generate a file consisting of the relevant, unique results of the VQE algorithm executions stored in the AcceleratorBuffer children as comma-separated values, it can be done as shown below, using the getAllUnique, getChildren, and getInformation methods.

ps = buffer.getAllUnique('parameters')
csv_name = "VQE_example_results"
f = open(csv_name+".csv", 'w')
for p in ps:
    f.write(str(p).replace('[', '').replace(']', ''))
    energy = 0.0
    for c in buffer.getChildren('parameters', p):
        exp = c.getInformation('exp-val-z')
        energy += exp * c.getInformation('coefficient')
        f.write(','+str(c.getInformation('exp-val-z')))
    f.write(','+str(energy)+'\n')
f.close()