Visualizing Results in Qiskit Experiments is Now Easier Than Ever

Qiskit
Qiskit
Published in
11 min readMar 29, 2023

By Conrad J. Haupt and Daniel J. Egger

Qiskit Experiments is an extension to core Qiskit with a focus on running experiments, analyses, and calibration of pulse schedules. The library already has functionality to run Randomized Benchmarking, Quantum Volume, and single- and two-qubit calibration experiments. Qiskit Experiments is still being developed, with many new features added to every new release. But an exciting recent addition is the visualization module, which allows for the automatic generation of plots from experiment and analysis results.

The visualization module defines a standard interface to create figures, allowing users and developers to more easily modify properties of a plot from an experiment without re-implementing the underlying code. The visualization module also allows the reuse of plotting code in different components of Qiskit Experiments and your own experiments. This means that plotting results is more standardized across Qiskit Experiments, and it’s easier to customize existing plots in the library.

Here, we introduce this new module, show how to customize figures, and demonstrate how to add plotting functionality to your own custom experiments.

New Module Overview

The new module qiskit_experiments.visualization contains plotter and drawer classes to create figures and interact with plotting libraries, respectively. You, as a user of Qiskit Experiments, will typically interact with plotters (not drawers) as they define the kind of figure to plot. Plotters interact directly with drawers, but are agnostic to the plotting library wrapped by the drawer. The default plotting library in Qiskit is Matplotlib, and so plotters use the Matplotlib drawer MplDrawer by default. However, there is nothing that prevents you from creating a new drawer for a library like Plotly by subclassing BaseDrawer.

Each plotter class defines a type of figure. The release of Qiskit experiments 0.5.0 has two plotter classes. The first, CurvePlotter, creates figures where Y values are plotted against X values. This is typically needed in experiments where models are being fitted to results or where we make measurements based on some control parameters. Examples include Rabi, T2Ramsey, and RoughDrag. The second, IQPlotter, displays level-1 or IQ measurements, i.e., complex numbers representing an integrated signal encoding the state of a qubit. This plotter is used in experiments that work with “low-level” measurements, such as the newly added MultiStateDiscrimination experiment which helps train discriminators to label IQ data as states, i.e., converting IQ data into counts. Plotting IQ data is very important for some researchers as it helps them investigate the behavior of quantum systems at a level which is not possible with counts. You may already be using IQ data in your experiments.

Now that you know something about the structure of the visualization module, we will see how to interact with the features it provides, either as a user of existing experiments, as a developer of a new experiment, or as a developer of new plotting functionality. You most likely identify with at least one of these use-cases. The next three sections cover these three use-cases.

Customizing figures as an experiment user

Qiskit Experiments distinguishes between running an experiment and analyzing the data. For example, consider the Rabi experiment: the Rabi experiment class uses the OscillationAnalysis analysis class, which processes the measurement data and fits a cosine to the results. The default plotter for OscillationAnalysis is an instance of CurvePlotter, which creates figures illustrating the result of a curve-fitting procedure. It can be important to know which class of plotter is used, as some configuration options are different (e.g., plot_sigma for CurvePlotter).

Suppose we are running a Rabi experiment on qubit 0 of our favorite backend, and we want to customize the generated figure. When we run the experiment in the same way as the previous version of Qiskit Experiments, we can retrieve figures from the returned ExperimentData instance; as shown below.

from qiskit import pulse
from qiskit.circuit import Parameter
from qiskit_experiments.library import Rabi

amp = Parameter("amp")
with pulse.build(name="x") as xp:
pulse.play(pulse.Drag(64, amp, 16, 0.1), pulse.DriveChannel(0))

rabi = Rabi(0, xp, backend=backend)
rabi_data = rabi.run()

# Wait for results.
rabi_data.block_for_results()

# Get figure.
rabi_data.figure(0)

This is the default figure generated by OscillationAnalysis for our Rabi experiment. What has changed for Rabi with Qiskit Experiments 0.5.0 is that this figure was generated using CurvePlotter. The fitted cosine is shown as a blue line with the individual measurements from the experiment shown as circles. We are also given a small fit report showing the rabi_rate. As a user, you may want to customize the figure. Our experiment’s analysis class has a plotter property which contains the plotter instance that generates the figure. We can customize the figure by setting the options of the plotter. In the code below, we modify the color and symbols of our plot and change the axis labels for the amplitude units.

# Retrieve plotter from analysis instance.
plotter = rabi.analysis.plotter
# Change the X axis unit values.

plotter.set_figure_options(
xval_unit="arb.", # The name of x-axis unit, short for "arbitrary".
xval_unit_scale=False, # Don't scale the unit using an SI prefix, like"kilo".
)

# Change the colour and symbol for the cosine.
plotter.figure_options.series_params.update(
{"cos": {"symbol": "x", "color": "r"}},
)

# We set "figsize" directly so we don't overwrite the entire style.
plotter.options.style["figsize"] = (6, 4)

# We call `figure()` to generate the figure.
plotter.figure()

Options and Figure-Options

Plotters have two sets of options that customize their behavior and the content of the figure: options and figure_options. Figure options contain values that directly control what is drawn on the figure, such as axis labels. This also includes series_params, which controls the symbols, colors, and legend labels for different data series in the figure. This is a nested dictionary keyed on the series names and then the option name. In the example above, we have a series called “cos” which is added to the plotter by the Rabi experiment. We modify its color to red and its symbol to an “x” instead of a circle. Since we do not want to overwrite other existing series parameters, we update the dictionary with our changes. We do the same for the plotter style when we set the figure size to 6x4 inches.

If we had set these options before running our experiment, the figure in rabi_data would contain the red results. As we modified the style afterwards, we call plotter.figure() to regenerate the figure. Since all plotted data is contained in the plotter we do not need to re-run our analysis to regenerate the figure.

Below is a more complicated example in which we customize the figure of a DRAG experiment.

from qiskit_experiments.library import RoughDrag
from qiskit_experiments.visualization import PlotStyle

beta = Parameter("beta")
with pulse.build(name="xp") as xp:
pulse.play(pulse.Drag(64, 0.66, 16, beta), pulse.DriveChannel(0))
# Create rough DRAG experiment.
drag = RoughDrag(0, xp, backend=backend)

## Set plotter options
plotter = drag.analysis.plotter

# Update series parameters.
plotter.figure_options.series_params.update(
{
"nrep=1": {
"color": (27 / 255, 158 / 255, 119 / 255),
"symbol": "^",
},
"nrep=3": {
"color": (217 / 255, 95 / 255, 2 / 255),
"symbol": "s",
},
"nrep=5": {
"color": (117 / 255, 112 / 255, 179 / 255),
"symbol": "o",
},
}
)
# Set figure options
plotter.set_figure_options(
xval_unit="arb.",
xval_unit_scale=False,
figure_title="Rough DRAG Experiment on Qubit 0",
)

# Set style parameters
plotter.options.style["symbol_size"] = 10
plotter.options.style["legend_loc"] = "upper center"

# Run experiment.
drag_data = drag.run()

# Wait for results.
drag_data.block_for_results()

# Get figure.
drag_data.figure(0)

Adding Plotting to Your Experiments

You can easily integrate plotters into custom analysis classes. To add a plotter instance to such a class, we define a new plotter property, pass it relevant data in the analysis class’ _run_analysis method, and return the generated figure alongside our analysis results. We use the IQPlotter class to illustrate how this is done for an arbitrary analysis class.

To ensure that we have an interface similar to existing analysis classes in Qiskit Experiments, we make our plotter accessible as an analysis.plotter property and analysis.options.plotter option. The code below accomplishes this for our example MyIQAnalysis analysis class. We set the drawer to MplDrawer to use Matplotlib by default. The plotter property of our analysis class makes it easier to access the plotter instance; i.e., using self.plotter and analysis.plotter.

We set default options and figure options in _default_options, but you can still override them as we did in the Rabi and Drag section above.

import numpy as np
from qiskit_experiments.framework import BaseAnalysis, Options from qiskit_experiments.visualization import (
BasePlotter,
IQPlotter,
MplDrawer,
PlotStyle,
)

class MYIQAnalysis(BaseAnalysis):
@classmethod
def _default_options(cls) -> Options:
options = super()._default_options()
# We create the plotter and create an option for it.
options.plotter = IQPlotter(MplDrawer())
options.plotter.set_figure_options(
xlabel="In-phase",
ylabel="Quadrature",
figure_title="My IQ Analysis Figure",
series_params={
"0": {"label": "|0>"},
"1": {"label": "|1>"},
"2": {"label": "|2>"},
},
)
return options

@property
def plotter(self) -> BasePlotter:
return self.options.plotter

The MyIQAnalysis class accepts single-shot/level-1* IQ data, which consists of an in-phase and quadrature measurement for each shot and circuit. _run_analysis is passed an ExperimentData instance which contains IQ data as a list of dictionaries (one per circuit) where their “memory” entries are lists of IQ values (one per shot). Each dictionary has a “metadata” entry, with the name of a prepared state: “0”, “1”, or “2”. These are our series names.

Our goal is to create a figure that displays the single-shot IQ values of each prepared-state (one per circuit). We process the “memory” data passed to the analysis class and set the points and centroid series data in the plotter. This is accomplished in the code below, where we also train a discriminator to label the IQ points as one of the three prepared states. IQPlotter supports plotting a discriminator as optional supplementary data, which will show predicted series over the axis area.

*For more on the different measurement levels and return types, see meas_level and meas_type in the Qiskit Compiler docs.

 # Inside MyIQAnalysis.
def _run_analysis(self, experiment_data):
data = experiment_data.data()

analysis_results = []
for datum in data:
# Analysis code
analysis_results.append(self._analysis_result(datum))

# Plotting code
series_name = datum["metadata"]["name"]
points = datum["memory"]
centroid = np.mean(points, axis=0)
self.plotter.set_series_data(
series_name,
points=points,
centroid=centroid,
)
# Add discriminator to IQPlotter.
discriminator = self._train_discriminator(data)
self.plotter.set_supplementary_data(discriminator=discriminator)

return analysis_results, [self.plotter.figure()]

If we run the above analysis on some appropriate experiment data, as previously described, our class will generate a figure showing IQ points and their centroids. Below, we run our analysis class on dummy data contained inside the ExperimentData instance exp_data. There are three series, labelled |0⟩, |1⟩, and |2⟩; each with a different color. As a discriminator is set in the plotter, a background of predictions is drawn and points that are misclassified are marked in red.

analysis = MYIQAnalysis()
exp_data = analysis.run(exp_data)
exp_data.block_for_results()
exp_data.figure(0)

1.3.1 Series and Supplementary Data

Plotter data can be either series or supplementary data. This separation into two types makes it clear where the values come from and what the plotter does with them. An easy way to tell whether data is series- or supplementary-data is by whether one would expect a legend entry. Series data typically has a legend entry, and includes values that are plotted in data-coordinates on the axes such as scatter points or X-Y values for a line. Supplementary data contains values and information for the figure as a whole and not a given series. These would not have a legend entry. Below, we use the DRAG plot from the first section to illustrate which parts of the figure are from series and supplementary data. The non-faded parts of each subfigure contain the corresponding data type. Combining the two data types with labels, titles, a legend, and the axis grid results in the final DRAG plot.

For the IQPlotter used in our analysis class, the discriminator is supplementary data as it would not have a legend entry and it is data for the entire figure, not an individual series drawn on the axes. The points and centroids are series data as they correspond to individual graphics plotted on the axis and have legend entries. It is important to understand the difference between series and supplementary data as plotters can take any number of series but only specific supplementary data.

When data is added to a plotter, it is identified by a data-key, which is the keyword provided in set_series_data and set_supplementary_data. Examples for IQPlotter include points and centroid for series data and discriminator for supplementary data. It is up to the plotter to define which data keys it supports. You can view the list of supported data keys, and what they represent, by looking at the Qiskit Experiments documentation or calling plotter.expected_series_data_keys() and plotter.expected_supplementary_data_keys().

analysis.plotter.expected_series_data_keys()

[‘points’, ‘centroid’]

analysis.plotter.expected_supplementary_data_keys()

[‘discriminator’, ‘fidelity’]

When we call self.plotter.figure() in _run_analysis, the IQPlotter will take all set data and generate an IQ figure with appropriate colors, symbols, and labels. The plotter can still be customized outside of the analysis class as we did in the first section. Because we defined a plotter property for our MyIQAnalysis class, we can access the plotter by calling analysis.plotter when customizing options. Below is an example of how we would change the color and label of the 2 series. Notice that we use the dict.update() method so we don’t override already-existing series parameters. Now we have a customized figure where the “2” state is in cyan!

analysis.plotter.figure_options.series_params.update(
{"2": {"symbol": "X", "color": "c", "label": "|2> (Leakage)"}}
)
analysis.plotter.figure()

1.4 Creating your own plotter

You can create a custom figure plotter by subclassing BasePlotter and overriding the following methods

  • expected_series_data_keys
  • expected_supplementary_data_keys
  • _plot_figure

The first two methods allow you to define a list of supported data-keys as strings, which identify the different data to plot. The third method, _plot_figure, must contain your code to generate a figure by calling methods on the plotter’s drawer instance (self.drawer). When plotter.figure() is called by an analysis class, the plotter calls _plot_figure and then returns your figure object which is added to the experiment data instance. It is also good practice to set default values for figure options, such as axis labels. You can do this by overriding the _default_figure_options method in your plotter subclass. Additional details are in the BasePlotter documentation.

Conclusion

Encapsulating the figure generation code in a plotter class allows various types of figures to be created without having to duplicate code. The separation between plotters and drawers means that new plotting libraries can be integrated into Qiskit Experiments and your own experiment code without needing to alter existing plotters. The visualization module is currently integrated into all CurveAnalysis analysis classes, as well as the new MultiStateDiscrimination experiment. To find out more about using the visualization module, incorporating plotters into your own experiments, and developing additional features, have a look at the module’s documentation and the Qiskit Experiments GitHub repository. If you have questions about using the visualization module or Qiskit Experiments in general, feel free to ask in the #experiments Qiskit Slack channel.

--

--

Qiskit
Qiskit

An open source quantum computing framework for writing quantum experiments and applications