# -*- coding: utf-8 -*-
# This code is part of Qiskit.
#
# (C) Copyright IBM 2019.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Functions used for the analysis of quantum volume results.
Based on Cross et al. "Validating quantum computers using
randomized model circuits", arXiv:1811.12926
"""
import math
import numpy as np
from qiskit import QiskitError
from ...utils import build_counts_dict_from_list
try:
from matplotlib import pyplot as plt
HAS_MATPLOTLIB = True
except ImportError:
HAS_MATPLOTLIB = False
[docs]class QVFitter:
"""Class for fitters for quantum volume."""
def __init__(self, backend_result=None, statevector_result=None,
qubit_lists=None):
"""
Args:
backend_result (list): list of results (qiskit.Result).
statevector_result (list): the ideal statevectors of each circuit
qubit_lists (list): list of qubit lists (what was passed to the
circuit generation)
"""
self._qubit_lists = qubit_lists
self._depths = [len(qubit_list) for qubit_list in qubit_lists]
self._ntrials = 0
self._result_list = []
self._heavy_output_counts = {}
self._circ_shots = {}
self._heavy_output_prob_ideal = {}
self._ydata = []
self._heavy_outputs = {}
self.add_statevectors(statevector_result)
self.add_data(backend_result)
@property
def depths(self):
"""Return depth list."""
return self._depths
@property
def qubit_lists(self):
"""Return depth list."""
return self._qubit_lists
@property
def results(self):
"""Return all the results."""
return self._result_list
@property
def heavy_outputs(self):
"""Return the ideal heavy outputs dictionary."""
return self._heavy_outputs
@property
def heavy_output_counts(self):
"""Return the number of heavy output counts as measured."""
return self._heavy_output_counts
@property
def heavy_output_prob_ideal(self):
"""Return the heavy output probability ideally."""
return self._heavy_output_prob_ideal
@property
def ydata(self):
"""Return the average and std of the output probability."""
return self._ydata
[docs] def add_statevectors(self, new_statevector_result):
"""
Add the ideal results and convert to the heavy outputs.
Assume the result is from 'statevector_simulator'
Args:
new_statevector_result (list): ideal results
Raises:
QiskitError: If the result has already been added for the circuit
"""
if new_statevector_result is None:
return
if not isinstance(new_statevector_result, list):
new_statevector_result = [new_statevector_result]
for result in new_statevector_result:
for qvcirc in result.results:
circname = qvcirc.header.name
# get the depth/width from the circuit name
# qv_depth_%d_trial_%d
depth = int(circname.split('_')[2])
if circname in self._heavy_outputs:
raise QiskitError("Already added the ideal result "
"for circuit %s" % circname)
# convert the result into probability dictionary
qstate = result.get_statevector(circname)
pvector = np.multiply(qstate, qstate.conjugate())
format_spec = "{0:0%db}" % depth
pmap = {format_spec.format(b):
float(np.real(pvector[b]))
for b in range(2**depth)}
median_prob = self._median_probabilities([pmap])
self._heavy_outputs[qvcirc.header.name] = \
self._heavy_strings(pmap, median_prob[0])
# calculate the heavy output probability
self._heavy_output_prob_ideal[circname] = \
self._subset_probability(
self._heavy_outputs[circname],
pmap)
[docs] def add_data(self, new_backend_result, rerun_fit=True):
"""
Add a new result. Re calculate fit
Args:
new_backend_result (list): list of qv results
rerun_fit (bool): re calculate the means and fit the result
Raises:
QiskitError: If the ideal distribution isn't loaded yet
Additional information:
Assumes that 'result' was executed is
the output of circuits generated by qv_circuits,
"""
if new_backend_result is None:
return
if not isinstance(new_backend_result, list):
new_backend_result = [new_backend_result]
for result in new_backend_result:
self._result_list.append(result)
# update the number of trials *if* new ones
# added.
for qvcirc in result.results:
ntrials_circ = int(qvcirc.header.name.split('_')[-1])
if (ntrials_circ+1) > self._ntrials:
self._ntrials = ntrials_circ+1
if qvcirc.header.name not in self._heavy_output_prob_ideal:
raise QiskitError('Ideal distribution '
'must be loaded first')
if rerun_fit:
self.calc_data()
self.calc_statistics()
[docs] def calc_data(self):
"""
Make a count dictionary for each unique circuit from all the results.
Calculate the heavy output probability.
Additional information:
Assumes that 'result' was executed is
the output of circuits generated by qv_circuits,
"""
circ_counts = {}
for trialidx in range(self._ntrials):
for _, depth in enumerate(self._depths):
circ_name = 'qv_depth_%d_trial_%d' % (depth, trialidx)
# get the counts form ALL executed circuits
count_list = []
for result in self._result_list:
try:
count_list.append(result.get_counts(circ_name))
except (QiskitError, KeyError):
pass
circ_counts[circ_name] = \
build_counts_dict_from_list(count_list)
self._circ_shots[circ_name] = \
sum(circ_counts[circ_name].values())
# calculate the heavy output probability
self._heavy_output_counts[circ_name] = \
self._subset_probability(
self._heavy_outputs[circ_name],
circ_counts[circ_name])
[docs] def calc_statistics(self):
"""
Convert the heavy outputs in the different trials into mean and error
for plotting.
Here we assume the error is due to a binomial distribution
"""
self._ydata = np.zeros([4, len(self._depths)], dtype=float)
exp_vals = np.zeros(self._ntrials, dtype=float)
ideal_vals = np.zeros(self._ntrials, dtype=float)
for depthidx, depth in enumerate(self._depths):
exp_shots = 0
for trialidx in range(self._ntrials):
cname = 'qv_depth_%d_trial_%d' % (depth, trialidx)
exp_vals[trialidx] = self._heavy_output_counts[cname]
exp_shots += self._circ_shots[cname]
ideal_vals[trialidx] = self._heavy_output_prob_ideal[cname]
self._ydata[0][depthidx] = np.sum(exp_vals)/np.sum(exp_shots)
self._ydata[1][depthidx] = (self._ydata[0][depthidx] *
(1.0-self._ydata[0][depthidx])
/ self._ntrials)**0.5
self._ydata[2][depthidx] = np.mean(ideal_vals)
self._ydata[3][depthidx] = (self._ydata[2][depthidx] *
(1.0-self._ydata[2][depthidx])
/ self._ntrials)**0.5
[docs] def plot_qv_data(self, ax=None, show_plt=True):
"""
Plot the qv data as a function of depth
Args:
ax (Axes or None): plot axis (if passed in).
show_plt (bool): display the plot.
Raises:
ImportError: If matplotlib is not installed.
"""
if not HAS_MATPLOTLIB:
raise ImportError('The function plot_rb_data needs matplotlib. '
'Run "pip install matplotlib" before.')
if ax is None:
plt.figure()
ax = plt.gca()
xdata = range(len(self._depths))
# Plot the experimental data with error bars
ax.errorbar(xdata, self._ydata[0],
yerr=self._ydata[1],
color='r', linestyle=None, marker='o', markersize=5,
label='Exp')
# Plot the ideal data with error bars
ax.errorbar(xdata, self._ydata[2],
yerr=self._ydata[3],
color='b', linestyle=None, marker='o', markersize=5,
label='Ideal')
# Plot the threshold
ax.plot(xdata,
np.ones(len(xdata))*2.0/3.0,
color='black', linestyle='--', linewidth=2, label='Threshold')
ax.tick_params(labelsize=14)
ax.set_xticks(xdata)
ax.set_xticklabels(self._qubit_lists, rotation=45)
ax.set_xlabel('Qubit Subset', fontsize=16)
ax.set_ylabel('Heavy Probability', fontsize=16)
ax.grid(True)
ax.legend()
if show_plt:
plt.show()
[docs] def qv_success(self):
"""Return whether each depth was successful (>2/3 with confidence
greater than 97.5) and the confidence
Returns:
list: List of lenth depth with eact element a 3 list with
- success True/False
- confidence
"""
success_list = []
for depth_ind, _ in enumerate(self._depths):
success_list.append([False, 0.0])
hmean = self._ydata[0][depth_ind]
if hmean > 2/3:
cfd = 0.5 * (1 +
math.erf((hmean - 2/3)
/ (1e-10 +
self._ydata[1][depth_ind])/2**0.5))
success_list[-1][1] = cfd
if cfd > 0.975:
success_list[-1][0] = True
return success_list
[docs] def quantum_volume(self):
"""Return the volume for each depth.
Returns:
list: List of quantum volumes
"""
qv_list = 2**np.array(self._depths)
return qv_list
def _heavy_strings(self, ideal_distribution, ideal_median):
"""Return the set of heavy output strings.
Args:
ideal_distribution (dict): dict of ideal output distribution
where keys are bit strings (as strings) and values are
probabilities of observing those strings
ideal_median (float): median probability across all outputs
Returns:
list: list the set of heavy output strings, i.e. those strings
whose ideal probability of occurrence exceeds the median.
"""
return list(filter(lambda x: ideal_distribution[x] > ideal_median,
list(ideal_distribution.keys())))
def _median_probabilities(self, distributions):
"""Return a list of median probabilities.
Args:
distributions (list): list of dicts mapping binary strings
(as strings) to probabilities.
Returns:
list: a list of median probabilities.
"""
medians = []
for dist in distributions:
values = np.array(list(dist.values()))
medians.append(float(np.real(np.median(values))))
return medians
def _subset_probability(self, strings, distribution):
"""Return the probability of a subset of outcomes.
Args:
strings (list): list of bit strings (as strings)
distribution (dict): dict where keys are bit strings (as strings)
and values are probabilities of observing those strings
Returns:
float: the probability of the subset of strings, i.e. the sum
of the probabilities of each string as given by the
distribution.
"""
return sum([distribution.get(value, 0) for value in strings])