Basics¶
Here we demonstrate leveraging the XACC framework for various quantum-classical programming tasks. We provide examples in both C++ and Python.
Accelerator Buffer¶
The AcceleratorBuffer
represents a register of qubits. Programmers allocate
this register of a certain size, and pass it by reference to all execution tasks.
These execution tasks are carried out by concrete instances of the Accelerator
interface, and these instances are responsible for persisting information to the
provided buffer reference. This ensures programmers have access to all execution results
and metadata upon execution completion.
Programmers can allocate a buffer through the xacc::qalloc(const int n)
(xacc.qalloc(int)
in Python) call.
After execution, measurement results can be queried as well as backend-specific
execution metadata. Below demonstrate some basic usage of the AcceleratorBuffer
#include "xacc.hpp"
...
// Create program somehow... detailed later
program = ...
auto buffer = xacc::qalloc(3);
auto qpu = xacc::getAccelerator("ibm:ibmq_valencia");
qpu->execute(buffer, program);
std::map<std::string, int> results = buffer->getMeasurementCounts();
auto fidelities = (*buffer)["1q-gate-fidelities"].as<std::vector<double>>();
auto expValZ = buffer->getExpectationValueZ();
in Python
import xacc
...
// Create program somehow... detailed later
program = ...
buffer = xacc.qalloc(3)
qpu = xacc.getAccelerator('ibm:ibmq_valencia', {'shots':8192})
qpu.execute(buffer, program)
results = buffer.getMeasurementCounts()
fidelities = buffer['1q-gate-fidelities']
expValZ = buffer.getExpectationValueZ()
Intermediate Representation, Kernels, and Compilers¶
Above we mentioned a program
variable but did not detail how it was created. This instance
is represented in XACC as a CompositeInstruction
. The creation of Instruction
and
CompositeInstruction
is demonstrated below. First, we create this instances via an
implementation of the IRProvider
, specifically a 3 instruction circuit with one
parameterized Ry
on a variable theta
.
#include "xacc.hpp"
...
auto provider = xacc::getIRProvider("quantum");
auto program = provider->createComposite("foo", {"theta"});
auto x = provider->createInstruction("X", {0});
auto ry = provider->createInstruction("Ry", {1}, {"theta"});
auto cx = provider->createInstruction("CX", {1,0});
program->addInstructions({x, ry, cx});
in Python
import xacc
...
provider = xacc.getIRProvider('quantum')
program = provider.createComposite('foo', ['theta'])
x = provider.createInstruction('X', [0])
ry = provider.createInstruction('Ry', [1], ['theta'])
cx = provider.createInstruction('CX', [1,0])
program.addInstructions([x,ry,cx])
We could also create IR through textual source code representations in a language
that is available to the framework. Availability here implies that there exists
a Compiler
implementation for the language being used. Compilers
take kernel
source strings and produce IR (one or many CompositeInstructions
). Here we demonstrate
the same circuit as above, but using a Quil kernel
#include "xacc.hpp"
...
auto qpu = xacc::getAccelerator("ibm");
auto quil = xacc::getCompiler("quil");
auto ir = quil->compile(R"(
__qpu__ void ansatz(AcceleratorBuffer q, double x) {
X 0
Ry(x) 1
CX 1 0
}
__qpu__ void X0X1(AcceleratorBuffer q, double x) {
ansatz(q, x)
H 0
H 1
MEASURE 0 [0]
MEASURE 1 [1]
}
)", qpu);
auto ansatz = ir->getComposite("ansatz");
auto x0x1 = ir->getComposite("X0X1");
in Python
import xacc
...
qpu = xacc.getAccelerator('ibm')
quil = xacc.getCompiler('quil')
ir = quil.compile('''
__qpu__ void ansatz(AcceleratorBuffer q, double x) {
X 0
Ry(x) 1
CX 1 0
}
__qpu__ void X0X1(AcceleratorBuffer q, double x) {
ansatz(q, x)
H 0
H 1
MEASURE 0 [0]
MEASURE 1 [1]
}
''', qpu)
ansatz = ir.getComposite('ansatz')
x0x1 = ir.getComposite('X0X1')
Here, x0x1
is a CompositeInstruction
that can be passed to Accelerator::execute()
for
backend execution.
Next we demonstrate how one might leverate IRTransformation
to perform general optimizations
on IR instances.
#include "xacc.hpp"
...
auto xasmCompiler = xacc::getCompiler("xasm");
auto ir = xasmCompiler->compile(R"(__qpu__ void bell(qbit q) {
H(q[0]);
CX(q[0], q[1]);
CX(q[0], q[1]);
CX(q[0], q[1]);
Measure(q[0]);
Measure(q[1]);
})", nullptr);
auto f = ir->getComposite("bell");
assert(6 == f->nInstructions());
auto opt = xacc::getIRTransformation("circuit-optimizer");
opt->apply(f, nullptr);
assert (4 == f->nInstructions());
in Python
import xacc
...
# Create a bell state program with too many cnots
xasm = xacc.getCompiler('xasm')
ir = xasm.compile('''__qpu__ void bell(qbit q) {
H(q[0]);
CX(q[0],q[1]);
CX(q[0],q[1]);
CX(q[0], q[1]);
Measure(q[0]);
Measure(q[1]);
}''')
f = ir.getComposite('bell')
assert(6 == f.nInstructions())
# Run the circuit-optimizer IRTransformation
optimizer = xacc.getIRTransformation('circuit-optimizer')
optimizer.apply(f, None, {})
# should have 4 instructions, not 6
assert(4 == f.nInstructions())
print(f.toString())
Observable¶
The Observable
concept in XACC dictates measurements to be performed
on unmeasured an CompositeInstruction
. XACC provides pauli
and fermion
Observable
implementations. Below we demonstrate how one might create these objects.
#include "xacc.hpp"
#include "xacc_observable.hpp"
...
auto x0x1 = xacc::quantum::getObservable("pauli");
x0x1->fromString('X0 X1');
// observe() returns a list of measured circuits
// here we only have one
auto measured_circuit = x0x1->observe(program)[0];
auto fermion = xacc::getObservable("fermion");
fermion->fromString("1^ 0");
auto jw = xacc::getService<ObservableTransform>("jordan-wigner");
auto spin = jw->transform(fermion);
in Python
import xacc
...
x0x1 = xacc.getObservable('pauli', 'X0 X1')
// observe() returns a list of measured circuits
// here we only have one
measured_circuit = x0x1.observe(program)[0]
fermion = xacc.getObservable('fermion', '1^ 0')
jw = xacc.getObservableTransform('jordan-wigner')
spin = jw.transform(fermion)
Accelerator¶
The Accelerator
is the primary interface to backend quantum computers and simulators for XACC.
The can be initialized with a heterogeneous map of input parameters, expose qubit connectivity information,
and implement execution capabilities given a valid AcceleratorBuffer
and CompositeInstruction
.
Here we demonstrate getting reference to an Accelerator
and using it to execute a simple bell state.
Note this is a full example, that leverages the xasm
compiler as well as requisite C++ framework
initialization and finalization.
#include "xacc.hpp"
int main(int argc, char **argv) {
xacc::Initialize(argc, argv);
// Get reference to the Accelerator
auto accelerator =
xacc::getAccelerator("qpp", {std::make_pair("shots", 5000)});
// Allocate some qubits
auto buffer = xacc::qalloc(2);
auto xasmCompiler = xacc::getCompiler("xasm");
auto ir = xasmCompiler->compile(R"(__qpu__ void bell(qbit q) {
H(q[0]);
CX(q[0], q[1]);
Measure(q[0]);
Measure(q[1]);
})", accelerator);
accelerator->execute(buffer, ir->getComposites()[0]);
buffer->print();
xacc::Finalize();
return 0;
}
in Python
import xacc
accelerator = xacc.getAccelerator('qpp', {'shots':5000})
buffer = xacc.qalloc(2)
xasm = xacc.getCompiler('xasm')
ir = xasm.compile('''__qpu__ void bell(qbit q) {
H(q[0]);
CX(q[0],q[1]);
Measure(q[0]);
Measure(q[1]);
}''', accelerator)
accelerator.execute(buffer, ir.getComposites()[0])
# note accelerators can execute lists of CompositeInstructions too
# usefule for executing many circuits with one remote qpu call
# accelerator.execute(buffer, ir.getComposites())
Optimizer¶
This abstraction is meant for the injection of general classical multi-variate function
optimization routines. XACC provides implementations leveraging NLOpt and MLPack C++ libraries.
Optimizer``s expose an ``optimize()
method that takes as input an OptFunction
, which serves as
a thin wrapper for functor-like objects exposing a specific argument structure (must take as first
arg a vector<double>
representing current iterate’s parameters, and another one representing the mutable
gradient vector). Below is a demonstration of how one might use this utility:
auto optimizer =
xacc::getOptimizer("nlopt");
optimizer->setOptions(
HeterogeneousMap{std::make_pair("nlopt-maxeval", 200),
std::make_pair("nlopt-optimizer", "l-bfgs")});
OptFunction f(
[](const std::vector<double> &x, std::vector<double> &grad) {
if (!grad.empty()) {
grad[0] = -2 * (1 - x[0]) + 400 * (std::pow(x[0], 3) - x[1] * x[0]);
grad[1] = 200 * (x[1] - std::pow(x[0],2));
}
return = 100 * std::pow(x[1] - std::pow(x[0], 2), 2) + std::pow(1 - x[0], 2);
},
2);
auto result = optimizer->optimize(f);
auto opt_val = result.first;
auto opt_params = result.second;
or in Python
def rosen_with_grad(x):
g = [-2*(1-x[0]) + 400.*(x[0]**3 - x[1]*x[0]), 200 * (x[1] - x[0]**2)]
xx = (1.-x[0])**2 + 100*(x[1]-x[0]**2)**2
return xx, g
optimizer = xacc.getOptimizer('mlpack',{'mlpack-optimizer':'l-bfgs'})
opt_val, opt_params = optimizer.optimize(rosen_with_grad,2)
xacc::qasm()¶
To improve programming efficiency, readability, and utility of the quantum kernel string
compilation, XACC exposes a qasm()
function. This function takes as input an enhanced quantum
kernel source string syntax and compiles it to XACC IR. This source string is enhanced in that
it requires that extra metadata be present in order to adequately compile the quantum code.
Specifically, the source string must contain the following key words:
a single .compiler NAME, to indicate which XACC compiler implementation to use.
one or many .circuit NAME calls to give the created CompositeInstruction (circuit) a name.
one .parameters PARAM 1, PARAM 2, .., PARAM N for each parameterized circuit to tell the Compiler the names of the parameters.
A .qbit NAME optional keyword can be provided when the source code itself makes reference to the
qbit
orAcceleratorBuffer
Running this command with the appropriately provided keywords will compile the source string to XACC IR and store it an
internal compilation database (standard map of CompositeInstruction names to CompositeInstructions), and users
can get reference to the individual CompositeInstructions via an exposed getCompiled()
XACC API call. The
code below demonstrates how one would use qasm()
and its overall utility.
#include "xacc.hpp"
...
xacc::qasm(R"(
.compiler xasm
.circuit deuteron_ansatz
.parameters x
.qbit q
for (int i = 0; i < 2; i++) {
H(q[0]);
}
exp_i_theta(q, x, {{"pauli", "X0 Y1 - Y0 X1"}});
)");
auto ansatz =
xacc::getCompiled("deuteron_ansatz");
// Quil example, multiple kernels
xacc::qasm(R"(.compiler quil
.circuit ansatz
.parameters theta, phi
X 0
H 2
CNOT 2 1
CNOT 0 1
Rz(theta) 0
Ry(phi) 1
H 0
.circuit x0x1
ansatz(theta, phi)
H 0
H 1
MEASURE 0 [0]
MEASURE 1 [1]
)");
auto x0x1 = xacc::getCompiled("x0x1");
or in Python
import xacc
...
xacc.qasm('''
.compiler xasm
.circuit deuteron_ansatz
.parameters x
.qbit q
for (int i = 0; i < 2; i++) {
X(q[0]);
}
exp_i_theta(q, x, {{"pauli", "X0 Y1 - Y0 X1"}});
''')
ansatz =
xacc.getCompiled('deuteron_ansatz')
# Quil example, multiple kernels
xacc.qasm('''.compiler quil
.circuit ansatz
.parameters theta, phi
X 0
H 2
CNOT 2 1
CNOT 0 1
Rz(theta) 0
Ry(phi) 1
H 0
.circuit x0x1
ansatz(theta, phi)
H 0
H 1
MEASURE 0 [0]
MEASURE 1 [1]
''')
x0x1 = xacc.getCompiled('x0x1')
Single-source Pythonic Programming¶
Benchmarks¶
Since XACC provides a hardware-agnostic framework for quantum-classical computing, it is well-suited for the development of general benchmark tasks that run on available backend quantum computers. XACC provides a Pythonic benchmarking tool that enables users to define benchmarks via an input file or python dictionary, and then distribute those files to be executed on available backends. Benchmarks can be low-level and hardware-specific, or high-level, application-style benchmarks.
The suite is extensible in the benchmark itself, as well as input data required for the benchmark.
All benchmarks can be defined as INI files. These files describe named sections of key-value pairs. Each benchmark requires an XACC section (for the definition of the backend accelerator, number of shots, etc.) and a Benchmark section (specifying the benchmark name and algorithm). Other sections are specified by the concrete benchmark sub-type.
Chemistry¶
This Benchmark implementation enables one to define an application-level benchmark which attempts to compute the accuracy with which a given quantum backend can compute the ground state energy of a specified electronic structure computation. Below is an example of such a benchmark input file
[XACC]
accelerator = ibm:ibmq_johannesburg
shots = 1024
verbose = True
[Decorators]
readout_error = True
[Benchmark]
name = chemistry
algorithm = vqe
[Ansatz]
source = .compiler xasm
.circuit ansatz2
.parameters x
.qbit q
X(q[0]);
X(q[2]);
ucc1(q, x[0]);
[Observable]
name = psi4
basis = sto-3g
geometry = 0 1
Na 0.000000 0.0 0.0
H 0.0 0.0 1.914388
symmetry c1
fo = [0, 1, 2, 3, 4, 10, 11, 12, 13, 14]
ao = [5, 9, 15, 19]
[Optimizer]
name = nlopt
nlopt-maxeval = 20
Stepping, through this, we see the benchmark is to be executed on the IBM Johannesburg backend, with 1024 shots. Next, we specify what benchmark algorithm to run - the Chemistry benchmark using the VQE algorithm. After that, this benchmark enables one to specify any AcceleratorDecorators to be used, here we turn on readout-error decoration to correct computed expectation values with respact to measurement readout errors. Moving down the file, one now specifies the specific state-preparation ansatz to be used for this VQE run, using the usual XACC qasm() format. Finally, we specify the Observable we are interested in studying, and the classical optimizer to be used in searching for the optimal expecation value for that observable.
One can run this benchmark with the following command (presuming it is in a file named chem_nah_vqe_ibm.ini)
$ python3 -m xacc --benchmark chem_nah_vqe_ibm.ini
Quantum Process Tomography¶
[XACC]
accelerator = ibm:ibmq_poughkeepsie
verbose = True
[Benchmark]
name = qpt
analysis = ['fidelity', 'heat-maps']
chi-theoretical-real = [0., 0., 0., 0., 0., 1., 0., 1., 0., 0., 0., 0., 0., 1., 0., 1.]
[Circuit]
# Logical circuit source code
source = .compiler xasm
.circuit hadamard
.qbit q
H(q[0]);
# Can specify physical qubit to run on
qubit-map = [1]