diff --git a/docs/howtos/figure_generation.rst b/docs/howtos/figure_generation.rst new file mode 100644 index 0000000000..be346a1144 --- /dev/null +++ b/docs/howtos/figure_generation.rst @@ -0,0 +1,45 @@ +Control figure generation +========================= + +Problem +------- + +You want to change the default behavior where figures are generated with every experiment. + +Solution +-------- + +For a single non-composite experiment, figure generation can be switched off by setting the analysis +option ``plot`` to ``False``: + +.. jupyter-input:: + + experiment.analysis.set_options(plot = False) + +For composite experiments, there is a ``generate_figures`` analysis option which controls how child figures are +generated. There are three options: + +- ``always``: The default behavior, generate figures for each child experiment. +- ``never``: Never generate figures for any child experiment. +- ``selective``: Only generate figures for analysis results where ``quality`` is ``bad``. This is useful + for large composite experiments where you only want to examine qubits with problems. + +This parameter should be set on the analysis of a composite experiment before the analysis runs: + +.. jupyter-input:: + + parallel_exp = ParallelExperiment( + [T1(physical_qubits=(i,), delays=delays) for i in range(2)] + ) + parallel_exp.analysis.set_options(generate_figures="selective") + +Discussion +---------- + +These options are useful for large composite experiments, where generating all figures incurs a significant +overhead. + +See Also +-------- + +* The `Visualization tutorial `_ discusses how to customize figures diff --git a/docs/tutorials/getting_started.rst b/docs/tutorials/getting_started.rst index 4509de94a7..7fc3f9031a 100644 --- a/docs/tutorials/getting_started.rst +++ b/docs/tutorials/getting_started.rst @@ -238,6 +238,8 @@ supports can be set: exp.set_run_options(shots=1000, meas_level=MeasLevel.CLASSIFIED) + print(f"Shots set to {exp.run_options.get('shots')}, " + "measurement level set to {exp.run_options.get('meas_level')}") Consult the documentation of the run method of your specific backend type for valid options. @@ -253,6 +255,7 @@ before execution: exp.set_transpile_options(scheduling_method='asap', optimization_level=3, basis_gates=["x", "sx", "rz"]) + print(f"Transpile options are {exp.transpile_options}") Consult the documentation of :func:`qiskit.compiler.transpile` for valid options. @@ -267,6 +270,7 @@ upon experiment instantiation, but can also be explicitly set via exp = T1(physical_qubits=(0,), delays=delays) new_delays=np.arange(1e-6, 600e-6, 50e-6) exp.set_experiment_options(delays=new_delays) + print(f"Experiment options are {exp.experiment_options}") Consult the :doc:`API documentation ` for the options of each experiment class. @@ -274,7 +278,7 @@ class. Analysis options ---------------- -These options are unique to each analysis class. Unlike the other options, analyis +These options are unique to each analysis class. Unlike the other options, analysis options are not directly set via the experiment object but use instead a method of the associated ``analysis``: @@ -295,7 +299,7 @@ Running experiments on multiple qubits ====================================== To run experiments across many qubits of the same device, we use **composite -experiments**. A composite experiment is a parent object that contains one or more child +experiments**. A :class:`.CompositeExperiment` is a parent object that contains one or more child experiments, which may themselves be composite. There are two core types of composite experiments: @@ -323,7 +327,7 @@ Note that when the transpile and run options are set for a composite experiment, child experiments's options are also set to the same options recursively. Let's examine how the parallel experiment is constructed by visualizing child and parent circuits. The child experiments can be accessed via the -:meth:`~.ParallelExperiment.component_experiment` method, which indexes from zero: +:meth:`~.CompositeExperiment.component_experiment` method, which indexes from zero: .. jupyter-execute:: @@ -333,6 +337,16 @@ child experiments can be accessed via the parallel_exp.component_experiment(1).circuits()[0].draw(output='mpl') +Similarly, the child analyses can be accessed via :meth:`.CompositeAnalysis.component_analysis` or via +the analysis of the child experiment class: + +.. jupyter-execute:: + + parallel_exp.component_experiment(0).analysis.set_options(plot = True) + + # This should print out what we set because it's the same option + print(parallel_exp.analysis.component_analysis(0).options.get("plot")) + The circuits of all experiments assume they're acting on virtual qubits starting from index 0. In the case of a parallel experiment, the child experiment circuits are composed together and then reassigned virtual qubit indices: diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index 830792b0f8..cee8c15b7c 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -10,11 +10,14 @@ They're suitable for beginners who want to get started with the package. The Basics ---------- +.. This toctree is hardcoded since Getting Started is already included in the sidebar for more visibility. + .. toctree:: - :maxdepth: 2 + :maxdepth: 1 intro - + getting_started + Exploring Modules ----------------- diff --git a/qiskit_experiments/curve_analysis/base_curve_analysis.py b/qiskit_experiments/curve_analysis/base_curve_analysis.py index 1fed4abdba..51fd9d29b2 100644 --- a/qiskit_experiments/curve_analysis/base_curve_analysis.py +++ b/qiskit_experiments/curve_analysis/base_curve_analysis.py @@ -160,8 +160,8 @@ def _default_options(cls) -> Options: the analysis result. plot_raw_data (bool): Set ``True`` to draw processed data points, dataset without formatting, on canvas. This is ``False`` by default. - plot (bool): Set ``True`` to create figure for fit result. - This is ``True`` by default. + plot (bool): Set ``True`` to create figure for fit result or ``False`` to + not create a figure. This overrides the behavior of ``generate_figures``. return_fit_parameters (bool): Set ``True`` to return all fit model parameters with details of the fit outcome. Default to ``True``. return_data_points (bool): Set ``True`` to include in the analysis result @@ -213,7 +213,6 @@ def _default_options(cls) -> Options: options.plotter = CurvePlotter(MplDrawer()) options.plot_raw_data = False - options.plot = True options.return_fit_parameters = True options.return_data_points = False options.data_processor = None @@ -333,7 +332,7 @@ def _evaluate_quality( Returns: String that represents fit result quality. Usually "good" or "bad". """ - if fit_data.reduced_chisq < 3.0: + if 0 < fit_data.reduced_chisq < 3.0: return "good" return "bad" diff --git a/qiskit_experiments/curve_analysis/composite_curve_analysis.py b/qiskit_experiments/curve_analysis/composite_curve_analysis.py index de3898e316..6232eda210 100644 --- a/qiskit_experiments/curve_analysis/composite_curve_analysis.py +++ b/qiskit_experiments/curve_analysis/composite_curve_analysis.py @@ -332,6 +332,15 @@ def _run_analysis( experiment_data: ExperimentData, ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: + # Flag for plotting can be "always", "never", or "selective" + # the analysis option overrides self._generate_figures if set + if self.options.get("plot", None): + plot = "always" + elif self.options.get("plot", None) is False: + plot = "never" + else: + plot = getattr(self, "_generate_figures", "always") + analysis_results = [] figures = [] @@ -355,6 +364,10 @@ def _run_analysis( else: quality = "bad" + # After the quality is determined, plot can become a boolean flag for whether + # to generate the figure + plot_bool = plot == "always" or (plot == "selective" and quality == "bad") + if self.options.return_fit_parameters: # Store fit status overview entry regardless of success. # This is sometime useful when debugging the fitting code. @@ -429,7 +442,7 @@ def _run_analysis( else: composite_results = [] - if self.options.plot: + if plot_bool: self.plotter.set_supplementary_data( fit_red_chi={k: v.reduced_chisq for k, v in fit_dataset.items() if v.success}, primary_results=composite_results, diff --git a/qiskit_experiments/curve_analysis/curve_analysis.py b/qiskit_experiments/curve_analysis/curve_analysis.py index 4ddd06cdec..7fec75b0b4 100644 --- a/qiskit_experiments/curve_analysis/curve_analysis.py +++ b/qiskit_experiments/curve_analysis/curve_analysis.py @@ -496,6 +496,15 @@ def _run_analysis( analysis_results = [] figures = [] + # Flag for plotting can be "always", "never", or "selective" + # the analysis option overrides self._generate_figures if set + if self.options.get("plot", None): + plot = "always" + elif self.options.get("plot", None) is False: + plot = "never" + else: + plot = getattr(self, "_generate_figures", "always") + # Prepare for fitting self._initialize(experiment_data) @@ -507,6 +516,10 @@ def _run_analysis( else: quality = "bad" + # After the quality is determined, plot can become a boolean flag for whether + # to generate the figure + plot_bool = plot == "always" or (plot == "selective" and quality == "bad") + if self.options.return_fit_parameters: # Store fit status overview entry regardless of success. # This is sometime useful when debugging the fitting code. @@ -565,7 +578,7 @@ def _run_analysis( ) ) - if self.options.plot: + if plot_bool: if fit_data.success: self.plotter.set_supplementary_data( fit_red_chi=fit_data.reduced_chisq, diff --git a/qiskit_experiments/curve_analysis/standard_analysis/decay.py b/qiskit_experiments/curve_analysis/standard_analysis/decay.py index 11044afd28..4e6df069f1 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/decay.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/decay.py @@ -98,13 +98,13 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three + - a reduced chi-squared lower than three and greater than zero - tau error is less than its value """ tau = fit_data.ufloat_params["tau"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, curve.utils.is_error_not_significant(tau), ] 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 cc8ae54a1a..fa224bd6b8 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/error_amplification_analysis.py @@ -185,14 +185,14 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three, + - a reduced chi-squared lower than three and greater than zero, - a measured angle error that is smaller than the allowed maximum good angle error. This quantity is set in the analysis options. """ fit_d_theta = fit_data.ufloat_params["d_theta"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, abs(fit_d_theta.nominal_value) < abs(self.options.max_good_angle_error), ] diff --git a/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py b/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py index 13b22d3975..2d8cd973a8 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py @@ -126,7 +126,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared less than 3, + - a reduced chi-squared less than 3 and greater than zero, - a peak within the scanned frequency range, - a standard deviation that is not larger than the scanned frequency range, - a standard deviation that is wider than the smallest frequency increment, @@ -149,7 +149,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: fit_data.x_range[0] <= fit_freq.n <= fit_data.x_range[1], 1.5 * freq_increment < fit_sigma.n, fit_width_ratio < 0.25, - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, curve.utils.is_error_not_significant(fit_sigma), snr > 2, ] diff --git a/qiskit_experiments/curve_analysis/standard_analysis/oscillation.py b/qiskit_experiments/curve_analysis/standard_analysis/oscillation.py index 27d564cb37..f173563c0e 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/oscillation.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/oscillation.py @@ -111,7 +111,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three, + - a reduced chi-squared lower than three and greater than zero, - more than a quarter of a full period, - less than 10 full periods, and - an error on the fit frequency lower than the fit frequency. @@ -119,7 +119,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: fit_freq = fit_data.ufloat_params["freq"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, 1.0 / 4.0 < fit_freq.nominal_value < 10.0, curve.utils.is_error_not_significant(fit_freq), ] @@ -260,7 +260,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three + - a reduced chi-squared lower than three and greater than zero - relative error of tau is less than its value - relative error of freq is less than its value """ @@ -268,7 +268,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: freq = fit_data.ufloat_params["freq"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, curve.utils.is_error_not_significant(tau), curve.utils.is_error_not_significant(freq), ] diff --git a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py index 1c6b811038..375b4cc166 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py @@ -126,7 +126,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared less than 3, + - a reduced chi-squared less than 3 and greater than zero, - a peak within the scanned frequency range, - a standard deviation that is not larger than the scanned frequency range, - a standard deviation that is wider than the smallest frequency increment, @@ -149,7 +149,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: fit_data.x_range[0] <= fit_freq.n <= fit_data.x_range[1], 1.5 * freq_increment < fit_kappa.n, fit_width_ratio < 0.25, - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, curve.utils.is_error_not_significant(fit_kappa), snr > 2, ] diff --git a/qiskit_experiments/framework/__init__.py b/qiskit_experiments/framework/__init__.py index c9a480e09e..c6d9ccbae8 100644 --- a/qiskit_experiments/framework/__init__.py +++ b/qiskit_experiments/framework/__init__.py @@ -101,6 +101,7 @@ .. autosummary:: :toctree: ../stubs/ + CompositeExperiment ParallelExperiment BatchExperiment CompositeAnalysis @@ -143,6 +144,7 @@ from .composite import ( ParallelExperiment, BatchExperiment, + CompositeExperiment, CompositeAnalysis, ) from .json import ExperimentEncoder, ExperimentDecoder diff --git a/qiskit_experiments/framework/composite/__init__.py b/qiskit_experiments/framework/composite/__init__.py index d308f3f38c..de0df5604a 100644 --- a/qiskit_experiments/framework/composite/__init__.py +++ b/qiskit_experiments/framework/composite/__init__.py @@ -13,6 +13,7 @@ """Composite Experiments""" # Base classes +from .composite_experiment import CompositeExperiment from .composite_analysis import CompositeAnalysis # Composite experiment classes diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index 85e8baf0a0..e3dfa1fa91 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -52,7 +52,12 @@ class CompositeAnalysis(BaseAnalysis): experiment data. """ - def __init__(self, analyses: List[BaseAnalysis], flatten_results: bool = None): + def __init__( + self, + analyses: List[BaseAnalysis], + flatten_results: bool = None, + generate_figures: Optional[str] = "always", + ): """Initialize a composite analysis class. Args: @@ -62,6 +67,9 @@ def __init__(self, analyses: List[BaseAnalysis], flatten_results: bool = None): nested composite experiments. If False save each component experiment results as a separate child ExperimentData container. + generate_figures: Optional flag to set the figure generation behavior. + If ``always``, figures are always generated. If ``never``, figures are never generated. + If ``selective``, figures are generated if the analysis ``quality`` is ``bad``. """ if flatten_results is None: # Backward compatibility for 0.6 @@ -79,6 +87,8 @@ def __init__(self, analyses: List[BaseAnalysis], flatten_results: bool = None): if flatten_results: self._set_flatten_results() + self._set_generate_figures(generate_figures) + def component_analysis( self, index: Optional[int] = None ) -> Union[BaseAnalysis, List[BaseAnalysis]]: @@ -113,7 +123,7 @@ def run( experiment_data = experiment_data.copy() if not self._flatten_results: - # Initialize child components if they are not initalized + # Initialize child components if they are not initialized # This only needs to be done if results are not being flattened self._add_child_data(experiment_data) @@ -342,6 +352,15 @@ def _set_flatten_results(self): if isinstance(analysis, CompositeAnalysis): analysis._set_flatten_results() + def _set_generate_figures(self, generate_figures): + """Recursively propagate ``generate_figures`` to all child experiments.""" + self._generate_figures = generate_figures + for analysis in self._analyses: + if isinstance(analysis, CompositeAnalysis): + analysis._set_generate_figures(generate_figures) + else: + analysis._generate_figures = generate_figures + def _combine_results( self, component_experiment_data: List[ExperimentData], diff --git a/qiskit_experiments/library/characterization/analysis/drag_analysis.py b/qiskit_experiments/library/characterization/analysis/drag_analysis.py index 111179d081..b6c9915a4f 100644 --- a/qiskit_experiments/library/characterization/analysis/drag_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/drag_analysis.py @@ -204,7 +204,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three, + - a reduced chi-squared lower than three and greater than zero, - a DRAG parameter value within the first period of the lowest number of repetitions, - an error on the drag beta smaller than the beta. """ @@ -212,7 +212,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: fit_freq = fit_data.ufloat_params["freq"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, abs(fit_beta.nominal_value) < 1 / fit_freq.nominal_value / 2, curve.utils.is_error_not_significant(fit_beta), ] diff --git a/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py b/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py index 7e73afd8a6..cf8d74515b 100644 --- a/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py @@ -194,13 +194,13 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three, + - a reduced chi-squared lower than three and greater than zero, - an error on the frequency smaller than the frequency. """ fit_freq = fit_data.ufloat_params["freq"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, curve.utils.is_error_not_significant(fit_freq), ] diff --git a/qiskit_experiments/library/characterization/analysis/t1_analysis.py b/qiskit_experiments/library/characterization/analysis/t1_analysis.py index 835bd16246..856f346de8 100644 --- a/qiskit_experiments/library/characterization/analysis/t1_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t1_analysis.py @@ -47,7 +47,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three + - a reduced chi-squared lower than three and greater than zero - absolute amp is within [0.9, 1.1] - base is less than 0.1 - amp error is less than 0.1 @@ -59,7 +59,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: base = fit_data.ufloat_params["base"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, abs(amp.nominal_value - 1.0) < 0.1, abs(base.nominal_value) < 0.1, curve.utils.is_error_not_significant(amp, absolute=0.1), @@ -94,7 +94,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three + - a reduced chi-squared lower than three and greater than zero - absolute amp is within [0.9, 1.1] - base is less than 0.1 - amp error is less than 0.1 @@ -106,7 +106,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: base = fit_data.ufloat_params["base"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, abs(amp.nominal_value - 1.0) < 0.1, abs(base.nominal_value) < 0.1, curve.utils.is_error_not_significant(amp, absolute=0.1), diff --git a/qiskit_experiments/library/characterization/analysis/t2hahn_analysis.py b/qiskit_experiments/library/characterization/analysis/t2hahn_analysis.py index 62a1345dfa..5099cc030c 100644 --- a/qiskit_experiments/library/characterization/analysis/t2hahn_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t2hahn_analysis.py @@ -50,7 +50,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three + - a reduced chi-squared lower than three and greater than zero - absolute amp is within [0.4, 0.6] - base is less is within [0.4, 0.6] - amp error is less than 0.1 @@ -62,7 +62,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: base = fit_data.ufloat_params["base"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, abs(amp.nominal_value - 0.5) < 0.1, abs(base.nominal_value - 0.5) < 0.1, curve.utils.is_error_not_significant(amp, absolute=0.1), diff --git a/qiskit_experiments/library/characterization/analysis/t2ramsey_analysis.py b/qiskit_experiments/library/characterization/analysis/t2ramsey_analysis.py index db58b80669..8c7e1738d1 100644 --- a/qiskit_experiments/library/characterization/analysis/t2ramsey_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t2ramsey_analysis.py @@ -41,7 +41,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - - a reduced chi-squared lower than three + - a reduced chi-squared lower than three and greater than zero - relative error of amp is less than 10 percent - relative error of tau is less than 10 percent - relative error of freq is less than 10 percent @@ -51,7 +51,7 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: freq = fit_data.ufloat_params["freq"] criteria = [ - fit_data.reduced_chisq < 3, + 0 < fit_data.reduced_chisq < 3, curve.utils.is_error_not_significant(amp, fraction=0.1), curve.utils.is_error_not_significant(tau, fraction=0.1), curve.utils.is_error_not_significant(freq, fraction=0.1), diff --git a/releasenotes/notes/selective-figure-generation-0864216f34d3486f.yaml b/releasenotes/notes/selective-figure-generation-0864216f34d3486f.yaml new file mode 100644 index 0000000000..14ac846d53 --- /dev/null +++ b/releasenotes/notes/selective-figure-generation-0864216f34d3486f.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The ``generate_figures`` parameter has been added to :class:`.CompositeAnalysis` to control figure + generation. By default, ``generate_figures`` is ``always``, meaning figures will always be generated. + If ``generate_figures`` is set to ``selective``, then only figures for analysis results of bad + quality will be generated. If ``generate_figures`` is set to ``never``, then figures will never be + generated. This behavior can be overridden for individual analyses by setting the analysis option + ``plot`` for :class:`.CurveAnalysis`. diff --git a/test/curve_analysis/test_baseclass.py b/test/curve_analysis/test_baseclass.py index 2aec96f5bc..7025dbd60f 100644 --- a/test/curve_analysis/test_baseclass.py +++ b/test/curve_analysis/test_baseclass.py @@ -17,6 +17,7 @@ from test.fake_experiment import FakeExperiment import numpy as np +from ddt import data, ddt, unpack from lmfit.models import ExpressionModel from qiskit.qobj.utils import MeasLevel @@ -79,6 +80,7 @@ def parallel_sampler(x, y1, y2, shots=10000, seed=123, **metadata): return expdata +@ddt class TestCurveAnalysis(CurveAnalysisTestCase): """A collection of CurveAnalysis unit tests and integration tests.""" @@ -226,6 +228,7 @@ def test_end_to_end_single_function(self): self.assertAlmostEqual(result.analysis_results("amp").value.nominal_value, 0.5, delta=0.1) self.assertAlmostEqual(result.analysis_results("tau").value.nominal_value, 0.3, delta=0.1) + self.assertEqual(len(result._figures), 0) def test_end_to_end_multi_objective(self): """Integration test for multi objective function.""" @@ -395,15 +398,18 @@ def _initialize(self, experiment_data): self.assertAlmostEqual(result.analysis_results("amp").value.nominal_value, 0.5, delta=0.1) self.assertAlmostEqual(result.analysis_results("tau").value.nominal_value, 0.3, delta=0.1) - def test_end_to_end_parallel_analysis(self): - """Integration test for running two curve analyses in parallel.""" + @data((False, "always", 0), (True, "never", 2), (None, "always", 2), (None, "never", 0)) + @unpack + def test_end_to_end_parallel_analysis(self, plot_flag, figure_flag, n_figures): + """Integration test for running two curve analyses in parallel, including + selective figure generation.""" analysis1 = CurveAnalysis(models=[ExpressionModel(expr="amp * exp(-x/tau)", name="test")]) analysis1.set_options( data_processor=DataProcessor(input_key="counts", data_actions=[Probability("1")]), p0={"amp": 0.5, "tau": 0.3}, result_parameters=["amp", "tau"], - plot=False, + plot=plot_flag, ) analysis2 = CurveAnalysis(models=[ExpressionModel(expr="amp * exp(-x/tau)", name="test")]) @@ -411,10 +417,12 @@ def test_end_to_end_parallel_analysis(self): data_processor=DataProcessor(input_key="counts", data_actions=[Probability("1")]), p0={"amp": 0.7, "tau": 0.5}, result_parameters=["amp", "tau"], - plot=False, + plot=plot_flag, ) - composite = CompositeAnalysis([analysis1, analysis2], flatten_results=True) + composite = CompositeAnalysis( + [analysis1, analysis2], flatten_results=True, generate_figures=figure_flag + ) amp1 = 0.5 tau1 = 0.3 amp2 = 0.7 @@ -437,6 +445,47 @@ def test_end_to_end_parallel_analysis(self): self.assertAlmostEqual(taus[0].value.nominal_value, tau1, delta=0.1) self.assertAlmostEqual(taus[1].value.nominal_value, tau2, delta=0.1) + self.assertEqual(len(result._figures), n_figures) + + def test_selective_figure_generation(self): + """Test that selective figure generation based on quality works as expected.""" + + # analysis with intentionally bad fit + analysis1 = CurveAnalysis(models=[ExpressionModel(expr="amp * exp(-x)", name="test")]) + analysis1.set_options( + data_processor=DataProcessor(input_key="counts", data_actions=[Probability("1")]), + p0={"amp": 0.7}, + result_parameters=["amp"], + ) + analysis2 = CurveAnalysis(models=[ExpressionModel(expr="amp * exp(-x/tau)", name="test")]) + analysis2.set_options( + data_processor=DataProcessor(input_key="counts", data_actions=[Probability("1")]), + p0={"amp": 0.7, "tau": 0.5}, + result_parameters=["amp", "tau"], + ) + composite = CompositeAnalysis( + [analysis1, analysis2], flatten_results=False, generate_figures="selective" + ) + amp1 = 0.7 + tau1 = 0.5 + amp2 = 0.7 + tau2 = 0.5 + + x = np.linspace(0, 1, 100) + y1 = amp1 * np.exp(-x / tau1) + y2 = amp2 * np.exp(-x / tau2) + + test_data = self.parallel_sampler(x, y1, y2) + result = composite.run(test_data) + self.assertExperimentDone(result) + + for res in result.child_data(): + # only generate a figure if the quality is bad + if res.analysis_results(0).quality == "bad": + self.assertEqual(len(res._figures), 1) + else: + self.assertEqual(len(res._figures), 0) + def test_end_to_end_zero_yerr(self): """Integration test for an edge case of having zero y error. @@ -507,7 +556,9 @@ def test_get_init_params(self): y_reproduced = analysis.models[0].eval(x=x, **overview.init_params) np.testing.assert_array_almost_equal(y_ref, y_reproduced) - def test_multi_composite_curve_analysis(self): + @data((False, "never", 0), (True, "never", 1), (None, "never", 0), (None, "always", 1)) + @unpack + def test_multi_composite_curve_analysis(self, plot, gen_figures, n_figures): """Integration test for composite curve analysis. This analysis consists of two curve fittings for cos and sin series. @@ -545,7 +596,8 @@ def test_multi_composite_curve_analysis(self): group_analysis = CompositeCurveAnalysis(analyses) group_analysis.analyses("group_A").set_options(p0={"amp": 0.3, "freq": 2.1, "b": 0.5}) group_analysis.analyses("group_B").set_options(p0={"amp": 0.5, "freq": 3.2, "b": 0.5}) - group_analysis.set_options(plot=False) + group_analysis.set_options(plot=plot) + group_analysis._generate_figures = gen_figures amp1 = 0.2 amp2 = 0.4 @@ -583,6 +635,7 @@ def test_multi_composite_curve_analysis(self): self.assertEqual(amps[1].extra["group"], "group_B") self.assertAlmostEqual(amps[0].value.n, 0.2, delta=0.1) self.assertAlmostEqual(amps[1].value.n, 0.4, delta=0.1) + self.assertEqual(len(result._figures), n_figures) class TestFitOptions(QiskitExperimentsTestCase):