Source code for qiskit_experiments.visualization.plotters.iq_plotter

# This code is part of Qiskit.
#
# (C) Copyright IBM 2022, 2023.
#
# 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.
"""Plotter for IQ data."""
import warnings
from itertools import product
from typing import List, Optional, Tuple

import numpy as np

from qiskit_experiments.data_processing.discriminator import BaseDiscriminator
from qiskit_experiments.framework import Options

from ..utils import DataExtentCalculator, ExtentTuple
from .base_plotter import BasePlotter


[docs] class IQPlotter(BasePlotter): """A plotter class to plot IQ data. :class:`.IQPlotter` plots results from experiments which used measurement-level 1, i.e. IQ data. This class also supports plotting predictions from a discriminator (subclass of :class:`.BaseDiscriminator`), which is used to classify IQ results into labels. The discriminator labels are matched with the series names to generate an image of the predictions. Points that are misclassified by the discriminator are flagged in the figure (see the ``flag_misclassified`` option). A canonical application of :class:`.IQPlotter` is for classification of single-qubit readout for different prepared states. Example: .. code-block:: python # Create plotter plotter = IQPlotter(MplDrawer()) # Iterate over results, one per prepared state. Add points and centroid to # plotter and set label for prepared states as |n> where n is the # prepared-state number. series_params = {} for res in results: # Get IQ points from result memory. points = res.memory # Compute centroid as mean of all points. centroid = np.mean(points, axis=1) # Get ``prep``, which is part of the result metadata. prep = res.prep # Create label as a ket instead of just a state number (i.e., prep). series_params[prep] = { "label":f"|{prep}>", } plotter.set_series_data(prep, points=points, centroid=centroid) plotter.set_figure_options(series_params=series_params) ... # Optional: Add trained discriminator. discrim = MyIQDiscriminator() # Discriminator labels are the same as series names. discrim.fit(train_data, train_labels) plotter.set_supplementary_data(discriminator=discrim) ... # Plot figure. fig = plotter.figure() """
[docs] @classmethod def expected_series_data_keys(cls) -> List[str]: """Returns the expected series data keys supported by this plotter. Data Keys: points: Single-shot IQ data. centroid: Averaged IQ data. """ return [ "points", "centroid", ]
[docs] @classmethod def expected_supplementary_data_keys(cls) -> List[str]: """Returns the expected figures data keys supported by this plotter. Data Keys: discriminator: A trained discriminator that classifies IQ points. If provided, the predictions of the discriminator will be sampled to generate a background image, indicating the regions for each predicted outcome. The predictions are assumed to be series names (``Union[str, int, float]``). The generated image allows viewers to see how well the discriminator classifies the provided series data. Must be a subclass of :class:`.BaseDiscriminator`. See :attr:`options` for ways to control the generation of the discriminator prediction image. fidelity: A float representing the fidelity of the discrimination. """ return [ "discriminator", "fidelity", ]
def _compute_extent(self) -> Optional[ExtentTuple]: """Computes the extent tuple of the data being plotted. Returns: The tuple ``(x_min, x_max, y_min, y_max)``, defining a rectangle containing all the data for this plotter. If the plotter contains no data, ``None`` is returned instead. """ ext_calc = DataExtentCalculator( multiplier=self.options.discriminator_multiplier, aspect_ratio=self.options.discriminator_aspect_ratio, ) has_registered_data = False for series in self.series: if self.data_exists_for(series, "points"): (points,) = self.data_for(series, "points") ext_calc.register_data(points) has_registered_data = True if self.data_exists_for(series, "centroid"): (centroid,) = self.data_for(series, "centroid") ext_calc.register_data(np.asarray(centroid).reshape(1, 2)) has_registered_data = True if self.figure_options.xlim: ext_calc.register_data(self.figure_options.xlim, dim=0) has_registered_data = True if self.figure_options.ylim: ext_calc.register_data(self.figure_options.ylim, dim=1) has_registered_data = True if has_registered_data: return ext_calc.extent() else: return None def _compute_discriminator_image( self, ) -> Tuple[Optional[np.ndarray], Optional[ExtentTuple]]: """Compute the array/image sampled from the discriminator predictions. Returns: The tuple ``(img, extent)`` where ``img`` is an optional 2D NumPy array of predictions and ``extent`` is a tuple of the extent ``(x_min, x_max, y_min, y_max)`` of the image. """ # If the discriminator is not provided, cannot compute the image. if "discriminator" not in self.supplementary_data: return None, None if self.options.discriminator_extent: extent = self.options.discriminator_extent else: extent = self._compute_extent() # If ``extent`` is None, we don't have data to plot and thus cannot generate the # discriminator image. if extent is None: return None, None # Get the discriminator and check if it is trained. If not, return. discrim: BaseDiscriminator = self.supplementary_data["discriminator"] if not discrim.is_trained(): return None, None # Compute discriminator resolution. extent_range = np.diff(np.asarray(extent).reshape(2, 2), axis=1).flatten() resolution = ( (extent_range / np.max(extent_range)) * self.options.discriminator_max_resolution ).astype(int) # Create coordinates for each pixel in the image, in the same units as `extent`. coords = [ [x, y] for x, y in product( np.linspace(extent[0], extent[1], resolution[0]), np.linspace(extent[2], extent[3], resolution[1]), ) ] # Get predictions for coordinates from the discriminator. predictions = discrim.predict(coords) # Unwrap predictions into a 2D array predictions = np.reshape(predictions, tuple(resolution)) return predictions, extent @classmethod def _default_options(cls) -> Options: """Return iq-plotter specific default plotter options. Options: plot_discriminator (bool): Whether to plot an image showing the predictions of the ``discriminator`` entry in :attr:`supplementary_data`. If True, the "discriminator" supplementary data entry must be set. discriminator_multiplier (float): The multiplier to use when computing the extent of the discriminator plot. The range of the series data is taken as the base value and multiplied by ``discriminator_extent_multiplier`` to compute the extent of the discriminator predictions. Defaults to 1.1. discriminator_aspect_ratio (float): The aspect ratio of the extent of the discriminator predictions, being ``width/height``. Defaults to ``1`` for a square region. discriminator_max_resolution (int): The number of pixels to use for the largest edge of the discriminator extent, used when sampling the discriminator to create the prediction image. Defaults to 1024. discriminator_alpha (float): The transparency of the discriminator prediction image. Defaults to 0.2 (i.e., 20%). discriminator_extent (Optional[ExtentTuple]): An optional tuple defining the extent of the image created by sampling from the discriminator. If ``None``, the extent tuple is computed using ``discriminator_multiplier``, ``discriminator_aspect_ratio``, and the series-data ``points`` and ``centroid``. Defaults to ``None``. flag_misclassified (bool): Whether to mark misclassified IQ values from all ``points`` series data, based on whether their series name is not the same as the prediction from the discriminator provided as supplementary data. If ``discriminator`` is not provided, ``flag_misclassified`` has no effect. Defaults to True. misclassified_symbol (str): Symbol for misclassified points, as a drawer-compatible string. Defaults to "x". misclassified_color (str | tuple): Color for misclassified points, as a drawer-compatible string or RGB tuple. Defaults to "r". """ options = super()._default_options() # Discriminator options options.plot_discriminator = True options.discriminator_multiplier = 1.1 options.discriminator_aspect_ratio = 1.0 options.discriminator_max_resolution = 1024 options.discriminator_alpha = 0.2 options.discriminator_extent = None # Points options options.flag_misclassified = True options.misclassified_symbol = "x" options.misclassified_color = "r" return options @classmethod def _default_figure_options(cls) -> Options: fig_opts = super()._default_figure_options() fig_opts.xlabel = "In-Phase" fig_opts.ylabel = "Quadrature" fig_opts.xval_unit = "arb." fig_opts.yval_unit = "arb." fig_opts.xval_unit_scale = False fig_opts.yval_unit_scale = False return fig_opts def _misclassified_points(self, series_name: str, points: np.ndarray) -> Optional[np.ndarray]: """Returns a list of IQ coordinates for points that are misclassified by the discriminator. Args: series_name: The series name to use as the expected discriminator label. If the discriminator returns a prediction that doesn't equal ``series_name``, it is marked as misclassified. points: The list of points to check for misclassification. Returns: A NumPy array of IQ points, being those that were misclassified by the discriminator. If the discriminator isn't set and trained, then `None` is returned. The array may be empty. """ # Check if we have a discriminator, and if it is trained. If not, return None. if "discriminator" not in self.supplementary_data: return None discrim: BaseDiscriminator = self.supplementary_data["discriminator"] if not discrim.is_trained(): return None classifications = discrim.predict(points) misclassified = np.argwhere(classifications != series_name) return points[misclassified, :].reshape(-1, 2) def _plot_figure(self): """Plots an IQ figure.""" # Plot discriminator first so that subsequent graphics change the automatic limits. This is a # function of the way `imshow` works for Matplotlib, in that the limits are automatically changed # to the extents of the image being plotted. If `image` is called after `scatter`, then the # automatic axis limits will be set to match the extent of the image. if "discriminator" in self.supplementary_data and self.options.plot_discriminator: if self.options.subplots != (1, 1): warnings.warn( "Plotting discriminator predictions with subplots is not well supported by " "IQPlotter as the predictions image will only be drawn on one subplot." ) discrim, extent = self._compute_discriminator_image() if discrim is None: warnings.warn( "Discriminator was provided but the sampled predictions image could not be " "generated." ) else: self.drawer.image( np.flip(discrim.transpose(), axis=0), extent, name="discriminator", cmap_use_series_colors=True, alpha=self.options.discriminator_alpha, ) # Plot points and centroids for ser in self.series: has_plotted_centroid = False if self.data_exists_for(ser, "centroid"): (centroid,) = self.data_for(ser, "centroid") self.drawer.scatter( centroid[0], centroid[1], name=ser, legend=True, zorder=4, s=20, edgecolor="k", marker="o", ) has_plotted_centroid = True if self.data_exists_for(ser, "points"): (points,) = self.data_for(ser, "points") self.drawer.scatter( points[:, 0], points[:, 1], name=ser, legend=not has_plotted_centroid, zorder=2, s=10, alpha=0.2, marker=".", ) if self.options.flag_misclassified: misclassified_points = self._misclassified_points(ser, points) if misclassified_points is not None: self.drawer.scatter( misclassified_points[:, 0], misclassified_points[:, 1], name="misclassified", legend=False, zorder=3, s=10, alpha=0.4, marker=self.options.misclassified_symbol, color=self.options.misclassified_color, ) # Fidelity report report = self._write_report() if len(report) > 0: self.drawer.textbox(report) def _write_report(self) -> str: """Write fidelity report with supplementary_data. Subclass can override this method to customize fidelity report. By default, this writes the fidelity of the discriminator in the fidelity report. Returns: Fidelity report. """ report = "" if "fidelity" in self.supplementary_data: fidelity = self.supplementary_data["fidelity"] report += f"fidelity = {fidelity: .4g}" return report