Automated batch generation

Note

Grasping the concept of Batch Generators may require the understanding about batches and jobs, QPUs and plugins

In some cases, the design of a quantum application could require the submission of several batches to a QPU. Moreover, the Result object returned by the QPU does not always describe perfectly the solution of a problem, the result will then need to be parsed to return a user-friendly data-structure.

The concept of Batch Generators has been introduced to help users to design quantum application, while keeping the flexibility of Plugins and QPUs. A Batch Generator is a service designed to be inserted at the beginning of a Qaptiva computation 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 argument (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 Ansatz, 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 generator can parse the result before returning it to the user. The result parsing 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 a Batch nor a Job)

  • the post_process method (this function takes a BatchResult or Result - 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 Problem generators for a list of all available generators.

Creating a remote Generator and accessing it

Any generator defined in our framework can be started in server mode, and can be accessed using myQLM or from any other Qaptiva Appliance, using a synchronous connection. This section explains the creation of a server and also how to connect to a remote Generator

Note

Qaptiva Access provides advanced tools to create dynamically remote generators (and even more) and access it remotely, using an asynchronous connection

Any generator has a method serve() to start this generator in server mode. This method takes the PORT and the IP as arguments. For instance:

from qat.generators import MaxCutGenerator

# Define a PORT and a IP
PORT = 1234
IP = "*"

# Define a generator
generator = MaxCutGenerator()
generator.serve(PORT, IP)

If a distant generator is started in server mode, our framework can be used as client of a connection. Assuming the server is listening to the port 1234 and the ip of the server is 127.0.0.1, RemoteBatchGenerator can be used to connect to this server:

from qat.plugins import RemoteBatchGenerator

# Define PORT and IP
PORT = 1234
IP = "127.0.0.1"

# Define a client
plugin = RemoteBatchGenerator(PORT, IP)

Warning

The connection is synchronous, therefore, if the client is disconnected during the batch generation, result of the execution is lost