Skip to content

Commit 32f02b1

Browse files
wshanksconradhaupt
andauthored
Convert QVAnalysis to use BasePlotter (#1348)
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. --------- Co-authored-by: Conrad Haupt <[email protected]>
1 parent 46e7eec commit 32f02b1

File tree

8 files changed

+238
-82
lines changed

8 files changed

+238
-82
lines changed

qiskit_experiments/library/quantum_volume/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,17 @@
3535
:template: autosummary/analysis.rst
3636
3737
QuantumVolumeAnalysis
38+
39+
40+
Plotter
41+
=======
42+
43+
.. autosummary::
44+
:toctree: ../stubs/
45+
:template: autosummary/plotter.rst
46+
47+
QuantumVolumePlotter
3848
"""
3949

4050
from .qv_experiment import QuantumVolume
41-
from .qv_analysis import QuantumVolumeAnalysis
51+
from .qv_analysis import QuantumVolumeAnalysis, QuantumVolumePlotter

qiskit_experiments/library/quantum_volume/qv_analysis.py

Lines changed: 120 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,130 @@
1515

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

2020
import numpy as np
2121
import uncertainties
2222
from qiskit_experiments.exceptions import AnalysisError
23-
from qiskit_experiments.curve_analysis.visualization import plot_scatter, plot_errorbar
2423
from qiskit_experiments.framework import (
2524
BaseAnalysis,
2625
AnalysisResultData,
2726
Options,
2827
)
28+
from qiskit_experiments.visualization import BasePlotter, MplDrawer
29+
30+
31+
class QuantumVolumePlotter(BasePlotter):
32+
"""Plotter for QuantumVolumeAnalysis
33+
34+
.. note::
35+
36+
This plotter only supports one series, named ``hops``, which it expects
37+
to have an ``individual`` data key containing the individual heavy
38+
output probabilities for each circuit in the experiment. Additional
39+
series will be ignored.
40+
"""
41+
42+
@classmethod
43+
def expected_series_data_keys(cls) -> List[str]:
44+
"""Returns the expected series data keys supported by this plotter.
45+
46+
Data Keys:
47+
individual: Heavy-output probability fraction for each individual circuit
48+
"""
49+
return ["individual"]
50+
51+
@classmethod
52+
def expected_supplementary_data_keys(cls) -> List[str]:
53+
"""Returns the expected figures data keys supported by this plotter.
54+
55+
Data Keys:
56+
depth: The depth of the quantun volume circuits used in the experiment
57+
"""
58+
return ["depth"]
59+
60+
def set_supplementary_data(self, **data_kwargs):
61+
"""Sets supplementary data for the plotter.
62+
63+
Args:
64+
data_kwargs: See :meth:`expected_supplementary_data_keys` for the
65+
expected supplementary data keys.
66+
"""
67+
# Hook method to capture the depth for inclusion in the plot title
68+
if "depth" in data_kwargs:
69+
self.set_figure_options(
70+
figure_title=(
71+
f"Quantum Volume experiment for depth {data_kwargs['depth']}"
72+
" - accumulative hop"
73+
),
74+
)
75+
super().set_supplementary_data(**data_kwargs)
76+
77+
@classmethod
78+
def _default_figure_options(cls) -> Options:
79+
options = super()._default_figure_options()
80+
options.xlabel = "Number of Trials"
81+
options.ylabel = "Heavy Output Probability"
82+
options.figure_title = "Quantum Volume experiment - accumulative hop"
83+
options.series_params = {
84+
"hop": {"color": "gray", "symbol": "."},
85+
"threshold": {"color": "black", "linestyle": "dashed", "linewidth": 1},
86+
"hop_cumulative": {"color": "r"},
87+
"hop_twosigma": {"color": "lightgray"},
88+
}
89+
return options
90+
91+
@classmethod
92+
def _default_options(cls) -> Options:
93+
options = super()._default_options()
94+
options.style["figsize"] = (6.4, 4.8)
95+
options.style["axis_label_size"] = 14
96+
options.style["symbol_size"] = 2
97+
return options
98+
99+
def _plot_figure(self):
100+
(hops,) = self.data_for("hops", ["individual"])
101+
trials = np.arange(1, 1 + len(hops))
102+
hop_accumulative = np.cumsum(hops) / trials
103+
hop_twosigma = 2 * (hop_accumulative * (1 - hop_accumulative) / trials) ** 0.5
104+
105+
self.drawer.line(
106+
trials,
107+
hop_accumulative,
108+
name="hop_cumulative",
109+
label="Cumulative HOP",
110+
legend=True,
111+
)
112+
self.drawer.hline(
113+
2 / 3,
114+
name="threshold",
115+
label="Threshold",
116+
legend=True,
117+
)
118+
self.drawer.scatter(
119+
trials,
120+
hops,
121+
name="hop",
122+
label="Individual HOP",
123+
legend=True,
124+
linewidth=1.5,
125+
)
126+
self.drawer.filled_y_area(
127+
trials,
128+
hop_accumulative - hop_twosigma,
129+
hop_accumulative + hop_twosigma,
130+
alpha=0.5,
131+
legend=True,
132+
name="hop_twosigma",
133+
label="2σ",
134+
)
135+
136+
self.drawer.set_figure_options(
137+
ylim=(
138+
max(hop_accumulative[-1] - 4 * hop_twosigma[-1], 0),
139+
min(hop_accumulative[-1] + 4 * hop_twosigma[-1], 1),
140+
),
141+
)
29142

30143

31144
class QuantumVolumeAnalysis(BaseAnalysis):
@@ -49,10 +162,12 @@ def _default_options(cls) -> Options:
49162
Analysis Options:
50163
plot (bool): Set ``True`` to create figure for fit result.
51164
ax (AxesSubplot): Optional. A matplotlib axis object to draw.
165+
plotter (BasePlotter): Plotter object to use for figure generation.
52166
"""
53167
options = super()._default_options()
54168
options.plot = True
55169
options.ax = None
170+
options.plotter = QuantumVolumePlotter(MplDrawer())
56171
return options
57172

58173
def _run_analysis(self, experiment_data):
@@ -77,8 +192,9 @@ def _run_analysis(self, experiment_data):
77192
hop_result, qv_result = self._calc_quantum_volume(heavy_output_prob_exp, depth, num_trials)
78193

79194
if self.options.plot:
80-
ax = self._format_plot(hop_result, ax=self.options.ax)
81-
figures = [ax.get_figure()]
195+
self.options.plotter.set_series_data("hops", individual=hop_result.extra["HOPs"])
196+
self.options.plotter.set_supplementary_data(depth=hop_result.extra["depth"])
197+
figures = [self.options.plotter.figure()]
82198
else:
83199
figures = None
84200
return [hop_result, qv_result], figures
@@ -238,73 +354,3 @@ def _calc_quantum_volume(self, heavy_output_prob_exp, depth, trials):
238354
},
239355
)
240356
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/legacy_curve_compat_drawer.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,37 @@ def line(
120120
"""
121121
self._curve_drawer.draw_fit_line(x_data, y_data, name, **options)
122122

123+
# pylint: disable=unused-argument
124+
def hline(
125+
self,
126+
y_value: float,
127+
name: Optional[str] = None,
128+
label: Optional[str] = None,
129+
legend: bool = False,
130+
**options,
131+
):
132+
"""Draw a horizontal line.
133+
134+
.. note::
135+
136+
This method was added to fulfill the
137+
:class:`~qiskit_experiments.visualization.BaseDrawer` interface,
138+
but it is not supported for this class since there was no
139+
equivalent in
140+
:class:`~qiskit_experiments.curve_analysis.visualization.BaseCurveDrawer`.
141+
142+
Args:
143+
y_value: Y value for line.
144+
name: Name of this series.
145+
label: Unsupported label option
146+
legend: Unsupported legend option
147+
options: Additional options
148+
"""
149+
warnings.warn(
150+
"hline is not supported by the LegacyCurveCompatDrawer",
151+
UserWarning,
152+
)
153+
123154
# pylint: disable=unused-argument
124155
def filled_y_area(
125156
self,

qiskit_experiments/visualization/drawers/mpl_drawer.py

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

437437
draw_ops = {
438438
"color": color,
439-
"linestyle": "-",
440-
"linewidth": 2,
439+
"linestyle": series_params.get("linestyle", "-"),
440+
"linewidth": series_params.get("linewidth", 2),
441441
}
442442
self._update_label_in_options(draw_ops, name, label, legend)
443443
draw_ops.update(**options)
444444
self._get_axis(axis).plot(x_data, y_data, **draw_ops)
445445

446+
def hline(
447+
self,
448+
y_value: float,
449+
name: Optional[SeriesName] = None,
450+
label: Optional[str] = None,
451+
legend: bool = False,
452+
**options,
453+
):
454+
series_params = self.figure_options.series_params.get(name, {})
455+
axis = series_params.get("canvas", None)
456+
color = series_params.get("color", self._get_default_color(name))
457+
458+
draw_ops = {
459+
"color": color,
460+
"linestyle": series_params.get("linestyle", "-"),
461+
"linewidth": series_params.get("linewidth", 2),
462+
}
463+
self._update_label_in_options(draw_ops, name, label, legend)
464+
draw_ops.update(**options)
465+
self._get_axis(axis).axhline(y_value, **draw_ops)
466+
446467
def filled_y_area(
447468
self,
448469
x_data: Sequence[float],
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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. See `#1348
7+
<https://github.com/Qiskit-Extensions/qiskit-experiments/pull/1348>`__.
8+
- |
9+
The
10+
:class:`~qiskit_experiments.library.quantum_volume.QuantumVolumeAnalysis`
11+
analysis class was updated to use
12+
:class:`~qiskit_experiments.library.quantum_volume.QuantumVolumePlotter`
13+
for its figure generation. The appearance of the figure should be the same
14+
as in previous
15+
releases, but now it is easier to customize the figure by setting options
16+
on the plotter object. See `#1348
17+
<https://github.com/Qiskit-Extensions/qiskit-experiments/pull/1348>`__.

test/base.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,7 @@ def setUpClass(cls):
125125
# ``QiskitTestCase`` sets all warnings to be treated as an error by
126126
# default.
127127
# pylint: disable=invalid-name
128-
allow_deprecationwarning_message = [
129-
# TODO: Remove in 0.6, when submodule `.curve_analysis.visualization` is removed.
130-
r".*Plotting and drawing functionality has been moved",
131-
r".*Legacy drawers from `.curve_analysis.visualization are deprecated",
132-
]
128+
allow_deprecationwarning_message = []
133129
for msg in allow_deprecationwarning_message:
134130
warnings.filterwarnings("default", category=DeprecationWarning, message=msg)
135131

0 commit comments

Comments
 (0)