diff --git a/qiskit_experiments/framework/__init__.py b/qiskit_experiments/framework/__init__.py index 08d0df4bd1..32cbd92c6a 100644 --- a/qiskit_experiments/framework/__init__.py +++ b/qiskit_experiments/framework/__init__.py @@ -208,6 +208,7 @@ ExperimentData FitVal AnalysisResultData + ExperimentConfig .. _composite-experiment: @@ -236,7 +237,7 @@ from qiskit_experiments.database_service.db_analysis_result import DbAnalysisResultV1 from qiskit_experiments.database_service.db_fitval import FitVal from .base_analysis import BaseAnalysis -from .base_experiment import BaseExperiment +from .base_experiment import BaseExperiment, ExperimentConfig, fix_class_docs from .analysis_result_data import AnalysisResultData from .experiment_data import ExperimentData from .composite import ( diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index f60a170081..b771ee9d73 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -15,17 +15,76 @@ from abc import ABC, abstractmethod import copy +import inspect +import dataclasses +from functools import wraps +from collections import OrderedDict from numbers import Integral -from typing import Sequence, Optional, Tuple, List, Dict, Union +from typing import Sequence, Optional, Tuple, List, Dict, Union, Any from qiskit import transpile, assemble, QuantumCircuit from qiskit.providers import BaseJob -from qiskit.providers.backend import Backend +from qiskit.providers import Backend, BaseBackend from qiskit.providers.basebackend import BaseBackend as LegacyBackend from qiskit.exceptions import QiskitError from qiskit.qobj.utils import MeasLevel -from qiskit_experiments.framework import Options +from qiskit.providers.options import Options from qiskit_experiments.framework.experiment_data import ExperimentData +from qiskit_experiments.version import __version__ + + +@dataclasses.dataclass(frozen=True) +class ExperimentConfig: + """Store configuration settings for an Experiment + + This stores the current configuration of a + :class:~qiskit_experiments.framework.BaseExperiment` and + can be used to reconstruct the experiment using either the + :meth:`experiment` property if the experiment class type is + currently stored, or the + :meth:~qiskit_experiments.framework.BaseExperiment.from_config` + class method of the appropriate experiment. + """ + + cls: type = None + args: Tuple[Any] = dataclasses.field(default_factory=tuple) + kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) + experiment_options: Dict[str, Any] = dataclasses.field(default_factory=dict) + transpile_options: Dict[str, Any] = dataclasses.field(default_factory=dict) + run_options: Dict[str, Any] = dataclasses.field(default_factory=dict) + version: str = __version__ + + @property + def experiment(self) -> "BaseExperiment": + """Return the experiment constructed from this config. + + Returns: + The experiment reconstructed from the config. + + Raises: + QiskitError: if the experiment class is not stored, + was not successful deserialized, or reconstruction + of the experiment fails. + """ + cls = self.cls + if cls is None: + raise QiskitError("No experiment class in experiment config") + if isinstance(cls, dict): + raise QiskitError( + "Unable to load experiment class. Try manually loading " + "experiment using `Experiment.from_config(config)` instead." + ) + try: + return cls.from_config(self) + except Exception as ex: + msg = "Unable to construct experiments from config." + if cls.version != __version__: + msg += ( + f" Note that config version ({cls.version}) differs from the current" + f" qiskit-experiments version ({__version__}). You could try" + " installing a compatible qiskit-experiments version." + ) + raise QiskitError("{}\nError Message:\n{}".format(msg, str(ex))) from ex class BaseExperiment(ABC): @@ -68,7 +127,7 @@ def __init__( # Backend self._backend = None - if backend is not None: + if isinstance(backend, (Backend, BaseBackend)): self._set_backend(backend) # Circuit parameters @@ -87,6 +146,45 @@ def __init__( 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() + self._set_transpile_options = set() + self._set_run_options = set() + self._set_analysis_options = set() + + def __new__(cls, *args, **kwargs): + """Store init args and kwargs for subclass __init__ methods""" + # This method automatically stores all arg and kwargs from subclass + # init methods for use in converting an experiment to config + + # Get all non-self init args and kwarg names for subclass + spec = inspect.getfullargspec(cls.__init__) + init_arg_names = spec.args[1:] + num_init_kwargs = len(spec.defaults) if spec.defaults else 0 + num_init_args = len(init_arg_names) - num_init_kwargs + + # Convert passed values for args and kwargs into an ordered dict + # This will sort args passed as kwargs and kwargs passed as + # positional args in the function call + num_call_args = len(args) + ord_args = OrderedDict() + ord_kwargs = OrderedDict() + for i, argname in enumerate(init_arg_names): + if i < num_init_args: + update = ord_args + else: + update = ord_kwargs + if i < num_call_args: + update[argname] = args[i] + elif argname in kwargs: + update[argname] = kwargs[argname] + + # pylint: disable = attribute-defined-outside-init + instance = super(BaseExperiment, cls).__new__(cls) + instance.__init_args__ = ord_args + instance.__init_kwargs__ = ord_kwargs + return instance + @property def experiment_type(self) -> str: """Return experiment type.""" @@ -132,6 +230,42 @@ def copy(self) -> "BaseExperiment": ret._analysis_options = copy.copy(self._analysis_options) return ret + @property + def config(self) -> ExperimentConfig: + """Return the config dataclass for this experiment""" + args = tuple(getattr(self, "__init_args__", OrderedDict()).values()) + kwargs = dict(getattr(self, "__init_kwargs__", OrderedDict())) + # Only store non-default valued options + experiment_options = dict( + (key, getattr(self._experiment_options, key)) for key in self._set_experiment_options + ) + transpile_options = dict( + (key, getattr(self._transpile_options, key)) for key in self._set_transpile_options + ) + run_options = dict((key, getattr(self._run_options, key)) for key in self._set_run_options) + return ExperimentConfig( + cls=type(self), + args=args, + kwargs=kwargs, + experiment_options=experiment_options, + transpile_options=transpile_options, + run_options=run_options, + ) + + @classmethod + def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": + """Initialize an experiment from experiment config""" + if isinstance(config, dict): + config = ExperimentConfig(**dict) + ret = cls(*config.args, **config.kwargs) + if config.experiment_options: + ret.set_experiment_options(**config.experiment_options) + if config.transpile_options: + ret.set_transpile_options(**config.transpile_options) + if config.run_options: + ret.set_run_options(**config.run_options) + return ret + def run( self, backend: Optional[Backend] = None, @@ -295,6 +429,7 @@ def set_experiment_options(self, **fields): f"Options field {field} is not valid for {type(self).__name__}" ) self._experiment_options.update_options(**fields) + self._set_experiment_options = self._set_experiment_options.union(fields) @classmethod def _default_transpile_options(cls) -> Options: @@ -324,6 +459,7 @@ def set_transpile_options(self, **fields): " as it is determined by the experiment physical qubits." ) self._transpile_options.update_options(**fields) + self._set_transpile_options = self._set_transpile_options.union(fields) @classmethod def _default_run_options(cls) -> Options: @@ -342,6 +478,7 @@ def set_run_options(self, **fields): fields: The fields to update the options """ self._run_options.update_options(**fields) + self._set_run_options = self._set_run_options.union(fields) @classmethod def _default_analysis_options(cls) -> Options: @@ -365,6 +502,7 @@ def set_analysis_options(self, **fields): fields: The fields to update the options """ self._analysis_options.update_options(**fields) + self._set_analysis_options = self._set_analysis_options.union(fields) def _postprocess_transpiled_circuits(self, circuits: List[QuantumCircuit], **run_options): """Additional post-processing of transpiled circuits before running on backend""" @@ -412,3 +550,20 @@ def _add_job_metadata(self, experiment_data: ExperimentData, jobs: BaseJob, **ru "run_options": copy.copy(run_options), } experiment_data._metadata["job_metadata"].append(metadata) + + +def fix_class_docs(wrapped_cls): + """Experiment class decorator to fix class doc formatting. + + This fixes the BaseExperiment subclass documentation so that + the correct init arg and kwargs are shown for the class documentation, + rather than the generic args of the BaseExperiment.__new__ method. + """ + + @wraps(wrapped_cls.__init__, assigned=("__annotations__",)) + def __new__(cls, *args, **kwargs): + return super(wrapped_cls, cls).__new__(cls, *args, **kwargs) + + wrapped_cls.__new__ = __new__ + + return wrapped_cls diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index 67ee527a09..74006e5d73 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -18,9 +18,11 @@ from qiskit import QuantumCircuit from qiskit.providers.backend import Backend +from qiskit_experiments.framework.base_experiment import fix_class_docs from .composite_experiment import CompositeExperiment, BaseExperiment +@fix_class_docs class BatchExperiment(CompositeExperiment): """Batch experiment class""" diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 484286aade..db696dcc66 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -19,6 +19,7 @@ from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment + from .composite_experiment_data import CompositeExperimentData from .composite_analysis import CompositeAnalysis diff --git a/qiskit_experiments/framework/composite/parallel_experiment.py b/qiskit_experiments/framework/composite/parallel_experiment.py index 4643b3f0e6..b3b4c5db3c 100644 --- a/qiskit_experiments/framework/composite/parallel_experiment.py +++ b/qiskit_experiments/framework/composite/parallel_experiment.py @@ -16,9 +16,11 @@ from qiskit import QuantumCircuit, ClassicalRegister from qiskit.providers.backend import Backend +from qiskit_experiments.framework.base_experiment import fix_class_docs from .composite_experiment import CompositeExperiment, BaseExperiment +@fix_class_docs class ParallelExperiment(CompositeExperiment): """Parallel Experiment class""" diff --git a/qiskit_experiments/library/calibration/drag.py b/qiskit_experiments/library/calibration/drag.py index 055c4b9e10..0be06c0d7e 100644 --- a/qiskit_experiments/library/calibration/drag.py +++ b/qiskit_experiments/library/calibration/drag.py @@ -20,11 +20,12 @@ from qiskit.providers.backend import Backend import qiskit.pulse as pulse -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, fix_class_docs from qiskit_experiments.exceptions import CalibrationError from qiskit_experiments.library.calibration.analysis.drag_analysis import DragCalAnalysis +@fix_class_docs class DragCal(BaseExperiment): r"""An experiment that scans the DRAG parameter to find the optimal value. diff --git a/qiskit_experiments/library/calibration/fine_amplitude.py b/qiskit_experiments/library/calibration/fine_amplitude.py index 63538736df..fe21079a31 100644 --- a/qiskit_experiments/library/calibration/fine_amplitude.py +++ b/qiskit_experiments/library/calibration/fine_amplitude.py @@ -23,10 +23,11 @@ BackendCalibrations, ) from qiskit_experiments.library.characterization import FineAmplitude -from qiskit_experiments.framework import ExperimentData, Options +from qiskit_experiments.framework import ExperimentData, Options, fix_class_docs from qiskit_experiments.calibration_management.update_library import BaseUpdater +@fix_class_docs class FineAmplitudeCal(BaseCalibrationExperiment, FineAmplitude): r"""A calibration version of the :class:`FineAmplitude` experiment. @@ -153,6 +154,7 @@ def update_calibrations(self, experiment_data: ExperimentData): ) +@fix_class_docs class FineXAmplitudeCal(FineAmplitudeCal): """A calibration experiment to calibrate the amplitude of the X schedule.""" @@ -197,6 +199,7 @@ def _default_analysis_options(cls) -> Options: return options +@fix_class_docs class FineSXAmplitudeCal(FineAmplitudeCal): """A calibration experiment to calibrate the amplitude of the SX schedule.""" diff --git a/qiskit_experiments/library/calibration/fine_drag.py b/qiskit_experiments/library/calibration/fine_drag.py index f8a6b6c82c..a6dacb9e5d 100644 --- a/qiskit_experiments/library/calibration/fine_drag.py +++ b/qiskit_experiments/library/calibration/fine_drag.py @@ -19,13 +19,13 @@ from qiskit.circuit import Gate from qiskit.circuit.library import XGate, SXGate from qiskit.providers.backend import Backend - -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, fix_class_docs from qiskit_experiments.library.calibration.analysis.fine_drag_analysis import ( FineDragAnalysis, ) +@fix_class_docs class FineDrag(BaseExperiment): r"""Fine DRAG experiment. @@ -228,6 +228,7 @@ def circuits(self) -> List[QuantumCircuit]: return circuits +@fix_class_docs class FineXDrag(FineDrag): """Class to fine calibrate the DRAG parameter of an X gate. diff --git a/qiskit_experiments/library/calibration/rabi.py b/qiskit_experiments/library/calibration/rabi.py index fcb97b7aea..9668381589 100644 --- a/qiskit_experiments/library/calibration/rabi.py +++ b/qiskit_experiments/library/calibration/rabi.py @@ -21,11 +21,12 @@ from qiskit.providers import Backend import qiskit.pulse as pulse -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, fix_class_docs from qiskit_experiments.curve_analysis import ParameterRepr, OscillationAnalysis from qiskit_experiments.exceptions import CalibrationError +@fix_class_docs class Rabi(BaseExperiment): """An experiment that scans the amplitude of a pulse to calibrate rotations between 0 and 1. diff --git a/qiskit_experiments/library/calibration/ramsey_xy.py b/qiskit_experiments/library/calibration/ramsey_xy.py index 3411a0f5a8..c22ffedda9 100644 --- a/qiskit_experiments/library/calibration/ramsey_xy.py +++ b/qiskit_experiments/library/calibration/ramsey_xy.py @@ -20,10 +20,11 @@ from qiskit.utils import apply_prefix from qiskit.providers.backend import Backend -from qiskit_experiments.framework import BaseExperiment +from qiskit_experiments.framework import BaseExperiment, fix_class_docs from qiskit_experiments.library.calibration.analysis.remsey_xy_analysis import RamseyXYAnalysis +@fix_class_docs class RamseyXY(BaseExperiment): r"""Ramsey XY experiment to measure the frequency of a qubit. diff --git a/qiskit_experiments/library/calibration/rough_frequency.py b/qiskit_experiments/library/calibration/rough_frequency.py index bc5dc1a77c..a7cfefc7d0 100644 --- a/qiskit_experiments/library/calibration/rough_frequency.py +++ b/qiskit_experiments/library/calibration/rough_frequency.py @@ -15,6 +15,7 @@ from typing import Iterable, Optional from qiskit.providers.backend import Backend +from qiskit_experiments.framework import fix_class_docs from qiskit_experiments.library.characterization.qubit_spectroscopy import QubitSpectroscopy from qiskit_experiments.library.characterization.ef_spectroscopy import EFSpectroscopy from qiskit_experiments.calibration_management.update_library import Frequency @@ -24,6 +25,7 @@ ) +@fix_class_docs class RoughFrequencyCal(BaseCalibrationExperiment, QubitSpectroscopy): """A calibration experiment that runs QubitSpectroscopy.""" @@ -68,6 +70,7 @@ def __init__( ) +@fix_class_docs class RoughEFFrequencyCal(BaseCalibrationExperiment, EFSpectroscopy): """A calibration experiment that runs QubitSpectroscopy.""" diff --git a/qiskit_experiments/library/characterization/cr_hamiltonian.py b/qiskit_experiments/library/characterization/cr_hamiltonian.py index ccb640e67d..67a316ac1f 100644 --- a/qiskit_experiments/library/characterization/cr_hamiltonian.py +++ b/qiskit_experiments/library/characterization/cr_hamiltonian.py @@ -20,11 +20,11 @@ from qiskit.exceptions import QiskitError from qiskit.providers import Backend from qiskit.utils import apply_prefix - -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, fix_class_docs from .cr_hamiltonian_analysis import CrossResonanceHamiltonianAnalysis +@fix_class_docs class CrossResonanceHamiltonian(BaseExperiment): r"""Cross resonance Hamiltonian tomography experiment. diff --git a/qiskit_experiments/library/characterization/ef_spectroscopy.py b/qiskit_experiments/library/characterization/ef_spectroscopy.py index 16a8ee3cb8..6ef339a7ef 100644 --- a/qiskit_experiments/library/characterization/ef_spectroscopy.py +++ b/qiskit_experiments/library/characterization/ef_spectroscopy.py @@ -17,9 +17,10 @@ from qiskit_experiments.curve_analysis import ParameterRepr from qiskit_experiments.library.characterization.qubit_spectroscopy import QubitSpectroscopy -from qiskit_experiments.framework import Options +from qiskit_experiments.framework import Options, fix_class_docs +@fix_class_docs class EFSpectroscopy(QubitSpectroscopy): """Class that runs spectroscopy on the e-f transition by scanning the frequency. diff --git a/qiskit_experiments/library/characterization/fine_amplitude.py b/qiskit_experiments/library/characterization/fine_amplitude.py index 3c4c4c5fa2..5f34a09f63 100644 --- a/qiskit_experiments/library/characterization/fine_amplitude.py +++ b/qiskit_experiments/library/characterization/fine_amplitude.py @@ -19,13 +19,14 @@ from qiskit.circuit import Gate from qiskit.circuit.library import XGate, SXGate from qiskit.providers.backend import Backend -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, fix_class_docs from qiskit_experiments.library.calibration.analysis.fine_amplitude_analysis import ( FineAmplitudeAnalysis, ) from qiskit_experiments.exceptions import CalibrationError +@fix_class_docs class FineAmplitude(BaseExperiment): r"""Error amplifying fine amplitude calibration experiment. @@ -210,6 +211,7 @@ def circuits(self) -> List[QuantumCircuit]: return circuits +@fix_class_docs class FineXAmplitude(FineAmplitude): r"""A fine amplitude experiment with all the options set for the :math:`\pi`-rotation. @@ -252,6 +254,7 @@ def _default_analysis_options(cls) -> Options: return options +@fix_class_docs class FineSXAmplitude(FineAmplitude): r"""A fine amplitude experiment with all the options set for the :math:`\pi/2`-rotation. diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index 09314d0131..d59c61efff 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -23,10 +23,11 @@ from qiskit.qobj.utils import MeasLevel from qiskit.utils import apply_prefix -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, fix_class_docs from qiskit_experiments.curve_analysis import ParameterRepr, ResonanceAnalysis +@fix_class_docs class QubitSpectroscopy(BaseExperiment): """Class that runs spectroscopy by sweeping the qubit frequency. diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 1a01cc722e..850305382c 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -20,10 +20,11 @@ from qiskit.providers.backend import Backend from qiskit.test.mock import FakeBackend -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, fix_class_docs from qiskit_experiments.library.characterization.t1_analysis import T1Analysis +@fix_class_docs class T1(BaseExperiment): r""" T1 experiment class diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index dbef1e8186..b5791738f2 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -23,10 +23,11 @@ from qiskit.providers.backend import Backend from qiskit.test.mock import FakeBackend -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, fix_class_docs from .t2ramsey_analysis import T2RamseyAnalysis +@fix_class_docs class T2Ramsey(BaseExperiment): r"""T2 Ramsey Experiment. diff --git a/qiskit_experiments/library/quantum_volume/qv_experiment.py b/qiskit_experiments/library/quantum_volume/qv_experiment.py index e2c0ce076e..9d9d1b3c65 100644 --- a/qiskit_experiments/library/quantum_volume/qv_experiment.py +++ b/qiskit_experiments/library/quantum_volume/qv_experiment.py @@ -27,10 +27,11 @@ from qiskit.circuit.library import QuantumVolume as QuantumVolumeCircuit from qiskit import transpile from qiskit.providers.backend import Backend -from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework import BaseExperiment, Options, fix_class_docs from .qv_analysis import QuantumVolumeAnalysis +@fix_class_docs class QuantumVolume(BaseExperiment): """Quantum Volume Experiment class. diff --git a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py index b6c9ce92a5..fab469b6e7 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -22,10 +22,11 @@ from qiskit.exceptions import QiskitError from qiskit.providers.backend import Backend -from .rb_experiment import StandardRB +from .rb_experiment import StandardRB, fix_class_docs from .interleaved_rb_analysis import InterleavedRBAnalysis +@fix_class_docs class InterleavedRB(StandardRB): """Interleaved randomized benchmarking experiment. diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index 231b7eee06..53b62f2d58 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -23,12 +23,13 @@ from qiskit.providers.backend import Backend import qiskit_experiments.data_processing as dp -from qiskit_experiments.framework import BaseExperiment, ParallelExperiment, Options +from qiskit_experiments.framework import BaseExperiment, ParallelExperiment, Options, fix_class_docs from .rb_analysis import RBAnalysis from .clifford_utils import CliffordUtils from .rb_utils import RBUtils +@fix_class_docs class StandardRB(BaseExperiment): """Standard randomized benchmarking experiment. diff --git a/qiskit_experiments/library/tomography/qpt_experiment.py b/qiskit_experiments/library/tomography/qpt_experiment.py index c2bf6bf40c..d92080d8de 100644 --- a/qiskit_experiments/library/tomography/qpt_experiment.py +++ b/qiskit_experiments/library/tomography/qpt_experiment.py @@ -16,12 +16,13 @@ from typing import Union, Optional, Iterable, List, Tuple from qiskit.circuit import QuantumCircuit, Instruction from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit_experiments.framework import Options +from qiskit_experiments.framework import Options, fix_class_docs from .tomography_experiment import TomographyExperiment from .qpt_analysis import ProcessTomographyAnalysis from . import basis +@fix_class_docs class ProcessTomography(TomographyExperiment): """Quantum process tomography experiment. diff --git a/qiskit_experiments/library/tomography/qst_experiment.py b/qiskit_experiments/library/tomography/qst_experiment.py index 876f2a61c5..548aaaf447 100644 --- a/qiskit_experiments/library/tomography/qst_experiment.py +++ b/qiskit_experiments/library/tomography/qst_experiment.py @@ -17,12 +17,13 @@ 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 qiskit_experiments.framework import Options, fix_class_docs from .tomography_experiment import TomographyExperiment from .qst_analysis import StateTomographyAnalysis from . import basis +@fix_class_docs class StateTomography(TomographyExperiment): """Quantum state tomography experiment. diff --git a/qiskit_experiments/version.py b/qiskit_experiments/version.py index 212d94db1a..344065fbe1 100644 --- a/qiskit_experiments/version.py +++ b/qiskit_experiments/version.py @@ -66,7 +66,7 @@ def get_version_info(): # up the build under Python 3. full_version = VERSION - if not os.path.exists(os.path.join(os.path.dirname(os.path.dirname(ROOT_DIR)), ".git")): + if not os.path.exists(os.path.join(os.path.dirname(ROOT_DIR), ".git")): return full_version try: release = _minimal_ext_cmd(["git", "tag", "-l", "--points-at", "HEAD"]) @@ -75,7 +75,6 @@ def get_version_info(): if not release: git_revision = git_version() full_version += ".dev0+" + git_revision[:7] - return full_version diff --git a/releasenotes/notes/exp-config-82b596887baaafdb.yaml b/releasenotes/notes/exp-config-82b596887baaafdb.yaml new file mode 100644 index 0000000000..4d83ae9e9f --- /dev/null +++ b/releasenotes/notes/exp-config-82b596887baaafdb.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Add :class:`~qiskit_experiments.framework.ExperimentConfig` dataclass + for storing the configuration of an experiment. This configuration can + be obtained by using the + :meth:`~qiskit_experiments.framework.BaseExperiment.config` property. + Experiments can also be reconstructed from their configuration using + the :meth:`~qiskit_experiments.framework.BaseExperiment.from_config` + class method. diff --git a/test/calibration/experiments/test_drag.py b/test/calibration/experiments/test_drag.py index 48d79d9126..80a140eac1 100644 --- a/test/calibration/experiments/test_drag.py +++ b/test/calibration/experiments/test_drag.py @@ -145,3 +145,11 @@ def test_reps(self): with self.assertRaises(CalibrationError): drag.set_experiment_options(reps=[1, 2, 3, 4]) + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = DragCal(0) + config = exp.config + loaded_exp = DragCal.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/calibration/experiments/test_fine_amplitude.py b/test/calibration/experiments/test_fine_amplitude.py index 0ba4cad1e4..5499ea5210 100644 --- a/test/calibration/experiments/test_fine_amplitude.py +++ b/test/calibration/experiments/test_fine_amplitude.py @@ -257,3 +257,11 @@ def test_run_sx_cal(self): # Requires allclose due to numerical precision. self.assertTrue(np.allclose(sx_cal.blocks[0].pulse.amp, new_amp)) self.assertFalse(np.allclose(sx_cal.blocks[0].pulse.amp, init_amp)) + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = FineSXAmplitudeCal(0, self.cals, "sx") + config = exp.config + loaded_exp = FineSXAmplitudeCal.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/calibration/experiments/test_fine_drag.py b/test/calibration/experiments/test_fine_drag.py index f53f5f7f59..ae57053623 100644 --- a/test/calibration/experiments/test_fine_drag.py +++ b/test/calibration/experiments/test_fine_drag.py @@ -71,3 +71,11 @@ def test_end_to_end_no_schedule(self): exp_data = FineXDrag(0).run(FineDragTestBackend()).block_for_results() self.assertEqual(exp_data.analysis_results(0).quality, "good") + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = FineDrag(0) + config = exp.config + loaded_exp = FineDrag.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index da7bca0be0..e3b06e37ac 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -112,6 +112,14 @@ def test_wrong_processor(self): self.assertEqual(len(result), 0) + def test_experiment_config(self): + """Test converting to and from config works""" + exp = Rabi(0) + config = exp.config + loaded_exp = Rabi.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) + class TestEFRabi(QiskitTestCase): """Test the ef_rabi experiment.""" @@ -153,6 +161,14 @@ def test_ef_rabi_circuit(self): self.assertEqual(circ.data[0][0].name, "x") self.assertEqual(circ.data[1][0].name, "Rabi") + def test_experiment_config(self): + """Test converting to and from config works""" + exp = EFRabi(0) + config = exp.config + loaded_exp = EFRabi.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) + class TestRabiCircuits(QiskitTestCase): """Test the circuits generated by the experiment and the options.""" diff --git a/test/calibration/experiments/test_ramsey_xy.py b/test/calibration/experiments/test_ramsey_xy.py index c2d3892a13..d0550ffc83 100644 --- a/test/calibration/experiments/test_ramsey_xy.py +++ b/test/calibration/experiments/test_ramsey_xy.py @@ -37,3 +37,11 @@ def test_end_to_end(self): test_data = ramsey.run(MockRamseyXY(freq_shift=freq_shift)).block_for_results() meas_shift = test_data.analysis_results(1).value.value self.assertTrue((meas_shift - freq_shift) < abs(test_tol * freq_shift)) + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = RamseyXY(0) + config = exp.config + loaded_exp = RamseyXY.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/calibration/experiments/test_rough_frequency.py b/test/calibration/experiments/test_rough_frequency.py index 1dc2d220ed..55e88b9736 100644 --- a/test/calibration/experiments/test_rough_frequency.py +++ b/test/calibration/experiments/test_rough_frequency.py @@ -67,3 +67,13 @@ def test_update_calibrations(self): # Check the updated frequency which should be shifted by 5MHz. post_freq = cals.get_parameter_value(cals.__qubit_freq_parameter__, (0,)) self.assertTrue(abs(post_freq - freq01 - 5e6) < 1e6) + + def test_experiment_config(self): + """Test converting to and from config works""" + cals = BackendCalibrations(FakeArmonk()) + frequencies = [1, 2, 3] + exp = RoughFrequencyCal(0, cals, frequencies) + config = exp.config + loaded_exp = RoughFrequencyCal.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/quantum_volume/test_qv.py b/test/quantum_volume/test_qv.py index fcc0b1a595..1f615c36d8 100644 --- a/test/quantum_volume/test_qv.py +++ b/test/quantum_volume/test_qv.py @@ -242,3 +242,11 @@ def test_qv_success(self): result.extra[key] == value, "result " + str(key) + " is not the same as the " "pre-calculated analysis", ) + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = QuantumVolume([0, 1, 2], seed=42) + config = exp.config + loaded_exp = QuantumVolume.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/randomized_benchmarking/test_rb.py b/test/randomized_benchmarking/test_rb.py index af593d2bc4..ac6a0c5bc9 100644 --- a/test/randomized_benchmarking/test_rb.py +++ b/test/randomized_benchmarking/test_rb.py @@ -159,6 +159,14 @@ def test_input(self): seed=exp_data["seed"], ) + def test_experiment_config(self): + """Test converting to and from config works""" + exp = StandardRB([0, 1], lengths=[10, 20, 30, 40], num_samples=10) + config = exp.config + loaded_exp = StandardRB.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) + @ddt class TestInterleavedRB(TestStandardRB): @@ -246,3 +254,11 @@ def test_non_clifford_interleaved_element(self): qubits, lengths, ) + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = InterleavedRB(CXGate(), [0, 1], lengths=[10, 20, 30, 40], num_samples=10) + config = exp.config + loaded_exp = InterleavedRB.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/test_cross_resonance_hamiltonian.py b/test/test_cross_resonance_hamiltonian.py index c93d52bf25..78e1f1e047 100644 --- a/test/test_cross_resonance_hamiltonian.py +++ b/test/test_cross_resonance_hamiltonian.py @@ -326,3 +326,18 @@ def test_integration(self, ix, iy, iz, zx, zy, zz): self.assertAlmostEqual(exp_data.analysis_results("omega_zx").value.value, zx, delta=2e4) self.assertAlmostEqual(exp_data.analysis_results("omega_zy").value.value, zy, delta=2e4) self.assertAlmostEqual(exp_data.analysis_results("omega_zz").value.value, zz, delta=2e4) + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = cr_hamiltonian.CrossResonanceHamiltonian( + qubits=(0, 1), + flat_top_widths=[500], + unit="ns", + amp=0.1, + sigma=20, + risefall=2, + ) + config = exp.config + loaded_exp = cr_hamiltonian.CrossResonanceHamiltonian.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index ecf63b015c..e3fe7e9f17 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -148,3 +148,11 @@ def test_spectroscopy12_end2end_classified(self): circ = spec.circuits()[0] self.assertEqual(circ.data[0][0].name, "x") self.assertEqual(circ.data[1][0].name, "Spec") + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = QubitSpectroscopy(1, np.linspace(100, 150, 20), unit="MHz") + config = exp.config + loaded_exp = QubitSpectroscopy.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/test_t1.py b/test/test_t1.py index 649dac95f6..bc2d0c3416 100644 --- a/test/test_t1.py +++ b/test/test_t1.py @@ -175,3 +175,11 @@ def test_t1_low_quality(self): res = T1Analysis()._run_analysis(data)[0][0] self.assertEqual(res.quality, "bad") + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = T1(0, [1, 2, 3, 4, 5], unit="s") + config = exp.config + loaded_exp = T1.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/test_t2ramsey.py b/test/test_t2ramsey.py index 26cef97b3b..4e8f7357fc 100644 --- a/test/test_t2ramsey.py +++ b/test/test_t2ramsey.py @@ -194,3 +194,11 @@ def test_t2ramsey_concat_2_experiments(self): ) self.assertLessEqual(results1[0].value.stderr, results0[0].value.stderr) self.assertEqual(len(expdata1.data()), len(delays0) + len(delays1)) + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = T2Ramsey(0, [1, 2, 3, 4, 5], unit="s") + config = exp.config + loaded_exp = T2Ramsey.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) diff --git a/test/test_tomography.py b/test/test_tomography.py index 2c2a360d27..0948d58d78 100644 --- a/test/test_tomography.py +++ b/test/test_tomography.py @@ -273,6 +273,14 @@ def test_parallel_exp(self): target_fid = qi.state_fidelity(state, targets[i], validate=False) self.assertAlmostEqual(fid, target_fid, places=6, msg="result fidelity is incorrect") + def test_experiment_config(self): + """Test converting to and from config works""" + exp = StateTomography(QuantumCircuit(3), measurement_qubits=[0, 2], qubits=[5, 7, 1]) + config = exp.config + loaded_exp = StateTomography.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) + @ddt.ddt class TestProcessTomography(QiskitTestCase): @@ -483,6 +491,14 @@ def test_parallel_exp(self): target_fid = qi.process_fidelity(state, targets[i], require_tp=False, require_cp=False) self.assertAlmostEqual(fid, target_fid, places=6, msg="result fidelity is incorrect") + def test_experiment_config(self): + """Test converting to and from config works""" + exp = ProcessTomography(teleport_circuit(), measurement_qubits=[2], preparation_qubits=[0]) + config = exp.config + loaded_exp = ProcessTomography.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config) + def teleport_circuit(): """Teleport qubit 0 to qubit 2"""