Visualizing Results in Qiskit Experiments is Now Easier Than Ever
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.