Running variational algorithms

QLM comes with a collection of tools to efficiently describe and run variational quantum algorithm. This page introduces the basic mechanics allowing you to write and run adaptive variational schemes.

For a more advanced usage Combinatorial optimization describes a high-level interface to described combinatorial optimization problems and automatically generate parametrized Ansätze.

Variational jobs

When running a variational quantum algorithm, we are, most of the time, interested in minimizing the energy of some observable \(H\) over some parametrized quantum state \(|\psi(\theta)\rangle\), i.e minimizing \(\langle \psi(\theta)| H |\psi(\theta) \rangle\).

In the QLM, it is possible to build a variational quantum circuit by introducing open parameters in a pyAQASM Program:

from qat.lang.AQASM import Program, RY, RZ

prog = Program()
qbits = prog.qalloc(1)

variable = prog.new_var(float, "a")
RY(variable)(qbits)
RZ(4 * variable)(qbits)

circuit = prog.to_circ()

print("Variables:", circuit.get_variables())

job = circuit.to_job()
print("Job variables:", job.get_variables())
Variables: ['a']
Job variables: ['a']

Additionally, the sampled observable itself can have parametrized coefficients:

from qat.core import Observable, Term, Variable

t = Variable("t")

obs = Observable(3)
# Obs = \sum_i \sigma_x^i
for i in range(3):
    obs += (1 - t) * Observable.sigma_x(i, 3)

print(obs)
print("Observable variables:", obs.get_variables())

from qat.lang.AQASM import Program, RY, RZ

prog = Program()
qbits = prog.qalloc(3)

variable = prog.new_var(float, "a")
for qbit in qbits:
    RY(variable)(qbit)
    RZ(4 * variable)(qbit)

circuit = prog.to_circ()

print("Circuit variables:", circuit.get_variables())

job = circuit.to_job(observable=obs)
print("Job variables:", job.get_variables())
(1 - t) * (X|[0]) +
(1 - t) * (X|[1]) +
(1 - t) * (X|[2])
Observable variables: ['t']
Circuit variables: ['a']
Job variables: ['a', 't']

This allows to have layered parametrized optimization, or even compilation tradeoffs where some variational parameters end up in the sampled observable.

Binding variables

Once we’ve built a parametrized job, its variables can be instantiated using the overloaded __call__ operator:

import numpy as np
from qat.lang.AQASM import Program, RY, RZ
from qat.core import Observable, Term, Variable

t = Variable("t")

obs = Observable(3)
for i in range(3):
    obs += (1 - t) * Observable.sigma_x(i, 3)

prog = Program()
qbits = prog.qalloc(3)

variable = prog.new_var(float, "a")
for qbit in qbits:
    RY(variable)(qbit)
    RZ(4 * variable)(qbit)
job = prog.to_circ().to_job(observable=obs)

job_2 = job(t=0.5)

print(job_2.observable)

job_3 = job(** {v: np.random.random() for v in job.get_variables()})

print(job_3.observable)
for op in job_3.circuit.iterate_simple():
    print(op)
0.5 * (X|[0]) +
0.5 * (X|[1]) +
0.5 * (X|[2])
0.19376009394501548 * (X|[0]) +
0.19376009394501548 * (X|[1]) +
0.19376009394501548 * (X|[2])
('RY', [0.05218544329296437], [0])
('RZ', [0.2087417731718575], [0])
('RY', [0.05218544329296437], [1])
('RZ', [0.2087417731718575], [1])
('RY', [0.05218544329296437], [2])
('RZ', [0.2087417731718575], [2])

Warning

When binding variables used inside a custom parametrized gate, a gate set containing the custom gate should be provided via the gate_set kwargs.

from qat.lang.AQASM import Program, AbstractGate

XX = AbstractGate("XX", [float], arity=2)

prog = Program()
qbits = prog.qalloc(2)
XX(prog.new_var(float, "theta"))(qbits)
circuit = prog.to_circ()

try:
    circuit_2 = circuit(theta=0.3)
except Exception as e:
    print("Caught: {}".format(e))

circuit_2 = circuit(theta=0.3, gate_set=prog.gate_set)
for op in circuit_2.iterate_simple():
    print(op)
('XX', [0.3], [0, 1])

Running variational algorithms

The simplest way to run a variational algorithm is to use a dedicated Plugin that will take care of the energy minimization.

The default variational Plugin wraps the scipy.optimize.minimize function: qat.plugins.ScipyMinimizePlugin.

import numpy as np
from qat.lang.AQASM import Program, RY, RZ
from qat.core import Observable, Term, Variable

t = Variable("t")

obs = Observable(3)
for i in range(3):
    obs += (1 - t) * Observable.sigma_x(i, 3)

prog = Program()
qbits = prog.qalloc(3)

variable = prog.new_var(float, "a")
for qbit in qbits:
    RY(variable)(qbit)
    RZ(4 * variable)(qbit)
job = prog.to_circ().to_job(observable=obs)

from qat.plugins import ScipyMinimizePlugin

optimize = ScipyMinimizePlugin(method="COBYLA", tol=1e-3, options={"maxiter": 150})

from qat.qpus import get_default_qpu

stack = optimize | get_default_qpu()

result = stack.submit(job)

print('final energy:', result.value)
print('best parameters:', result.meta_data['parameters'])
print('trace:', result.meta_data['optimization_trace'])
final energy: -9.322011516646588
best parameters: [0.8404604370593229, -3.2744581093021234]
trace: [-1.6542044178243307, 1.522528606876513, 0.4600418823233312, -0.19763252326101638, 0.12161851355556108, -1.3367288858795783, -1.1568120512241027, -1.5240708875401292, -1.7831758640430047, -1.783067431339242, -1.898582108787429, -1.9961616751259958, -2.035175466107516, -1.7695199366353465, -2.2226276735202988, -2.3289637912053656, -2.4628249732793783, -2.5844756108726177, -2.622647945482948, -2.8045624008752794, -2.879432831302896, -3.0303402310996335, -3.167845748857512, -3.302692160012131, -3.43833989763662, -3.5748241598427573, -3.7093242169637235, -3.8146953550543374, -3.9623776975642038, -4.09177847203119, -4.227709306745507, -4.364089490015164, -4.501374054153844, -4.639606407421018, -4.77512683192955, -4.911702619833916, -5.048112237525279, -5.1843125329549204, -5.316038697612379, -5.449307420598486, -5.57793902944917, -5.6361915583827695, -5.786592915166521, -5.720844440897336, -5.781041703199319, -5.817806730076392, -5.83049397834022, -5.6904389935974855, -5.876840761902623, -5.915496543522651, -5.94922016387747, -5.982528133025671, -6.0167055675630206, -6.050791549517733, -6.0848173932422, -6.118898928096417, -6.152978405686907, -6.187055272698263, -6.221131375418384, -6.255204130988062, -6.289275717158464, -6.323351569541634, -6.357424586396318, -6.391500847843571, -6.425575960899747, -6.459624010946081, -6.493703524347264, -6.527782046661525, -6.5618582839687445, -6.595934105490131, -6.630010118426331, -6.6640861190609275, -6.698162112094824, -6.732237990281151, -6.766314020142756, -6.800389994416243, -6.834465990421256, -6.868541984510246, -6.90261797316362, -6.936693910940624, -6.970769824910599, -7.004845782272799, -7.038921668017336, -7.072996457082628, -7.1070727512992375, -7.141148288885076, -7.175224347388305, -7.20930037686013, -7.243376385643818, -7.277452376826689, -7.311528170818232, -7.345604162470208, -7.379680110506615, -7.413756121411927, -7.44783211838713, -7.481908104936262, -7.515984104546942, -7.550060098281098, -7.584136092763954, -7.6182120890665015, -7.652288085348309, -7.686364059645324, -7.720440063494587, -7.7545160633591514, -7.788591531979922, -7.8226676738230605, -7.856743628038478, -7.890819588268419, -7.924895589229755, -7.958971564260205, -7.993047564639911, -8.027123557178639, -8.061199557695536, -8.095275555928685, -8.129351554389057, -8.163427553391942, -8.197503552068534, -8.231579551057816, -8.265655549941322, -8.299731548853709, -8.33380754777702, -8.367883546692681, -8.401959545623853, -8.436035544546751, -8.470111543474445, -8.504187542402331, -8.538263541329417, -8.572339540256761, -8.606415539183535, -8.640491538110993, -8.674567537038394, -8.708643535965752, -8.742719534892679, -8.776795533820081, -8.810871532747338, -8.844947531674707, -8.87902353060206, -8.913099529527612, -8.947175528455496, -8.981251527382554, -9.015327526309827, -9.049403525236993, -9.083479524163451, -9.117555523080766, -9.15163152200929, -9.185707520937996, -9.219783519865612, -9.253859518792881, -9.287935517720179, -9.322011516646588]

This plugin also supports a nice feature: it can read optimization parameters directly from the job’s meta data or directly from the result. This allows you to build a stack with no particular choice of optimization parameters and attach these parameters directly to the job when submitting it. In this setting, the previous example becomes:

import numpy as np
import json
from qat.lang.AQASM import Program, RY, RZ
from qat.core import Observable, Term, Variable
t = Variable("t")

obs = Observable(3)
for i in range(3):
    obs += (1 - t) * Observable.sigma_x(i, 3)

prog = Program()
qbits = prog.qalloc(3)

variable = prog.new_var(float, "a")
for qbit in qbits:
    RY(variable)(qbit)
    RZ(4 * variable)(qbit)
job = prog.to_circ().to_job(observable=obs)

from qat.plugins import ScipyMinimizePlugin

optimize = ScipyMinimizePlugin()



from qat.qpus import get_default_qpu

stack = optimize | get_default_qpu()

optimizer_args = {
    "method": "COBYLA",
    "tol": 1e-3,
    "options": {"maxiter": 150}
}
result = stack.submit(job, meta_data={"ScipyMinimizePlugin": json.dumps(optimizer_args)})

print('final energy:', result.value)
print('best parameters:', result.meta_data['parameters'])
print('best parameters (dictionary):', result.parameter_map)
print('trace:', result.meta_data['optimization_trace'])
final energy: -10.37897827416127
best parameters: [0.8404659162867164, -3.759113178926179]
best parameters (dictionary): {'a': 0.8404659162867164, 't': -3.759113178926179}
trace: [-1.2143076105966697, 0.7588791337710157, 0.9665542416203782, 0.512210219453474, -0.5865243763525982, -0.7774354038825964, -0.6597150401936086, -1.2993337498308635, -0.869034473462408, -1.620796132306022, -1.8452061735267566, -2.1265190876302325, -2.4087078252751435, -2.673159899680575, -2.9495190627158694, -3.2134865954867573, -3.4889450747203306, -3.7393109721655406, -3.9765244074891912, -4.2322774027464405, -4.483599946687795, -4.674731035263205, -3.3036052016168123, -4.715893656140858, -4.865811124997464, -5.039250682023593, -5.226567556684156, -5.407037977139794, -5.559873965352185, -5.690421901977887, -5.794681095837122, -5.942463902816103, -6.081475118047292, -6.2097368583381565, -6.33505886311209, -6.460352799150998, -6.545081827972163, -6.655087025563991, -6.614815676901753, -6.516733890799152, -6.693267184092688, -6.725131612643694, -6.757358875148346, -6.788928935669324, -6.8107557788558335, -6.847462453560443, -6.878157710140355, -6.9124383435005825, -6.946615828768124, -6.980720767587634, -7.014738312603643, -7.0485174608492835, -7.078486654030733, -7.113947708342362, -7.146508217551165, -7.180671016516593, -7.214779782928378, -7.248845502964948, -7.282840429247688, -7.316504481362573, -7.3506924736481105, -7.384355805196323, -7.418241878768785, -7.452234038230894, -7.486056504099665, -7.517240305615317, -7.552092550848734, -7.5850158316577065, -7.619251112076947, -7.653411374036782, -7.6875105325987, -7.721589675861759, -7.75563640539851, -7.7897236661816205, -7.823786395615709, -7.857863451990067, -7.89194026390933, -7.926016720056387, -7.960092698161248, -7.994166946697607, -8.027940513995542, -8.062085638682005, -8.096164859831756, -8.130220893009133, -8.164302244746017, -8.198373485438085, -8.232448623095129, -8.266522534944881, -8.30059022020823, -8.334664708101265, -8.368732276110787, -8.402736786486223, -8.43681236760626, -8.470846329597371, -8.504925919021792, -8.538986176978218, -8.573066441918233, -8.607139977725225, -8.641215441065869, -8.675290189875579, -8.70936085579368, -8.743435786373107, -8.777506135465755, -8.811533570760357, -8.845608203483067, -8.879652557012, -8.913728096373022, -8.947788951367093, -8.98186845368109, -9.015938283754974, -9.05001568090243, -9.08409144230122, -9.118166952028595, -9.15224312289666, -9.186318843363354, -9.220394904748028, -9.254470905423796, -9.288546902243498, -9.3226228620446, -9.356698860909946, -9.390774859289728, -9.424850793219255, -9.458926803952226, -9.493002797206262, -9.527078791550823, -9.561154780617695, -9.595230780113681, -9.629306767838207, -9.66338276573509, -9.697458764598908, -9.731534736324333, -9.765610735345266, -9.799686731506121, -9.833762715584285, -9.867838717015513, -9.901914715647134, -9.93599025370216, -9.970066399547012, -10.004142246782209, -10.038218258159901, -10.072294278318518, -10.106370288198688, -10.140446287261305, -10.174522285655959, -10.208598284389575, -10.242674283166398, -10.276750281482002, -10.31082627982783, -10.34490227809733, -10.37897827416127]

An alternative is the qat.plugins.SeqOptim Plugin, which only works for a certain class of circuits but stands out for its shot-noise resilience.

Differentiating jobs

Many variational algorithms require computing the gradient of the cost function \(E(\vec{\theta}) = \langle \psi(\vec{\theta}) | H | \psi(\vec{\theta})\rangle\). The gradient can be used in gradient-based optimization methods. QLM jobs come with methods to compute the derivative of \(E(\vec{\theta})\) automically: differentiate() and gradient().

Examples of use of this feature are given in this notebook.

Testing for several initializations of the parameters

Variational optimization tends to be sensitive to the initial parameters it started from. You can insert the qat.plugins.MultipleLaunchesAnalyzer Plugin before a VQE optimizer to automatically perform several VQE runs and keep the lowest-energy result. This is illustrated in the notebook NOTEBOOK LINK ERROR.