From 02a46b5876245e061e1b54021a6984b1ebe646c0 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Fri, 3 Dec 2021 13:15:44 -0500 Subject: [PATCH 1/7] Change BaseExperiment to be initialized with analysis class --- qiskit_experiments/framework/__init__.py | 3 - .../framework/base_experiment.py | 142 +++++++++++------- 2 files changed, 87 insertions(+), 58 deletions(-) diff --git a/qiskit_experiments/framework/__init__.py b/qiskit_experiments/framework/__init__.py index 3ec1b6913d..baace9f39e 100644 --- a/qiskit_experiments/framework/__init__.py +++ b/qiskit_experiments/framework/__init__.py @@ -151,9 +151,6 @@ Arguments in the constructor can be overridden so that a subclass can be initialized with some experiment configuration. -- Set :attr:`BaseExperiment.__analysis_class__` class attribute to - specify the :class:`BaseAnalysis` subclass for analyzing result data. - Optionally the following methods can also be overridden in the subclass to allow configuring various experiment and execution options diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index c5ffeeff73..92eb2a2da4 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -17,6 +17,7 @@ import copy from collections import OrderedDict from typing import Sequence, Optional, Tuple, List, Dict, Union, Any +import warnings from qiskit import transpile, assemble, QuantumCircuit from qiskit.providers import BaseJob @@ -26,26 +27,18 @@ from qiskit.qobj.utils import MeasLevel from qiskit.providers.options import Options from qiskit_experiments.framework.store_init_args import StoreInitArgs +from qiskit_experiments.framework.base_analysis import BaseAnalysis from qiskit_experiments.framework.experiment_data import ExperimentData from qiskit_experiments.framework.configs import ExperimentConfig class BaseExperiment(ABC, StoreInitArgs): - """Abstract base class for experiments. - - Class Attributes: - - __analysis_class__: Optional, the default Analysis class to use for - data analysis. If None no data analysis will be - done on experiment data (Default: None). - """ - - # Analysis class for experiment - __analysis_class__ = None + """Abstract base class for experiments.""" def __init__( self, qubits: Sequence[int], + analysis: Optional[BaseAnalysis] = None, backend: Optional[Backend] = None, experiment_type: Optional[str] = None, ): @@ -72,7 +65,6 @@ def __init__( self._experiment_options = self._default_experiment_options() self._transpile_options = self._default_transpile_options() self._run_options = self._default_run_options() - self._analysis_options = self._default_analysis_options() # Store keys of non-default options self._set_experiment_options = set() @@ -80,6 +72,22 @@ def __init__( self._set_run_options = set() self._set_analysis_options = set() + # Set analysis + self._analysis = None + if analysis: + self.analysis = analysis + # TODO: Hack for backwards compatibility with old base class. + # Remove after updating subclasses + elif hasattr(self, "__analysis_class__"): + warnings.warn( + "Defining a default BaseAnalysis class for an experiment using the " + "__analysis_class__ attribute is deprecated as of 0.2.0. " + "Use the `analysis` kwarg of BaseExperiment.__init__ " + "to specify a default analysis class." + ) + analysis_cls = getattr(self, "__analysis_class__") + self.analysis = analysis_cls() # pylint: disable = not-callable + # Set backend # This should be called last incase `_set_backend` access any of the # attributes created during initialization @@ -102,6 +110,18 @@ def num_qubits(self) -> int: """Return the number of qubits for the experiment.""" return self._num_qubits + @property + def analysis(self) -> Union[BaseAnalysis, None]: + """Return the analysis class for the experiment""" + return self._analysis + + @analysis.setter + def analysis(self, analysis: Union[BaseAnalysis, None]) -> None: + """Set the backend for the experiment""" + if not isinstance(analysis, BaseAnalysis): + raise TypeError("Input is not a BaseAnalysis subclass.") + self._analysis = analysis + @property def backend(self) -> Union[Backend, None]: """Return the backend for the experiment""" @@ -110,6 +130,8 @@ def backend(self) -> Union[Backend, None]: @backend.setter def backend(self, backend: Union[Backend, None]) -> None: """Set the backend for the experiment""" + if not isinstance(backend, (Backend, BaseBackend)): + raise TypeError("Input is not a backend.") self._set_backend(backend) def _set_backend(self, backend: Backend): @@ -126,15 +148,16 @@ def copy(self) -> "BaseExperiment": # need to also copy the Options structures so that if they are # updated on the copy they don't effect the original. ret = copy.copy(self) + if self._analysis: + ret._analysis = self._analysis.copy() + ret._experiment_options = copy.copy(self._experiment_options) ret._run_options = copy.copy(self._run_options) ret._transpile_options = copy.copy(self._transpile_options) - ret._analysis_options = copy.copy(self._analysis_options) ret._set_experiment_options = copy.copy(self._set_experiment_options) ret._set_transpile_options = copy.copy(self._set_transpile_options) ret._set_run_options = copy.copy(self._set_run_options) - ret._set_analysis_options = copy.copy(self._set_analysis_options) return ret def config(self) -> ExperimentConfig: @@ -222,8 +245,8 @@ def run( experiment._add_job_metadata(experiment_data.metadata, jobs, **run_opts) # Optionally run analysis - if analysis and self.__analysis_class__ is not None: - return self.run_analysis(experiment_data) + if analysis and self._analysis is not None: + return self.analysis.run(experiment_data) else: return experiment_data @@ -238,6 +261,11 @@ def run_analysis( See :meth:`BaseAnalysis.run` for additional information. + .. deprecated:: 0.2.0 + This is replaced by calling ``experiment.analysis.run`` using + the :meth:`analysis` property and + :meth:`~qiskit_experiments.framework.BaseAnalysis.run` method. + Args: experiment_data: the experiment data to analyze. replace_results: if True clear any existing analysis results and @@ -253,14 +281,13 @@ def run_analysis( Raises: QiskitError: if experiment_data container is not valid for analysis. """ - # Get analysis options - analysis_options = copy.copy(self.analysis_options) - analysis_options.update_options(**options) - analysis_options = analysis_options.__dict__ - - # Run analysis - analysis = self.analysis() - return analysis.run(experiment_data, replace_results=replace_results, **analysis_options) + warnings.warn( + "`BaseExperiment.run_analysis` is deprecated as of qiskit-experiments" + " 0.2.0 and will be removed in the 0.3.0 release." + " Use `experiment.analysis.run` instead", + DeprecationWarning, + ) + return self.analysis.run(experiment_data, replace_results=replace_results, **options) def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[BaseJob]: """Run circuits on backend as 1 or more jobs.""" @@ -286,14 +313,6 @@ def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[BaseJ jobs.append(job) return jobs - @classmethod - def analysis(cls): - """Return the default Analysis class for the experiment.""" - if cls.__analysis_class__ is None: - raise QiskitError(f"Experiment {cls.__name__} does not have a default Analysis class") - # pylint: disable = not-callable - return cls.__analysis_class__() - @abstractmethod def circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits. @@ -391,29 +410,41 @@ def set_run_options(self, **fields): self._run_options.update_options(**fields) self._set_run_options = self._set_run_options.union(fields) - @classmethod - def _default_analysis_options(cls) -> Options: - """Default options for analysis of experiment results.""" - # Experiment subclasses can override this method if they need - # to set specific analysis options defaults that are different - # from the Analysis subclass `_default_options` values. - if cls.__analysis_class__: - return cls.__analysis_class__._default_options() - return Options() - @property def analysis_options(self) -> Options: - """Return the analysis options for :meth:`run` analysis.""" - return self._analysis_options + """Return the analysis options for :meth:`run` analysis. + + .. deprecated:: 0.2.0 + This is replaced by calling ``experiment.analysis.options`` using + the :meth:`analysis`and :meth:`~qiskit_experiments.framework.BaseAnalysis.options` + properties. + """ + warnings.warn( + "`BaseExperiment.analysis_options` is deprecated as of qiskit-experiments" + " 0.2.0 and will be removed in the 0.3.0 release." + " Use `experiment.analysis.options instead", + DeprecationWarning, + ) + return self._analysis.options def set_analysis_options(self, **fields): """Set the analysis options for :meth:`run` method. Args: fields: The fields to update the options + + .. deprecated:: 0.2.0 + This is replaced by calling ``experiment.analysis.set_options`` using + the :meth:`analysis` property and + :meth:`~qiskit_experiments.framework.BaseAnalysis.set_options` method. """ - self._analysis_options.update_options(**fields) - self._set_analysis_options = self._set_analysis_options.union(fields) + warnings.warn( + "`BaseExperiment.set_analysis_options` is deprecated as of qiskit-experiments" + " 0.2.0 and will be removed in the 0.3.0 release." + " Use `experiment.analysis.set_options instead", + DeprecationWarning, + ) + self._analysis.options.update_options(**fields) def _postprocess_transpiled_circuits(self, circuits: List[QuantumCircuit], **run_options): """Additional post-processing of transpiled circuits before running on backend""" @@ -452,15 +483,16 @@ def _add_job_metadata(self, metadata: Dict[str, Any], jobs: BaseJob, **run_optio jobs: the job objects. run_options: backend run options for the job. """ - metadata["job_metadata"] = [ - { - "job_ids": [job.job_id() for job in jobs], - "experiment_options": copy.copy(self.experiment_options.__dict__), - "transpile_options": copy.copy(self.transpile_options.__dict__), - "analysis_options": copy.copy(self.analysis_options.__dict__), - "run_options": copy.copy(run_options), - } - ] + values = { + "job_ids": [job.job_id() for job in jobs], + "experiment_options": copy.copy(self.experiment_options.__dict__), + "transpile_options": copy.copy(self.transpile_options.__dict__), + "run_options": copy.copy(run_options), + } + if self._analysis is not None: + values["analysis_options"] = copy.copy(self.analysis.options.__dict__) + + metadata["job_metadata"] = [values] def __json_encode__(self): """Convert to format that can be JSON serialized""" From 61ece65c8293c0807d2a0f51f3e6ccafcfea5065 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Fri, 3 Dec 2021 13:15:51 -0500 Subject: [PATCH 2/7] Update exisiting experiments --- docs/tutorials/quantum_volume.ipynb | 2 +- docs/tutorials/randomized_benchmarking.ipynb | 2 +- docs/tutorials/state_tomography.ipynb | 2 +- .../tutorials/t2ramsey_characterization.ipynb | 4 +- .../base_calibration_experiment.py | 3 -- .../standard_analysis/resonance.py | 11 ++++ .../framework/composite/composite_analysis.py | 2 +- .../composite/composite_experiment.py | 9 ++-- .../library/calibration/fine_amplitude.py | 38 +++++++------- .../library/characterization/__init__.py | 2 - .../characterization/analysis/__init__.py | 1 - .../analysis/fine_drag_analysis.py | 12 +++++ .../analysis/fine_half_angle_analysis.py | 22 +++++++- .../analysis/fine_x_amplitude_analysis.py | 34 ------------- .../characterization/cr_hamiltonian.py | 4 +- .../library/characterization/drag.py | 7 ++- .../characterization/ef_spectroscopy.py | 13 ++--- .../characterization/fine_amplitude.py | 46 ++++++----------- .../library/characterization/fine_drag.py | 15 +----- .../library/characterization/half_angle.py | 23 +-------- .../characterization/qubit_spectroscopy.py | 23 ++------- .../library/characterization/rabi.py | 6 +-- .../library/characterization/ramsey_xy.py | 4 +- .../library/characterization/readout_angle.py | 4 +- .../library/characterization/t1.py | 4 +- .../library/characterization/t2ramsey.py | 3 +- .../library/quantum_volume/qv_experiment.py | 5 +- .../interleaved_rb_experiment.py | 4 +- .../randomized_benchmarking/rb_experiment.py | 7 +-- .../library/tomography/qpt_analysis.py | 22 +++++--- .../library/tomography/qpt_experiment.py | 50 +------------------ .../library/tomography/qst_analysis.py | 25 ++++++---- .../library/tomography/qst_experiment.py | 26 +--------- .../tomography/tomography_experiment.py | 6 +-- test/calibration/experiments/test_drag.py | 4 +- .../experiments/test_fine_amplitude.py | 12 ++--- test/calibration/experiments/test_rabi.py | 2 +- test/fake_experiment.py | 4 +- test/quantum_volume/test_qv.py | 10 ++-- .../rb_generate_data.py | 4 +- .../test_rb_analysis.py | 8 +-- test/test_composite.py | 8 +-- test/test_t1.py | 6 +-- test/test_t2ramsey.py | 8 +-- test/test_tomography.py | 4 +- 45 files changed, 182 insertions(+), 329 deletions(-) delete mode 100644 qiskit_experiments/library/characterization/analysis/fine_x_amplitude_analysis.py diff --git a/docs/tutorials/quantum_volume.ipynb b/docs/tutorials/quantum_volume.ipynb index 1f4db0e7fc..5417d344ae 100644 --- a/docs/tutorials/quantum_volume.ipynb +++ b/docs/tutorials/quantum_volume.ipynb @@ -223,7 +223,7 @@ "qv_exp.set_experiment_options(trials=60)\n", "expdata2 = qv_exp.run(backend, analysis=False).block_for_results()\n", "expdata2.add_data(expdata.data())\n", - "qv_exp.run_analysis(expdata2).block_for_results()\n", + "qv_exp.analysis.run(expdata2).block_for_results()\n", "\n", "# View result data\n", "display(expdata2.figure(0))\n", diff --git a/docs/tutorials/randomized_benchmarking.ipynb b/docs/tutorials/randomized_benchmarking.ipynb index 815fea4389..3f0c8c5270 100644 --- a/docs/tutorials/randomized_benchmarking.ipynb +++ b/docs/tutorials/randomized_benchmarking.ipynb @@ -181,7 +181,7 @@ "exp2 = StandardRB(qubits, lengths, num_samples=num_samples, seed=seed)\n", "\n", "# Use the EPG data of the 1-qubit runs to ensure correct 2-qubit EPG computation\n", - "exp2.set_analysis_options(epg_1_qubit=epg_1q)\n", + "exp2.analysis.set_options(epg_1_qubit=epg_1q)\n", "\n", "# Run the 2-qubit experiment\n", "expdata2 = exp2.run(backend).block_for_results()\n", diff --git a/docs/tutorials/state_tomography.ipynb b/docs/tutorials/state_tomography.ipynb index b069fb3453..4fcb8693c7 100644 --- a/docs/tutorials/state_tomography.ipynb +++ b/docs/tutorials/state_tomography.ipynb @@ -300,7 +300,7 @@ " import cvxpy\n", " \n", " # Set analysis option for cvxpy fitter\n", - " qstexp1.set_analysis_options(fitter='cvxpy_gaussian_lstsq')\n", + " qstexp1.analysis.set_options(fitter='cvxpy_gaussian_lstsq')\n", " \n", " # Re-run experiment\n", " qstdata2 = qstexp1.run(backend, seed_simulation=100).block_for_results()\n", diff --git a/docs/tutorials/t2ramsey_characterization.ipynb b/docs/tutorials/t2ramsey_characterization.ipynb index 1c104ef36d..942699137d 100644 --- a/docs/tutorials/t2ramsey_characterization.ipynb +++ b/docs/tutorials/t2ramsey_characterization.ipynb @@ -258,7 +258,7 @@ " \"B\": 0.5\n", " }\n", "exp_with_p0 = T2Ramsey(qubit, delays, unit=unit, osc_freq=1e5)\n", - "exp_with_p0.set_analysis_options(p0=user_p0)\n", + "exp_with_p0.analysis.set_options(p0=user_p0)\n", "expdata_with_p0 = exp_with_p0.run(backend=backend, shots=2000)\n", "expdata_with_p0.block_for_results()\n", "\n", @@ -380,7 +380,7 @@ " \"phi\": 0,\n", " \"B\": 0.5\n", "}\n", - "exp_in_ns.set_analysis_options(p0=user_p0_ns)\n", + "exp_in_ns.analysis.set_options(p0=user_p0_ns)\n", "\n", "# Run experiment\n", "expdata_in_ns = exp_in_ns.run(backend=backend_in_ns, shots=2000).block_for_results()\n", diff --git a/qiskit_experiments/calibration_management/base_calibration_experiment.py b/qiskit_experiments/calibration_management/base_calibration_experiment.py index 21a1e97ba3..5c081599fe 100644 --- a/qiskit_experiments/calibration_management/base_calibration_experiment.py +++ b/qiskit_experiments/calibration_management/base_calibration_experiment.py @@ -79,9 +79,6 @@ class should be this mixin and the second class should be the characterization :mod:`qiskit_experiments.calibration_management.update_library`. See also :class:`qiskit_experiments.calibration_management.update_library.BaseUpdater`. If no updater is specified the experiment will still run but no update of the calibrations will be performed. - - In addition to the calibration specific requirements, the developer must set the analysis method - with the class variable :code:`__analysis_class__` and any default experiment options. """ def __init_subclass__(cls, **kwargs): diff --git a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py index b6f651b65c..0fa3390225 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py @@ -17,6 +17,7 @@ import numpy as np import qiskit_experiments.curve_analysis as curve +from qiskit_experiments.framework import Options class ResonanceAnalysis(curve.CurveAnalysis): @@ -67,6 +68,16 @@ class ResonanceAnalysis(curve.CurveAnalysis): ) ] + @classmethod + def _default_options(cls) -> Options: + options = super()._default_options() + 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( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index fc4f53f929..ddf6671cb8 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -83,7 +83,7 @@ def _run_analysis(self, experiment_data: ExperimentData): # Run analysis # Since copy for replace result is handled at the parent level # we always run with replace result on component analysis - sub_exp.run_analysis(sub_exp_data, replace_results=True) + sub_exp.analysis.run(sub_exp_data, replace_results=True) # Record the component experiment id and type as an analysis result # for evidence analysis has started and to display in the service DB diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index f97be35f0b..34325eda4d 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -24,8 +24,6 @@ class CompositeExperiment(BaseExperiment): """Composite Experiment base class""" - __analysis_class__ = CompositeAnalysis - def __init__( self, experiments: List[BaseExperiment], @@ -43,7 +41,12 @@ def __init__( """ self._experiments = experiments self._num_experiments = len(experiments) - super().__init__(qubits, backend=backend, experiment_type=experiment_type) + super().__init__( + qubits, + analysis=CompositeAnalysis(), + backend=backend, + experiment_type=experiment_type, + ) @abstractmethod def circuits(self): diff --git a/qiskit_experiments/library/calibration/fine_amplitude.py b/qiskit_experiments/library/calibration/fine_amplitude.py index b4f9de537a..475527ad81 100644 --- a/qiskit_experiments/library/calibration/fine_amplitude.py +++ b/qiskit_experiments/library/calibration/fine_amplitude.py @@ -12,6 +12,7 @@ """Fine amplitude calibration experiment.""" +import functools from typing import Optional import numpy as np @@ -25,7 +26,6 @@ from qiskit_experiments.library.characterization import FineAmplitude from qiskit_experiments.framework import ExperimentData, Options from qiskit_experiments.calibration_management.update_library import BaseUpdater -from qiskit_experiments.library.characterization.analysis import FineXAmplitudeAnalysis class FineAmplitudeCal(BaseCalibrationExperiment, FineAmplitude): @@ -146,7 +146,14 @@ def update_calibrations(self, experiment_data: ExperimentData): class FineXAmplitudeCal(FineAmplitudeCal): """A calibration experiment to calibrate the amplitude of the X schedule.""" - __analysis_class__ = FineXAmplitudeAnalysis + @functools.wraps(FineAmplitudeCal.__init__) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.analysis.set_options( + angle_per_gate=np.pi, + phase_offset=np.pi / 2, + amp=1, + ) @classmethod def _default_experiment_options(cls) -> Options: @@ -179,20 +186,18 @@ def _default_transpile_options(cls): return options - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = super()._default_analysis_options() - options.angle_per_gate = np.pi - options.phase_offset = np.pi / 2 - options.amp = 1 - - return options - class FineSXAmplitudeCal(FineAmplitudeCal): """A calibration experiment to calibrate the amplitude of the SX schedule.""" + @functools.wraps(FineAmplitudeCal.__init__) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.analysis.set_options( + angle_per_gate=np.pi / 2, + phase_offset=np.pi, + ) + @classmethod def _default_experiment_options(cls) -> Options: r"""Default values for the fine amplitude experiment. @@ -229,12 +234,3 @@ def _default_transpile_options(cls): options.basis_gates = ["x", "sx"] return options - - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = super()._default_analysis_options() - options.angle_per_gate = np.pi / 2 - options.phase_offset = np.pi - - return options diff --git a/qiskit_experiments/library/characterization/__init__.py b/qiskit_experiments/library/characterization/__init__.py index 00680c5fa6..6735bb6706 100644 --- a/qiskit_experiments/library/characterization/__init__.py +++ b/qiskit_experiments/library/characterization/__init__.py @@ -56,7 +56,6 @@ analysis.FineHalfAngleAnalysis analysis.FineDragAnalysis analysis.FineAmplitudeAnalysis - analysis.FineXAmplitudeAnalysis analysis.RamseyXYAnalysis analysis.ReadoutAngleAnalysis """ @@ -66,7 +65,6 @@ FineHalfAngleAnalysis, FineDragAnalysis, FineAmplitudeAnalysis, - FineXAmplitudeAnalysis, RamseyXYAnalysis, T2RamseyAnalysis, T1Analysis, diff --git a/qiskit_experiments/library/characterization/analysis/__init__.py b/qiskit_experiments/library/characterization/analysis/__init__.py index 9d52c65770..e52a6ccaaa 100644 --- a/qiskit_experiments/library/characterization/analysis/__init__.py +++ b/qiskit_experiments/library/characterization/analysis/__init__.py @@ -14,7 +14,6 @@ from .drag_analysis import DragCalAnalysis from .fine_half_angle_analysis import FineHalfAngleAnalysis -from .fine_x_amplitude_analysis import FineXAmplitudeAnalysis from .fine_amplitude_analysis import FineAmplitudeAnalysis from .fine_drag_analysis import FineDragAnalysis from .remsey_xy_analysis import RamseyXYAnalysis diff --git a/qiskit_experiments/library/characterization/analysis/fine_drag_analysis.py b/qiskit_experiments/library/characterization/analysis/fine_drag_analysis.py index d9f5572d86..4b23620959 100644 --- a/qiskit_experiments/library/characterization/analysis/fine_drag_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/fine_drag_analysis.py @@ -12,7 +12,9 @@ """Fine DRAG calibration analysis.""" +import numpy as np from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis +from qiskit_experiments.framework import Options class FineDragAnalysis(ErrorAmplificationAnalysis): @@ -29,3 +31,13 @@ class FineDragAnalysis(ErrorAmplificationAnalysis): """ __fixed_parameters__ = ["angle_per_gate", "phase_offset", "amp"] + + @classmethod + def _default_options(cls) -> Options: + """Default analysis options.""" + options = super()._default_options() + options.normalization = True + options.angle_per_gate = 0.0 + options.phase_offset = np.pi / 2 + options.amp = 1.0 + return options diff --git a/qiskit_experiments/library/characterization/analysis/fine_half_angle_analysis.py b/qiskit_experiments/library/characterization/analysis/fine_half_angle_analysis.py index 36937ad62e..2f57511731 100644 --- a/qiskit_experiments/library/characterization/analysis/fine_half_angle_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/fine_half_angle_analysis.py @@ -12,7 +12,9 @@ """Fine half angle calibration analysis.""" -from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis +import numpy as np +from qiskit_experiments.framework import Options +from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis, ParameterRepr class FineHalfAngleAnalysis(ErrorAmplificationAnalysis): @@ -28,3 +30,21 @@ class FineHalfAngleAnalysis(ErrorAmplificationAnalysis): """ __fixed_parameters__ = ["angle_per_gate", "phase_offset", "amp"] + + @classmethod + def _default_options(cls) -> Options: + r"""Default analysis options. + + If the rotation error is very small the fit may chose a d_theta close to + :math:`\pm\pi`. To prevent this we impose bounds on d_theta. Note that the + options angle per gate, phase offset and amp are not intended to be changed. + """ + options = super()._default_options() + options.result_parameters = [ParameterRepr("d_theta", "d_hac", "rad")] + options.normalization = True + options.angle_per_gate = np.pi + options.phase_offset = -np.pi / 2 + options.amp = 1.0 + options.bounds.update({"d_theta": (-np.pi / 2, np.pi / 2)}) + + return options diff --git a/qiskit_experiments/library/characterization/analysis/fine_x_amplitude_analysis.py b/qiskit_experiments/library/characterization/analysis/fine_x_amplitude_analysis.py deleted file mode 100644 index 364a49a105..0000000000 --- a/qiskit_experiments/library/characterization/analysis/fine_x_amplitude_analysis.py +++ /dev/null @@ -1,34 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Fine Amplitude calibration analysis.""" - -from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis - - -class FineXAmplitudeAnalysis(ErrorAmplificationAnalysis): - r"""An analysis class for fine amplitude calibrations to define the fixed parameters. - - # section: note - - The following parameters are fixed. - - * :math:`{\rm apg}` The angle per gate is set by the user, for example pi for a pi-pulse. - * :math:`{\rm phase\_offset}` The phase offset in the cosine oscillation, for example, - :math:`\pi/2` if a square-root of X gate is added before the repeated gates. - * :math:`{\rm amp}` The amplitude of the oscillation. - """ - - # The intended angle per gat of the gate being calibrated, e.g. pi for a pi-pulse. - - # TODO remove amp from fixed parameter. - __fixed_parameters__ = ["angle_per_gate", "phase_offset", "amp"] diff --git a/qiskit_experiments/library/characterization/cr_hamiltonian.py b/qiskit_experiments/library/characterization/cr_hamiltonian.py index e0ba73b8e9..e5c2f7847b 100644 --- a/qiskit_experiments/library/characterization/cr_hamiltonian.py +++ b/qiskit_experiments/library/characterization/cr_hamiltonian.py @@ -120,8 +120,6 @@ class CrossResonanceHamiltonian(BaseExperiment): https://qiskit.org/textbook/ch-quantum-hardware/hamiltonian-tomography.html """ - __analysis_class__ = CrossResonanceHamiltonianAnalysis - # Number of CR pulses. The flat top duration per pulse is divided by this number. __n_cr_pulses__ = 1 @@ -148,7 +146,7 @@ def __init__( Raises: QiskitError: When ``qubits`` length is not 2. """ - super().__init__(qubits, backend=backend) + super().__init__(qubits, analysis=CrossResonanceHamiltonianAnalysis(), backend=backend) if len(qubits) != 2: raise QiskitError( diff --git a/qiskit_experiments/library/characterization/drag.py b/qiskit_experiments/library/characterization/drag.py index a7dce60792..d77fdf3923 100644 --- a/qiskit_experiments/library/characterization/drag.py +++ b/qiskit_experiments/library/characterization/drag.py @@ -71,8 +71,6 @@ class RoughDrag(BaseExperiment): """ - __analysis_class__ = DragCalAnalysis - @classmethod def _default_experiment_options(cls) -> Options: r"""Default values for the rough drag experiment. @@ -94,7 +92,7 @@ def _default_experiment_options(cls) -> Options: @classmethod def _default_analysis_options(cls) -> Options: """Default analysis options.""" - options = super()._default_analysis_options() + options = Options() options.normalization = True return options @@ -141,7 +139,8 @@ def __init__( QiskitError: if the schedule does not have a free parameter. """ - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=DragCalAnalysis(), backend=backend) + self.analysis.set_options(**self._default_analysis_options.__dict__) if betas is not None: self.set_experiment_options(betas=betas) diff --git a/qiskit_experiments/library/characterization/ef_spectroscopy.py b/qiskit_experiments/library/characterization/ef_spectroscopy.py index 16a8ee3cb8..52507968e7 100644 --- a/qiskit_experiments/library/characterization/ef_spectroscopy.py +++ b/qiskit_experiments/library/characterization/ef_spectroscopy.py @@ -12,12 +12,12 @@ """Spectroscopy for the e-f transition.""" +import functools from qiskit import QuantumCircuit from qiskit.circuit import Gate from qiskit_experiments.curve_analysis import ParameterRepr from qiskit_experiments.library.characterization.qubit_spectroscopy import QubitSpectroscopy -from qiskit_experiments.framework import Options class EFSpectroscopy(QubitSpectroscopy): @@ -35,13 +35,10 @@ class EFSpectroscopy(QubitSpectroscopy): """ - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = super()._default_analysis_options() - options.result_parameters = [ParameterRepr("freq", "f12", "Hz")] - - return options + @functools.wraps(QubitSpectroscopy.__init__) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.analysis.set_options(result_parameters=[ParameterRepr("freq", "f12", "Hz")]) def _template_circuit(self, freq_param) -> QuantumCircuit: """Return the template quantum circuit.""" diff --git a/qiskit_experiments/library/characterization/fine_amplitude.py b/qiskit_experiments/library/characterization/fine_amplitude.py index e9947fa795..4bcbe43229 100644 --- a/qiskit_experiments/library/characterization/fine_amplitude.py +++ b/qiskit_experiments/library/characterization/fine_amplitude.py @@ -20,10 +20,7 @@ from qiskit.circuit.library import XGate, SXGate from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment, Options -from qiskit_experiments.library.characterization.analysis import ( - FineAmplitudeAnalysis, - FineXAmplitudeAnalysis, -) +from qiskit_experiments.library.characterization.analysis import FineAmplitudeAnalysis from qiskit_experiments.exceptions import CalibrationError @@ -91,8 +88,6 @@ class FineAmplitude(BaseExperiment): """ - __analysis_class__ = FineAmplitudeAnalysis - @classmethod def _default_experiment_options(cls) -> Options: r"""Default values for the fine amplitude experiment. @@ -126,7 +121,7 @@ def __init__(self, qubit: int, gate: Gate, backend: Optional[Backend] = None): gate: The gate that will be repeated. backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=FineAmplitudeAnalysis(), backend=backend) self.set_experiment_options(gate=gate) def _pre_circuit(self) -> QuantumCircuit: @@ -161,8 +156,8 @@ def circuits(self) -> List[QuantumCircuit]: # because it will be treated as a half pulse instead of a full pulse. However, since # the qubit population is first-order insensitive to rotation errors for an xp pulse # this point won't contribute much to inferring the angle error. - angle_per_gate = self.analysis_options.get("angle_per_gate", None) - phase_offset = self.analysis_options.get("phase_offset") + angle_per_gate = self.analysis.options.get("angle_per_gate", None) + phase_offset = self.analysis.options.get("phase_offset") if angle_per_gate is None: raise CalibrationError( @@ -212,11 +207,15 @@ class FineXAmplitude(FineAmplitude): the appropriate values for the default options. """ - __analysis_class__ = FineXAmplitudeAnalysis - def __init__(self, qubit: int, backend: Optional[Backend] = None): """Initialize the experiment.""" super().__init__(qubit, XGate(), backend=backend) + # Set default analysis options + self.analysis.set_options( + angle_per_gate=np.pi, + phase_offset=np.pi / 2, + amp=1, + ) @classmethod def _default_experiment_options(cls) -> Options: @@ -234,17 +233,6 @@ def _default_experiment_options(cls) -> Options: options.gate = XGate() options.add_sx = True options.add_xp_circuit = True - - return options - - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = super()._default_analysis_options() - options.angle_per_gate = np.pi - options.phase_offset = np.pi / 2 - options.amp = 1.0 - return options @@ -260,6 +248,11 @@ class FineSXAmplitude(FineAmplitude): def __init__(self, qubit: int, backend: Optional[Backend] = None): """Initialize the experiment.""" super().__init__(qubit, SXGate(), backend=backend) + # Set default analysis options + self.analysis.set_options( + angle_per_gate=np.pi / 2, + phase_offset=np.pi, + ) @classmethod def _default_experiment_options(cls) -> Options: @@ -283,12 +276,3 @@ def _default_experiment_options(cls) -> Options: options.repetitions = [0, 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 21, 23, 25] return options - - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = super()._default_analysis_options() - options.angle_per_gate = np.pi / 2 - options.phase_offset = np.pi - - return options diff --git a/qiskit_experiments/library/characterization/fine_drag.py b/qiskit_experiments/library/characterization/fine_drag.py index 9d6b170782..12b4b338ae 100644 --- a/qiskit_experiments/library/characterization/fine_drag.py +++ b/qiskit_experiments/library/characterization/fine_drag.py @@ -133,8 +133,6 @@ class FineDrag(BaseExperiment): .. ref_arxiv:: 2 1011.1949 """ - __analysis_class__ = FineDragAnalysis - @classmethod def _default_experiment_options(cls) -> Options: r"""Default values for the fine amplitude experiment. @@ -152,17 +150,6 @@ def _default_experiment_options(cls) -> Options: return options - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = super()._default_analysis_options() - options.normalization = True - options.angle_per_gate = 0.0 - options.phase_offset = np.pi / 2 - options.amp = 1.0 - - return options - def __init__(self, qubit: int, gate: Gate, backend: Optional[Backend] = None): """Setup a fine amplitude experiment on the given qubit. @@ -171,7 +158,7 @@ def __init__(self, qubit: int, gate: Gate, backend: Optional[Backend] = None): gate: The gate that will be repeated. backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=FineDragAnalysis(), backend=backend) self.set_experiment_options(gate=gate) @staticmethod diff --git a/qiskit_experiments/library/characterization/half_angle.py b/qiskit_experiments/library/characterization/half_angle.py index 288bed9714..98474e015a 100644 --- a/qiskit_experiments/library/characterization/half_angle.py +++ b/qiskit_experiments/library/characterization/half_angle.py @@ -20,7 +20,6 @@ from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.library.characterization.analysis import FineHalfAngleAnalysis -from qiskit_experiments.curve_analysis import ParameterRepr class HalfAngle(BaseExperiment): @@ -51,8 +50,6 @@ class HalfAngle(BaseExperiment): .. ref_arxiv:: 1 1504.06597 """ - __analysis_class__ = FineHalfAngleAnalysis - @classmethod def _default_experiment_options(cls) -> Options: r"""Default values for the half angle experiment. @@ -77,24 +74,6 @@ def _default_transpile_options(cls) -> Options: options.inst_map = None return options - @classmethod - def _default_analysis_options(cls) -> Options: - r"""Default analysis options. - - If the rotation error is very small the fit may chose a d_theta close to - :math:`\pm\pi`. To prevent this we impose bounds on d_theta. Note that the - options angle per gate, phase offset and amp are not intended to be changed. - """ - options = super()._default_analysis_options() - options.result_parameters = [ParameterRepr("d_theta", "d_hac", "rad")] - options.normalization = True - options.angle_per_gate = np.pi - options.phase_offset = -np.pi / 2 - options.amp = 1.0 - options.bounds.update({"d_theta": (-np.pi / 2, np.pi / 2)}) - - return options - def __init__(self, qubit: int, backend: Optional[Backend] = None): """Setup a half angle experiment on the given qubit. @@ -102,7 +81,7 @@ def __init__(self, qubit: int, backend: Optional[Backend] = None): qubit: The qubit on which to run the fine amplitude calibration experiment. backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=FineHalfAngleAnalysis(), backend=backend) @staticmethod def _pre_circuit() -> QuantumCircuit: diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index 09314d0131..750d337940 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -24,7 +24,7 @@ from qiskit.utils import apply_prefix from qiskit_experiments.framework import BaseExperiment, Options -from qiskit_experiments.curve_analysis import ParameterRepr, ResonanceAnalysis +from qiskit_experiments.curve_analysis import ResonanceAnalysis class QubitSpectroscopy(BaseExperiment): @@ -45,7 +45,6 @@ class QubitSpectroscopy(BaseExperiment): pulse. A list of circuits is generated, each with a different frequency "freq". """ - __analysis_class__ = ResonanceAnalysis __spec_gate_name__ = "Spec" @classmethod @@ -79,18 +78,6 @@ def _default_experiment_options(cls) -> Options: return options - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options.""" - options = super()._default_analysis_options() - options.result_parameters = [ParameterRepr("freq", "f01", "Hz")] - options.normalization = True - options.xlabel = "Frequency" - options.ylabel = "Signal (arb. units)" - options.xval_unit = "Hz" - - return options - def __init__( self, qubit: int, @@ -121,7 +108,7 @@ def __init__( QiskitError: if there are less than three frequency shifts or if the unit is not known. """ - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=ResonanceAnalysis(), backend=backend) if len(frequencies) < 3: raise QiskitError("Spectroscopy requires at least three frequencies.") @@ -134,11 +121,11 @@ def __init__( self._absolute = absolute if not self._absolute: - self.set_analysis_options(xlabel="Frequency shift") + self.analysis.set_options(xlabel="Frequency shift") else: - self.set_analysis_options(xlabel="Frequency") + self.analysis.set_options(xlabel="Frequency") - self.set_analysis_options(ylabel="Signal [arb. unit]") + self.analysis.set_options(ylabel="Signal [arb. unit]") def _spec_gate_schedule( self, backend: Optional[Backend] = None diff --git a/qiskit_experiments/library/characterization/rabi.py b/qiskit_experiments/library/characterization/rabi.py index 39acc029aa..430b4151b8 100644 --- a/qiskit_experiments/library/characterization/rabi.py +++ b/qiskit_experiments/library/characterization/rabi.py @@ -54,7 +54,6 @@ class Rabi(BaseExperiment): """ - __analysis_class__ = OscillationAnalysis __gate_name__ = "Rabi" @classmethod @@ -87,7 +86,7 @@ def _default_experiment_options(cls) -> Options: @classmethod def _default_analysis_options(cls) -> Options: """Default analysis options.""" - options = super()._default_analysis_options() + options = Options() options.result_parameters = [ParameterRepr("freq", "rabi_rate")] options.xlabel = "Amplitude" options.ylabel = "Signal (arb. units)" @@ -112,7 +111,8 @@ def __init__( specified it will default to :code:`np.linspace(-0.95, 0.95, 51)`. backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=OscillationAnalysis(), backend=backend) + self.analysis.set_options(**self._default_analysis_options().__dict__) if amplitudes is not None: self.experiment_options.amplitudes = amplitudes diff --git a/qiskit_experiments/library/characterization/ramsey_xy.py b/qiskit_experiments/library/characterization/ramsey_xy.py index 0a0d5b820d..52777012f1 100644 --- a/qiskit_experiments/library/characterization/ramsey_xy.py +++ b/qiskit_experiments/library/characterization/ramsey_xy.py @@ -80,8 +80,6 @@ class RamseyXY(BaseExperiment): circuit above it appears as the delay-dependent angle θ(τ). """ - __analysis_class__ = RamseyXYAnalysis - @classmethod def _default_experiment_options(cls): """Default values for the Ramsey XY experiment. @@ -119,7 +117,7 @@ def __init__( osc_freq: the oscillation frequency induced by the user through a virtual Rz rotation. This quantity is given in Hz. """ - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=RamseyXYAnalysis(), backend=backend) delays = delays or self.experiment_options.delays self.set_experiment_options(delays=delays, unit=unit, osc_freq=osc_freq) diff --git a/qiskit_experiments/library/characterization/readout_angle.py b/qiskit_experiments/library/characterization/readout_angle.py index 25a694110e..ae2a4f0c1f 100644 --- a/qiskit_experiments/library/characterization/readout_angle.py +++ b/qiskit_experiments/library/characterization/readout_angle.py @@ -50,8 +50,6 @@ class ReadoutAngle(BaseExperiment): """ - __analysis_class__ = ReadoutAngleAnalysis - @classmethod def _default_run_options(cls) -> Options: """Default run options.""" @@ -75,7 +73,7 @@ def __init__( backend: Optional, the backend to run the experiment on. """ # Initialize base experiment - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=ReadoutAngleAnalysis(), backend=backend) def circuits(self) -> List[QuantumCircuit]: """ diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 3ff24d35c6..87dd6a57c3 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -46,8 +46,6 @@ class T1(BaseExperiment): """ - __analysis_class__ = T1Analysis - @classmethod def _default_experiment_options(cls) -> Options: """Default experiment options. @@ -86,7 +84,7 @@ def __init__( ValueError: if the number of delays is smaller than 3 """ # Initialize base experiment - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=T1Analysis(), backend=backend) # Set experiment options self.set_experiment_options(delays=delays, unit=unit) diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index c86846259b..73666c4668 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -58,7 +58,6 @@ class T2Ramsey(BaseExperiment): :doc:`/tutorials/t2ramsey_characterization` """ - __analysis_class__ = T2RamseyAnalysis @classmethod def _default_experiment_options(cls) -> Options: @@ -101,7 +100,7 @@ def __init__( The frequency is given in Hz. """ - super().__init__([qubit], backend=backend) + super().__init__([qubit], analysis=T2RamseyAnalysis(), backend=backend) self.set_experiment_options(delays=delays, unit=unit, osc_freq=osc_freq) def _set_backend(self, backend: Backend): diff --git a/qiskit_experiments/library/quantum_volume/qv_experiment.py b/qiskit_experiments/library/quantum_volume/qv_experiment.py index dc14a32336..d115a18a2c 100644 --- a/qiskit_experiments/library/quantum_volume/qv_experiment.py +++ b/qiskit_experiments/library/quantum_volume/qv_experiment.py @@ -67,9 +67,6 @@ class QuantumVolume(BaseExperiment): """ - # Analysis class for experiment - __analysis_class__ = QuantumVolumeAnalysis - def __init__( self, qubits: Sequence[int], @@ -92,7 +89,7 @@ def __init__( (in case :class:`AerSimulator` is not installed :class:`qiskit.quantum_info.Statevector` will be used). """ - super().__init__(qubits, backend=backend) + super().__init__(qubits, analysis=QuantumVolumeAnalysis(), backend=backend) # Set configurable options self.set_experiment_options(trials=trials) diff --git a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py index ac7ec741c9..1d101c6001 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -44,9 +44,6 @@ class InterleavedRB(StandardRB): """ - # Analysis class for experiment - __analysis_class__ = InterleavedRBAnalysis - def __init__( self, interleaved_element: Union[QuantumCircuit, Instruction, Clifford], @@ -83,6 +80,7 @@ def __init__( seed=seed, full_sampling=full_sampling, ) + self.analysis = InterleavedRBAnalysis() def _sample_circuits(self, lengths, seed=None): circuits = [] diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index 40dee19a51..5ec37cc996 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -56,9 +56,6 @@ class StandardRB(BaseExperiment): """ - # Analysis class for experiment - __analysis_class__ = RBAnalysis - def __init__( self, qubits: Sequence[int], @@ -84,12 +81,12 @@ def __init__( The default is False. """ # Initialize base experiment - super().__init__(qubits, backend=backend) + super().__init__(qubits, analysis=RBAnalysis(), backend=backend) self._verify_parameters(lengths, num_samples) # Set configurable options self.set_experiment_options(lengths=list(lengths), num_samples=num_samples) - self.set_analysis_options( + self.analysis.set_options( data_processor=dp.DataProcessor( input_key="counts", data_actions=[dp.Probability(outcome="0" * self.num_qubits)], diff --git a/qiskit_experiments/library/tomography/qpt_analysis.py b/qiskit_experiments/library/tomography/qpt_analysis.py index 9fe379f29d..08bffeee77 100644 --- a/qiskit_experiments/library/tomography/qpt_analysis.py +++ b/qiskit_experiments/library/tomography/qpt_analysis.py @@ -69,21 +69,27 @@ def _default_options(cls) -> Options: """Default analysis options Analysis Options: - target (Union[str, :class:`~qiskit.quantum_info..operators.channel.quantum_channel`, + measurement_basis (:class:`~basis.BaseFitterMeasurementBasis`): A custom measurement + basis for analysis. By default the :meth:`experiment_options` measurement basis + will be used. + preparation_basis (:class:`~basis.BaseFitterPreparationBasis`): A custom preparation + basis for analysis. By default the :meth:`experiment_options` preparation basis + will be used. + fitter (str or Callable): The fitter function to use for reconstruction. + rescale_psd (bool): If True rescale the fitted state to be positive-semidefinite + (Default: True). + fitter_options (Dict[str, Any]): Additional kwargs will be supplied to the + fitter function. + rescale_trace (bool): If True rescale the state returned by the fitter have either + trace 1 (Default: True). + target (Union[str, :class:`~qiskit.quantum_info.operators.channel.quantum_channel`, :class:`~qiskit.quantum_info.Operator`]): Set a custom target quantum channel for computing the :func:~qiskit.quantum_info.process_fidelity` of the fitted process against. If ``"default"`` the ideal process corresponding for the input circuit will be used. If ``None`` no fidelity will be computed (Default: "default"). - """ options = super()._default_options() - options.measurement_basis = PauliMeasurementBasis() options.preparation_basis = PauliPreparationBasis() - options.fitter = "linear_inversion" - options.rescale_positive = True - options.rescale_trace = True - options.target = "default" - return options diff --git a/qiskit_experiments/library/tomography/qpt_experiment.py b/qiskit_experiments/library/tomography/qpt_experiment.py index 91733f7c78..d228679943 100644 --- a/qiskit_experiments/library/tomography/qpt_experiment.py +++ b/qiskit_experiments/library/tomography/qpt_experiment.py @@ -16,7 +16,6 @@ from typing import Union, Optional, Iterable, List, Tuple, Sequence from qiskit.circuit import QuantumCircuit, Instruction from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit_experiments.framework import Options from .tomography_experiment import TomographyExperiment from .qpt_analysis import ProcessTomographyAnalysis from . import basis @@ -44,54 +43,6 @@ class ProcessTomography(TomographyExperiment): """ - __analysis_class__ = ProcessTomographyAnalysis - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options. - - Experiment Options: - measurement_basis (:class:`~basis.BaseTomographyMeasurementBasis`): The - Tomography measurement basis to use for the experiment. - The default basis is the :class:`~basis.PauliMeasurementBasis` which - performs measurements in the Pauli Z, X, Y bases for each qubit - measurement. - preparation_basis (:class:`~basis.BaseTomographyPreparationBasis`): - The Tomography measurement basis to use for the experiment. - The default basis is the :class:`~basis.PauliPreparationBasis` which - prepares the :math:`|0\\rangle, |1\\rangle, |+\\rangle |+i\\rangle` - states on each prepared qubit. - """ - options = super()._default_experiment_options() - - return options - - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options. - - Analysis Options: - measurement_basis (:class:`~basis.BaseFitterMeasurementBasis`): A custom measurement - basis for analysis. By default the :meth:`experiment_options` measurement basis - will be used. - preparation_basis (:class:`~basis.BaseFitterPreparationBasis`): A custom preparation - basis for analysis. By default the :meth:`experiment_options` preparation basis - will be used. - fitter (str or Callable): The fitter function to use for reconstruction. - rescale_psd (bool): If True rescale the fitted state to be positive-semidefinite - (Default: True). - rescale_trace (bool): If True rescale the state returned by the fitter have either - trace 1 (Default: True). - kwargs: Additional kwargs will be supplied to the fitter function. - - """ - options = super()._default_analysis_options() - - options.measurement_basis = basis.PauliMeasurementBasis() - options.preparation_basis = basis.PauliPreparationBasis() - - return options - def __init__( self, circuit: Union[QuantumCircuit, Instruction, BaseOperator], @@ -135,3 +86,4 @@ def __init__( basis_indices=basis_indices, qubits=qubits, ) + self.analysis = ProcessTomographyAnalysis() diff --git a/qiskit_experiments/library/tomography/qst_analysis.py b/qiskit_experiments/library/tomography/qst_analysis.py index 3bc105e28a..64cc0136a7 100644 --- a/qiskit_experiments/library/tomography/qst_analysis.py +++ b/qiskit_experiments/library/tomography/qst_analysis.py @@ -68,18 +68,23 @@ def _default_options(cls) -> Options: """Default analysis options Analysis Options: + measurement_basis (:class:`~basis.BaseFitterMeasurementBasis`): A custom measurement + basis for analysis. By default the :meth:`experiment_options` measurement basis + will be used. + fitter (str or Callable): The fitter function to use for reconstruction. + rescale_psd (bool): If True rescale the fitted state to be positive-semidefinite + (Default: True). + fitter_options (Dict[str, Any]): Additional kwargs will be supplied to the + fitter function. + rescale_trace (bool): If True rescale the state returned by the fitter have either + trace 1 (Default: True). target (Union[str, :class:`~qiskit.quantum_info.DensityMatrix`, - :class:`~qiskit.quantum_info.StateVector`]): Set a custom target state - for computing the :func:`~qiskit.quantum_info.state_fidelity` of the fitted - state against. If ``"default"`` the ideal state prepared by the input circuit - will be used. If ``None`` no fidelity will be computed (Default: "default"). + :class:`~qiskit.quantum_info.Statevector`]): Set a custom target quantum + state for computing the :func:~qiskit.quantum_info.state_fidelity` of the + fitted state against. If ``"default"`` the ideal state corresponding for + the input circuit will be used. If ``None`` no fidelity will be computed + (Default: "default"). """ options = super()._default_options() - options.measurement_basis = PauliMeasurementBasis() - options.fitter = "linear_inversion" - options.rescale_positive = True - options.rescale_trace = True - options.target = "default" - return options diff --git a/qiskit_experiments/library/tomography/qst_experiment.py b/qiskit_experiments/library/tomography/qst_experiment.py index 781bdf3933..33fd86cf83 100644 --- a/qiskit_experiments/library/tomography/qst_experiment.py +++ b/qiskit_experiments/library/tomography/qst_experiment.py @@ -17,7 +17,6 @@ from qiskit.circuit import QuantumCircuit, Instruction from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.quantum_info import Statevector -from qiskit_experiments.framework import Options from .tomography_experiment import TomographyExperiment from .qst_analysis import StateTomographyAnalysis from . import basis @@ -44,30 +43,6 @@ class StateTomography(TomographyExperiment): """ - __analysis_class__ = StateTomographyAnalysis - - @classmethod - def _default_analysis_options(cls) -> Options: - """Default analysis options. - - Analysis Options: - measurement_basis (:class`~basis.BaseFitterMeasurementBasis`): A custom - measurement basis for analysis. By default the :meth:`experiment_options` - measurement basis will be used. - fitter (``str`` or ``Callable``): The fitter function to use for reconstruction. - rescale_psd (``bool``): If True rescale the fitted state to be - positive-semidefinite (Default: True). - rescale_trace (``bool``): If True rescale the state returned by the fitter - have either trace 1 (Default: True). - kwargs: Additional kwargs will be supplied to the fitter function. - - """ - options = super()._default_analysis_options() - - options.measurement_basis = basis.PauliMeasurementBasis().matrix - - return options - def __init__( self, circuit: Union[QuantumCircuit, Instruction, BaseOperator, Statevector], @@ -110,3 +85,4 @@ def __init__( basis_indices=basis_indices, qubits=qubits, ) + self.analysis = StateTomographyAnalysis() diff --git a/qiskit_experiments/library/tomography/tomography_experiment.py b/qiskit_experiments/library/tomography/tomography_experiment.py index f52c361484..8a500addcc 100644 --- a/qiskit_experiments/library/tomography/tomography_experiment.py +++ b/qiskit_experiments/library/tomography/tomography_experiment.py @@ -31,8 +31,6 @@ class TomographyExperiment(BaseExperiment): """Base experiment for quantum state and process tomography""" - __analysis_class__ = TomographyAnalysis - @classmethod def _default_experiment_options(cls) -> Options: """Default experiment options. @@ -84,7 +82,7 @@ def __init__( # Initialize BaseExperiment if qubits is None: qubits = range(circuit.num_qubits) - super().__init__(qubits, backend=backend) + super().__init__(qubits, analysis=TomographyAnalysis(), backend=backend) # Get the target tomography circuit if isinstance(circuit, QuantumCircuit): @@ -145,7 +143,7 @@ def __init__( analysis_options["measurement_basis"] = measurement_basis if preparation_basis: analysis_options["preparation_basis"] = preparation_basis - self.set_analysis_options(**analysis_options) + self.analysis.set_options(**analysis_options) def _metadata(self): metadata = super()._metadata() diff --git a/test/calibration/experiments/test_drag.py b/test/calibration/experiments/test_drag.py index 0892e1ccb9..d3200e1eef 100644 --- a/test/calibration/experiments/test_drag.py +++ b/test/calibration/experiments/test_drag.py @@ -71,7 +71,7 @@ def test_end_to_end(self): backend = DragBackend(error=0.0051, gate_name="Drag(xp)") drag = RoughDrag(0, self.x_plus) - drag.set_analysis_options(p0={"beta": 1.2}) + drag.analysis.set_options(p0={"beta": 1.2}) exp_data = drag.run(backend) result = exp_data.analysis_results(1) @@ -83,7 +83,7 @@ def test_end_to_end(self): drag = RoughDrag(1, self.x_plus, betas=np.linspace(-4, 4, 31)) drag.set_run_options(shots=200) - drag.set_analysis_options(p0={"beta": 1.8, "freq0": 0.08, "freq1": 0.16, "freq2": 0.32}) + drag.analysis.set_options(p0={"beta": 1.8, "freq0": 0.08, "freq1": 0.16, "freq2": 0.32}) exp_data = drag.run(backend) result = exp_data.analysis_results(1) diff --git a/test/calibration/experiments/test_fine_amplitude.py b/test/calibration/experiments/test_fine_amplitude.py index b411cb5f85..d7e8d6af66 100644 --- a/test/calibration/experiments/test_fine_amplitude.py +++ b/test/calibration/experiments/test_fine_amplitude.py @@ -45,7 +45,7 @@ def test_end_to_end_under_rotation(self, pi_ratio): amp_exp = FineAmplitude(0, Gate("xp", 1, [])) amp_exp.set_transpile_options(basis_gates=["xp", "x", "sx"]) amp_exp.set_experiment_options(add_sx=True) - amp_exp.set_analysis_options(angle_per_gate=np.pi, phase_offset=np.pi / 2) + amp_exp.analysis.set_options(angle_per_gate=np.pi, phase_offset=np.pi / 2) error = -np.pi * pi_ratio backend = MockFineAmp(error, np.pi, "xp") @@ -66,7 +66,7 @@ def test_end_to_end_over_rotation(self, pi_ratio): amp_exp = FineAmplitude(0, Gate("xp", 1, [])) amp_exp.set_transpile_options(basis_gates=["xp", "x", "sx"]) amp_exp.set_experiment_options(add_sx=True) - amp_exp.set_analysis_options(angle_per_gate=np.pi, phase_offset=np.pi / 2) + amp_exp.analysis.set_options(angle_per_gate=np.pi, phase_offset=np.pi / 2) error = np.pi * pi_ratio backend = MockFineAmp(error, np.pi, "xp") @@ -129,8 +129,8 @@ def test_fine_x_amp(self): self.assertTrue(exp.experiment_options.add_sx) self.assertTrue(exp.experiment_options.add_xp_circuit) - self.assertEqual(exp.analysis_options.angle_per_gate, np.pi) - self.assertEqual(exp.analysis_options.phase_offset, np.pi / 2) + self.assertEqual(exp.analysis.options.angle_per_gate, np.pi) + self.assertEqual(exp.analysis.options.phase_offset, np.pi / 2) self.assertEqual(exp.experiment_options.gate, XGate()) def test_fine_sx_amp(self): @@ -143,8 +143,8 @@ def test_fine_sx_amp(self): expected = [0, 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 21, 23, 25] self.assertEqual(exp.experiment_options.repetitions, expected) - self.assertEqual(exp.analysis_options.angle_per_gate, np.pi / 2) - self.assertEqual(exp.analysis_options.phase_offset, np.pi) + self.assertEqual(exp.analysis.options.angle_per_gate, np.pi / 2) + self.assertEqual(exp.analysis.options.phase_offset, np.pi) self.assertEqual(exp.experiment_options.gate, SXGate()) diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 2249d8e113..7e528781bf 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -86,7 +86,7 @@ def test_wrong_processor(self): fail_key = "fail_key" - rabi.set_analysis_options(data_processor=DataProcessor(fail_key, [])) + rabi.analysis.set_options(data_processor=DataProcessor(fail_key, [])) rabi.set_run_options(shots=2) data = rabi.run(backend) result = data.analysis_results() diff --git a/test/fake_experiment.py b/test/fake_experiment.py index fc2dc55416..0430553cb4 100644 --- a/test/fake_experiment.py +++ b/test/fake_experiment.py @@ -37,8 +37,6 @@ def _run_analysis(self, experiment_data, **options): class FakeExperiment(BaseExperiment): """Fake experiment class for testing.""" - __analysis_class__ = FakeAnalysis - @classmethod def _default_experiment_options(cls) -> Options: return Options(dummyoption=None) @@ -47,7 +45,7 @@ def __init__(self, qubits=None): """Initialise the fake experiment.""" if qubits is None: qubits = [0] - super().__init__(qubits) + super().__init__(qubits, analysis=FakeAnalysis()) def circuits(self): """Fake circuits.""" diff --git a/test/quantum_volume/test_qv.py b/test/quantum_volume/test_qv.py index 6e3ed839b2..b42db0c032 100644 --- a/test/quantum_volume/test_qv.py +++ b/test/quantum_volume/test_qv.py @@ -108,7 +108,7 @@ def test_qv_sigma_decreasing(self): result_data1 = expdata1.analysis_results(0) expdata2 = qv_exp.run(backend, analysis=False) expdata2.add_data(expdata1.data()) - qv_exp.run_analysis(expdata2) + qv_exp.analysis.run(expdata2) result_data2 = expdata2.analysis_results(0) self.assertTrue(result_data1.extra["trials"] == 2, "number of trials is incorrect") @@ -138,7 +138,7 @@ def test_qv_failure_insufficient_trials(self): exp_data = ExperimentData(experiment=qv_exp, backend=backend) exp_data.add_data(insufficient_trials_data) - qv_exp.run_analysis(exp_data) + qv_exp.analysis.run(exp_data) qv_result = exp_data.analysis_results(1) self.assertTrue( qv_result.extra["success"] is False and qv_result.value == 1, @@ -162,7 +162,7 @@ def test_qv_failure_insufficient_hop(self): exp_data = ExperimentData(experiment=qv_exp, backend=backend) exp_data.add_data(insufficient_hop_data) - qv_exp.run_analysis(exp_data) + qv_exp.analysis.run(exp_data) qv_result = exp_data.analysis_results(1) self.assertTrue( qv_result.extra["success"] is False and qv_result.value == 1, @@ -187,7 +187,7 @@ def test_qv_failure_insufficient_confidence(self): exp_data = ExperimentData(experiment=qv_exp, backend=backend) exp_data.add_data(insufficient_confidence_data) - qv_exp.run_analysis(exp_data) + qv_exp.analysis.run(exp_data) qv_result = exp_data.analysis_results(1) self.assertTrue( qv_result.extra["success"] is False and qv_result.value == 1, @@ -211,7 +211,7 @@ def test_qv_success(self): exp_data = ExperimentData(experiment=qv_exp, backend=backend) exp_data.add_data(successful_data) - qv_exp.run_analysis(exp_data) + qv_exp.analysis.run(exp_data) results_json_file = "qv_result_moderate_noise_300_trials.json" with open(os.path.join(dir_name, results_json_file), "r") as json_file: successful_results = json.load(json_file, cls=ExperimentDecoder) diff --git a/test/randomized_benchmarking/rb_generate_data.py b/test/randomized_benchmarking/rb_generate_data.py index 63de31550e..12c1244d12 100644 --- a/test/randomized_benchmarking/rb_generate_data.py +++ b/test/randomized_benchmarking/rb_generate_data.py @@ -102,7 +102,7 @@ def _generate_rb_fitter_data(dir_name: str, rb_exp_name: str, exp_attributes: di num_samples=exp_attributes["num_samples"], seed=exp_attributes["seed"], ) - rb_exp.set_analysis_options(gate_error_ratio=gate_error_ratio) + rb_exp.analysis.set_options(gate_error_ratio=gate_error_ratio) print("Running experiment") experiment_obj = rb_exp.run(backend, noise_model=noise_model, basis_gates=transpiled_base_gate) experiment_obj.block_for_results() @@ -198,7 +198,7 @@ def _generate_int_rb_fitter_data(dir_name: str, rb_exp_name: str, exp_attributes num_samples=exp_attributes["num_samples"], seed=exp_attributes["seed"], ) - rb_exp.set_analysis_options(gate_error_ratio=gate_error_ratio) + rb_exp.analysis.set_options(gate_error_ratio=gate_error_ratio) print("Running experiment") experiment_obj = rb_exp.run(backend, noise_model=noise_model, basis_gates=transpiled_base_gate) experiment_obj.block_for_results() diff --git a/test/randomized_benchmarking/test_rb_analysis.py b/test/randomized_benchmarking/test_rb_analysis.py index 6802848bd8..45b3b08b91 100644 --- a/test/randomized_benchmarking/test_rb_analysis.py +++ b/test/randomized_benchmarking/test_rb_analysis.py @@ -204,8 +204,8 @@ def _load_rb_data(self, rb_exp_data_file_name: str): ((0,), "x"): 1, ((0, 1), "cx"): 1, } - rb_exp.set_analysis_options(gate_error_ratio=gate_error_ratio) - analysis_results = rb_exp.run_analysis(expdata1).block_for_results() + rb_exp.analysis.set_options(gate_error_ratio=gate_error_ratio) + analysis_results = rb_exp.analysis.run(expdata1).block_for_results() return data, analysis_results @@ -259,8 +259,8 @@ def _load_rb_data(self, rb_exp_data_file_name: str): ((0,), "x"): 1, ((0, 1), "cx"): 1, } - rb_exp.set_analysis_options(gate_error_ratio=gate_error_ratio) - analysis_results = rb_exp.run_analysis(expdata1).block_for_results() + rb_exp.analysis.set_options(gate_error_ratio=gate_error_ratio) + analysis_results = rb_exp.analysis.run(expdata1).block_for_results() return data, analysis_results def test_interleaved_rb_analysis_test(self): diff --git a/test/test_composite.py b/test/test_composite.py index 197377b9c7..4e85511da8 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -50,7 +50,7 @@ def test_parallel_options(self): exp2.set_experiment_options(dummyoption="test") exp2.set_run_options(shots=2000) exp2.set_transpile_options(optimization_level=1) - exp2.set_analysis_options(dummyoption="test") + exp2.analysis.set_options(dummyoption="test") par_exp = ParallelExperiment([exp0, exp2]) @@ -62,7 +62,7 @@ def test_parallel_options(self): self.assertEqual(par_exp.experiment_options, Options()) self.assertEqual(par_exp.run_options, Options(meas_level=2)) self.assertEqual(par_exp.transpile_options, Options(optimization_level=0)) - self.assertEqual(par_exp.analysis_options, Options()) + self.assertEqual(par_exp.analysis.options, Options()) par_exp.run(FakeBackend()) @@ -182,7 +182,7 @@ def test_analysis_replace_results_true(self): data1.add_child_data(extra_data) # Replace results - data2 = par_exp.run_analysis(data1, replace_results=True) + data2 = par_exp.analysis.run(data1, replace_results=True) self.assertEqual(data1, data2) self.assertEqual(len(data1.child_data()), len(data2.child_data())) for sub1, sub2 in zip(data1.child_data(), data2.child_data()): @@ -203,7 +203,7 @@ def test_analysis_replace_results_false(self): data1.add_child_data(extra_data) # Replace results - data2 = par_exp.run_analysis(data1, replace_results=False) + data2 = par_exp.analysis.run(data1, replace_results=False) self.assertNotEqual(data1.experiment_id, data2.experiment_id) self.assertEqual(len(data1.child_data()), len(data2.child_data())) for sub1, sub2 in zip(data1.child_data(), data2.child_data()): diff --git a/test/test_t1.py b/test/test_t1.py index e05ed0190d..6a88d96483 100644 --- a/test/test_t1.py +++ b/test/test_t1.py @@ -51,7 +51,7 @@ def test_t1_end2end(self): ) exp = T1(0, delays, unit="dt") - exp.set_analysis_options(p0={"amp": 1, "tau": t1 / dt_factor, "base": 0}) + exp.analysis.set_options(p0={"amp": 1, "tau": t1 / dt_factor, "base": 0}) exp_data = exp.run(backend, shots=10000) res = exp_data.analysis_results("T1") fitval = res.value @@ -103,9 +103,9 @@ def test_t1_parallel_different_analysis_options(self): delays = list(range(1, 40, 3)) exp0 = T1(0, delays) - exp0.set_analysis_options(p0={"tau": 30}) + exp0.analysis.set_options(p0={"tau": 30}) exp1 = T1(1, delays) - exp1.set_analysis_options(p0={"tau": 1000000}) + exp1.analysis.set_options(p0={"tau": 1000000}) par_exp = ParallelExperiment([exp0, exp1]) res = par_exp.run(T1Backend([t1, t1])).block_for_results() diff --git a/test/test_t2ramsey.py b/test/test_t2ramsey.py index be764e3a2f..07c4c92c5d 100644 --- a/test/test_t2ramsey.py +++ b/test/test_t2ramsey.py @@ -75,7 +75,7 @@ def test_t2ramsey_run_end2end(self): conversion_factor=conversion_factor, ) for user_p0 in [default_p0, dict()]: - exp.set_analysis_options(p0=user_p0) + exp.analysis.set_options(p0=user_p0) expdata = exp.run(backend=backend, shots=2000) expdata.block_for_results() # Wait for job/analysis to finish. result = expdata.analysis_results("T2star") @@ -158,7 +158,7 @@ def test_t2ramsey_concat_2_experiments(self): "phi": 0, "B": 0.5, } - exp0.set_analysis_options(p0=default_p0) + exp0.analysis.set_options(p0=default_p0) backend = T2RamseyBackend( p0={ "A": [0.5], @@ -179,10 +179,10 @@ def test_t2ramsey_concat_2_experiments(self): # second experiment delays1 = list(range(2, 65, 2)) exp1 = T2Ramsey(qubit, delays1, unit=unit) - exp1.set_analysis_options(p0=default_p0) + exp1.analysis.set_options(p0=default_p0) expdata1 = exp1.run(backend=backend, analysis=False, shots=1000) expdata1.add_data(expdata0.data()) - exp1.run_analysis(expdata1) + exp1.analysis.run(expdata1) res_t2star_1 = expdata1.analysis_results("T2star") res_freq_1 = expdata1.analysis_results("Frequency") diff --git a/test/test_tomography.py b/test/test_tomography.py index a903d02c7e..9560e2d2be 100644 --- a/test/test_tomography.py +++ b/test/test_tomography.py @@ -50,7 +50,7 @@ def test_full_qst(self, num_qubits, fitter): target = qi.random_statevector(2 ** num_qubits, seed=seed) qstexp = StateTomography(target) if fitter: - qstexp.set_analysis_options(fitter=fitter) + qstexp.analysis.set_options(fitter=fitter) expdata = qstexp.run(backend) results = expdata.analysis_results() @@ -296,7 +296,7 @@ def test_full_qpt(self, num_qubits, fitter): target = qi.random_unitary(2 ** num_qubits, seed=seed) qstexp = ProcessTomography(target) if fitter: - qstexp.set_analysis_options(fitter=fitter) + qstexp.analysis.set_options(fitter=fitter) expdata = qstexp.run(backend) results = expdata.analysis_results() From ef93eda6456a3749e73bf8e738c5b8531e83db5d Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Mon, 6 Dec 2021 14:07:02 -0500 Subject: [PATCH 3/7] Remove analysis class from exp docs This is temporary and can be added back in for new formatting later --- docs/_ext/custom_styles/styles.py | 36 ------------------------------- 1 file changed, 36 deletions(-) diff --git a/docs/_ext/custom_styles/styles.py b/docs/_ext/custom_styles/styles.py index c5420b6c1a..6d07067535 100644 --- a/docs/_ext/custom_styles/styles.py +++ b/docs/_ext/custom_styles/styles.py @@ -182,12 +182,6 @@ def _extra_sections(self) -> Dict[str, List[str]]: """Generate extra sections.""" parsed_sections = {} - # add analysis class reference - analysis_class = getattr(self._target_cls, "__analysis_class__", None) - if analysis_class: - analysis_ref = f":py:class:`~{analysis_class.__module__}.{analysis_class.__name__}`" - parsed_sections["analysis_ref"] = [analysis_ref] - # add experiment option exp_option_desc = [] @@ -213,36 +207,6 @@ def _extra_sections(self) -> Dict[str, List[str]]: parsed_sections["experiment_opts"] = exp_option_desc - # add analysis option - analysis_option_desc = [] - - if analysis_class: - default_analysis_options = self._target_cls._default_analysis_options().__dict__ - - analysis_docs_config = copy.copy(self._config) - analysis_docs_config.napoleon_custom_sections = [("analysis options", "args")] - analysis_option = _generate_options_documentation( - current_class=analysis_class, - method_name="_default_options", - target_args=list(default_analysis_options.keys()), - config=analysis_docs_config, - indent=self._indent, - ) - - if analysis_option: - analysis_option_desc.extend(analysis_option) - analysis_option_desc.append("") - analysis_option_desc.extend( - _format_default_options( - defaults=default_analysis_options, - indent=self._indent, - ) - ) - else: - analysis_option_desc.append("No analysis option available for this experiment.") - - parsed_sections["analysis_opts"] = analysis_option_desc - # add transpiler option transpiler_option_desc = [ "This option is used for circuit optimization. ", From 2cf31312de6fe5dcf1192dd57264a8d5b597188e Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Mon, 6 Dec 2021 14:38:55 -0500 Subject: [PATCH 4/7] Address review comments --- .../framework/base_experiment.py | 36 +++++++++------- .../library/calibration/fine_amplitude.py | 41 +++++++++++++++---- .../characterization/ef_spectroscopy.py | 15 +++++-- 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 92eb2a2da4..b759075f3a 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -112,12 +112,12 @@ def num_qubits(self) -> int: @property def analysis(self) -> Union[BaseAnalysis, None]: - """Return the analysis class for the experiment""" + """Return the analysis instance for the experiment""" return self._analysis @analysis.setter def analysis(self, analysis: Union[BaseAnalysis, None]) -> None: - """Set the backend for the experiment""" + """Set the analysis instance for the experiment""" if not isinstance(analysis, BaseAnalysis): raise TypeError("Input is not a BaseAnalysis subclass.") self._analysis = analysis @@ -148,8 +148,8 @@ def copy(self) -> "BaseExperiment": # need to also copy the Options structures so that if they are # updated on the copy they don't effect the original. ret = copy.copy(self) - if self._analysis: - ret._analysis = self._analysis.copy() + if self.analysis: + ret.analysis = self.analysis.copy() ret._experiment_options = copy.copy(self._experiment_options) ret._run_options = copy.copy(self._run_options) @@ -198,7 +198,7 @@ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": def run( self, backend: Optional[Backend] = None, - analysis: bool = True, + analysis: Optional[Union[BaseAnalysis, None]] = "default", **run_options, ) -> ExperimentData: """Run an experiment and perform analysis. @@ -207,7 +207,10 @@ def run( backend: Optional, the backend to run the experiment on. This will override any currently set backends for the single execution. - analysis: If True run analysis on the experiment data. + analysis: Optional, a custom analysis instance to use for performing + analysis. If None analysis will not be run. If ``"default"`` + the experiments :meth:`analysis` instance will be used if + it contains one. run_options: backend runtime options used for circuit execution. Returns: @@ -217,11 +220,16 @@ def run( QiskitError: if experiment is run with an incompatible existing ExperimentData container. """ - if backend is None: - experiment = self - else: + if backend is not None or isinstance(analysis, BaseAnalysis): + # Make a copy to update analysis or backend if one is provided at runtime experiment = self.copy() - experiment._set_backend(backend) + if backend: + experiment._set_backend(backend) + if isinstance(analysis, BaseAnalysis): + experiment.analysis = analysis + else: + experiment = self + if experiment.backend is None: raise QiskitError("Cannot run experiment, no backend has been set.") @@ -245,7 +253,7 @@ def run( experiment._add_job_metadata(experiment_data.metadata, jobs, **run_opts) # Optionally run analysis - if analysis and self._analysis is not None: + if analysis and experiment.analysis: return self.analysis.run(experiment_data) else: return experiment_data @@ -425,7 +433,7 @@ def analysis_options(self) -> Options: " Use `experiment.analysis.options instead", DeprecationWarning, ) - return self._analysis.options + return self.analysis.options def set_analysis_options(self, **fields): """Set the analysis options for :meth:`run` method. @@ -444,7 +452,7 @@ def set_analysis_options(self, **fields): " Use `experiment.analysis.set_options instead", DeprecationWarning, ) - self._analysis.options.update_options(**fields) + self.analysis.options.update_options(**fields) def _postprocess_transpiled_circuits(self, circuits: List[QuantumCircuit], **run_options): """Additional post-processing of transpiled circuits before running on backend""" @@ -489,7 +497,7 @@ def _add_job_metadata(self, metadata: Dict[str, Any], jobs: BaseJob, **run_optio "transpile_options": copy.copy(self.transpile_options.__dict__), "run_options": copy.copy(run_options), } - if self._analysis is not None: + if self.analysis is not None: values["analysis_options"] = copy.copy(self.analysis.options.__dict__) metadata["job_metadata"] = [values] diff --git a/qiskit_experiments/library/calibration/fine_amplitude.py b/qiskit_experiments/library/calibration/fine_amplitude.py index 475527ad81..25a73d1497 100644 --- a/qiskit_experiments/library/calibration/fine_amplitude.py +++ b/qiskit_experiments/library/calibration/fine_amplitude.py @@ -12,7 +12,6 @@ """Fine amplitude calibration experiment.""" -import functools from typing import Optional import numpy as np @@ -146,9 +145,23 @@ def update_calibrations(self, experiment_data: ExperimentData): class FineXAmplitudeCal(FineAmplitudeCal): """A calibration experiment to calibrate the amplitude of the X schedule.""" - @functools.wraps(FineAmplitudeCal.__init__) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__( + self, + qubit: int, + calibrations: BackendCalibrations, + schedule_name: str, + backend: Optional[Backend] = None, + cal_parameter_name: Optional[str] = "amp", + auto_update: bool = True, + ): + super().__init__( + qubit, + calibrations, + schedule_name, + backend=backend, + cal_parameter_name=cal_parameter_name, + auto_update=auto_update, + ) self.analysis.set_options( angle_per_gate=np.pi, phase_offset=np.pi / 2, @@ -190,9 +203,23 @@ def _default_transpile_options(cls): class FineSXAmplitudeCal(FineAmplitudeCal): """A calibration experiment to calibrate the amplitude of the SX schedule.""" - @functools.wraps(FineAmplitudeCal.__init__) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__( + self, + qubit: int, + calibrations: BackendCalibrations, + schedule_name: str, + backend: Optional[Backend] = None, + cal_parameter_name: Optional[str] = "amp", + auto_update: bool = True, + ): + super().__init__( + qubit, + calibrations, + schedule_name, + backend=backend, + cal_parameter_name=cal_parameter_name, + auto_update=auto_update, + ) self.analysis.set_options( angle_per_gate=np.pi / 2, phase_offset=np.pi, diff --git a/qiskit_experiments/library/characterization/ef_spectroscopy.py b/qiskit_experiments/library/characterization/ef_spectroscopy.py index 52507968e7..499ec37324 100644 --- a/qiskit_experiments/library/characterization/ef_spectroscopy.py +++ b/qiskit_experiments/library/characterization/ef_spectroscopy.py @@ -12,8 +12,9 @@ """Spectroscopy for the e-f transition.""" -import functools +from typing import Iterable, Optional from qiskit import QuantumCircuit +from qiskit.providers import Backend from qiskit.circuit import Gate from qiskit_experiments.curve_analysis import ParameterRepr @@ -35,9 +36,15 @@ class EFSpectroscopy(QubitSpectroscopy): """ - @functools.wraps(QubitSpectroscopy.__init__) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__( + self, + qubit: int, + frequencies: Iterable[float], + backend: Optional[Backend] = None, + unit: str = "Hz", + absolute: bool = True, + ): + super().__init__(qubit, frequencies, backend=backend, unit=unit, absolute=absolute) self.analysis.set_options(result_parameters=[ParameterRepr("freq", "f12", "Hz")]) def _template_circuit(self, freq_param) -> QuantumCircuit: From a5a52edcd2246d1a8e6733c85c3ac705fcc32760 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Mon, 6 Dec 2021 14:39:03 -0500 Subject: [PATCH 5/7] Add release note --- .../notes/analysis-b76909b1307d653b.yaml | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 releasenotes/notes/analysis-b76909b1307d653b.yaml diff --git a/releasenotes/notes/analysis-b76909b1307d653b.yaml b/releasenotes/notes/analysis-b76909b1307d653b.yaml new file mode 100644 index 0000000000..a0c89a6663 --- /dev/null +++ b/releasenotes/notes/analysis-b76909b1307d653b.yaml @@ -0,0 +1,72 @@ +--- +feature: + - | + Added a :meth:`~qiskit_experiments.framework.BaseAnalysis.set_options` + method and :meth:`~qiskit_experiments.framework.BaseAnalysis.options` + property to the :class:`qiskit_experiments.framework.BaseAnalysis` class + to store and retrieve any analysis options in the state of the analysis + instance. + - | + The ``analysis`` kwarg of + :meth:`qiskit_experiments.framework.BaseExperiment.run` can now optionally + be passed a :class:`qiskit_experiments.framework.BaseAnalysis` instance to + use for analysis of that single execution. If no instance is provided the + current stored :meth:`~qiskit_experiments.framework.BaseExperiment.analysis` + instance for that experiment will be used. + Setting ``analysis=None`` or ``analysis=False`` still disables analysis + for the specific execution. +upgrade: + - | + The :meth:`qiskit_experiments.framework.BaseExperiment.analysis` property + has been changed to return a :class:`qiskit_experiments.framework.BaseAnalysis` + *instance* rather than a class type. This method also now has a setter + which allows setting an analysis instance for use by an experiment. +deprecations: + - | + The :meth:`qiskit_experiments.framework.BaseExperiment.set_analysis_options` + method has been deprecated, use the + :meth:`qiskit_experiments.framework.BaseAnalysis.set_options` method + for the experiments analysis class instead. This can be accessed from the + experiment instance using the + :meth:`qiskit_experiments.framework.BaseExperiment.analysis` property as + ``experiment.analysis.set_options(**options)``. + - | + The :meth:`qiskit_experiments.framework.BaseExperiment.analysis_options` + property has been deprecated, use the + :meth:`qiskit_experiments.framework.BaseAnalysis.options` property + for the experiments analysis class instead. This can be accessed from the + experiment instance using the + :meth:`qiskit_experiments.framework.BaseExperiment.analysis` property as + ``experiment.analysis.options``. + - | + The :meth:`qiskit_experiments.framework.BaseExperiment.run_analysis` and + method has been deprecated, use the + :meth:`qiskit_experiments.framework.BaseAnalysis.run` method + for the experiments analysis class instead. This can be accessed from the + experiment instance using the + :meth:`qiskit_experiments.framework.BaseExperiment.analysis` property as + ``experiment.analysis.run(**kwargs)``. +developer: + - | + The :class:`qiskit_experiments.framework.BaseAnalysis` class has + been changed to be an initialized class. + + This class now stores its set analysis options using the + :meth:`~qiskit_experiments.framework.BaseAnalysis.set_options` and + :meth:`~qiskit_experiments.framework.BaseAnalysis.options` and + ``_default_options`` methods. + The signature of the abstract method ``_run_analysis`` that must be + implemented by subclasses has been changed to remove the ``**kwargs``. + + Note that the state of this class should only be used to store option + values and derived configuration. The + :meth:`~qiskit_experiments.framework.BaseAnalysis.run` and + ``_run_analysis`` methods should not change the state of the instance. + - | + The :class:`qiskit_experiments.framework.BaseExperiment` class has + been changed to optionally store an instance of a + :class:`qiskit_experiments.framework.BaseAnalysis` class during + its initialization. Any default analysis options specific to a + particular experiment subclass should be set during that experiments + init method, or as default options of the analysis subclass used by + that experiment. From 1ebe88e7919b83b90fe86d1bfcbed1cb5d5e7bb4 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Mon, 6 Dec 2021 14:53:16 -0500 Subject: [PATCH 6/7] black --- qiskit_experiments/framework/base_experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index b759075f3a..f30e858010 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -210,7 +210,7 @@ def run( analysis: Optional, a custom analysis instance to use for performing analysis. If None analysis will not be run. If ``"default"`` the experiments :meth:`analysis` instance will be used if - it contains one. + it contains one. run_options: backend runtime options used for circuit execution. Returns: From 0558765af650897ba00eaa631d9013784182dfc5 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Mon, 6 Dec 2021 15:50:21 -0500 Subject: [PATCH 7/7] Deprecate bool values of analysis kwarg in run --- docs/tutorials/quantum_volume.ipynb | 2 +- .../base_calibration_experiment.py | 10 ++++--- .../framework/base_experiment.py | 27 ++++++++++++++++--- .../notes/analysis-b76909b1307d653b.yaml | 10 ++++--- test/quantum_volume/test_qv.py | 2 +- test/test_t2ramsey.py | 2 +- 6 files changed, 41 insertions(+), 12 deletions(-) diff --git a/docs/tutorials/quantum_volume.ipynb b/docs/tutorials/quantum_volume.ipynb index 5417d344ae..8cdfc829f1 100644 --- a/docs/tutorials/quantum_volume.ipynb +++ b/docs/tutorials/quantum_volume.ipynb @@ -221,7 +221,7 @@ ], "source": [ "qv_exp.set_experiment_options(trials=60)\n", - "expdata2 = qv_exp.run(backend, analysis=False).block_for_results()\n", + "expdata2 = qv_exp.run(backend, analysis=None).block_for_results()\n", "expdata2.add_data(expdata.data())\n", "qv_exp.analysis.run(expdata2).block_for_results()\n", "\n", diff --git a/qiskit_experiments/calibration_management/base_calibration_experiment.py b/qiskit_experiments/calibration_management/base_calibration_experiment.py index 5c081599fe..e0d427a65e 100644 --- a/qiskit_experiments/calibration_management/base_calibration_experiment.py +++ b/qiskit_experiments/calibration_management/base_calibration_experiment.py @@ -13,7 +13,7 @@ """Base class for calibration-type experiments.""" from abc import ABC -from typing import List, Optional, Type +from typing import List, Optional, Type, Union import warnings from qiskit.providers.backend import Backend @@ -21,6 +21,7 @@ from qiskit_experiments.calibration_management.calibrations import Calibrations from qiskit_experiments.calibration_management.update_library import BaseUpdater +from qiskit_experiments.framework.base_analysis import BaseAnalysis from qiskit_experiments.framework.base_experiment import BaseExperiment from qiskit_experiments.framework.experiment_data import ExperimentData from qiskit_experiments.exceptions import CalibrationError @@ -219,7 +220,7 @@ def _add_cal_metadata(self, experiment_data: ExperimentData): def run( self, backend: Optional[Backend] = None, - analysis: bool = True, + analysis: Optional[Union[BaseAnalysis, None]] = "default", **run_options, ) -> ExperimentData: """Run an experiment, perform analysis, and update any calibrations. @@ -228,7 +229,10 @@ def run( backend: Optional, the backend to run the experiment on. This will override any currently set backends for the single execution. - analysis: If True run analysis on the experiment data. + analysis: Optional, a custom analysis instance to use for performing + analysis. If None analysis will not be run. If ``"default"`` + the experiments :meth:`analysis` instance will be used if + it contains one. run_options: backend runtime options used for circuit execution. Returns: diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index f30e858010..7653690b30 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -118,8 +118,8 @@ def analysis(self) -> Union[BaseAnalysis, None]: @analysis.setter def analysis(self, analysis: Union[BaseAnalysis, None]) -> None: """Set the analysis instance for the experiment""" - if not isinstance(analysis, BaseAnalysis): - raise TypeError("Input is not a BaseAnalysis subclass.") + if analysis is not None and not isinstance(analysis, BaseAnalysis): + raise TypeError("Input is not a None or a BaseAnalysis subclass.") self._analysis = analysis @property @@ -220,7 +220,28 @@ def run( QiskitError: if experiment is run with an incompatible existing ExperimentData container. """ - if backend is not None or isinstance(analysis, BaseAnalysis): + # Handle deprecated analysis kwarg values + if isinstance(analysis, bool): + if analysis: + analysis = "default" + warnings.warn( + "Setting analysis=True in BaseExperiment.run is deprecated as of " + "qiskit-experiments 0.2.0 and will be removed in the 0.3.0 release." + " Use analysis='default' instead.", + DeprecationWarning, + stacklevel=2, + ) + else: + analysis = None + warnings.warn( + "Setting analysis=False in BaseExperiment.run is deprecated as of " + "qiskit-experiments 0.2.0 and will be removed in the 0.3.0 release." + " Use analysis=None instead.", + DeprecationWarning, + stacklevel=2, + ) + + if backend is not None or analysis != "default": # Make a copy to update analysis or backend if one is provided at runtime experiment = self.copy() if backend: diff --git a/releasenotes/notes/analysis-b76909b1307d653b.yaml b/releasenotes/notes/analysis-b76909b1307d653b.yaml index a0c89a6663..0f47d84d98 100644 --- a/releasenotes/notes/analysis-b76909b1307d653b.yaml +++ b/releasenotes/notes/analysis-b76909b1307d653b.yaml @@ -12,9 +12,8 @@ feature: be passed a :class:`qiskit_experiments.framework.BaseAnalysis` instance to use for analysis of that single execution. If no instance is provided the current stored :meth:`~qiskit_experiments.framework.BaseExperiment.analysis` - instance for that experiment will be used. - Setting ``analysis=None`` or ``analysis=False`` still disables analysis - for the specific execution. + instance for that experiment will be used. Setting ``analysis=None`` disables + analysis for the specific execution. upgrade: - | The :meth:`qiskit_experiments.framework.BaseExperiment.analysis` property @@ -46,6 +45,11 @@ deprecations: experiment instance using the :meth:`qiskit_experiments.framework.BaseExperiment.analysis` property as ``experiment.analysis.run(**kwargs)``. + - | + Boolean values for the ``analysis`` kwarg in + :meth:`qiskit_experiments.framework.BaseExperiment.run` have been deprecated. + Use ``analysis="default"`` instead of ``analysis=True``, and + ``analysis=None``instead of ``analysis=False``. developer: - | The :class:`qiskit_experiments.framework.BaseAnalysis` class has diff --git a/test/quantum_volume/test_qv.py b/test/quantum_volume/test_qv.py index b42db0c032..05d6fbb92a 100644 --- a/test/quantum_volume/test_qv.py +++ b/test/quantum_volume/test_qv.py @@ -106,7 +106,7 @@ def test_qv_sigma_decreasing(self): qv_exp.set_experiment_options(trials=2) expdata1 = qv_exp.run(backend) result_data1 = expdata1.analysis_results(0) - expdata2 = qv_exp.run(backend, analysis=False) + expdata2 = qv_exp.run(backend, analysis=None) expdata2.add_data(expdata1.data()) qv_exp.analysis.run(expdata2) result_data2 = expdata2.analysis_results(0) diff --git a/test/test_t2ramsey.py b/test/test_t2ramsey.py index 07c4c92c5d..1e7503cb0b 100644 --- a/test/test_t2ramsey.py +++ b/test/test_t2ramsey.py @@ -180,7 +180,7 @@ def test_t2ramsey_concat_2_experiments(self): delays1 = list(range(2, 65, 2)) exp1 = T2Ramsey(qubit, delays1, unit=unit) exp1.analysis.set_options(p0=default_p0) - expdata1 = exp1.run(backend=backend, analysis=False, shots=1000) + expdata1 = exp1.run(backend=backend, analysis=None, shots=1000) expdata1.add_data(expdata0.data()) exp1.analysis.run(expdata1)