Building advanced computation stacks
Designing quantum application could require to submit several batches to a QPU. Moreover, the Result
object returned by
the QPU does not always describe perfectly a solution of the problem, one want to parse this result to return a user-friendly data-structure.
The concept of Batch generators has been designed to help users to design application, while keeping the flexibility of Plugins and QPUs. A Batch Generator is a service designed to be inserted at the beginning of a stack:
my_stack = BatchGenerator | Plugin | ... | QPU
- This service would be used to:
Generate batches (as many batches as needed)
Parse the final batch result (optional)
The batch generation will be defined with the generate()
function of a generator,
while the final batch result parsing (optional) can be defined with a post_process function.
The method generate()
is used to generate one of several batches that will be submitted
to the rest of the stack. This method can take any arguments (there is a single mandatory argument - specs - containing the hardware
specification of the hardware the generator is linked to).
A batch generator can be designed to submit a single job to the QPU. For instance, the following batch generator submit a cat state to the QPU
from qat.generators import AbstractGenerator
from qat.lang.AQASM import Program, H, CNOT
from qat.qpus import get_default_qpu
class CatStateGenerator(AbstractGenerator):
"""
Generator creating a cat state job. This generator will
take the number of qubits in arguments to generate the right
circuit
"""
def generate(self, specs, nbqbits):
"Generate the circuit"
prog = Program()
qbits = prog.qalloc(nbqbits)
prog.apply(H, qbits[0])
for ctrl, target in zip(qbits[0:], qbits[1:]):
prog.apply(CNOT, ctrl, target)
return prog.to_circ().to_job()
application = CatStateGenerator() | get_default_qpu()
result = application.execute(nbqbits=3)
for sample in result[0]:
print(f"{sample.state}: {sample.probability:.2f}")
|000>: 0.50
|111>: 0.50
A batch generator can submit iteratively several batches of jobs using the yield operator (which will return the output of each submission). The advantage of an iterative batch generator rather than submitting a batch consisting of several jobs is that each batch will be able to use the results of the previous batches as intermediate results in its jobs.
This example tries to minimize the energy of an observable. Given an observable, this generator will create a dummy Ansatz that will be used to find the ground state of this observable (in our example, our Ansatz will be \(\prod RX_i(\alpha_i)\) to keep this example simple). This generator will create the Ansatz, then:
submit a variational job based on this Ansatz - in observable mode - to find the best angles
submit job having fixed angles (with the best angles found before) based on the same Ansarz, in sampling mode. The result of this job is returned to the user
from functools import reduce
from operator import add
import numpy as np
from qat.generators import AbstractGenerator
from qat.plugins import ScipyMinimizePlugin
from qat.lang.AQASM import Program, RX
from qat.core import Observable as Obs
from qat.qpus import get_default_qpu
class DummyEnergyMinimizer(AbstractGenerator):
"""
Dummy generator that tries to find a state that minimize the energy
of an Observable
The Ansatz used is very dummy (a wall or RX gates). In practise, a better
Ansatz must be used.
"""
def generate(self, specs, observable):
"Generate the circuit"
# Create dummy Ansatz. This Ansatz is a wall of RX gate
# each RX having its own angle
prog = Program()
qbits = prog.qalloc(observable.nbqbits)
for index, qb in enumerate(qbits):
angle = prog.new_var(float, f"V{index}")
prog.apply(RX(angle), qb)
circ = prog.to_circ()
# Find best angles -> submit first job
result = yield circ.to_job("OBS", observable=observable)
angles = result.parameter_map
# Create sample job -> submit second job
best_angles_circ = circ(**angles)
yield best_angles_circ.to_job()
This generator can be used in a stack composed of our generator, an optimizer plugin (to find optimal angles) and a QPU.
This example tries to minimize the observable \(\sum Z_i\). The ground state of this observable is known, this example should find the state \(|11..1\rangle\)
# Create a dummy observable (Σ Z_i)
observable = reduce(add, (Obs.sigma_z(idx, 5) for idx in range(5)))
# Find ground state
application = DummyEnergyMinimizer() | ScipyMinimizePlugin() | get_default_qpu()
result = application.execute(observable=observable)
# Print result
print("=== Following state(s) ===")
for sample in result[0]:
if np.isclose(sample.probability, 0., atol=1e-2):
continue
print(f"{sample.state}: {sample.probability:.2f}")
print("\n=== Minimize the energy of: ===")
print(observable)
=== Following state(s) ===
|11111>: 1.00
=== Minimize the energy of: ===
1.0 * (Z|[0]) +
1.0 * (Z|[1]) +
1.0 * (Z|[2]) +
1.0 * (Z|[3]) +
1.0 * (Z|[4])
A batch generate can parse the result before returning it to the user. The parse result could be done using:
the yield operator in the function
generate()
(i.e. the last yield item is returned to the user if this object in neither aBatch
nor aJob
)the post_process method (this function takes a
BatchResult
orResult
- depending on the object’s type returned by the generate method - and return an object of any type)
The following sample of code returns a parsed result:
from qat.generators import AbstractGenerator
from qat.lang.AQASM import Program, H, CNOT
from qat.qpus import get_default_qpu
class CatStateResult:
"""
User friendly parsed result
"""
def __init__(self, result):
# Check length
assert len(result) == 2, "Invalid number of sample"
for sample in result:
assert set(sample.state.bitstring) in [{"0"}, {"1"}], "Invalid state - expected |0..0> or |1..1>"
self.result = result
def display(self):
"Display result"
print("== Displaying a cat state result ==")
for sample in self.result:
print(f"{sample.state}: {sample.probability:.2f}")
class CatStateGenerator(AbstractGenerator):
"""
Generator creating a cat state job. This generator will
take the number of qubits in arguments to generate the right
circuit
"""
def generate(self, specs, nbqbits):
"Generate the circuit"
prog = Program()
qbits = prog.qalloc(nbqbits)
prog.apply(H, qbits[0])
for ctrl, target in zip(qbits[0:], qbits[1:]):
prog.apply(CNOT, ctrl, target)
return prog.to_circ().to_job()
def post_process(self, result):
"Parse result"
return CatStateResult(result)
application = CatStateGenerator() | get_default_qpu()
result = application.execute(nbqbits=3)
result.display()
== Displaying a cat state result ==
|000>: 0.50
|111>: 0.50
See Generators for a list of all available generators.