diff --git a/qiskit_experiments/curve_analysis/__init__.py b/qiskit_experiments/curve_analysis/__init__.py index 5053735859..90a2c74555 100644 --- a/qiskit_experiments/curve_analysis/__init__.py +++ b/qiskit_experiments/curve_analysis/__init__.py @@ -31,6 +31,7 @@ FitData ParameterRepr FitOptions + MplCurveDrawer Standard Analysis ================= @@ -119,7 +120,7 @@ process_curve_data, process_multi_curve_data, ) -from .visualization import plot_curve_fit, plot_errorbar, plot_scatter, FitResultPlotters +from .visualization import MplCurveDrawer from . import guess from . import fit_function @@ -132,3 +133,6 @@ GaussianAnalysis, ErrorAmplificationAnalysis, ) + +# deprecated +from .visualization import plot_curve_fit, plot_errorbar, plot_scatter, FitResultPlotters diff --git a/qiskit_experiments/curve_analysis/curve_analysis.py b/qiskit_experiments/curve_analysis/curve_analysis.py index 573db1b7ce..1a249f491d 100644 --- a/qiskit_experiments/curve_analysis/curve_analysis.py +++ b/qiskit_experiments/curve_analysis/curve_analysis.py @@ -28,6 +28,7 @@ from uncertainties import unumpy as unp from qiskit.providers import Backend +from qiskit.utils import detach_prefix from qiskit_experiments.curve_analysis.curve_data import ( CurveData, SeriesDef, @@ -37,7 +38,7 @@ ) from qiskit_experiments.curve_analysis.curve_fit import multi_curve_fit from qiskit_experiments.curve_analysis.data_processing import multi_mean_xy_data, data_sort -from qiskit_experiments.curve_analysis.visualization import FitResultPlotters, PlotterStyle +from qiskit_experiments.curve_analysis.visualization import MplCurveDrawer, BaseCurveDrawer from qiskit_experiments.data_processing import DataProcessor from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.data_processing.processor_library import get_processor @@ -290,11 +291,22 @@ def parameters(self) -> List[str]: """Return parameters of this curve analysis.""" return [s for s in self._fit_params() if s not in self.options.fixed_parameters] + @property + def drawer(self) -> BaseCurveDrawer: + """A short-cut for curve drawer instance.""" + return self._options.curve_plotter + @classmethod def _default_options(cls) -> Options: """Return default analysis options. Analysis Options: + curve_plotter (BaseCurveDrawer): A curve drawer instance to visualize + the analysis result. + plot_raw_data (bool): Set ``True`` to draw un-formatted data points on canvas. + This is ``True`` by default. + plot (bool): Set ``True`` to create figure for fit result. + This is ``False`` by default. curve_fitter (Callable): A callback function to perform fitting with formatted data. See :func:`~qiskit_experiments.analysis.multi_curve_fit` for example. data_processor (Callable): A callback function to format experiment data. @@ -306,23 +318,6 @@ def _default_options(cls) -> Options: bounds (Dict[str, Tuple[float, float]]): Array-like or dictionary of (min, max) tuple of fit parameter boundaries. x_key (str): Circuit metadata key representing a scanned value. - plot (bool): Set ``True`` to create figure for fit result. - axis (AxesSubplot): Optional. A matplotlib axis object to draw. - xlabel (str): X label of fit result figure. - ylabel (str): Y label of fit result figure. - xlim (Tuple[float, float]): Min and max value of horizontal axis of the fit plot. - ylim (Tuple[float, float]): Min and max value of vertical axis of the fit plot. - xval_unit (str): SI unit of x values. No prefix is needed here. - For example, when the x values represent time, this option will be just "s" - rather than "ms". In the fit result plot, the prefix is automatically selected - based on the maximum value. If your x values are in [1e-3, 1e-4], they - are displayed as [1 ms, 10 ms]. This option is likely provided by the - analysis class rather than end-users. However, users can still override - if they need different unit notation. By default, this option is set to ``None``, - and no scaling is applied. X axis will be displayed in the scientific notation. - yval_unit (str): Unit of y values. Same as ``xval_unit``. - This value is not provided in most experiments, because y value is usually - population or expectation values. result_parameters (List[Union[str, ParameterRepr]): Parameters reported in the database as a dedicated entry. This is a list of parameter representation which is either string or ParameterRepr object. If you provide more @@ -331,13 +326,6 @@ def _default_options(cls) -> Options: The parameter name should be defined in the series definition. Representation should be printable in standard output, i.e. no latex syntax. return_data_points (bool): Set ``True`` to return formatted XY data. - curve_plotter (str): A name of plotter function used to generate - the curve fit result figure. This refers to the mapper - :py:class:`~qiskit_experiments.curve_analysis.visualization.FitResultPlotters` - to retrieve the corresponding callback function. - style (PlotterStyle): An instance of - :py:class:`~qiskit_experiments.curve_analysis.visualization.style.PlotterStyle` - that contains a set of configurations to create a fit plot. extra (Dict[str, Any]): A dictionary that is appended to all database entries as extra information. curve_fitter_options (Dict[str, Any]) Options that are passed to the @@ -348,22 +336,15 @@ def _default_options(cls) -> Options: """ options = super()._default_options() + options.curve_plotter = MplCurveDrawer() + options.plot_raw_data = False + options.plot = True options.curve_fitter = multi_curve_fit options.data_processor = None options.normalization = False options.x_key = "xval" - options.plot = True - options.axis = None - options.xlabel = None - options.ylabel = None - options.xlim = None - options.ylim = None - options.xval_unit = None - options.yval_unit = None options.result_parameters = None options.return_data_points = False - options.curve_plotter = "mpl_single_canvas" - options.style = PlotterStyle() options.extra = dict() options.curve_fitter_options = dict() options.p0 = {} @@ -372,6 +353,58 @@ def _default_options(cls) -> Options: return options + def set_options(self, **fields): + """Set the analysis options for :meth:`run` method. + + Args: + fields: The fields to update the options + + Raises: + KeyError: When removed option ``curve_fitter`` is set. + TypeError: When invalid drawer instance is provided. + """ + # TODO remove this in Qiskit Experiments v0.4 + if "curve_plotter" in fields and isinstance(fields["curve_plotter"], str): + plotter_str = fields["curve_plotter"] + warnings.warn( + f"The curve plotter '{plotter_str}' has been deprecated. " + "The option is replaced with 'MplCurveDrawer' instance. " + "If this is a loaded analysis, please save this instance again to update option value. " + "This warning will be removed with backport in Qiskit Experiments 0.4.", + DeprecationWarning, + stacklevel=2, + ) + fields["curve_plotter"] = MplCurveDrawer() + + if "curve_plotter" in fields and not isinstance(fields["curve_plotter"], BaseCurveDrawer): + plotter_obj = fields["curve_plotter"] + raise TypeError( + f"'{plotter_obj.__class__.__name__}' object is not valid curve drawer instance." + ) + + # pylint: disable=no-member + draw_options = set(self.drawer.options.__dict__.keys()) | {"style"} + deprecated = draw_options & fields.keys() + if any(deprecated): + warnings.warn( + f"Option(s) {deprecated} have been moved to draw_options and will be removed soon. " + "Use self.drawer.set_options instead. " + "If this is a loaded analysis, please save this instance again to update option value. " + "This warning will be removed with backport in Qiskit Experiments 0.4.", + DeprecationWarning, + stacklevel=2, + ) + draw_options = dict() + for depopt in deprecated: + if depopt == "style": + for k, v in fields.pop("style").items(): + draw_options[k] = v + else: + draw_options[depopt] = fields.pop(depopt) + self.drawer.set_options(**draw_options) + + super().set_options(**fields) + def _generate_fit_guesses(self, user_opt: FitOptions) -> Union[FitOptions, List[FitOptions]]: """Create algorithmic guess with analysis options and curve data. @@ -786,6 +819,7 @@ def _run_analysis( for series_def in self.__series__: dict_def = dataclasses.asdict(series_def) dict_def["fit_func"] = functools.partial(series_def.fit_func, **assigned_params) + del dict_def["signature"] assigned_series.append(SeriesDef(**dict_def)) self.__series__ = assigned_series @@ -946,8 +980,8 @@ def _run_analysis( name=DATA_ENTRY_PREFIX + self.__class__.__name__, value=raw_data_dict, extra={ - "x-unit": self.options.xval_unit, - "y-unit": self.options.yval_unit, + "x-unit": self.drawer.options.xval_unit, + "y-unit": self.drawer.options.yval_unit, }, ) analysis_results.append(raw_data_entry) @@ -956,24 +990,73 @@ def _run_analysis( # 6. Create figures # if self.options.plot: - fit_figure = FitResultPlotters[self.options.curve_plotter].value.draw( - series_defs=self.__series__, - raw_samples=[self._data(ser.name, "raw_data") for ser in self.__series__], - fit_samples=[self._data(ser.name, "fit_ready") for ser in self.__series__], - tick_labels={ - "xval_unit": self.options.xval_unit, - "yval_unit": self.options.yval_unit, - "xlabel": self.options.xlabel, - "ylabel": self.options.ylabel, - "xlim": self.options.xlim, - "ylim": self.options.ylim, - }, - fit_data=fit_result, - result_entries=analysis_results, - style=self.options.style, - axis=self.options.axis, - ) - figures = [fit_figure] + # Initialize axis + self.drawer.initialize_canvas() + # Write raw data + if self.options.plot_raw_data: + for s in self.__series__: + raw_data = self._data(label="raw_data", series_name=s.name) + self.drawer.draw_raw_data( + x_data=raw_data.x, + y_data=raw_data.y, + ax_index=s.canvas, + ) + # Write data points + for s in self.__series__: + curve_data = self._data(label="fit_ready", series_name=s.name) + self.drawer.draw_formatted_data( + x_data=curve_data.x, + y_data=curve_data.y, + y_err_data=curve_data.y_err, + name=s.name, + ax_index=s.canvas, + color=s.plot_color, + marker=s.plot_symbol, + ) + # Write fit results if fitting succeeded + if fit_result: + for s in self.__series__: + interp_x = np.linspace(*fit_result.x_range, 100) + + params = {} + for fitpar in s.signature: + if fitpar in self.options.fixed_parameters: + params[fitpar] = self.options.fixed_parameters[fitpar] + else: + params[fitpar] = fit_result.fitval(fitpar) + + y_data_with_uncertainty = s.fit_func(interp_x, **params) + y_mean = unp.nominal_values(y_data_with_uncertainty) + y_std = unp.std_devs(y_data_with_uncertainty) + # Draw fit line + self.drawer.draw_fit_line( + x_data=interp_x, + y_data=y_mean, + ax_index=s.canvas, + color=s.plot_color, + ) + # Draw confidence intervals with different n_sigma + sigmas = unp.std_devs(y_data_with_uncertainty) + if np.isfinite(sigmas).all(): + for n_sigma, alpha in self.drawer.options.plot_sigma: + self.drawer.draw_confidence_interval( + x_data=interp_x, + y_ub=y_mean + n_sigma * y_std, + y_lb=y_mean - n_sigma * y_std, + ax_index=s.canvas, + alpha=alpha, + color=s.plot_color, + ) + + # Write fitting report + report_description = "" + for res in analysis_results: + if isinstance(res.value, (float, uncertainties.UFloat)): + report_description += f"{analysis_result_to_repr(res)}\n" + report_description += r"Fit $\chi^2$ = " + f"{fit_result.reduced_chisq: .4g}" + self.drawer.draw_fit_report(description=report_description) + self.drawer.format_canvas() + figures = [self.drawer.figure] else: figures = [] @@ -1034,3 +1117,51 @@ def is_error_not_significant( return True return False + + +def analysis_result_to_repr(result: AnalysisResultData) -> str: + """A helper function to create string representation from analysis result data object. + + Args: + result: Analysis result data. + + Returns: + String representation of the data. + """ + if not isinstance(result.value, (float, uncertainties.UFloat)): + return AnalysisError(f"Result data {result.name} is not a valid fit parameter data type.") + + unit = result.extra.get("unit", None) + + def _format_val(value): + # Return value with unit with prefix, i.e. 1000 Hz -> 1 kHz. + if unit: + try: + val, val_prefix = detach_prefix(value, decimal=3) + except ValueError: + val = value + val_prefix = "" + return f"{val: .3g}", f" {val_prefix}{unit}" + if np.abs(value) < 1e-3 or np.abs(value) > 1e3: + return f"{value: .4e}", "" + return f"{value: .4g}", "" + + if isinstance(result.value, float): + # Only nominal part + n_repr, n_unit = _format_val(result.value) + value_repr = n_repr + n_unit + else: + # Nominal part + n_repr, n_unit = _format_val(result.value.nominal_value) + + # Standard error part + if result.value.std_dev is not None and np.isfinite(result.value.std_dev): + s_repr, s_unit = _format_val(result.value.std_dev) + if n_unit == s_unit: + value_repr = f" {n_repr} \u00B1 {s_repr}{n_unit}" + else: + value_repr = f" {n_repr + n_unit} \u00B1 {s_repr + s_unit}" + else: + value_repr = n_repr + n_unit + + return f"{result.name} = {value_repr}" diff --git a/qiskit_experiments/curve_analysis/curve_data.py b/qiskit_experiments/curve_analysis/curve_data.py index 294a74f5ac..861e83dde9 100644 --- a/qiskit_experiments/curve_analysis/curve_data.py +++ b/qiskit_experiments/curve_analysis/curve_data.py @@ -15,6 +15,7 @@ """ import dataclasses +import inspect from typing import Any, Dict, Callable, Union, List, Tuple, Optional, Iterable import numpy as np @@ -47,6 +48,20 @@ class SeriesDef: # Index of canvas if the result figure is multi-panel canvas: Optional[int] = None + # Automatically extracted signature of the fit function + signature: List[str] = dataclasses.field(init=False) + + def __post_init__(self): + """Parse the fit function signature to extract the names of the variables. + + Fit functions take arguments F(x, p0, p1, p2, ...) thus the first value should be excluded. + """ + signature = list(inspect.signature(self.fit_func).parameters.keys()) + fitparams = signature[1:] + + # Note that this dataclass is frozen + object.__setattr__(self, "signature", fitparams) + @dataclasses.dataclass(frozen=True) class CurveData: diff --git a/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py b/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py index a863fe3998..0e8749bcfe 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py @@ -109,10 +109,12 @@ def _default_options(cls): considered as good. Defaults to :math:`\pi/2`. """ default_options = super()._default_options() + default_options.curve_plotter.set_options( + xlabel="Number of gates (n)", + ylabel="Population", + ylim=(0, 1.0), + ) default_options.result_parameters = ["d_theta"] - default_options.xlabel = "Number of gates (n)" - default_options.ylabel = "Population" - default_options.ylim = [0, 1.0] default_options.max_good_angle_error = np.pi / 2 return default_options diff --git a/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py b/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py index d9187a2e6b..dd56293391 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py @@ -71,11 +71,13 @@ class GaussianAnalysis(curve.CurveAnalysis): @classmethod def _default_options(cls) -> Options: options = super()._default_options() + options.curve_plotter.set_options( + xlabel="Frequency", + ylabel="Signal (arb. units)", + xval_unit="Hz", + ) options.result_parameters = [curve.ParameterRepr("freq", "f01", "Hz")] options.normalization = True - options.xlabel = "Frequency" - options.ylabel = "Signal (arb. units)" - options.xval_unit = "Hz" return options def _generate_fit_guesses( diff --git a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py index 05a240a42b..7c45f88437 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py @@ -71,11 +71,13 @@ class ResonanceAnalysis(curve.CurveAnalysis): @classmethod def _default_options(cls) -> Options: options = super()._default_options() + options.curve_plotter.set_options( + xlabel="Frequency", + ylabel="Signal (arb. units)", + xval_unit="Hz", + ) options.result_parameters = [curve.ParameterRepr("freq", "f01", "Hz")] options.normalization = True - options.xlabel = "Frequency" - options.ylabel = "Signal (arb. units)" - options.xval_unit = "Hz" return options def _generate_fit_guesses( diff --git a/qiskit_experiments/curve_analysis/visualization/__init__.py b/qiskit_experiments/curve_analysis/visualization/__init__.py index c6d00ecdc7..0c85169e32 100644 --- a/qiskit_experiments/curve_analysis/visualization/__init__.py +++ b/qiskit_experiments/curve_analysis/visualization/__init__.py @@ -15,6 +15,9 @@ from enum import Enum +from .base_drawer import BaseCurveDrawer +from .mpl_drawer import MplCurveDrawer + from . import fit_result_plotters from .curves import plot_scatter, plot_errorbar, plot_curve_fit from .style import PlotterStyle diff --git a/qiskit_experiments/curve_analysis/visualization/base_drawer.py b/qiskit_experiments/curve_analysis/visualization/base_drawer.py new file mode 100644 index 0000000000..06a71a95e6 --- /dev/null +++ b/qiskit_experiments/curve_analysis/visualization/base_drawer.py @@ -0,0 +1,280 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Curve drawer abstract class.""" + +from abc import ABC, abstractmethod +from typing import Dict, Sequence, Optional + +from qiskit_experiments.framework import Options + + +class BaseCurveDrawer(ABC): + """Abstract class for the serializable Qiskit Experiments curve drawer. + + A curve drawer may be implemented by different drawing backends such as matplotlib + or plotly. Sub-classes that wrap these backends by subclassing `BaseCurveDrawer` must + implement the following abstract methods. + + initialize_canvas + + This method should implement a protocol to initialize a drawing canvas + with user input ``axis`` object. Note that curve analysis drawer + supports visualization of experiment results in multiple canvases + tiled into N (row) x M (column) inset grids, which is specified in the option ``subplots``. + By default, this is N=1, M=1 and thus no inset grid will be initialized. + The data points to draw might be provided with a canvas number defined in + :attr:`SeriesDef.canvas` which defaults to ``None``, i.e. no-inset grids. + + This method should first check the drawing options for the axis object + and initialize the axis only when it is not provided by the options. + Once axis is initialized, this is set to the instance member ``self._axis``. + + format_canvas + + This method should implement a protocol to format the appearance of canvas. + Typically, it updates axis and tick labels. Note that the axis SI unit + may be specified in the drawing options. In this case, axis numbers should be + auto-scaled with the unit prefix. + + draw_raw_data + + This method is called after data processing is completed. + This method draws raw experiment data points on the canvas. + + draw_formatted_data + + This method is called after data formatting is completed. + The formatted data might be averaged over the same x values, + or smoothed by a filtering algorithm, depending on how analysis class is implemented. + This method is called with error bars of y values and the name of the curve. + + draw_fit_line + + This method is called after fitting is completed and when there is valid fit outcome. + This method is called with the interpolated x and y values. + + draw_confidence_interval + + This method is called after fitting is completed and when there is valid fit outcome. + This method is called with the interpolated x and a pair of y values + that represent the upper and lower bound within certain confidence interval. + This might be called multiple times with different interval sizes. + + draw_fit_report + + This method is called after fitting is completed and when there is valid fit outcome. + This method is called with the list of analysis results and the reduced chi-squared values. + The fit report should be generated to show this information on the canvas. + + """ + + def __init__(self): + self._options = self._default_options() + self._set_options = set() + self._axis = None + + @property + def options(self) -> Options: + """Return the drawing options.""" + return self._options + + @classmethod + def _default_options(cls) -> Options: + """Return default draw options. + + Draw Options: + axis (Any): Arbitrary object that can be used as a drawing canvas. + subplots (Tuple[int, int]): Number of rows and columns when the experimental + result is drawn in the multiple windows. + xlabel (Union[str, List[str]]): X-axis label string of the output figure. + If there are multiple columns in the canvas, this could be a list of labels. + ylabel (Union[str, List[str]]): Y-axis label string of the output figure. + If there are multiple rows in the canvas, this could be a list of labels. + xlim (Tuple[float, float]): Min and max value of the horizontal axis. + If not provided, it is automatically scaled based on the input data points. + ylim (Tuple[float, float]): Min and max value of the vertical axis. + If not provided, it is automatically scaled based on the input data points. + xval_unit (str): SI unit of x values. No prefix is needed here. + For example, when the x values represent time, this option will be just "s" + rather than "ms". In the output figure, the prefix is automatically selected + based on the maximum value in this axis. If your x values are in [1e-3, 1e-4], + they are displayed as [1 ms, 10 ms]. This option is likely provided by the + analysis class rather than end-users. However, users can still override + if they need different unit notation. By default, this option is set to ``None``, + and no scaling is applied. If nothing is provided, the axis numbers will be + displayed in the scientific notation. + yval_unit (str): Unit of y values. See ``xval_unit`` for details. + figsize (Tuple[int, int]): A tuple of two numbers representing the size of + the output figure (width, height). Note that this is applicable + only when ``axis`` object is not provided. If any canvas object is provided, + the figure size associated with the axis is preferentially applied. + legend_loc (str): Vertical and horizontal location of the curve legend window in + a single string separated by a space. This defaults to ``center right``. + Vertical position can be ``upper``, ``center``, ``lower``. + Horizontal position can be ``right``, ``center``, ``left``. + tick_label_size (int): Size of text representing the axis tick numbers. + axis_label_size (int): Size of text representing the axis label. + fit_report_rpos (Tuple[int, int]): A tuple of numbers showing the location of + the fit report window. These numbers are horizontal and vertical position + of the top left corner of the window in the relative coordinate + on the output figure, i.e. ``[0, 1]``. + The fit report window shows the selected fit parameters and the reduced + chi-squared value. + fit_report_text_size (int): Size of text in the fit report window. + plot_sigma (List[Tuple[float, float]]): A list of two number tuples + showing the configuration to write confidence intervals for the fit curve. + The first argument is the relative sigma (n_sigma), and the second argument is + the transparency of the interval plot in ``[0, 1]``. + Multiple n_sigma intervals can be drawn for the single curve. + """ + return Options( + axis=None, + subplots=(1, 1), + xlabel=None, + ylabel=None, + xlim=None, + ylim=None, + xval_unit=None, + yval_unit=None, + figsize=(8, 5), + legend_loc="center right", + tick_label_size=14, + axis_label_size=16, + fit_report_rpos=(0.6, 0.95), + fit_report_text_size=14, + plot_sigma=[(1.0, 0.7), (3.0, 0.3)], + ) + + def set_options(self, **fields): + """Set the drawing options. + Args: + fields: The fields to update the options + """ + self._options.update_options(**fields) + self._set_options = self._set_options.union(fields) + + @abstractmethod + def initialize_canvas(self): + """Initialize the drawing canvas.""" + + @abstractmethod + def format_canvas(self): + """Final cleanup for the canvas appearance.""" + + @abstractmethod + def draw_raw_data( + self, + x_data: Sequence[float], + y_data: Sequence[float], + ax_index: Optional[int] = None, + **options, + ): + """Draw raw data. + + Args: + x_data: X values. + y_data: Y values. + ax_index: Index of canvas if multiple inset axis exist. + options: Valid options for the drawer backend API. + """ + + @abstractmethod + def draw_formatted_data( + self, + x_data: Sequence[float], + y_data: Sequence[float], + y_err_data: Sequence[float], + name: Optional[str] = None, + ax_index: Optional[int] = None, + **options, + ): + """Draw the formatted data that is used for fitting. + + Args: + x_data: X values. + y_data: Y values. + y_err_data: Standard deviation of Y values. + name: Name of this curve. + ax_index: Index of canvas if multiple inset axis exist. + options: Valid options for the drawer backend API. + """ + + @abstractmethod + def draw_fit_line( + self, + x_data: Sequence[float], + y_data: Sequence[float], + ax_index: Optional[int] = None, + **options, + ): + """Draw fit line. + + Args: + x_data: X values. + y_data: Fit Y values. + ax_index: Index of canvas if multiple inset axis exist. + options: Valid options for the drawer backend API. + """ + + @abstractmethod + def draw_confidence_interval( + self, + x_data: Sequence[float], + y_ub: Sequence[float], + y_lb: Sequence[float], + ax_index: Optional[int] = None, + **options, + ): + """Draw cofidence interval. + + Args: + x_data: X values. + y_ub: The upper boundary of Y values. + y_lb: The lower boundary of Y values. + ax_index: Index of canvas if multiple inset axis exist. + options: Valid options for the drawer backend API. + """ + + @abstractmethod + def draw_fit_report( + self, + description: str, + **options, + ): + """Draw text box that shows fit reports. + + Args: + description: A string to describe the fiting outcome. + options: Valid options for the drawer backend API. + """ + + @property + @abstractmethod + def figure(self): + """Return figure object handler to be saved in the database.""" + + def config(self) -> Dict: + """Return the config dictionary for this drawing.""" + options = dict((key, getattr(self._options, key)) for key in self._set_options) + + return {"cls": type(self), "options": options} + + def __json_encode__(self): + return self.config() + + @classmethod + def __json_decode__(cls, value): + instance = cls() + if "options" in value: + instance.set_options(**value["options"]) + return instance diff --git a/qiskit_experiments/curve_analysis/visualization/mpl_drawer.py b/qiskit_experiments/curve_analysis/visualization/mpl_drawer.py new file mode 100644 index 0000000000..a01de36975 --- /dev/null +++ b/qiskit_experiments/curve_analysis/visualization/mpl_drawer.py @@ -0,0 +1,324 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Curve drawer for matplotlib backend.""" + +from typing import Sequence, Optional + +import numpy as np +from matplotlib.axes import Axes +from matplotlib.figure import Figure +from matplotlib.ticker import ScalarFormatter, Formatter + +from qiskit.utils import detach_prefix +from qiskit_experiments.framework.matplotlib import get_non_gui_ax + +from .base_drawer import BaseCurveDrawer + + +class MplCurveDrawer(BaseCurveDrawer): + """Curve drawer for MatplotLib backend.""" + + class PrefixFormatter(Formatter): + """Matplotlib axis formatter to detach prefix. + + If a value is, e.g., x=1000.0 and the factor is 1000, then it will be shown + as 1.0 in the ticks and its unit will be shown with the prefactor 'k' + in the axis label. + """ + + def __init__(self, factor: float): + self.factor = factor + + def __call__(self, x, pos=None): + return self.fix_minus("{:.3g}".format(x * self.factor)) + + def initialize_canvas(self): + # Create axis if empty + if not self.options.axis: + axis = get_non_gui_ax() + figure = axis.get_figure() + figure.set_size_inches(*self.options.figsize) + else: + axis = self.options.axis + + n_rows, n_cols = self.options.subplots + n_subplots = n_cols * n_rows + if n_subplots > 1: + # Add inset axis. User may provide a single axis object via the analysis option, + # while this analysis tries to draw its result in multiple canvases, + # especially when the analysis consists of multiple curves. + # Inset axis is experimental implementation of matplotlib 3.0 so maybe unstable API. + # This draws inset axes with shared x and y axis. + inset_ax_h = 1 / n_rows + inset_ax_w = 1 / n_cols + for i in range(n_rows): + for j in range(n_cols): + # x0, y0, width, height + bounds = [ + inset_ax_w * j, + 1 - inset_ax_h * (i + 1), + inset_ax_w, + inset_ax_h, + ] + sub_ax = axis.inset_axes(bounds, transform=axis.transAxes, zorder=1) + if j != 0: + # remove y axis except for most-left plot + sub_ax.set_yticklabels([]) + else: + # this axis locates at left, write y-label + if self.options.ylabel: + label = self.options.ylabel + if isinstance(label, list): + # Y label can be given as a list for each sub axis + label = label[i] + sub_ax.set_ylabel(label, fontsize=self.options.axis_label_size) + if i != n_rows - 1: + # remove x axis except for most-bottom plot + sub_ax.set_xticklabels([]) + else: + # this axis locates at bottom, write x-label + if self.options.xlabel: + label = self.options.xlabel + if isinstance(label, list): + # X label can be given as a list for each sub axis + label = label[j] + sub_ax.set_xlabel(label, fontsize=self.options.axis_label_size) + if j == 0 or i == n_rows - 1: + # Set label size for outer axes where labels are drawn + sub_ax.tick_params(labelsize=self.options.tick_label_size) + sub_ax.grid() + + # Remove original axis frames + axis.axis("off") + else: + axis.set_xlabel(self.options.xlabel, fontsize=self.options.axis_label_size) + axis.set_ylabel(self.options.ylabel, fontsize=self.options.axis_label_size) + axis.tick_params(labelsize=self.options.tick_label_size) + axis.grid() + + self._axis = axis + + def format_canvas(self): + if self._axis.child_axes: + # Multi canvas mode + all_axes = self._axis.child_axes + else: + all_axes = [self._axis] + + # Add data labels if there are multiple labels registered per sub_ax. + for sub_ax in all_axes: + _, labels = sub_ax.get_legend_handles_labels() + if len(labels) > 1: + sub_ax.legend() + + # Format x and y axis + for ax_type in ("x", "y"): + # Get axis formatter from drawing options + if ax_type == "x": + lim = self.options.xlim + unit = self.options.xval_unit + else: + lim = self.options.ylim + unit = self.options.yval_unit + + # Compute data range from auto scale + if not lim: + v0 = np.nan + v1 = np.nan + for sub_ax in all_axes: + if ax_type == "x": + this_v0, this_v1 = sub_ax.get_xlim() + else: + this_v0, this_v1 = sub_ax.get_ylim() + v0 = np.nanmin([v0, this_v0]) + v1 = np.nanmax([v1, this_v1]) + lim = (v0, v1) + + # Format axis number notation + if unit: + # If value is specified, automatically scale axis magnitude + # and write prefix to axis label, i.e. 1e3 Hz -> 1 kHz + maxv = max(np.abs(lim[0]), np.abs(lim[1])) + try: + scaled_maxv, prefix = detach_prefix(maxv, decimal=3) + prefactor = scaled_maxv / maxv + except ValueError: + prefix = "" + prefactor = 1 + + formatter = MplCurveDrawer.PrefixFormatter(prefactor) + units_str = f" [{prefix}{unit}]" + else: + # Use scientific notation with 3 digits, 1000 -> 1e3 + formatter = ScalarFormatter() + formatter.set_scientific(True) + formatter.set_powerlimits((-3, 3)) + + units_str = "" + + for sub_ax in all_axes: + if ax_type == "x": + ax = getattr(sub_ax, "xaxis") + tick_labels = sub_ax.get_xticklabels() + else: + ax = getattr(sub_ax, "yaxis") + tick_labels = sub_ax.get_yticklabels() + + if tick_labels: + # Set formatter only when tick labels exist + ax.set_major_formatter(formatter) + if units_str: + # Add units to label if both exist + label_txt_obj = ax.get_label() + label_str = label_txt_obj.get_text() + if label_str: + label_txt_obj.set_text(label_str + units_str) + + # Auto-scale all axes to the first sub axis + if ax_type == "x": + all_axes[0].get_shared_x_axes().join(*all_axes) + all_axes[0].set_xlim(lim) + else: + all_axes[0].get_shared_y_axes().join(*all_axes) + all_axes[0].set_ylim(lim) + + def _get_axis(self, index: Optional[int] = None) -> Axes: + """A helper method to get inset axis. + + Args: + index: Index of inset axis. If nothing is provided, it returns the entire axis. + + Returns: + Corresponding axis object. + + Raises: + IndexError: When axis index is specified but no inset axis is found. + """ + if index is not None: + try: + return self._axis.child_axes[index] + except IndexError as ex: + raise IndexError( + f"Canvas index {index} is out of range. " + f"Only {len(self._axis.child_axes)} subplots are initialized." + ) from ex + else: + return self._axis + + def draw_raw_data( + self, + x_data: Sequence[float], + y_data: Sequence[float], + ax_index: Optional[int] = None, + **options, + ): + draw_options = { + "color": "grey", + "marker": "x", + "alpha": 0.8, + "zorder": 2, + } + draw_options.update(**options) + self._get_axis(ax_index).scatter(x_data, y_data, **draw_options) + + def draw_formatted_data( + self, + x_data: Sequence[float], + y_data: Sequence[float], + y_err_data: Sequence[float], + name: Optional[str] = None, + ax_index: Optional[int] = None, + **options, + ): + draw_ops = { + "markersize": 9, + "alpha": 0.8, + "zorder": 4, + "linestyle": "", + } + draw_ops.update(**options) + if name: + draw_ops["label"] = name + self._get_axis(ax_index).errorbar(x_data, y_data, yerr=y_err_data, **draw_ops) + + def draw_fit_line( + self, + x_data: Sequence[float], + y_data: Sequence[float], + ax_index: Optional[int] = None, + **options, + ): + draw_ops = { + "zorder": 5, + "linestyle": "-", + "linewidth": 2, + } + draw_ops.update(**options) + self._get_axis(ax_index).plot(x_data, y_data, **draw_ops) + + def draw_confidence_interval( + self, + x_data: Sequence[float], + y_ub: Sequence[float], + y_lb: Sequence[float], + ax_index: Optional[int] = None, + **options, + ): + draw_ops = { + "zorder": 3, + "alpha": 0.1, + "color": "black", + } + draw_ops.update(**options) + self._get_axis(ax_index).fill_between(x_data, y1=y_lb, y2=y_ub, **draw_ops) + + def draw_fit_report( + self, + description: str, + **options, + ): + bbox_props = { + "boxstyle": "square, pad=0.3", + "fc": "white", + "ec": "black", + "lw": 1, + "alpha": 0.8, + } + bbox_props.update(**options) + + report_handler = self._axis.text( + *self.options.fit_report_rpos, + s=description, + ha="center", + va="top", + size=self.options.fit_report_text_size, + transform=self._axis.transAxes, + ) + report_handler.set_bbox(bbox_props) + + @property + def figure(self) -> Figure: + """Return figure object handler to be saved in the database. + + In the MatplotLib the ``Figure`` and ``Axes`` are different object. + User can pass a part of the figure (i.e. multi-axes) to the drawer option ``axis``. + For example, a user wants to combine two different experiment results in the + same figure, one can call ``pyplot.subplots`` with two rows and pass one of the + generated two axes to each experiment drawer. Once all the experiments complete, + the user will obtain the single figure collecting all experimental results. + + Note that this method returns the entire figure object, rather than a single axis. + Thus, the experiment data saved in the database might have a figure + collecting all child axes drawings. + """ + return self._axis.get_figure() diff --git a/qiskit_experiments/library/calibration/rough_amplitude_cal.py b/qiskit_experiments/library/calibration/rough_amplitude_cal.py index aef1732a36..c0da89d926 100644 --- a/qiskit_experiments/library/calibration/rough_amplitude_cal.py +++ b/qiskit_experiments/library/calibration/rough_amplitude_cal.py @@ -20,11 +20,10 @@ from qiskit.circuit import Parameter from qiskit.providers.backend import Backend -from qiskit_experiments.framework import ExperimentData, Options +from qiskit_experiments.framework import ExperimentData from qiskit_experiments.calibration_management import BaseCalibrationExperiment, Calibrations from qiskit_experiments.library.characterization import Rabi from qiskit_experiments.calibration_management.update_library import BaseUpdater -from qiskit_experiments.curve_analysis import ParameterRepr AnglesSchedules = namedtuple( "AnglesSchedules", ["target_angle", "parameter", "schedule", "previous_value"] @@ -38,6 +37,8 @@ class RoughAmplitudeCal(BaseCalibrationExperiment, Rabi): qiskit_experiments.library.characterization.rabi.Rabi """ + __outcome__ = "freq" + def __init__( self, qubit: int, @@ -199,6 +200,8 @@ class RoughXSXAmplitudeCal(RoughAmplitudeCal): qiskit_experiments.library.characterization.rabi.Rabi """ + __outcome__ = "rabi_rate" + def __init__( self, qubit: int, @@ -232,6 +235,8 @@ class EFRoughXSXAmplitudeCal(RoughAmplitudeCal): qiskit_experiments.library.characterization.rabi.Rabi """ + __outcome__ = "rabi_rate_12" + def __init__( self, qubit: int, @@ -277,14 +282,6 @@ def __init__( ), ] - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = super()._default_analysis_options() - options.result_parameters = [ParameterRepr("freq", "rabi_rate_12")] - - return options - def _pre_circuit(self) -> QuantumCircuit: """A circuit with operations to perform before the Rabi.""" circ = QuantumCircuit(1) diff --git a/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py b/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py index 9e9fe48cac..6eafbdd95c 100644 --- a/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py @@ -19,6 +19,7 @@ import numpy as np import qiskit_experiments.curve_analysis as curve + import qiskit_experiments.data_processing as dp from qiskit_experiments.database_service.device_component import Qubit from qiskit_experiments.framework import AnalysisResultData @@ -196,20 +197,24 @@ class CrossResonanceHamiltonianAnalysis(curve.CurveAnalysis): def _default_options(cls): """Return the default analysis options.""" default_options = super()._default_options() - default_options.data_processor = dp.DataProcessor( - input_key="counts", - data_actions=[dp.Probability("1"), dp.BasisExpectationValue()], - ) - default_options.curve_plotter = "mpl_multiv_canvas" - default_options.xlabel = "Flat top width" - default_options.ylabel = ",," - default_options.xval_unit = "s" - default_options.style = curve.visualization.PlotterStyle( + default_options.curve_plotter.set_options( + subplots=(3, 1), + xlabel="Flat top width", + ylabel=[ + r"$\langle$X(t)$\rangle$", + r"$\langle$Y(t)$\rangle$", + r"$\langle$Z(t)$\rangle$", + ], + xval_unit="s", figsize=(8, 10), legend_loc="lower right", fit_report_rpos=(0.28, -0.10), + ylim=(-1, 1), + ) + default_options.data_processor = dp.DataProcessor( + input_key="counts", + data_actions=[dp.Probability("1"), dp.BasisExpectationValue()], ) - default_options.ylim = (-1, 1) return default_options diff --git a/qiskit_experiments/library/characterization/analysis/drag_analysis.py b/qiskit_experiments/library/characterization/analysis/drag_analysis.py index c13a873c3e..82c79f865a 100644 --- a/qiskit_experiments/library/characterization/analysis/drag_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/drag_analysis.py @@ -120,9 +120,11 @@ def _default_options(cls): descriptions of analysis options. """ default_options = super()._default_options() + default_options.curve_plotter.set_options( + xlabel="Beta", + ylabel="Signal (arb. units)", + ) default_options.result_parameters = ["beta"] - default_options.xlabel = "Beta" - default_options.ylabel = "Signal (arb. units)" default_options.fixed_parameters = {"reps0": 1, "reps1": 3, "reps2": 5} default_options.normalization = True diff --git a/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py b/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py index e8fdefc4f4..cd96d8d391 100644 --- a/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py @@ -95,10 +95,12 @@ def _default_options(cls): descriptions of analysis options. """ default_options = super()._default_options() + default_options.curve_plotter.set_options( + xlabel="Delay", + ylabel="Signal (arb. units)", + xval_unit="s", + ) default_options.result_parameters = ["freq"] - default_options.xlabel = "Delay" - default_options.xval_unit = "s" - default_options.ylabel = "Signal (arb. units)" return default_options diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 09d826b59c..5db564b6bf 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -50,7 +50,7 @@ def _run_analysis( if self.options.plot_iq_data: axis = get_non_gui_ax() figure = axis.get_figure() - figure.set_size_inches(*self.options.style.figsize) + figure.set_size_inches(*self.drawer.options.figsize) iqs = [] @@ -69,12 +69,12 @@ def _run_analysis( iqs = np.vstack(iqs) axis.scatter(iqs[:, 0], iqs[:, 1], color="b") axis.set_xlabel( - "In phase [arb. units]", fontsize=self.options.style.axis_label_size + "In phase [arb. units]", fontsize=self.drawer.options.axis_label_size ) axis.set_ylabel( - "Quadrature [arb. units]", fontsize=self.options.style.axis_label_size + "Quadrature [arb. units]", fontsize=self.drawer.options.axis_label_size ) - axis.tick_params(labelsize=self.options.style.tick_label_size) + axis.tick_params(labelsize=self.drawer.options.tick_label_size) axis.grid(True) figures.append(figure) diff --git a/qiskit_experiments/library/characterization/analysis/t1_analysis.py b/qiskit_experiments/library/characterization/analysis/t1_analysis.py index 0d44b0f869..a2cd46a1dd 100644 --- a/qiskit_experiments/library/characterization/analysis/t1_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t1_analysis.py @@ -15,7 +15,6 @@ from typing import Union import qiskit_experiments.curve_analysis as curve - from qiskit_experiments.framework import Options @@ -31,9 +30,11 @@ class T1Analysis(curve.DecayAnalysis): def _default_options(cls) -> Options: """Default analysis options.""" options = super()._default_options() - options.xlabel = "Delay" - options.ylabel = "P(1)" - options.xval_unit = "s" + options.curve_plotter.set_options( + xlabel="Delay", + ylabel="P(1)", + xval_unit="s", + ) options.result_parameters = [curve.ParameterRepr("tau", "T1", "s")] return options diff --git a/qiskit_experiments/library/characterization/analysis/t2hahn_analysis.py b/qiskit_experiments/library/characterization/analysis/t2hahn_analysis.py index d8dd5761b6..c769d88c67 100644 --- a/qiskit_experiments/library/characterization/analysis/t2hahn_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t2hahn_analysis.py @@ -34,6 +34,11 @@ class T2HahnAnalysis(curve.DecayAnalysis): def _default_options(cls) -> Options: """Default analysis options.""" options = super()._default_options() + options.curve_plotter.set_options( + xlabel="Delay", + ylabel="P(0)", + xval_unit="s", + ) options.data_processor = DataProcessor( input_key="counts", data_actions=[Probability(outcome="0")] ) @@ -42,9 +47,6 @@ def _default_options(cls) -> Options: "tau": (0.0, np.inf), "base": (0.0, 1.0), } - options.xlabel = "Delay" - options.ylabel = "P(0)" - options.xval_unit = "s" options.result_parameters = [curve.ParameterRepr("tau", "T2", "s")] return options diff --git a/qiskit_experiments/library/characterization/analysis/t2ramsey_analysis.py b/qiskit_experiments/library/characterization/analysis/t2ramsey_analysis.py index e024c1acb7..3add85dfd5 100644 --- a/qiskit_experiments/library/characterization/analysis/t2ramsey_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t2ramsey_analysis.py @@ -30,12 +30,14 @@ class T2RamseyAnalysis(curve.DumpedOscillationAnalysis): def _default_options(cls) -> Options: """Default analysis options.""" options = super()._default_options() + options.curve_plotter.set_options( + xlabel="Delay", + ylabel="P(0)", + xval_unit="s", + ) options.data_processor = DataProcessor( input_key="counts", data_actions=[Probability(outcome="0")] ) - options.xlabel = "Delay" - options.ylabel = "P(0)" - options.xval_unit = "s" options.result_parameters = [ curve.ParameterRepr("freq", "Frequency", "Hz"), curve.ParameterRepr("tau", "T2star", "s"), diff --git a/qiskit_experiments/library/characterization/rabi.py b/qiskit_experiments/library/characterization/rabi.py index b3f4a9a106..78314cd12c 100644 --- a/qiskit_experiments/library/characterization/rabi.py +++ b/qiskit_experiments/library/characterization/rabi.py @@ -57,6 +57,7 @@ class Rabi(BaseExperiment): """ __gate_name__ = "Rabi" + __outcome__ = "rabi_rate" @classmethod def _default_run_options(cls) -> Options: @@ -85,17 +86,6 @@ def _default_experiment_options(cls) -> Options: return options - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = Options() - options.result_parameters = [ParameterRepr("freq", "rabi_rate")] - options.xlabel = "Amplitude" - options.ylabel = "Signal (arb. units)" - options.normalization = True - - return options - def __init__( self, qubit: int, @@ -114,7 +104,15 @@ def __init__( backend: Optional, the backend to run the experiment on. """ super().__init__([qubit], analysis=OscillationAnalysis(), backend=backend) - self.analysis.set_options(**self._default_analysis_options().__dict__) + + self.analysis.set_options( + result_parameters=[ParameterRepr("freq", self.__outcome__)], + normalization=True, + ) + self.analysis.drawer.set_options( + xlabel="Amplitude", + ylabel="Signal (arb. units)", + ) if amplitudes is not None: self.experiment_options.amplitudes = amplitudes @@ -196,13 +194,7 @@ class EFRabi(Rabi): """ - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = super()._default_analysis_options() - options.result_parameters = [ParameterRepr("freq", "rabi_rate_12")] - - return options + __outcome__ = "rabi_rate_12" def _pre_circuit(self) -> QuantumCircuit: """A circuit with operations to perform before the Rabi.""" diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/library/randomized_benchmarking/rb_analysis.py index 775e90fdbc..d3253fbe65 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_analysis.py @@ -81,8 +81,11 @@ def _default_options(cls): """ default_options = super()._default_options() - default_options.xlabel = "Clifford Length" - default_options.ylabel = "P(0)" + default_options.curve_plotter.set_options( + xlabel="Clifford Length", + ylabel="P(0)", + ) + default_options.plot_raw_data = True default_options.result_parameters = ["alpha"] default_options.error_dict = None default_options.epg_1_qubit = None diff --git a/releasenotes/notes/curve-analysis-drawer-instance-bcfa18570915db2c.yaml b/releasenotes/notes/curve-analysis-drawer-instance-bcfa18570915db2c.yaml new file mode 100644 index 0000000000..3fca12536e --- /dev/null +++ b/releasenotes/notes/curve-analysis-drawer-instance-bcfa18570915db2c.yaml @@ -0,0 +1,23 @@ +--- +features: + - | + :class:`MplCurveDrawer` has been added for curve analysis visualization. + This class instance is JSON serializable with experiment encoder and + it implements public methods to draw analysis results in several different formats. + Its instance is attached to :class:`CurveAnalysis` instance as an analysis options ``curve_plotter``. + This class is a drop-in replacement of :class:`MplDrawSingleCanvas` and :class:`MplDrawMultiCanvasVstack`. + This instance has dedicated drawing options. + New option ``subplots``, which is a tuple of two integer representing ``(n_rows, n_cols)``, + defines arbitrary 2D array subplots without using :class:`MplDrawMultiCanvasVstack`. + - | + Drawing options are moved from :attr:`CurveAnalysis.options` to :attr:`MplCurveDrawer.options`. +deprecations: + - | + Conventional curve visualization classes :class:`MplDrawSingleCanvas`, + :class:`MplDrawMultiCanvasVstack` and the stylesheet :class:`PlotterStyle` have been deprecated + and now replaced with :class:`MplCurveDrawer`. + These classes had been attached to the analysis instance as a ``curve_plotter`` which is a string + and mapped to the class method ``.draw`` at runtime via :FitResultPlotters: Enum. + It was almost impossible to track the code and hurted the readability. + In addition, this implementation was problematic due to dependency on the + raw data points saved in an instance variable. See qiskit-experiments/#737 for details. diff --git a/test/base.py b/test/base.py index 25b1396b6e..5fc851d8f1 100644 --- a/test/base.py +++ b/test/base.py @@ -31,6 +31,7 @@ BaseExperiment, BaseAnalysis, ) +from qiskit_experiments.curve_analysis.visualization.base_drawer import BaseCurveDrawer class QiskitExperimentsTestCase(QiskitTestCase): @@ -106,7 +107,7 @@ def assertRoundTripPickle(self, obj: Any, check_func: Optional[Callable] = None) def json_equiv(cls, data1, data2) -> bool: """Check if two experiments are equivalent by comparing their configs""" # pylint: disable = too-many-return-statements - configrable_type = (BaseExperiment, BaseAnalysis) + configrable_type = (BaseExperiment, BaseAnalysis, BaseCurveDrawer) compare_repr = (DataAction, DataProcessor) list_type = (list, tuple, set) skipped = tuple()