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 all 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
4 changes: 2 additions & 2 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`.
Users can set a custom plotter in :class:`CurveAnalysis` classes, to customize
figures, by setting the :attr:`~CurveAnalysis.plotter` attribute.
Finally, it returns the list of created analysis results and Matplotlib figure.
Expand Down
66 changes: 56 additions & 10 deletions qiskit_experiments/curve_analysis/base_curve_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,28 @@

import warnings
from abc import ABC, abstractmethod
from typing import List, Dict, Union
from typing import Dict, List, Union

import lmfit

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 .curve_data import CurveData, ParameterRepr, CurveFitResult
from .visualization import MplCurveDrawer, BaseCurveDrawer
from qiskit_experiments.framework import (
AnalysisResultData,
BaseAnalysis,
ExperimentData,
Options,
)
from qiskit_experiments.visualization import (
BaseDrawer,
BasePlotter,
CurvePlotter,
LegacyCurveCompatDrawer,
MplDrawer,
)
from qiskit_experiments.warnings import deprecated_function

from .curve_data import CurveData, CurveFitResult, ParameterRepr

PARAMS_ENTRY_PREFIX = "@Parameters_"
DATA_ENTRY_PREFIX = "@Data_"
Expand Down Expand Up @@ -113,16 +126,28 @@ 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 to the curve plotter instance."""
return self._options.plotter

@property
@deprecated_function(
last_version="0.6",
msg="Replaced by `plotter` from the new visualization submodule.",
)
def drawer(self) -> BaseDrawer:
"""A short-cut for curve drawer instance, if set. ``None`` otherwise."""
if isinstance(self.plotter.drawer, LegacyCurveCompatDrawer):
return self.plotter.drawer._curve_drawer
else:
return None

@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 +193,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 +212,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 All @@ -211,6 +236,27 @@ def set_options(self, **fields):
)
fields["lmfit_options"] = fields.pop("curve_fitter_options")

# TODO remove this in Qiskit Experiments 0.6
if "curve_drawer" in fields:
warnings.warn(
"The option 'curve_drawer' is replaced with 'plotter'. "
"This option will be removed in Qiskit Experiments 0.6.",
DeprecationWarning,
stacklevel=2,
)
# Set the plotter drawer to `curve_drawer`. If `curve_drawer` is the right type, set it
# directly. If not, wrap it in a compatibility drawer.
if isinstance(fields["curve_drawer"], BaseDrawer):
plotter = self.options.plotter
plotter.drawer = fields.pop("curve_drawer")
fields["plotter"] = plotter
else:
drawer = fields["curve_drawer"]
compat_drawer = LegacyCurveCompatDrawer(drawer)
plotter = self.options.plotter
plotter.drawer = compat_drawer
fields["plotter"] = plotter

super().set_options(**fields)

@abstractmethod
Expand Down
133 changes: 85 additions & 48 deletions qiskit_experiments/curve_analysis/composite_curve_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,31 @@
"""
# pylint: disable=invalid-name
import warnings
from typing import Dict, List, Tuple, Optional, Union
from typing import Dict, List, Optional, Tuple, Union

import lmfit
import numpy as np
from uncertainties import unumpy as unp, UFloat

from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options
from .base_curve_analysis import BaseCurveAnalysis, PARAMS_ENTRY_PREFIX
from uncertainties import UFloat
from uncertainties import unumpy as unp

from qiskit_experiments.framework import (
AnalysisResultData,
BaseAnalysis,
ExperimentData,
Options,
)
from qiskit_experiments.visualization import (
BaseDrawer,
BasePlotter,
CurvePlotter,
LegacyCurveCompatDrawer,
MplDrawer,
)
from qiskit_experiments.warnings import deprecated_function

from .base_curve_analysis import PARAMS_ENTRY_PREFIX, BaseCurveAnalysis
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 +138,21 @@ 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 to the plotter instance."""
return self._options.plotter

@property
@deprecated_function(
last_version="0.6",
msg="Replaced by `plotter` from the new visualization submodule.",
)
def drawer(self) -> BaseDrawer:
"""A short-cut for curve drawer instance, if set. ``None`` otherwise."""
if hasattr(self._options, "curve_drawer"):
return self._options.curve_drawer
else:
return None

def analyses(
self, index: Optional[Union[str, int]] = None
Expand Down Expand Up @@ -187,7 +213,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,19 +226,40 @@ 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

def set_options(self, **fields):
# TODO remove this in Qiskit Experiments 0.6
if "curve_drawer" in fields:
warnings.warn(
"The option 'curve_drawer' is replaced with 'plotter'. "
"This option will be removed in Qiskit Experiments 0.6.",
DeprecationWarning,
stacklevel=2,
)
# Set the plotter drawer to `curve_drawer`. If `curve_drawer` is the right type, set it
# directly. If not, wrap it in a compatibility drawer.
if isinstance(fields["curve_drawer"], BaseDrawer):
plotter = self.options.plotter
plotter.drawer = fields.pop("curve_drawer")
fields["plotter"] = plotter
else:
drawer = fields["curve_drawer"]
compat_drawer = LegacyCurveCompatDrawer(drawer)
plotter = self.options.plotter
plotter.drawer = compat_drawer
fields["plotter"] = plotter

for field in fields:
if not hasattr(self.options, field):
warnings.warn(
Expand All @@ -232,10 +279,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 +294,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 @@ -299,34 +342,30 @@ def _run_analysis(

# Draw fit result
if self.options.plot:
interp_x = np.linspace(
x_interp = np.linspace(
np.min(formatted_data.x), np.max(formatted_data.x), num=100
)
for model in analysis.models:
y_data_with_uncertainty = eval_with_uncertainties(
x=interp_x,
x=x_interp,
model=model,
params=fit_data.ufloat_params,
)
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 + f"_{analysis.name}",
y_interp = unp.nominal_values(y_data_with_uncertainty)
# Add fit line data
self.plotter.set_series_data(
model._name + f"_{analysis.name}",
x_interp=x_interp,
y_interp=y_interp,
)
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,
)
# Add confidence interval data
y_interp_err = unp.std_devs(y_data_with_uncertainty)
if np.isfinite(y_interp_err).all():
self.plotter.set_series_data(
model._name + f"_{analysis.name}",
y_interp_err=y_interp_err,
)

# Add raw data points
if self.options.return_data_points:
Expand Down Expand Up @@ -355,10 +394,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_supplementary_data(report_text=report)

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

return analysis_results, []
Loading