Source code for qiskit.ignis.verification.quantum_volume.fitters

# -*- 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
# 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

    from matplotlib import pyplot as plt
except ImportError:

[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 = # 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[] = \ 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('_')[-1]) if (ntrials_circ+1) > self._ntrials: self._ntrials = ntrials_circ+1 if 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:
[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])