# 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).

Example of Batch generator

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 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.