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
28 changes: 3 additions & 25 deletions qiskit_experiments/curve_analysis/curve_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ class AnalysisExample(CurveAnalysis):

def __init__(self):
"""Initialize data fields that are privately accessed by methods."""
super().__init__()

#: Dict[str, Any]: Experiment metadata
self.__experiment_metadata = None
Expand Down Expand Up @@ -821,35 +822,12 @@ def _get_option(self, arg_name: str) -> Any:
) from ex

def _run_analysis(
self, experiment_data: ExperimentData, **options
self, experiment_data: ExperimentData
) -> Tuple[List[AnalysisResultData], List["pyplot.Figure"]]:
"""Run analysis on circuit data.

Args:
experiment_data: the experiment data to analyze.
options: kwarg options for analysis function.

Returns:
tuple: A pair ``(analysis_results, figures)`` where ``analysis_results``
is a list of :class:`AnalysisResultData` objects, and ``figures``
is a list of any figures for the experiment.

Raises:
AnalysisError: If the analysis fails.
DataProcessorError: When data processing failed.
"""

#
# 1. Parse arguments
#

# Pop arguments that are not given to the fitter,
# and update class attributes with the arguments that are given to the fitter
# (arguments that have matching attributes in the class)
analysis_options = self._default_options().__dict__
analysis_options.update(options)

extra_options = self._arg_parse(**analysis_options)
extra_options = self._arg_parse(**self.options.__dict__)

# Update all fit functions in the series definitions if fixed parameter is defined.
# Fixed parameters should be provided by the analysis options.
Expand Down
4 changes: 3 additions & 1 deletion qiskit_experiments/framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
FitVal
AnalysisResultData
ExperimentConfig
AnalysisConfig
ExperimentEncoder
ExperimentDecoder

Expand Down Expand Up @@ -238,7 +239,8 @@
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, ExperimentConfig
from .base_experiment import BaseExperiment
from .configs import ExperimentConfig, AnalysisConfig
from .analysis_result_data import AnalysisResultData
from .experiment_data import ExperimentData
from .composite import (
Expand Down
105 changes: 88 additions & 17 deletions qiskit_experiments/framework/base_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@
"""

from abc import ABC, abstractmethod
from typing import List, Tuple
import copy
from collections import OrderedDict
from typing import List, Tuple, Union, Dict

from qiskit_experiments.database_service.device_component import Qubit
from qiskit_experiments.framework import Options
from qiskit_experiments.framework.store_init_args import StoreInitArgs
from qiskit_experiments.framework.experiment_data import ExperimentData
from qiskit_experiments.framework.configs import AnalysisConfig
from qiskit_experiments.framework.analysis_result_data import AnalysisResultData
from qiskit_experiments.database_service import DbAnalysisResultV1


class BaseAnalysis(ABC):
class BaseAnalysis(ABC, StoreInitArgs):
"""Abstract base class for analyzing Experiment data.

The data produced by experiments (i.e. subclasses of BaseExperiment)
Expand All @@ -32,17 +36,74 @@ class BaseAnalysis(ABC):
For example, an analysis may perform some data processing of the
measured data and a fit to a function to extract a parameter.

When designing Analysis subclasses default values for any kwarg
analysis options of the `run` method should be set by overriding
the `_default_options` class method. When calling `run` these
default values will be combined with all other option kwargs in the
run method and passed to the `_run_analysis` function.
Analysis subclasses must implement the abstract method `_run_analysis`.
This method should not have side-effects on the analysis class itself
since it could potentially be called asynchronously in multiple threads.
Any configurable option values should be specified in the `_default_options`
class method. These values can be overriden by a user by calling the
`set_options` method or for a single-run can be specified by passing kwarg
options to the :meth:`run` method.
"""

def __init__(self):
"""Initialize the analysis object."""
# Analysis options
self._options = self._default_options()

# Store keys of non-default options
self._set_options = set()

def config(self) -> AnalysisConfig:
"""Return the config dataclass for this analysis"""
args = tuple(getattr(self, "__init_args__", OrderedDict()).values())
kwargs = dict(getattr(self, "__init_kwargs__", OrderedDict()))
# Only store non-default valued options
options = dict((key, getattr(self._options, key)) for key in self._set_options)
return AnalysisConfig(
cls=type(self),
args=args,
kwargs=kwargs,
options=options,
)

@classmethod
def from_config(cls, config: Union[AnalysisConfig, Dict]) -> "BaseAnalysis":
"""Initialize an analysis class from analysis config"""
if isinstance(config, dict):
config = AnalysisConfig(**dict)
ret = cls(*config.args, **config.kwargs)
Comment thread
chriseclectic marked this conversation as resolved.
if config.options:
ret.set_options(**config.options)
return ret

def copy(self) -> "BaseAnalysis":
"""Return a copy of the analysis"""
# We want to avoid a deep copy be default for performance so we
# 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)
ret._options = copy.copy(self._options)
ret._set_options = copy.copy(self._set_options)
return ret

@classmethod
def _default_options(cls) -> Options:
return Options()

@property
def options(self) -> Options:
"""Return the analysis options for :meth:`run` method."""
return self._options

def set_options(self, **fields):
"""Set the analysis options for :meth:`run` method.

Args:
fields: The fields to update the options
"""
self._options.update_options(**fields)
self._set_options = self._set_options.union(fields)

def run(
self,
experiment_data: ExperimentData,
Expand Down Expand Up @@ -96,16 +157,20 @@ def run(
else:
experiment_components = []

# Get analysis options
analysis_options = self._default_options()
analysis_options.update_options(**options)
analysis_options = analysis_options.__dict__
# Set Analysis options
if not options:
analysis = self
else:
analysis = self.copy()
analysis.set_options(**options)

def run_analysis(expdata):
results, figures = self._run_analysis(expdata, **analysis_options)
results, figures = analysis._run_analysis(expdata)
# Add components
analysis_results = [
self._format_analysis_result(result, expdata.experiment_id, experiment_components)
analysis._format_analysis_result(
result, expdata.experiment_id, experiment_components
)
for result in results
]
# Update experiment data with analysis results
Expand Down Expand Up @@ -139,15 +204,13 @@ def _format_analysis_result(self, data, experiment_id, experiment_components=Non

@abstractmethod
def _run_analysis(
self, experiment_data: ExperimentData, **options
self,
experiment_data: ExperimentData,
) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]:
"""Run analysis on circuit data.

Args:
experiment_data: the experiment data to analyze.
options: additional options for analysis. By default the fields and
values in :meth:`options` are used and any provided values
can override these.

Returns:
A pair ``(analysis_results, figures)`` where ``analysis_results``
Expand All @@ -157,4 +220,12 @@ def _run_analysis(
Raises:
AnalysisError: if the analysis fails.
"""
# NOTE: passing kwarg options to _run_analysis should be removed once
pass

def __json_encode__(self):
return self.config()

@classmethod
def __json_decode__(cls, value):
return cls.from_config(value)
61 changes: 6 additions & 55 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

from abc import ABC, abstractmethod
import copy
import dataclasses
from collections import OrderedDict
from typing import Sequence, Optional, Tuple, List, Dict, Union, Any

Expand All @@ -28,60 +27,7 @@
from qiskit.providers.options import Options
from qiskit_experiments.framework.store_init_args import StoreInitArgs
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__

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
from qiskit_experiments.framework.configs import ExperimentConfig


class BaseExperiment(ABC, StoreInitArgs):
Expand Down Expand Up @@ -184,6 +130,11 @@ def copy(self) -> "BaseExperiment":
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:
Expand Down
18 changes: 1 addition & 17 deletions qiskit_experiments/framework/composite/composite_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,7 @@ class CompositeAnalysis(BaseAnalysis):
reconstructed from the parent composite experiment data.
"""

# pylint: disable = arguments-differ
def _run_analysis(self, experiment_data: ExperimentData, **options):
"""Run analysis on composite experiment circuit data.

Args:
experiment_data: the experiment data to analyze.
options: kwarg options for analysis function.

Returns:
tuple: A pair ``(analysis_results, figures)`` where ``analysis_results``
is a list of :class:`AnalysisResultData` objects, and ``figures``
is a list of any figures for the experiment.

Raises:
QiskitError: if analysis is attempted on non-composite
experiment data.
"""
def _run_analysis(self, experiment_data: ExperimentData):
# Extract job metadata for the component experiments so it can be added
# to the child experiment data incase it is required by the child experiments
# analysis classes
Expand Down
Loading