Skip to content

Commit 01cf9fb

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 01cf9fb

File tree

8 files changed

+230
-82
lines changed

8 files changed

+230
-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: 113 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,123 @@
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+
@classmethod
35+
def expected_series_data_keys(cls) -> List[str]:
36+
"""Returns the expected series data keys supported by this plotter.
37+
38+
Data Keys:
39+
hops: Heavy-output probability fraction for each circuit
40+
"""
41+
return ["hops"]
42+
43+
@classmethod
44+
def expected_supplementary_data_keys(cls) -> List[str]:
45+
"""Returns the expected figures data keys supported by this plotter.
46+
47+
Data Keys:
48+
depth: The depth of the quantun volume circuits used in the experiment
49+
"""
50+
return ["depth"]
51+
52+
def set_supplementary_data(self, **data_kwargs):
53+
"""Sets supplementary data for the plotter.
54+
55+
Args:
56+
data_kwargs: See :meth:`expected_supplementary_data_keys` for the
57+
expected supplementary data keys.
58+
"""
59+
# Hook method to capture the depth for inclusion in the plot title
60+
if "depth" in data_kwargs:
61+
self.set_figure_options(
62+
figure_title=(
63+
f"Quantum Volume experiment for depth {data_kwargs['depth']}"
64+
" - accumulative hop"
65+
),
66+
)
67+
super().set_supplementary_data(**data_kwargs)
68+
69+
@classmethod
70+
def _default_figure_options(cls) -> Options:
71+
options = super()._default_figure_options()
72+
options.xlabel = "Number of Trials"
73+
options.ylabel = "Heavy Output Probability"
74+
options.figure_title = "Quantum Volume experiment - accumulative hop"
75+
options.series_params = {
76+
"hop": {"color": "gray", "symbol": "."},
77+
"threshold": {"color": "black", "linestyle": "dashed", "linewidth": 1},
78+
"hop_cumulative": {"color": "r"},
79+
"hop_twosigma": {"color": "lightgray"},
80+
}
81+
return options
82+
83+
@classmethod
84+
def _default_options(cls) -> Options:
85+
options = super()._default_options()
86+
options.style["figsize"] = (6.4, 4.8)
87+
options.style["axis_label_size"] = 14
88+
options.style["symbol_size"] = 2
89+
return options
90+
91+
def _plot_figure(self):
92+
series = self.series[0]
93+
(hops,) = self.data_for(series, ["hops"])
94+
trials = np.arange(1, 1 + len(hops))
95+
hop_accumulative = np.cumsum(hops) / trials
96+
hop_twosigma = 2 * (hop_accumulative * (1 - hop_accumulative) / trials) ** 0.5
97+
98+
self.drawer.line(
99+
trials,
100+
hop_accumulative,
101+
name="hop_cumulative",
102+
label="Cumulative HOP",
103+
legend=True,
104+
)
105+
self.drawer.hline(
106+
2 / 3,
107+
name="threshold",
108+
label="Threshold",
109+
legend=True,
110+
)
111+
self.drawer.scatter(
112+
trials,
113+
hops,
114+
name="hop",
115+
label="Individual HOP",
116+
legend=True,
117+
linewidth=1.5,
118+
)
119+
self.drawer.filled_y_area(
120+
trials,
121+
hop_accumulative - hop_twosigma,
122+
hop_accumulative + hop_twosigma,
123+
alpha=0.5,
124+
legend=True,
125+
name="hop_twosigma",
126+
label="2σ",
127+
)
128+
129+
self.drawer.set_figure_options(
130+
ylim=(
131+
max(hop_accumulative[-1] - 4 * hop_twosigma[-1], 0),
132+
min(hop_accumulative[-1] + 4 * hop_twosigma[-1], 1),
133+
),
134+
)
29135

30136

31137
class QuantumVolumeAnalysis(BaseAnalysis):
@@ -49,10 +155,12 @@ def _default_options(cls) -> Options:
49155
Analysis Options:
50156
plot (bool): Set ``True`` to create figure for fit result.
51157
ax (AxesSubplot): Optional. A matplotlib axis object to draw.
158+
plotter (BasePlotter): Plotter object to use for figure generation.
52159
"""
53160
options = super()._default_options()
54161
options.plot = True
55162
options.ax = None
163+
options.plotter = QuantumVolumePlotter(MplDrawer())
56164
return options
57165

58166
def _run_analysis(self, experiment_data):
@@ -77,8 +185,9 @@ def _run_analysis(self, experiment_data):
77185
hop_result, qv_result = self._calc_quantum_volume(heavy_output_prob_exp, depth, num_trials)
78186

79187
if self.options.plot:
80-
ax = self._format_plot(hop_result, ax=self.options.ax)
81-
figures = [ax.get_figure()]
188+
self.options.plotter.set_series_data("hops", hops=hop_result.extra["HOPs"])
189+
self.options.plotter.set_supplementary_data(depth=hop_result.extra["depth"])
190+
figures = [self.options.plotter.figure()]
82191
else:
83192
figures = None
84193
return [hop_result, qv_result], figures
@@ -238,73 +347,3 @@ def _calc_quantum_volume(self, heavy_output_prob_exp, depth, trials):
238347
},
239348
)
240349
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
@@ -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: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,7 @@ def setUpClass(cls):
105105
# ``QiskitTestCase`` sets all warnings to be treated as an error by
106106
# default.
107107
# pylint: disable=invalid-name
108-
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",
112-
]
108+
allow_deprecationwarning_message = []
113109
for msg in allow_deprecationwarning_message:
114110
warnings.filterwarnings("default", category=DeprecationWarning, message=msg)
115111

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)