Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion qiskit_experiments/framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
ExperimentData
FitVal
AnalysisResultData
ExperimentConfig

.. _composite-experiment:

Expand Down Expand Up @@ -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 (
Expand Down
163 changes: 159 additions & 4 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
nkanazawa1989 marked this conversation as resolved.
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)
Comment thread
nkanazawa1989 marked this conversation as resolved.
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):
Comment thread
nkanazawa1989 marked this conversation as resolved.
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):
Expand Down Expand Up @@ -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
Expand All @@ -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):
Comment thread
nkanazawa1989 marked this conversation as resolved.
"""Store init args and kwargs for subclass __init__ methods"""
# This method automatically stores all arg and kwargs from subclass
Comment thread
nkanazawa1989 marked this conversation as resolved.
# 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."""
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@chriseclectic This probably was supposed to be **config, right?

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
Comment thread
nkanazawa1989 marked this conversation as resolved.

def run(
self,
backend: Optional[Backend] = None,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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"""
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions qiskit_experiments/framework/composite/batch_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions qiskit_experiments/framework/composite/parallel_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down
3 changes: 2 additions & 1 deletion qiskit_experiments/library/calibration/drag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 4 additions & 1 deletion qiskit_experiments/library/calibration/fine_amplitude.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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."""

Expand Down
5 changes: 3 additions & 2 deletions qiskit_experiments/library/calibration/fine_drag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion qiskit_experiments/library/calibration/rabi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion qiskit_experiments/library/calibration/ramsey_xy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions qiskit_experiments/library/calibration/rough_frequency.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,6 +25,7 @@
)


@fix_class_docs
class RoughFrequencyCal(BaseCalibrationExperiment, QubitSpectroscopy):
"""A calibration experiment that runs QubitSpectroscopy."""

Expand Down Expand Up @@ -68,6 +70,7 @@ def __init__(
)


@fix_class_docs
class RoughEFFrequencyCal(BaseCalibrationExperiment, EFSpectroscopy):
"""A calibration experiment that runs QubitSpectroscopy."""

Expand Down
Loading