Skip to content

Commit

Permalink
Convert QVAnalysis to use BasePlotter
Browse files Browse the repository at this point in the history
Add an hline method to BaseDrawer and expose linewidth and
linestyle as series options.

Catch expected warnings about insufficient trials in analysis tests.

Remove filters preventing test failures when using the deprecated
visualization APIs.
  • Loading branch information
wshanks committed Dec 19, 2023
1 parent f7750d5 commit d91b7f6
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 88 deletions.
166 changes: 93 additions & 73 deletions qiskit_experiments/library/quantum_volume/qv_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import math
import warnings
from typing import Optional
from typing import List, Optional

import numpy as np
import uncertainties
Expand All @@ -26,6 +26,94 @@
AnalysisResultData,
Options,
)
from qiskit_experiments.visualization import BasePlotter, MplDrawer


class QVPlotter(BasePlotter):
@classmethod
def expected_series_data_keys(cls) -> List[str]:
return ["hops"]

@classmethod
def expected_supplementary_data_keys(cls) -> List[str]:
return ["depth"]

def set_supplementary_data(self, **data_kwargs):
if "depth" in data_kwargs:
self.set_figure_options(
figure_title=(
f"Quantum Volume experiment for depth {data_kwargs['depth']}"
" - accumulative hop"
),
)
super().set_supplementary_data(**data_kwargs)

@classmethod
def _default_figure_options(cls) -> Options:
options = super()._default_figure_options()
options.xlabel = "Number of Trials"
options.ylabel = "Heavy Output Probability"
options.figure_title = "Quantum Volume experiment - accumulative hop"
options.series_params = {
"hop": {"color": "gray", "symbol": "."},
"threshold": {"color": "black", "linestyle": "dashed", "linewidth": 1},
"hop_cumulative": {"color": "r"},
"hop_twosigma": {"color": "lightgray"},
}
return options

@classmethod
def _default_options(cls) -> Options:
options = super()._default_options()
options.style["figsize"] = (6.4, 4.8)
options.style["axis_label_size"] = 14
options.style["symbol_size"] = 2
return options

def _plot_figure(self):
series = self.series[0]
hops, = self.data_for(series, ["hops"])
trials = np.arange(1, 1 + len(hops))
hop_accumulative = np.cumsum(hops) / trials
hop_twosigma = 2 * (hop_accumulative * (1 - hop_accumulative) / trials) ** 0.5

self.drawer.line(
trials,
hop_accumulative,
name="hop_cumulative",
label="Cumulative HOP",
legend=True,
)
self.drawer.hline(
2 / 3,
name="threshold",
label="Threshold",
legend=True,
)
self.drawer.scatter(
trials,
hops,
name="hop",
label="Individual HOP",
legend=True,
linewidth=1.5,
)
self.drawer.filled_y_area(
trials,
hop_accumulative - hop_twosigma,
hop_accumulative + hop_twosigma,
alpha=0.5,
legend=True,
name="hop_twosigma",
label="2σ",
)

self.drawer.set_figure_options(
ylim=(
max(hop_accumulative[-1] - 4 * hop_twosigma[-1], 0),
min(hop_accumulative[-1] + 4 * hop_twosigma[-1], 1),
),
)


class QuantumVolumeAnalysis(BaseAnalysis):
Expand Down Expand Up @@ -53,6 +141,7 @@ def _default_options(cls) -> Options:
options = super()._default_options()
options.plot = True
options.ax = None
options.plotter = QVPlotter(MplDrawer())
return options

def _run_analysis(self, experiment_data):
Expand All @@ -77,8 +166,9 @@ def _run_analysis(self, experiment_data):
hop_result, qv_result = self._calc_quantum_volume(heavy_output_prob_exp, depth, num_trials)

if self.options.plot:
ax = self._format_plot(hop_result, ax=self.options.ax)
figures = [ax.get_figure()]
self.options.plotter.set_series_data("hops", hops=hop_result.extra["HOPs"])
self.options.plotter.set_supplementary_data(depth=hop_result.extra["depth"])
figures = [self.options.plotter.figure()]
else:
figures = None
return [hop_result, qv_result], figures
Expand Down Expand Up @@ -238,73 +328,3 @@ def _calc_quantum_volume(self, heavy_output_prob_exp, depth, trials):
},
)
return hop_result, qv_result

@staticmethod
def _format_plot(
hop_result: AnalysisResultData, ax: Optional["matplotlib.pyplot.AxesSubplot"] = None
):
"""Format the QV plot
Args:
hop_result: the heavy output probability analysis result.
ax: matplotlib axis to add plot to.
Returns:
AxesSubPlot: the matplotlib axes containing the plot.
"""
trials = hop_result.extra["trials"]
heavy_probs = hop_result.extra["HOPs"]
trial_list = np.arange(1, trials + 1) # x data

hop_accumulative = np.cumsum(heavy_probs) / trial_list
two_sigma = 2 * (hop_accumulative * (1 - hop_accumulative) / trial_list) ** 0.5

# Plot individual HOP as scatter
ax = plot_scatter(
trial_list,
heavy_probs,
ax=ax,
s=3,
zorder=3,
label="Individual HOP",
)
# Plot accumulative HOP
ax.plot(trial_list, hop_accumulative, color="r", label="Cumulative HOP")

# Plot two-sigma shaded area
ax = plot_errorbar(
trial_list,
hop_accumulative,
two_sigma,
ax=ax,
fmt="none",
ecolor="lightgray",
elinewidth=20,
capsize=0,
alpha=0.5,
label="2$\\sigma$",
)
# Plot 2/3 success threshold
ax.axhline(2 / 3, color="k", linestyle="dashed", linewidth=1, label="Threshold")

ax.set_ylim(
max(hop_accumulative[-1] - 4 * two_sigma[-1], 0),
min(hop_accumulative[-1] + 4 * two_sigma[-1], 1),
)

ax.set_xlabel("Number of Trials", fontsize=14)
ax.set_ylabel("Heavy Output Probability", fontsize=14)

ax.set_title(
"Quantum Volume experiment for depth "
+ str(hop_result.extra["depth"])
+ " - accumulative hop",
fontsize=14,
)

# Re-arrange legend order
handles, labels = ax.get_legend_handles_labels()
handles = [handles[1], handles[2], handles[0], handles[3]]
labels = [labels[1], labels[2], labels[0], labels[3]]
ax.legend(handles, labels)
return ax
24 changes: 24 additions & 0 deletions qiskit_experiments/visualization/drawers/base_drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,30 @@ def line(
options: Valid options for the drawer backend API.
"""

@abstractmethod
def hline(
self,
y_value: float,
name: Optional[SeriesName] = None,
label: Optional[str] = None,
legend: bool = False,
**options,
):
"""Draw a horizontal line.
Args:
y_value: Y value for line.
name: Name of this series.
label: Optional legend label to override ``name`` and ``series_params``.
legend: Whether the drawn area must have a legend entry. Defaults to False.
The series label in the legend will be ``label`` if it is not None. If
it is, then ``series_params`` is searched for a ``"label"`` entry for
the series identified by ``name``. If this is also ``None``, then
``name`` is used as the fallback. If no ``name`` is provided, then no
legend entry is generated.
options: Valid options for the drawer backend API.
"""

@abstractmethod
def filled_y_area(
self,
Expand Down
25 changes: 23 additions & 2 deletions qiskit_experiments/visualization/drawers/mpl_drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,13 +427,34 @@ def line(

draw_ops = {
"color": color,
"linestyle": "-",
"linewidth": 2,
"linestyle": series_params.get("linestyle", "-"),
"linewidth": series_params.get("linewidth", 2),
}
self._update_label_in_options(draw_ops, name, label, legend)
draw_ops.update(**options)
self._get_axis(axis).plot(x_data, y_data, **draw_ops)

def hline(
self,
y_value: float,
name: Optional[SeriesName] = None,
label: Optional[str] = None,
legend: bool = False,
**options,
):
series_params = self.figure_options.series_params.get(name, {})
axis = series_params.get("canvas", None)
color = series_params.get("color", self._get_default_color(name))

draw_ops = {
"color": color,
"linestyle": series_params.get("linestyle", "-"),
"linewidth": series_params.get("linewidth", 2),
}
self._update_label_in_options(draw_ops, name, label, legend)
draw_ops.update(**options)
self._get_axis(axis).axhline(y_value, **draw_ops)

def filled_y_area(
self,
x_data: Sequence[float],
Expand Down
16 changes: 16 additions & 0 deletions releasenotes/notes/qvplotter-04efe280aaa9d555.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
features:
- |
An :meth:`~qiskit_experiments.visualization.BasePlotter.hline` method was
added to :class:`~qiskit_experiments.visualization.BasePlotter` for
generating horizontal lines.
- |
The
:class:`~qiskit_experiments.library.quantum_volume.QuantumVolumeAnalysis`
analysis class was updated to use
:class:`~qiskit_experiments.visualization.BasePlotter` for its figure
generation. The appearance of the figure should be the same as in previous
releases. It is easier to customize the figure by setting options on the
plotter object, but currently the user must consult the plotter code as the
options are not documented. See `#1348
<https://github.com/Qiskit-Extensions/qiskit-experiments/pull/1348>`__.
3 changes: 0 additions & 3 deletions test/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,6 @@ def setUpClass(cls):
# default.
# pylint: disable=invalid-name
allow_deprecationwarning_message = [
# TODO: Remove in 0.6, when submodule `.curve_analysis.visualization` is removed.
r".*Plotting and drawing functionality has been moved",
r".*Legacy drawers from `.curve_analysis.visualization are deprecated",
]
for msg in allow_deprecationwarning_message:
warnings.filterwarnings("default", category=DeprecationWarning, message=msg)
Expand Down
24 changes: 14 additions & 10 deletions test/library/quantum_volume/test_qv.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""
A Tester for the Quantum Volume experiment
"""
import warnings
from test.base import QiskitExperimentsTestCase
import json
import os
Expand Down Expand Up @@ -106,15 +107,17 @@ def test_qv_sigma_decreasing(self):

qv_exp = QuantumVolume(range(num_of_qubits), seed=SEED)
# set number of trials to a low number to make the test faster
qv_exp.set_experiment_options(trials=2)
expdata1 = qv_exp.run(backend)
self.assertExperimentDone(expdata1)
result_data1 = expdata1.analysis_results(0)
expdata2 = qv_exp.run(backend, analysis=None)
self.assertExperimentDone(expdata2)
expdata2.add_data(expdata1.data())
qv_exp.analysis.run(expdata2)
result_data2 = expdata2.analysis_results(0)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Must use at least 100 trials")
qv_exp.set_experiment_options(trials=2)
expdata1 = qv_exp.run(backend)
self.assertExperimentDone(expdata1)
result_data1 = expdata1.analysis_results(0)
expdata2 = qv_exp.run(backend, analysis=None)
self.assertExperimentDone(expdata2)
expdata2.add_data(expdata1.data())
qv_exp.analysis.run(expdata2)
result_data2 = expdata2.analysis_results(0)

self.assertTrue(result_data1.extra["trials"] == 2, "number of trials is incorrect")
self.assertTrue(
Expand Down Expand Up @@ -145,7 +148,8 @@ def test_qv_failure_insufficient_trials(self):
exp_data = ExperimentData(experiment=qv_exp, backend=backend)
exp_data.add_data(insufficient_trials_data)

qv_exp.analysis.run(exp_data)
with self.assertWarns(UserWarning):
qv_exp.analysis.run(exp_data)
qv_result = exp_data.analysis_results(1)
self.assertTrue(
qv_result.extra["success"] is False and qv_result.value == 1,
Expand Down
11 changes: 11 additions & 0 deletions test/visualization/mock_drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ def line(
"""Does nothing."""
pass

def hline(
self,
y_value: float,
name: Optional[str] = None,
label: Optional[str] = None,
legend: bool = False,
**options,
):
"""Does nothing."""
pass

def filled_y_area(
self,
x_data: Sequence[float],
Expand Down

0 comments on commit d91b7f6

Please sign in to comment.