Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert QVAnalysis to use BasePlotter #1348

Merged
merged 3 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
194 changes: 120 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,130 @@

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

.. note::

This plotter only supports one series, named ``hops``, which it expects
to have an ``individual`` data key containing the individual heavy
output probabilities for each circuit in the experiment. Additional
series will be ignored.
"""

@classmethod
def expected_series_data_keys(cls) -> List[str]:
"""Returns the expected series data keys supported by this plotter.

Data Keys:
individual: Heavy-output probability fraction for each individual circuit
"""
return ["individual"]

@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):
(hops,) = self.data_for("hops", ["individual"])
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 +162,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 +192,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", individual=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 +354,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 @@ -436,13 +436,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
17 changes: 17 additions & 0 deletions releasenotes/notes/qvplotter-04efe280aaa9d555.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
features:
- |
An :meth:`~qiskit_experiments.visualization.BasePlotter.hline` method was
added to :class:`~qiskit_experiments.visualization.BasePlotter` for
generating horizontal lines. See `#1348
<https://github.com/Qiskit-Extensions/qiskit-experiments/pull/1348>`__.
- |
The
:class:`~qiskit_experiments.library.quantum_volume.QuantumVolumeAnalysis`
analysis class was updated to use
:class:`~qiskit_experiments.library.quantum_volume.QuantumVolumePlotter`
for its figure generation. The appearance of the figure should be the same
as in previous
releases, but now it is easier to customize the figure by setting options
on the plotter object. 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 @@ -125,11 +125,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
Loading
Loading