Selectable discriminators

Discriminators and kernels can now be selected on a per-qubit basis in both circuits and OpenPulse. This allows us to choose between hardware and software discriminators and kernels.

Using the hardware discriminator, in particular, can cut down on execution time as it does not require calibration circuits and requires no software analysis. If you instead want to calibrate qubits on the fly (rather than relying on the hardware defaults), you can do that by specifying one of the software discriminators (or requesting meas_level=1 and using an Ignis discriminator).

import numpy as np
import matplotlib.pyplot as plt

import qiskit
from qiskit_ibm_provider import IBMProvider
from qiskit import QuantumCircuit, execute, assemble, schedule, transpile
from qiskit.result import marginal_counts
from qiskit.tools.visualization import plot_histogram
IBMProvider()

# Fill in your hub/group/provider
provider = IBMProvider(instance='hub/group/project')

# Fill in your backend
backend = provider.get_backend('some-backend')

config = backend.configuration()

config is the backend.configuration(). This will be referenced throughout this tutorial.

Info: These features require qiskit>0.22.0 as they rely on qiskit-terra>0.16.0.

We can check to see which kernels and discriminators are supported by the backend.

print("Supported kernels: ", config.meas_kernels)
print("Supported discriminators: ", config.discriminators)
Supported kernels:  ['hw_boxcar']
Supported discriminators:  ['quadratic_discriminator', 'linear_discriminator', 'hw_centroid']

We will just focus on setting discriminators for now, but the process is analogous for kernels.

Note: The supported kernels/discriminators vary by device. Make sure the kernels/discriminators you select are compatible with the types returned from backend.configuration().

Circuit Model

QASM (circuit) discriminators are specified as a dictionary mapping of the qubit to the discriminator. View the schema here.

You can specify params via a params entry in the dict. However, it is recommended this be left blank except for expert users. Empty params will use default backend parameters.

We apply a hardware discriminator on qubit 0, a linear discriminator (see here) on qubit 1, and a quadratic discriminator (see here) on qubit 2.

Note: Hardware discriminators may not be supported on all devices and the type may vary from device to device.

discrim_config = {"q0": {"name": "hw_centroid"},
                  "q1": {"name": "linear_discriminator"},
                  "q2": {"name": "quadratic_discriminator"}}

We run 3 simple circuits. One with measure only, one with x on all qubits and then measure, and one with hadamard on all qubits and then measure.

We verify the expected results (all |000> for measure, all |111> for x and equal distribution in all 8 basis states for hadamard).

qc_meas = QuantumCircuit(3, 3)
qc_meas.measure([0,1,2], [0,1,2])

qc_x = QuantumCircuit(3, 3)
qc_x.x(0)
qc_x.x(1)
qc_x.x(2)
qc_x.measure([0,1,2], [0,1,2])

qc_h = QuantumCircuit(3, 3)
qc_h.h(0)
qc_h.h(1)
qc_h.h(2)
qc_h.measure([0,1,2], [0,1,2])


meas_transpiled = transpile(qc_meas, backend=backend)
x_transpiled = transpile(qc_x, backend=backend)
h_transpiled = transpile(qc_h, backend=backend)

We assemble the object to verify that the discriminators are added to our qobj["config"].

qasm_sel_qobj = assemble([meas_transpiled, x_transpiled, h_transpiled], backend, meas_map=[[0],[1],[2]], discriminators=discrim_config)
print(qasm_sel_qobj.to_dict()["config"]["discriminators"])
{'q0': {'name': 'hw_centroid'}, 'q1': {'name': 'linear_discriminator'}, 'q2': {'name': 'quadratic_discriminator'}}
qasm_sel_job = backend.run(qasm_sel_qobj)
print(qasm_sel_job.job_id())
5f999d8c03680700139eb568
qasm_sel_counts = qasm_sel_job.result().get_counts()
print(qasm_sel_counts)
plot_histogram(qasm_sel_counts, legend=['meas(0,1,2)', 'x(0,1,2)', 'h(0,1,2)'], title='hardware, linear and quadratic discrim: QASM')
[{'000': 994, '001': 4, '010': 5, '100': 20, '110': 1}, {'000': 7, '001': 1, '010': 5, '011': 49, '100': 1, '101': 37, '110': 26, '111': 898}, {'000': 138, '001': 140, '010': 121, '011': 130, '100': 131, '101': 135, '110': 115, '111': 114}]
../../../../_images/eb43a9a5be5d301f28e1530269e0ead020b5b88beca300b1fadef70698c35677.png

Pulse

Now we show how to select per-qubit discriminators in OpenPulse. We utilize the same programs as for QASM (all measure, all x, all hadamard) and the same order of discriminators (q0 -> hardware, q1 -> linear_discriminator, q2 -> quadratic_discriminator).

We get x and measure from the instruction_schedule_map and get hadamard by transpiling and then scheduling the gate.

def pulse_h(qubit):
    """Return hadamard schedule on qubit."""
    qc_h = QuantumCircuit(n_qubits)
    qc_h.h(qubit)
    sched_h = schedule(transpile(qc_h, backend), backend)
    return sched_h
defaults = backend.defaults()
x0 = defaults.instruction_schedule_map.get('x', qubits=[0])
x1 = defaults.instruction_schedule_map.get('x', qubits=[1])
x2 = defaults.instruction_schedule_map.get('x', qubits=[2])
measure = defaults.instruction_schedule_map.get('measure', qubits=config.meas_map[0])

h0 = pulse_h(0)
h1 = pulse_h(1)
h2 = pulse_h(2)

The discriminators are defined by name. You can again specify parameters, but we recommend leaving that field blank and using the backend defaults.

import qiskit.pulse as pulse
hw_centroid = pulse.Discriminator('hw_centroid')
quad_discrim = pulse.Discriminator('quadratic_discriminator')
lin_discrim = pulse.Discriminator('linear_discriminator')

We filter the measure to only include the qubits of interest (0, 1, 2).

measure = measure.filter(channels=[pulse.MeasureChannel(0), 
                                   pulse.AcquireChannel(0), 
                                   pulse.MeasureChannel(1), 
                                   pulse.AcquireChannel(1),
                                   pulse.MeasureChannel(2), 
                                   pulse.AcquireChannel(2)])

We define a method to add the discriminators to our measure schedule. At present, there is no “setter” as schedules are immutable so we must define a new schedule.

def update_discriminators(schedule, discrim_dict):
    """Update the schedule w/ the custom discriminators.
    
    NOTE: discrim_dict is a dict w/ keys that are pulse channels and values that are the associated discriminators.
    """
    new_sched = pulse.Schedule()
    for time, inst in schedule.instructions:
        if isinstance(inst, pulse.instructions.Acquire):
            channel = inst.channel
            inst = pulse.instructions.Acquire(inst.duration, channel, inst.mem_slot, 
                                              discriminator=discrim_dict.get(channel, None))
        new_sched = new_sched.insert(time, inst)
    return new_sched

We assign discriminators to the proper acquire channels and define a new measure schedule containing the custom discriminators.

discrim_dict = {pulse.AcquireChannel(0): hw_centroid, 
                pulse.AcquireChannel(1): lin_discrim, 
                pulse.AcquireChannel(2): quad_discrim}
select_discrim_meas = update_discriminators(measure, discrim_dict)

We verify that the custom discriminators are provided in each Acquire.

select_discrim_acq = select_discrim_meas.filter(channels=[pulse.AcquireChannel(0), 
                                                          pulse.AcquireChannel(1), 
                                                          pulse.AcquireChannel(2)])
for _, inst in select_discrim_acq.instructions:
    print(inst)
Acquire(26160, AcquireChannel(0), MemorySlot(0), Discriminator('hw_centroid', ))
Acquire(26160, AcquireChannel(1), MemorySlot(1), Discriminator('linear_discriminator', ))
Acquire(26160, AcquireChannel(2), MemorySlot(2), Discriminator('quadratic_discriminator', ))

We construct the pulse schedules for measure, x and hadamard.

meas_sched = pulse.Schedule()
meas_sched |= select_discrim_meas

x_sched = pulse.Schedule()
x_sched |= x0
x_sched |= x1
x_sched |= x2
x_sched |= select_discrim_meas << x_sched.duration

h_sched = pulse.Schedule()
h_sched |= h0
h_sched |= h1
h_sched |= h2
h_sched |= select_discrim_meas << h_sched.duration

Finally, we assemble and run the qobj. We require a custom meas_map as we are acquiring a subset of qubits (rather than all qubits on the device).

pulse_sel_qobj = assemble([meas_sched, x_sched, h_sched], backend, meas_map=[[0],[1],[2]])
pulse_sel_job = backend.run(pulse_sel_qobj)
print(pulse_sel_job.job_id())
/opt/miniconda3/envs/qiskit/lib/python3.7/site-packages/qiskit/compiler/assemble.py:305: RuntimeWarning: Dynamic rep rates are supported on this backend. 'rep_delay' will be used instead of 'rep_time'.
  "used instead of 'rep_time'.", RuntimeWarning)
5f999db535070e0013b3fc95
pulse_sel_marg = marginal_counts(pulse_sel_job.result(refresh=True), indices=[0,1,2])
pulse_sel_counts = pulse_sel_marg.get_counts()
print(pulse_sel_counts)
plot_histogram(pulse_sel_counts, 
               legend=['meas(0,1,2)', 'x(0,1,2)', 'h(0,1,2)'], 
               title='hardware, linear and quadratic discrim: pulse')
[{'000': 967, '001': 6, '010': 18, '100': 32, '110': 1}, {'000': 6, '001': 5, '010': 2, '011': 48, '101': 45, '110': 25, '111': 893}, {'000': 135, '001': 128, '010': 128, '011': 126, '100': 139, '101': 124, '110': 119, '111': 125}]
../../../../_images/3708c79bc9550d2be672569f94d8c053c5e0609500aeea08f86d0f1aa8bcd420.png
import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright