Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new plotting module for experiment and analysis figures #902

Merged
merged 49 commits into from
Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
45b04e6
Move curve visualization to elevated submodule
conradhaupt Sep 2, 2022
95a2be4
Split *CurveDrawer into *Drawer and *CurveDrawer
conradhaupt Sep 7, 2022
ad6c73c
Fix failing lint
conradhaupt Sep 8, 2022
6e64138
Revert "Split *CurveDrawer into *Drawer and *CurveDrawer"
conradhaupt Sep 9, 2022
fc22ff1
Move drawers to .visualization.drawers submodule
conradhaupt Sep 12, 2022
fb74410
Rename CurveDrawer to Drawer
conradhaupt Sep 12, 2022
451a6ca
Add plotters to visualization submodule
conradhaupt Sep 14, 2022
f63b2ce
Replace drawer usage with plotter
conradhaupt Sep 14, 2022
14ab82f
Add new PlotStyle class and move old PlotterStyle to fit_result_plott…
conradhaupt Sep 14, 2022
b45264f
Update plotter and drawers to split options into options and plot_opt…
conradhaupt Sep 15, 2022
6bb2142
Update analysis classes to use new plotter and drawer visualization m…
conradhaupt Sep 15, 2022
9f17386
Fix bugs in BasePlotter and CurveAnalysis test
conradhaupt Sep 15, 2022
c40594c
Add serialization test for plotter and nested drawer
conradhaupt Sep 16, 2022
9857079
Merge branch 'main' into cjh_visualization
conradhaupt Sep 16, 2022
d9658c4
Merge branch 'main' into cjh_visualization
conradhaupt Sep 19, 2022
1833052
Fix lint
conradhaupt Sep 19, 2022
f9d5ab4
Add original curve_analysis.visualization module for deprecation purp…
conradhaupt Sep 22, 2022
032a1c1
Merge branch 'main' into cjh_visualization
conradhaupt Sep 22, 2022
492fdce
Deprecate `curve_analysis.visualization`
conradhaupt Sep 26, 2022
f6b6774
Remove duplicate visualization code that will be deprecated
conradhaupt Sep 26, 2022
d506c5a
Edit docstrings from review feedback.
conradhaupt Sep 26, 2022
7101687
Add back deprecated option `curve_drawer`
conradhaupt Sep 26, 2022
94158f4
Refactor BaseDrawer to be more generic and less specific to CurveAnal…
conradhaupt Sep 26, 2022
9f4e1ee
Fix and expand tests
conradhaupt Sep 26, 2022
956696a
Rename plot-options to figure-options
conradhaupt Sep 26, 2022
ac1a984
Fix tests
conradhaupt Sep 26, 2022
7732065
Cleanup docstrings
conradhaupt Sep 26, 2022
8c588dc
Add description of options and figure_options for plotters and drawers
conradhaupt Sep 26, 2022
8035121
Plotter and drawer code cleanup
conradhaupt Sep 26, 2022
88d2784
Fix lint
conradhaupt Sep 26, 2022
99bc4a6
Fix tests failing because of deprecation warning in curve_analysis.vi…
conradhaupt Sep 27, 2022
28a8c80
Add compatibility wrapper for legacy BaseCurveDrawer to support depre…
conradhaupt Sep 27, 2022
544e4a2
Update BaseDrawer docstrings
conradhaupt Sep 27, 2022
288dcb6
Revert curve_analysis module docs to same as main, given visualizatio…
conradhaupt Sep 29, 2022
b604350
Update legend generation to use interface defined in #926
conradhaupt Sep 30, 2022
6bdc6ef
Update docstrings
conradhaupt Oct 3, 2022
7c3ca90
Refactor PlotStyle to inherit from dict
conradhaupt Oct 3, 2022
8ed2c9f
Revert namespace renaming of PlotStyle parameters
conradhaupt Oct 3, 2022
f1d1fc2
Rename figure_data to supplementary_data
conradhaupt Oct 3, 2022
9a909f5
Rename CurvePlotter data-keys to be more consistent
conradhaupt Oct 3, 2022
fdf1c23
Rename CurvePlotter data-keys to be more consistent, follow-up
conradhaupt Oct 3, 2022
84ef573
Refactor visualization classes to have clearer function names and doc…
conradhaupt Oct 3, 2022
42f949c
Fix deprecation warning with Matplotlib 3.6.0
conradhaupt Oct 3, 2022
3728969
Fix lint
conradhaupt Oct 3, 2022
6cccd6b
Fix failing tests resonator spectroscopy analysis
conradhaupt Oct 3, 2022
2f7c650
Merge branch 'main' into cjh_visualization
conradhaupt Oct 3, 2022
2448150
Add release notes
conradhaupt Oct 3, 2022
9184038
Rename drawer drawing methods
conradhaupt Oct 6, 2022
8f461d1
Fix lint
conradhaupt Oct 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions qiskit_experiments/curve_analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,8 @@ class AnalysisB(CurveAnalysis):
compute custom quantities based on the raw fit parameters.
See :ref:`curve_analysis_results` for details.
Afterwards, the analysis draws several curves in the Matplotlib figure.
User can set custom drawer to the option ``curve_drawer``.
The drawer defaults to the :class:`MplCurveDrawer`.
User can set custom plotter to the option ``plotter``.
eggerdj marked this conversation as resolved.
Show resolved Hide resolved
The plotter defaults to the :class:`CurvePlotter`.
Finally, it returns the list of created analysis results and Matplotlib figure.


Expand Down Expand Up @@ -481,15 +481,6 @@ def _create_analysis_results(self, fit_data, quality, **metadata):
ParameterRepr
FitOptions

Visualization
=============

.. autosummary::
:toctree: ../stubs/

BaseCurveDrawer
MplCurveDrawer

Standard Analysis Library
=========================

Expand Down Expand Up @@ -565,7 +556,6 @@ def _create_analysis_results(self, fit_data, quality, **metadata):
process_curve_data,
process_multi_curve_data,
)
from .visualization import BaseCurveDrawer, MplCurveDrawer
from . import guess
from . import fit_function
from . import utils
Expand All @@ -580,6 +570,3 @@ def _create_analysis_results(self, fit_data, quality, **metadata):
ErrorAmplificationAnalysis,
BlochTrajectoryAnalysis,
)

# deprecated
from .visualization import plot_curve_fit, plot_errorbar, plot_scatter, FitResultPlotters
14 changes: 7 additions & 7 deletions qiskit_experiments/curve_analysis/base_curve_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
from qiskit_experiments.data_processing import DataProcessor
from qiskit_experiments.data_processing.processor_library import get_processor
from qiskit_experiments.framework import BaseAnalysis, AnalysisResultData, Options, ExperimentData
from qiskit_experiments.visualization import MplDrawer, BasePlotter, CurvePlotter
from .curve_data import CurveData, ParameterRepr, CurveFitResult
from .visualization import MplCurveDrawer, BaseCurveDrawer

PARAMS_ENTRY_PREFIX = "@Parameters_"
DATA_ENTRY_PREFIX = "@Data_"
Expand Down Expand Up @@ -113,16 +113,16 @@ def models(self) -> List[lmfit.Model]:
"""Return fit models."""

@property
def drawer(self) -> BaseCurveDrawer:
"""A short-cut for curve drawer instance."""
return self._options.curve_drawer
def plotter(self) -> BasePlotter:
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
"""A short-cut for curve plotter instance."""
conradhaupt marked this conversation as resolved.
Show resolved Hide resolved
return self._options.plotter

@classmethod
def _default_options(cls) -> Options:
"""Return default analysis options.

Analysis Options:
curve_drawer (BaseCurveDrawer): A curve drawer instance to visualize
plotter (BasePlotter): A curve plotter instance to visualize
the analysis result.
plot_raw_data (bool): Set ``True`` to draw processed data points,
dataset without formatting, on canvas. This is ``False`` by default.
Expand Down Expand Up @@ -168,7 +168,7 @@ def _default_options(cls) -> Options:
"""
options = super()._default_options()

options.curve_drawer = MplCurveDrawer()
options.plotter = CurvePlotter(MplDrawer())
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
options.plot_raw_data = False
options.plot = True
options.return_fit_parameters = True
Expand All @@ -187,7 +187,7 @@ def _default_options(cls) -> Options:

# Set automatic validator for particular option values
options.set_validator(field="data_processor", validator_value=DataProcessor)
options.set_validator(field="curve_drawer", validator_value=BaseCurveDrawer)
options.set_validator(field="plotter", validator_value=BasePlotter)

return options

Expand Down
59 changes: 23 additions & 36 deletions qiskit_experiments/curve_analysis/composite_curve_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
from uncertainties import unumpy as unp, UFloat

from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options
from qiskit_experiments.visualization import MplDrawer, CurvePlotter, BasePlotter
from .base_curve_analysis import BaseCurveAnalysis, PARAMS_ENTRY_PREFIX
from .curve_data import CurveFitResult
from .utils import analysis_result_to_repr, eval_with_uncertainties
from .visualization import MplCurveDrawer, BaseCurveDrawer


class CompositeCurveAnalysis(BaseAnalysis):
Expand Down Expand Up @@ -124,9 +124,9 @@ def models(self) -> Dict[str, List[lmfit.Model]]:
return models

@property
def drawer(self) -> BaseCurveDrawer:
"""A short-cut for curve drawer instance."""
return self._options.curve_drawer
def plotter(self) -> BasePlotter:
"""A short-cut for plotter instance."""
conradhaupt marked this conversation as resolved.
Show resolved Hide resolved
return self._options.plotter

def analyses(
self, index: Optional[Union[str, int]] = None
Expand Down Expand Up @@ -187,7 +187,7 @@ def _default_options(cls) -> Options:
"""Default analysis options.

Analysis Options:
curve_drawer (BaseCurveDrawer): A curve drawer instance to visualize
plotter (BasePlotter): A plotter instance to visualize
the analysis result.
plot (bool): Set ``True`` to create figure for fit result.
This is ``True`` by default.
Expand All @@ -200,15 +200,15 @@ def _default_options(cls) -> Options:
"""
options = super()._default_options()
options.update_options(
curve_drawer=MplCurveDrawer(),
plotter=CurvePlotter(MplDrawer()),
plot=True,
return_fit_parameters=True,
return_data_points=False,
extra={},
)

# Set automatic validator for particular option values
options.set_validator(field="curve_drawer", validator_value=BaseCurveDrawer)
options.set_validator(field="plotter", validator_value=BasePlotter)

return options

Expand All @@ -232,10 +232,6 @@ def _run_analysis(

analysis_results = []

# Initialize canvas
if self.options.plot:
self.drawer.initialize_canvas()

fit_dataset = {}
for analysis in self._analyses:
analysis._initialize(experiment_data)
Expand All @@ -251,22 +247,22 @@ def _run_analysis(
if self.options.plot and analysis.options.plot_raw_data:
for model in analysis.models:
sub_data = processed_data.get_subset_of(model._name)
self.drawer.draw_raw_data(
x_data=sub_data.x,
y_data=sub_data.y,
name=model._name + f"_{analysis.name}",
self.plotter.set_series_data(
conradhaupt marked this conversation as resolved.
Show resolved Hide resolved
model._name + f"_{analysis.name}",
x=sub_data.x,
y=sub_data.y,
)

# Format data
formatted_data = analysis._format_data(processed_data)
if self.options.plot:
for model in analysis.models:
sub_data = formatted_data.get_subset_of(model._name)
self.drawer.draw_formatted_data(
x_data=sub_data.x,
y_data=sub_data.y,
y_err_data=sub_data.y_err,
name=model._name + f"_{analysis.name}",
self.plotter.set_series_data(
model._name + f"_{analysis.name}",
x_formatted=sub_data.x,
y_formatted=sub_data.y,
y_formatted_err=sub_data.y_err,
)

# Run fitting
Expand Down Expand Up @@ -310,23 +306,16 @@ def _run_analysis(
)
y_mean = unp.nominal_values(y_data_with_uncertainty)
# Draw fit line
eggerdj marked this conversation as resolved.
Show resolved Hide resolved
self.drawer.draw_fit_line(
x_data=interp_x,
y_data=y_mean,
name=model._name + f"_{analysis.name}",
self.plotter.set_series_data(
model._name + f"_{analysis.name}",
x_interp=interp_x,
y_mean=y_mean,
)
if fit_data.covar is not None:
# 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 * sigmas,
y_lb=y_mean - n_sigma * sigmas,
name=model._name + f"_{analysis.name}",
alpha=alpha,
)
self.plotter.set_series_data(model._name, sigmas=sigmas)
conradhaupt marked this conversation as resolved.
Show resolved Hide resolved

# Add raw data points
if self.options.return_data_points:
Expand Down Expand Up @@ -355,10 +344,8 @@ def _run_analysis(
for group, fit_data in fit_dataset.items():
chisqs.append(r"reduced-$\chi^2$ = " + f"{fit_data.reduced_chisq: .4g} ({group})")
report += "\n".join(chisqs)
self.drawer.draw_fit_report(description=report)
self.plotter.set_figure_data(report_text=report)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively can we delegate the generation of text data to CurvePlotter? This allows us to much flexibly generate report by subclassing the plotter. We can do in follow-up, but likely such change causes API break? For example, when we run analysis with many curves, the report will occupy huge portion of the canvas and we cannot see any curve. In this case we may want to drop chi-squared. I think we can set List[AnalysisResultData] to figure data and let the curve plotter generate default report with capability of hooking. This doesn't block merging, just a suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the idea of passing a list of AnalysisResultData instances to the plotter. To keep this PR from exploding, I think we should address this in a follow-up PR. But it's definitely something I think will be useful. I'll open an issue about this once this PR has been merged.


# Finalize canvas
self.drawer.format_canvas()
return analysis_results, [self.drawer.figure]
return analysis_results, [self.plotter.figure()]

return analysis_results, []
53 changes: 22 additions & 31 deletions qiskit_experiments/curve_analysis/curve_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def __init__(
)
# pylint: disable=no-member
models = []
plot_options = {}
series_params = {}
for series_def in self.__series__:
models.append(
lmfit.Model(
Expand All @@ -149,12 +149,12 @@ def __init__(
data_sort_key=series_def.filter_kwargs,
)
)
plot_options[series_def.name] = {
series_params[series_def.name] = {
"color": series_def.plot_color,
"symbol": series_def.plot_symbol,
"canvas": series_def.canvas,
}
self.drawer.set_options(plot_options=plot_options)
self.plotter.set_plot_options(series_params=series_params)

self._models = models or []
self._name = name or self.__class__.__name__
Expand Down Expand Up @@ -467,10 +467,6 @@ def _run_analysis(
self._initialize(experiment_data)
analysis_results = []

# Initialize canvas
if self.options.plot:
self.drawer.initialize_canvas()

# Run data processing
processed_data = self._run_data_processing(
raw_data=experiment_data.data(),
Expand All @@ -480,10 +476,10 @@ def _run_analysis(
if self.options.plot and self.options.plot_raw_data:
for model in self._models:
sub_data = processed_data.get_subset_of(model._name)
self.drawer.draw_raw_data(
x_data=sub_data.x,
y_data=sub_data.y,
name=model._name,
self.plotter.set_series_data(
model._name,
x=sub_data.x,
y=sub_data.y,
)
# for backward compatibility, will be removed in 0.4.
self.__processed_data_set["raw_data"] = processed_data
Expand All @@ -493,11 +489,11 @@ def _run_analysis(
if self.options.plot:
for model in self._models:
sub_data = formatted_data.get_subset_of(model._name)
self.drawer.draw_formatted_data(
x_data=sub_data.x,
y_data=sub_data.y,
y_err_data=sub_data.y_err,
name=model._name,
self.plotter.set_series_data(
model._name,
x_formatted=sub_data.x,
y_formatted=sub_data.y,
y_formatted_err=sub_data.y_err,
)
# for backward compatibility, will be removed in 0.4.
self.__processed_data_set["fit_ready"] = formatted_data
Expand Down Expand Up @@ -562,31 +558,27 @@ def _run_analysis(
)
y_mean = unp.nominal_values(y_data_with_uncertainty)
# Draw fit line
self.drawer.draw_fit_line(
x_data=interp_x,
y_data=y_mean,
name=model._name,
self.plotter.set_series_data(
model._name,
x_interp=interp_x,
y_mean=y_mean,
)
if fit_data.covar is not None:
# 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 * sigmas,
y_lb=y_mean - n_sigma * sigmas,
name=model._name,
alpha=alpha,
)
self.plotter.set_series_data(
model._name,
sigmas=sigmas,
)

# Write fitting report
report_description = ""
for res in analysis_results:
if isinstance(res.value, (float, UFloat)):
report_description += f"{analysis_result_to_repr(res)}\n"
report_description += r"reduced-$\chi^2$ = " + f"{fit_data.reduced_chisq: .4g}"
self.drawer.draw_fit_report(description=report_description)
self.plotter.set_figure_data(report_text=report_description)

# Add raw data points
if self.options.return_data_points:
Expand All @@ -596,8 +588,7 @@ def _run_analysis(

# Finalize plot
if self.options.plot:
self.drawer.format_canvas()
return analysis_results, [self.drawer.figure]
return analysis_results, [self.plotter.figure()]

return analysis_results, []

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def _default_options(cls):
input_key="counts",
data_actions=[dp.Probability("1"), dp.BasisExpectationValue()],
)
default_options.curve_drawer.set_options(
default_options.plotter.set_plot_options(
xlabel="Flat top width",
ylabel="Pauli expectation values",
xval_unit="s",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def _default_options(cls):
considered as good. Defaults to :math:`\pi/2`.
"""
default_options = super()._default_options()
default_options.curve_drawer.set_options(
default_options.plotter.set_plot_options(
xlabel="Number of gates (n)",
ylabel="Population",
ylim=(0, 1.0),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def __init__(
@classmethod
def _default_options(cls) -> Options:
options = super()._default_options()
options.curve_drawer.set_options(
options.plotter.set_plot_options(
xlabel="Frequency",
ylabel="Signal (arb. units)",
xval_unit="Hz",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def __init__(
@classmethod
def _default_options(cls) -> Options:
options = super()._default_options()
options.curve_drawer.set_options(
options.plotter.set_plot_options(
xlabel="Frequency",
ylabel="Signal (arb. units)",
xval_unit="Hz",
Expand Down
Loading