Skip to content

Commit

Permalink
Convert QVAnalysis to use BasePlotter
Browse files Browse the repository at this point in the history
Also, add an hline method to BaseDrawer and expose linewidth and
linestyle as series options.
  • Loading branch information
wshanks committed Dec 1, 2023
1 parent 5a9f50e commit d54c3f9
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 88 deletions.
166 changes: 93 additions & 73 deletions qiskit_experiments/library/quantum_volume/qv_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

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

import numpy as np
import uncertainties
Expand All @@ -26,6 +26,94 @@
AnalysisResultData,
Options,
)
from qiskit_experiments.visualization import BasePlotter, MplDrawer


class QVPlotter(BasePlotter):
@classmethod
def expected_series_data_keys(cls) -> List[str]:
return ["hops"]

@classmethod
def expected_supplementary_data_keys(cls) -> List[str]:
return ["depth"]

def set_supplementary_data(self, **data_kwargs):
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 Down Expand Up @@ -53,6 +141,7 @@ def _default_options(cls) -> Options:
options = super()._default_options()
options.plot = True
options.ax = None
options.plotter = QVPlotter(MplDrawer())
return options

def _run_analysis(self, experiment_data):
Expand All @@ -77,8 +166,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 +328,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
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
30 changes: 17 additions & 13 deletions test/library/quantum_volume/test_qv.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""
A Tester for the Quantum Volume experiment
"""
import warnings
from test.base import QiskitExperimentsTestCase
import json
import os
Expand Down Expand Up @@ -106,15 +107,17 @@ def test_qv_sigma_decreasing(self):

qv_exp = QuantumVolume(range(num_of_qubits), seed=SEED)
# set number of trials to a low number to make the test faster
qv_exp.set_experiment_options(trials=2)
expdata1 = qv_exp.run(backend)
self.assertExperimentDone(expdata1)
result_data1 = expdata1.analysis_results(0)
expdata2 = qv_exp.run(backend, analysis=None)
self.assertExperimentDone(expdata2)
expdata2.add_data(expdata1.data())
qv_exp.analysis.run(expdata2)
result_data2 = expdata2.analysis_results(0)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Must use at least 100 trials")
qv_exp.set_experiment_options(trials=2)
expdata1 = qv_exp.run(backend)
self.assertExperimentDone(expdata1)
result_data1 = expdata1.analysis_results(0)
expdata2 = qv_exp.run(backend, analysis=None)
self.assertExperimentDone(expdata2)
expdata2.add_data(expdata1.data())
qv_exp.analysis.run(expdata2).block_for_results()
result_data2 = expdata2.analysis_results(0)

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

qv_exp.analysis.run(exp_data)
with self.assertWarns(UserWarning):
qv_exp.analysis.run(exp_data).block_for_results()
qv_result = exp_data.analysis_results(1)
self.assertTrue(
qv_result.extra["success"] is False and qv_result.value == 1,
Expand All @@ -171,7 +175,7 @@ def test_qv_failure_insufficient_hop(self):
exp_data = ExperimentData(experiment=qv_exp, backend=backend)
exp_data.add_data(insufficient_hop_data)

qv_exp.analysis.run(exp_data)
qv_exp.analysis.run(exp_data).block_for_results()
qv_result = exp_data.analysis_results(1)
self.assertTrue(
qv_result.extra["success"] is False and qv_result.value == 1,
Expand All @@ -198,7 +202,7 @@ def test_qv_failure_insufficient_confidence(self):
exp_data = ExperimentData(experiment=qv_exp, backend=backend)
exp_data.add_data(insufficient_confidence_data)

qv_exp.analysis.run(exp_data)
qv_exp.analysis.run(exp_data).block_for_results()
qv_result = exp_data.analysis_results(1)
self.assertTrue(
qv_result.extra["success"] is False and qv_result.value == 1,
Expand All @@ -222,7 +226,7 @@ def test_qv_success(self):
exp_data = ExperimentData(experiment=qv_exp, backend=backend)
exp_data.add_data(successful_data)

qv_exp.analysis.run(exp_data)
qv_exp.analysis.run(exp_data).block_for_results()
results_json_file = "qv_result_moderate_noise_300_trials.json"
with open(os.path.join(dir_name, results_json_file), "r", encoding="utf-8") as json_file:
successful_results = json.load(json_file, cls=ExperimentDecoder)
Expand Down

0 comments on commit d54c3f9

Please sign in to comment.