Source code for kaleidoscope.interactive.histogram

# -*- coding: utf-8 -*-

# This code is part of Kaleidoscope.
#
# (C) Copyright IBM 2020.
#
# 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.

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2018.
#
# 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.

# The Hamming distance and internal plot histogram data are part of
# Qiskit.  The latter will be modified at some point.

"""Interactive histogram from experiment counts"""

import functools
from collections import Counter, OrderedDict
import numpy as np
import plotly.graph_objects as go
from kaleidoscope.errors import KaleidoscopeError
from kaleidoscope.colors import COLORS1, COLORS2, COLORS3, COLORS4, COLORS5, COLORS14
from kaleidoscope.colors.utils import find_text_color
from .plotly_wrapper import PlotlyWidget, PlotlyFigure


def hamming_distance(str1, str2):
    """Calculate the Hamming distance between two bit strings

    Args:
        str1 (str): First string.
        str2 (str): Second string.
    Returns:
        int: Distance between strings.
    Raises:
        ValueError: Strings not same length
    """
    if len(str1) != len(str2):
        raise ValueError('Strings not same length.')
    return sum(s1 != s2 for s1, s2 in zip(str1, str2))


VALID_SORTS = ['asc', 'desc', 'hamming']
DIST_MEAS = {'hamming': hamming_distance}


[docs]def probability_distribution(data, figsize=(None, None), colors=None, scale='linear', number_to_keep=None, sort='asc', target_string=None, legend=None, bar_labels=True, state_labels_kind='bits', state_labels=None, title=None, plot_background_color='#e5e0df', background_color='#ffffff', as_widget=False): """Interactive histogram plot of probability distributions. Parameters: data (list or dict): This is either a list of dictionaries or a single dict containing the values to represent (ex {'001': 130}) figsize (tuple): Figure size in pixels. colors (list or str): String or list of strings for histogram bar colors. scale (str): Probability scale 'linear' (default) or 'log'. number_to_keep (int): The number of terms to plot and rest is made into a single bar called 'rest'. sort (string): Could be 'asc', 'desc', or 'hamming'. target_string (str): Target string if 'sort' is a distance measure. legend(list): A list of strings to use for labels of the data. The number of entries must match the length of data (if data is a list or 1 if it's a dict) bar_labels (bool): Label each bar in histogram with probability value. state_labels_kind (str): 'bits' (default) or 'ints'. state_labels (list): A list of custom state labels. title (str): A string to use for the plot title. plot_background_color (str): Set the background color behind histogram bars. background_color (str): Set the background color for axes. as_widget (bool): Return figure as an ipywidget. Returns: PlotlyFigure or PlotlyWidget: A figure for the rendered histogram. Raises: ValueError: When legend is provided and the length doesn't match the input data. ImportError: When failed to load plotly. KaleidoscopeError: When state labels is not correct length. Example: .. jupyter-execute:: from qiskit import * import kaleidoscope.qiskit from kaleidoscope.qiskit.services import Simulators from kaleidoscope.interactive import probability_distribution sim = Simulators.aer_vigo_simulator qc = QuantumCircuit(3, 3) >> sim qc.h(1) qc.cx(1,0) qc.cx(1,2) qc.measure(range(3), range(3)) counts = qc.transpile().sample().result_when_done() probability_distribution(counts) """ if sort not in VALID_SORTS: raise ValueError("Value of sort option, %s, isn't a " "valid choice. Must be 'asc', " "'desc', or 'hamming'") if sort in DIST_MEAS.keys() and target_string is None: err_msg = 'Must define target_string when using distance measure.' raise ValueError(err_msg) if isinstance(data, dict): data = [data] if legend and len(legend) != len(data): raise ValueError("Length of legendL (%s) doesn't match " "number of input executions: %s" % (len(legend), len(data))) text_color = find_text_color(background_color) labels = list(sorted( functools.reduce(lambda x, y: x.union(y.keys()), data, set()))) if number_to_keep is not None: labels.append('rest') if sort in DIST_MEAS.keys(): dist = [] for item in labels: dist.append(DIST_MEAS[sort](item, target_string)) labels = [list(x) for x in zip(*sorted(zip(dist, labels), key=lambda pair: pair[0]))][1] # Set bar colors if colors is None: if len(data) == 1: colors = COLORS1 elif len(data) == 2: colors = COLORS2 elif len(data) == 3: colors = COLORS3 elif len(data) == 4: colors = COLORS4 elif len(data) == 5: colors = COLORS5 else: colors = COLORS14 elif isinstance(colors, str): colors = [colors] width = 1/(len(data)+1) # the width of the bars labels_dict, all_pvalues, _ = _plot_histogram_data(data, labels, number_to_keep) fig = go.Figure() for item, _ in enumerate(data): xvals = [] yvals = [] for idx, val in enumerate(all_pvalues[item]): xvals.append(idx+item*width) yvals.append(val) labels = list(labels_dict.keys()) if state_labels_kind == 'ints': labels = [int(label, 2) for label in labels] if state_labels: if len(state_labels) != len(labels): raise KaleidoscopeError('Number of input state labels does not match data.') labels = state_labels hover_template = "<b>{x}</b><br>P = {y}" hover_text = [hover_template.format(x=labels[kk], y=np.round(yvals[kk], 3)) for kk in range(len(yvals))] fig.add_trace(go.Bar(x=xvals, y=yvals, width=width, hoverinfo="text", hovertext=hover_text, marker_color=colors[item % len(colors)], name=legend[item] if legend else '', text=np.round(yvals, 3) if bar_labels else None, textposition='auto' )) xaxes_labels = list(labels_dict.keys()) if state_labels_kind == 'ints': xaxes_labels = [int(label, 2) for label in xaxes_labels] if state_labels: if len(state_labels) != len(xaxes_labels): raise KaleidoscopeError('Number of input state labels does not match data.') xaxes_labels = state_labels fig.update_xaxes(title='Basis state', titlefont_size=16, tickvals=list(range(len(labels_dict.keys()))), ticktext=xaxes_labels, tickfont_size=14, showline=True, linewidth=1, linecolor=text_color if text_color == 'white' else None, ) fig.update_yaxes(title='Probability', titlefont_size=16, tickfont_size=14, showline=True, linewidth=1, linecolor=text_color if text_color == 'white' else None, ) if scale == 'log': lower = np.min([min(item.values())/sum(item.values()) for item in data]) lower = int(np.floor(np.log10(lower))) fig.update_yaxes(type="log", range=[lower, 0]) fig.update_layout(yaxis=dict(tickmode='array', tickvals=[10**k for k in range(lower, 1)], ticktext=["10<sup>{}</sup>".format(k) for k in range(lower, 1)] )) fig.update_layout(xaxis_tickangle=-70, showlegend=(legend is not None), width=figsize[0], height=figsize[1], plot_bgcolor=plot_background_color, paper_bgcolor=background_color, title=dict(text=title, x=0.5), title_font_size=18, font=dict(color=text_color), ) if as_widget: return PlotlyWidget(fig) return PlotlyFigure(fig)
def _plot_histogram_data(data, labels, number_to_keep): """Generate the data needed for plotting counts. Parameters: data (list or dict): This is either a list of dictionaries or a single dict containing the values to represent (ex {'001': 130}) labels (list): The list of bitstring labels for the plot. number_to_keep (int): The number of terms to plot and rest is made into a single bar called 'rest'. Returns: tuple: tuple containing: (dict): The labels actually used in the plotting. (list): List of ndarrays for the bars in each experiment. (list): Indices for the locations of the bars for each experiment. """ labels_dict = OrderedDict() all_pvalues = [] all_inds = [] for execution in data: if number_to_keep is not None: data_temp = dict(Counter(execution).most_common(number_to_keep)) data_temp["rest"] = sum(execution.values()) - sum(data_temp.values()) execution = data_temp values = [] for key in labels: if key not in execution: if number_to_keep is None: labels_dict[key] = 1 values.append(0) else: values.append(-1) else: labels_dict[key] = 1 values.append(execution[key]) values = np.array(values, dtype=float) where_idx = np.where(values >= 0)[0] pvalues = values[where_idx] / sum(values[where_idx]) all_pvalues.append(pvalues) numelem = len(values[where_idx]) ind = np.arange(numelem) # the x locations for the groups all_inds.append(ind) return labels_dict, all_pvalues, all_inds