Make you own junction

If Plugins can be seen as two-way pipes that transform quantum programs on the way in and execution result on the way back, Junction can be seen as, well, junctions, in this piping system. More precisely, they provide a simple interface to embed repeated, adaptive, classical computations in the middle on the execution stack.

The simpler, and most widely used example would the one of a variational optimizer dealing with a variational eigensolving procedure. In this setting, the incoming Job is an “abstract” job with open angles or variables. The optimizer would like to start and iteratively give these angles some value, evaluate the energy for this set of values and iterate until satfisfied. Of couse, it is completely possible to deal with this type of routines outside of the Qaptiva stack, but this might prevent some optimized workflow where the job is first compiled and optimized for a given architecture and only then enters the variational solver.

With junctions, it is rather trivial to embed any such adaptive treatment after the compilation and optimization stage of the stack. In our framework, a junction should inherit from qat.plugins.Junction and must define method run() . This method can perform as many call to the execute() method which submits a Job to the QPU and return the computed Result.

Warning

If your Junction implement its own constructor, please ensure the parent constructor is called

from qat.plugins import Junction

class IterativeExploration(Junction):
    def __init__(self, nsteps=23):
        super().__init__()
        self.nsteps = nsteps

A concrete example

In the following example, we will construct a quite naive junction that will process a quantum circuit with a single parameter and iteratively try all the values for this parameter with some step width. After having explored the search space, it will return the best (i.e least) ecountered value.

import numpy as np
from qat.plugins import Junction
from qat.core import Result

class IterativeExploration(Junction):
    def __init__(self, nsteps=23):
        super().__init__()
        self.nsteps = nsteps

    def run(self, initial_job, meta_data):
        job = initial_job
        variable = job.get_variables().pop()
        angles = np.linspace(0, 2 * np.pi, self.nsteps)
        all_values = []
        for val in angles:
            current_job = job(**{variable: val})
            result = self.execute(current_job)
            all_values.append(result.value)
        min_val = min(all_values)
        best_index = all_values.index(min_val)
        best_param = angles[best_index]
        return Result(value=min_val, parameter_map={variable: best_param})

So how does it work? The run method is the entry point of our repeated procedure. This method will be called by the junction upon reception of a new abstract job from the higher part of the stack. It receives an incoming job and the associated meta data (in case you would like to offer some additional control to the user submitting the job). You can write anything you want inside this method. In addition, the junction interface gives you acces to another method: the execute() method. This method can be seen as a submit method. It takes a qlm job and transmit it down to the rest of the stack and get back the result.

On our example, we simply iteratively bind the value of the parameter (using the overloaded __call__ operator of the Job object), execute this job and store the result in a list.

Notice also that we need to return a proper Qaptiva result object. This is so that the result can be, in turn, post processed by the upper part of the stack. Indeed, a Junction can be piped to any Qaptiva Plugin or QPU:

from qat.qpus import get_default_qpu

# Building a simple stack
qpu = get_default_qpu()
stack = IterativeExploration(50) | qpu

This QPU stack accept variational jobs an optimize the angle to minimize the average value of the observable:

# and a simple job
from qat.core import Observable
from qat.lang import qrout, RY

@qrout
def variational_circ(beta):
    RY(beta)(0)

job = variational_circ.to_job(observable=Observable.sigma_z(0, 1))

result = stack.submit(job)
print("Best value:", result.value, "for beta =", result.parameter_map["beta"])
Best value: -0.9979453927503363 for beta = 3.077478517802246

If you don’t want to bother with the (quite low) administrative burden of binding the variables and extracting the value attribute, the qat.plugins.Optimizer class provides a slightly simpler API that particularizes the junction API to fit to the one required by most variational optimizers (see the source code documentation for more precisions).