Inserting an operator

Now that we have a simulation to play around with, let's add an operator.

There are many types of operators conceivable. To keep things simple here, we'll use an operator that just affects measurements as an example. Here's the code:

from dqcsim.plugin import *

@plugin("Measurement-Error", "Tutorial", "0.1")
class MyOperator(Operator):

    def __init__(self):
        super().__init__()
        self.one_error = 0.0
        self.zero_error = 0.0

    def handle_host_measurementError_setOneError(self, value=0.0):
        self.one_error = value

    def handle_host_measurementError_setZeroError(self, value=0.0):
        self.zero_error = value

    def handle_measurement(self, measurement):
        if measurement.value == 1:
            measurement.value = self.random_float() < self.one_error
        elif measurement.value == 0:
            measurement.value = self.random_float() >= self.zero_error
        return measurement

MyOperator().run()

To use it within the simulation created in the previous section, replace the following line:

with Simulator('my-plugin.py', 'null') as sim:

With this:

with Simulator('my-plugin.py', 'my-operator.py', 'null') as sim:

What this does should be obvious – it sticks the operator we just made in between, assuming you used the same filename.

Running it doesn't change much though:

$ python3 simulate.py 
... Info dqcsim  Starting Simulation with seed: 14673804979996191647
... Info back    Running null backend initialization callback
... Info dqcsim  PluginProcess exited with status code: 0
... Info dqcsim  PluginProcess exited with status code: 0
... Info dqcsim  PluginProcess exited with status code: 0
Number of balanced outcomes: 498
Number of constant outcomes: 502

Still 50/50. Of course, that's because we never set any error rates.

Dissecting the new plugin

from dqcsim.plugin import *

@plugin("Measurement-Error", "Tutorial", "0.1")
class MyOperator(Operator):
    ...

MyOperator().run()

This part is the same as what we've seen before, except we've renamed some things and are deriviing from Operator instead of Frontend. You'll see these lines in every DQCsim plugin; it's the boilerplate code that turns an otherwise regular Python script into a valid plugin.

def __init__(self):
    super().__init__()
    self.one_error = 0.0
    self.zero_error = 0.0

We've built the plugin such that we can set the measurement error rates. Specifically, we specify two probabilities:

  • one_error is the chance that a qubit observed to be in the one state is measured as zero;
  • zero_error is the chance that a qubit observed to be in the zero state is measured as one.

We can store these parameters within our operator class by defining them in its initializer, as done here.

Note specifically the super().__init__() line. It's often forgotten, but in fact, you have to call this any time you override any Python class' constructor, or your superclass' constructor will simply never be called. In this case, not doing it will break DQCsim, although it will tell you what you need to do to fix it in the error message.

def handle_host_measurementError_setOneError(self, value=0.0):
    self.one_error = value

def handle_host_measurementError_setZeroError(self, value=0.0):
    self.zero_error = value

These two functions provide entry points for any ArbCmds sent to us by the host or by the initialization callback (since we didn't override it). The interface and operation IDs are in the handler's name, as is the source of the command, which can be host or upstream.

The Python module detects which interfaces your plugin supports by looking for handlers of the form handle_host_<interface>_<operation>() inside the plugin constructor. Interface and operation IDs preferably don't contain any underscores, because this makes the split between the interface and operation ID ambiguous. In this case, the automatic detection algorithm will throw an error, and ask you to specify which interfaces your plugin supports through a keyword argument to the plugin's constructor.

In this case, the detection algorithm works just fine, and determines that the plugin supports the measurementError interface. Within the interface, two operations are defined, setOneError and setZeroError; any other operation will return an error.

def handle_measurement(self, measurement):
    if measurement.value == 1:
        measurement.value = self.random_float() < self.one_error
    elif measurement.value == 0:
        measurement.value = self.random_float() >= self.zero_error
    return measurement

This function is the core of the plugin. If it exists, it is called by DQCsim whenever a measurement passes through the operator, to allow the operator to return a modified measurement instead. It can also turn a single measurement into a list of measurements or block propagation of the measurement, but you wouldn't need to do this unless you're making some complex mapping algorithm.

The implementation provided here modifies the measurement value based on the perfect measurement received from downstream, DQCsim's random generator, and the configured probabilities.

Testing the error model

Let's change the Simulator() arguments again. It's getting a little complex now, so we'll fold it apart for clarity:

with Simulator(
    'my-plugin.py',
    (
        'my-operator.py',
        {
            'init': [
                ArbCmd('measurementError', 'setZeroError', value=0.5)
            ]
        }
    ),
    'null'
) as sim:

This adds an initialization command to our error model operator, that sets the measurement error probability for a zero observation to 0.5. When we run the program now, we get something like this:

$ python3 simulate.py
... Info dqcsim  Starting Simulation with seed: 8830780357769760084
... Info back    Running null backend initialization callback
... Info dqcsim  PluginProcess exited with status code: 0
... Info dqcsim  PluginProcess exited with status code: 0
... Info dqcsim  PluginProcess exited with status code: 0
Number of balanced outcomes: 266
Number of constant outcomes: 734

Success – this is clearly not 50/50 anymore! Half of the balanced outcomes are converted to constant by the error model, so the probability is 25/75 now.

To make the Simulator() syntax a bit more aesthetically pleasing, we can configure the simulation in multiple steps, like this:

sim_config = Simulator('my-plugin.py', 'null')
sim_config.with_operator(
    'my-operator.py',
    init=ArbCmd('measurementError', 'setZeroError', value=0.5))
with sim_config as sim:

Functionally, this is exactly the same.

Changing error model parameters at runtime

Because our operator allows the values to be set with run-time ArbCmds as well, we can also do the following:

with Simulator('my-plugin.py', 'my-operator.py', 'null') as sim:
    sim.start(oracle=oracle, runs=runs)
    results = [sim.recv()['result'] for _ in range(runs)]
    sim.wait()

    sim.arb('op1', 'measurementError', 'setZeroError', value=0.5)

    sim.start(oracle=oracle, runs=runs)
    results += [sim.recv()['result'] for _ in range(runs)]
    sim.wait()

Now half of our simulation runs at 50/50 probability, and half of it at 25/75. So we expect to get around 38/62. Indeed:

$ python3 simulate.py 
... Info dqcsim  Starting Simulation with seed: 6379995085809620608
... Info back    Running null backend initialization callback
... Info dqcsim  PluginProcess exited with status code: 0
... Info dqcsim  PluginProcess exited with status code: 0
... Info dqcsim  PluginProcess exited with status code: 0
Number of balanced outcomes: 722
Number of constant outcomes: 1278

Note however, that sending a new probability in the middle of our recv() calls will not work. This is because the frontend does not wait for recv() to be called; send() is asynchronous! To make that work, you'd have to have the frontend wait for the host through a recv() call, or have the frontend update the operator's parameters while it's running. In the latter case, you also have to define handle_upstream_*() equivalents for the handle_host_*() callbacks.