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 20, 2023
1 parent f7750d5 commit 01cf9fb
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 82 deletions.
12 changes: 11 additions & 1 deletion qiskit_experiments/library/quantum_volume/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,17 @@
:template: autosummary/analysis.rst
QuantumVolumeAnalysis
Plotter
=======
.. autosummary::
:toctree: ../stubs/
:template: autosummary/plotter.rst
QuantumVolumePlotter
"""

from .qv_experiment import QuantumVolume
from .qv_analysis import QuantumVolumeAnalysis
from .qv_analysis import QuantumVolumeAnalysis, QuantumVolumePlotter
187 changes: 113 additions & 74 deletions qiskit_experiments/library/quantum_volume/qv_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,123 @@

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

import numpy as np
import uncertainties
from qiskit_experiments.exceptions import AnalysisError
from qiskit_experiments.curve_analysis.visualization import plot_scatter, plot_errorbar
from qiskit_experiments.framework import (
BaseAnalysis,
AnalysisResultData,
Options,
)
from qiskit_experiments.visualization import BasePlotter, MplDrawer


class QuantumVolumePlotter(BasePlotter):
"""Plotter for QuantumVolumeAnalysis"""

@classmethod
def expected_series_data_keys(cls) -> List[str]:
"""Returns the expected series data keys supported by this plotter.
Data Keys:
hops: Heavy-output probability fraction for each circuit
"""
return ["hops"]

@classmethod
def expected_supplementary_data_keys(cls) -> List[str]:
"""Returns the expected figures data keys supported by this plotter.
Data Keys:
depth: The depth of the quantun volume circuits used in the experiment
"""
return ["depth"]

def set_supplementary_data(self, **data_kwargs):
"""Sets supplementary data for the plotter.
Args:
data_kwargs: See :meth:`expected_supplementary_data_keys` for the
expected supplementary data keys.
"""
# Hook method to capture the depth for inclusion in the plot title
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 All @@ -49,10 +155,12 @@ def _default_options(cls) -> Options:
Analysis Options:
plot (bool): Set ``True`` to create figure for fit result.
ax (AxesSubplot): Optional. A matplotlib axis object to draw.
plotter (BasePlotter): Plotter object to use for figure generation.
"""
options = super()._default_options()
options.plot = True
options.ax = None
options.plotter = QuantumVolumePlotter(MplDrawer())
return options

def _run_analysis(self, experiment_data):
Expand All @@ -77,8 +185,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 +347,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
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,37 @@ def line(
"""
self._curve_drawer.draw_fit_line(x_data, y_data, name, **options)

# pylint: disable=unused-argument
def hline(
self,
y_value: float,
name: Optional[str] = None,
label: Optional[str] = None,
legend: bool = False,
**options,
):
"""Draw a horizontal line.
.. note::
This method was added to fulfill the
:class:`~qiskit_experiments.visualization.BaseDrawer` interface,
but it is not supported for this class since there was no
equivalent in
:class:`~qiskit_experiments.curve_analysis.visualization.BaseCurveDrawer`.
Args:
y_value: Y value for line.
name: Name of this series.
label: Unsupported label option
legend: Unsupported legend option
options: Additional options
"""
warnings.warn(
"hline is not supported by the LegacyCurveCompatDrawer",
UserWarning,
)

# pylint: disable=unused-argument
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>`__.
6 changes: 1 addition & 5 deletions test/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,7 @@ def setUpClass(cls):
# ``QiskitTestCase`` sets all warnings to be treated as an error by
# 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",
]
allow_deprecationwarning_message = []
for msg in allow_deprecationwarning_message:
warnings.filterwarnings("default", category=DeprecationWarning, message=msg)

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

Please sign in to comment.