Skip to content

Commit d91b7f6

Browse files
committed
Convert QVAnalysis to use BasePlotter
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.
1 parent f7750d5 commit d91b7f6

File tree

7 files changed

+181
-88
lines changed

7 files changed

+181
-88
lines changed

qiskit_experiments/library/quantum_volume/qv_analysis.py

Lines changed: 93 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import math
1717
import warnings
18-
from typing import Optional
18+
from typing import List, Optional
1919

2020
import numpy as np
2121
import uncertainties
@@ -26,6 +26,94 @@
2626
AnalysisResultData,
2727
Options,
2828
)
29+
from qiskit_experiments.visualization import BasePlotter, MplDrawer
30+
31+
32+
class QVPlotter(BasePlotter):
33+
@classmethod
34+
def expected_series_data_keys(cls) -> List[str]:
35+
return ["hops"]
36+
37+
@classmethod
38+
def expected_supplementary_data_keys(cls) -> List[str]:
39+
return ["depth"]
40+
41+
def set_supplementary_data(self, **data_kwargs):
42+
if "depth" in data_kwargs:
43+
self.set_figure_options(
44+
figure_title=(
45+
f"Quantum Volume experiment for depth {data_kwargs['depth']}"
46+
" - accumulative hop"
47+
),
48+
)
49+
super().set_supplementary_data(**data_kwargs)
50+
51+
@classmethod
52+
def _default_figure_options(cls) -> Options:
53+
options = super()._default_figure_options()
54+
options.xlabel = "Number of Trials"
55+
options.ylabel = "Heavy Output Probability"
56+
options.figure_title = "Quantum Volume experiment - accumulative hop"
57+
options.series_params = {
58+
"hop": {"color": "gray", "symbol": "."},
59+
"threshold": {"color": "black", "linestyle": "dashed", "linewidth": 1},
60+
"hop_cumulative": {"color": "r"},
61+
"hop_twosigma": {"color": "lightgray"},
62+
}
63+
return options
64+
65+
@classmethod
66+
def _default_options(cls) -> Options:
67+
options = super()._default_options()
68+
options.style["figsize"] = (6.4, 4.8)
69+
options.style["axis_label_size"] = 14
70+
options.style["symbol_size"] = 2
71+
return options
72+
73+
def _plot_figure(self):
74+
series = self.series[0]
75+
hops, = self.data_for(series, ["hops"])
76+
trials = np.arange(1, 1 + len(hops))
77+
hop_accumulative = np.cumsum(hops) / trials
78+
hop_twosigma = 2 * (hop_accumulative * (1 - hop_accumulative) / trials) ** 0.5
79+
80+
self.drawer.line(
81+
trials,
82+
hop_accumulative,
83+
name="hop_cumulative",
84+
label="Cumulative HOP",
85+
legend=True,
86+
)
87+
self.drawer.hline(
88+
2 / 3,
89+
name="threshold",
90+
label="Threshold",
91+
legend=True,
92+
)
93+
self.drawer.scatter(
94+
trials,
95+
hops,
96+
name="hop",
97+
label="Individual HOP",
98+
legend=True,
99+
linewidth=1.5,
100+
)
101+
self.drawer.filled_y_area(
102+
trials,
103+
hop_accumulative - hop_twosigma,
104+
hop_accumulative + hop_twosigma,
105+
alpha=0.5,
106+
legend=True,
107+
name="hop_twosigma",
108+
label="2σ",
109+
)
110+
111+
self.drawer.set_figure_options(
112+
ylim=(
113+
max(hop_accumulative[-1] - 4 * hop_twosigma[-1], 0),
114+
min(hop_accumulative[-1] + 4 * hop_twosigma[-1], 1),
115+
),
116+
)
29117

30118

31119
class QuantumVolumeAnalysis(BaseAnalysis):
@@ -53,6 +141,7 @@ def _default_options(cls) -> Options:
53141
options = super()._default_options()
54142
options.plot = True
55143
options.ax = None
144+
options.plotter = QVPlotter(MplDrawer())
56145
return options
57146

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

79168
if self.options.plot:
80-
ax = self._format_plot(hop_result, ax=self.options.ax)
81-
figures = [ax.get_figure()]
169+
self.options.plotter.set_series_data("hops", hops=hop_result.extra["HOPs"])
170+
self.options.plotter.set_supplementary_data(depth=hop_result.extra["depth"])
171+
figures = [self.options.plotter.figure()]
82172
else:
83173
figures = None
84174
return [hop_result, qv_result], figures
@@ -238,73 +328,3 @@ def _calc_quantum_volume(self, heavy_output_prob_exp, depth, trials):
238328
},
239329
)
240330
return hop_result, qv_result
241-
242-
@staticmethod
243-
def _format_plot(
244-
hop_result: AnalysisResultData, ax: Optional["matplotlib.pyplot.AxesSubplot"] = None
245-
):
246-
"""Format the QV plot
247-
248-
Args:
249-
hop_result: the heavy output probability analysis result.
250-
ax: matplotlib axis to add plot to.
251-
252-
Returns:
253-
AxesSubPlot: the matplotlib axes containing the plot.
254-
"""
255-
trials = hop_result.extra["trials"]
256-
heavy_probs = hop_result.extra["HOPs"]
257-
trial_list = np.arange(1, trials + 1) # x data
258-
259-
hop_accumulative = np.cumsum(heavy_probs) / trial_list
260-
two_sigma = 2 * (hop_accumulative * (1 - hop_accumulative) / trial_list) ** 0.5
261-
262-
# Plot individual HOP as scatter
263-
ax = plot_scatter(
264-
trial_list,
265-
heavy_probs,
266-
ax=ax,
267-
s=3,
268-
zorder=3,
269-
label="Individual HOP",
270-
)
271-
# Plot accumulative HOP
272-
ax.plot(trial_list, hop_accumulative, color="r", label="Cumulative HOP")
273-
274-
# Plot two-sigma shaded area
275-
ax = plot_errorbar(
276-
trial_list,
277-
hop_accumulative,
278-
two_sigma,
279-
ax=ax,
280-
fmt="none",
281-
ecolor="lightgray",
282-
elinewidth=20,
283-
capsize=0,
284-
alpha=0.5,
285-
label="2$\\sigma$",
286-
)
287-
# Plot 2/3 success threshold
288-
ax.axhline(2 / 3, color="k", linestyle="dashed", linewidth=1, label="Threshold")
289-
290-
ax.set_ylim(
291-
max(hop_accumulative[-1] - 4 * two_sigma[-1], 0),
292-
min(hop_accumulative[-1] + 4 * two_sigma[-1], 1),
293-
)
294-
295-
ax.set_xlabel("Number of Trials", fontsize=14)
296-
ax.set_ylabel("Heavy Output Probability", fontsize=14)
297-
298-
ax.set_title(
299-
"Quantum Volume experiment for depth "
300-
+ str(hop_result.extra["depth"])
301-
+ " - accumulative hop",
302-
fontsize=14,
303-
)
304-
305-
# Re-arrange legend order
306-
handles, labels = ax.get_legend_handles_labels()
307-
handles = [handles[1], handles[2], handles[0], handles[3]]
308-
labels = [labels[1], labels[2], labels[0], labels[3]]
309-
ax.legend(handles, labels)
310-
return ax

qiskit_experiments/visualization/drawers/base_drawer.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,30 @@ def line(
403403
options: Valid options for the drawer backend API.
404404
"""
405405

406+
@abstractmethod
407+
def hline(
408+
self,
409+
y_value: float,
410+
name: Optional[SeriesName] = None,
411+
label: Optional[str] = None,
412+
legend: bool = False,
413+
**options,
414+
):
415+
"""Draw a horizontal line.
416+
417+
Args:
418+
y_value: Y value for line.
419+
name: Name of this series.
420+
label: Optional legend label to override ``name`` and ``series_params``.
421+
legend: Whether the drawn area must have a legend entry. Defaults to False.
422+
The series label in the legend will be ``label`` if it is not None. If
423+
it is, then ``series_params`` is searched for a ``"label"`` entry for
424+
the series identified by ``name``. If this is also ``None``, then
425+
``name`` is used as the fallback. If no ``name`` is provided, then no
426+
legend entry is generated.
427+
options: Valid options for the drawer backend API.
428+
"""
429+
406430
@abstractmethod
407431
def filled_y_area(
408432
self,

qiskit_experiments/visualization/drawers/mpl_drawer.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,13 +427,34 @@ def line(
427427

428428
draw_ops = {
429429
"color": color,
430-
"linestyle": "-",
431-
"linewidth": 2,
430+
"linestyle": series_params.get("linestyle", "-"),
431+
"linewidth": series_params.get("linewidth", 2),
432432
}
433433
self._update_label_in_options(draw_ops, name, label, legend)
434434
draw_ops.update(**options)
435435
self._get_axis(axis).plot(x_data, y_data, **draw_ops)
436436

437+
def hline(
438+
self,
439+
y_value: float,
440+
name: Optional[SeriesName] = None,
441+
label: Optional[str] = None,
442+
legend: bool = False,
443+
**options,
444+
):
445+
series_params = self.figure_options.series_params.get(name, {})
446+
axis = series_params.get("canvas", None)
447+
color = series_params.get("color", self._get_default_color(name))
448+
449+
draw_ops = {
450+
"color": color,
451+
"linestyle": series_params.get("linestyle", "-"),
452+
"linewidth": series_params.get("linewidth", 2),
453+
}
454+
self._update_label_in_options(draw_ops, name, label, legend)
455+
draw_ops.update(**options)
456+
self._get_axis(axis).axhline(y_value, **draw_ops)
457+
437458
def filled_y_area(
438459
self,
439460
x_data: Sequence[float],
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
features:
3+
- |
4+
An :meth:`~qiskit_experiments.visualization.BasePlotter.hline` method was
5+
added to :class:`~qiskit_experiments.visualization.BasePlotter` for
6+
generating horizontal lines.
7+
- |
8+
The
9+
:class:`~qiskit_experiments.library.quantum_volume.QuantumVolumeAnalysis`
10+
analysis class was updated to use
11+
:class:`~qiskit_experiments.visualization.BasePlotter` for its figure
12+
generation. The appearance of the figure should be the same as in previous
13+
releases. It is easier to customize the figure by setting options on the
14+
plotter object, but currently the user must consult the plotter code as the
15+
options are not documented. See `#1348
16+
<https://github.com/Qiskit-Extensions/qiskit-experiments/pull/1348>`__.

test/base.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,6 @@ def setUpClass(cls):
106106
# default.
107107
# pylint: disable=invalid-name
108108
allow_deprecationwarning_message = [
109-
# TODO: Remove in 0.6, when submodule `.curve_analysis.visualization` is removed.
110-
r".*Plotting and drawing functionality has been moved",
111-
r".*Legacy drawers from `.curve_analysis.visualization are deprecated",
112109
]
113110
for msg in allow_deprecationwarning_message:
114111
warnings.filterwarnings("default", category=DeprecationWarning, message=msg)

test/library/quantum_volume/test_qv.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"""
1414
A Tester for the Quantum Volume experiment
1515
"""
16+
import warnings
1617
from test.base import QiskitExperimentsTestCase
1718
import json
1819
import os
@@ -106,15 +107,17 @@ def test_qv_sigma_decreasing(self):
106107

107108
qv_exp = QuantumVolume(range(num_of_qubits), seed=SEED)
108109
# set number of trials to a low number to make the test faster
109-
qv_exp.set_experiment_options(trials=2)
110-
expdata1 = qv_exp.run(backend)
111-
self.assertExperimentDone(expdata1)
112-
result_data1 = expdata1.analysis_results(0)
113-
expdata2 = qv_exp.run(backend, analysis=None)
114-
self.assertExperimentDone(expdata2)
115-
expdata2.add_data(expdata1.data())
116-
qv_exp.analysis.run(expdata2)
117-
result_data2 = expdata2.analysis_results(0)
110+
with warnings.catch_warnings():
111+
warnings.filterwarnings("ignore", message="Must use at least 100 trials")
112+
qv_exp.set_experiment_options(trials=2)
113+
expdata1 = qv_exp.run(backend)
114+
self.assertExperimentDone(expdata1)
115+
result_data1 = expdata1.analysis_results(0)
116+
expdata2 = qv_exp.run(backend, analysis=None)
117+
self.assertExperimentDone(expdata2)
118+
expdata2.add_data(expdata1.data())
119+
qv_exp.analysis.run(expdata2)
120+
result_data2 = expdata2.analysis_results(0)
118121

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

148-
qv_exp.analysis.run(exp_data)
151+
with self.assertWarns(UserWarning):
152+
qv_exp.analysis.run(exp_data)
149153
qv_result = exp_data.analysis_results(1)
150154
self.assertTrue(
151155
qv_result.extra["success"] is False and qv_result.value == 1,

test/visualization/mock_drawer.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ def line(
7575
"""Does nothing."""
7676
pass
7777

78+
def hline(
79+
self,
80+
y_value: float,
81+
name: Optional[str] = None,
82+
label: Optional[str] = None,
83+
legend: bool = False,
84+
**options,
85+
):
86+
"""Does nothing."""
87+
pass
88+
7889
def filled_y_area(
7990
self,
8091
x_data: Sequence[float],

0 commit comments

Comments
 (0)