Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion docs/tutorials/quantum_volume.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/randomized_benchmarking.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/state_tomography.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions docs/tutorials/t2ramsey_characterization.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
11 changes: 11 additions & 0 deletions qiskit_experiments/curve_analysis/standard_analysis/resonance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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]]:
Expand Down
3 changes: 0 additions & 3 deletions qiskit_experiments/framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
142 changes: 87 additions & 55 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,

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.

I like it! This will increase flexibility of workflow. However, can we directly set callback to experiment data, i.e. not limited to analysis? For example,

def __init__(self, qubits, backend, experiment_type, callbacks: List[Callable]):

if we run two different analysis, we can write

MyExperiment([0, 1], my_backend, [MyAnalysis1(), MyAnalysis2()])

or we can do whatever we want

MyExperiment([0, 1], my_backend, [MyAnalysis(), my_custom_validate_analysis()])

probably this will be a real headache for serialization.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Lets save this for later discussion since it sounds quite complicated. I would argue that you should do that sort of thing by defining a custom analysis class that can combine other analysis classes to keep experiment class straight forward.

backend: Optional[Backend] = None,
experiment_type: Optional[str] = None,
):
Expand All @@ -72,14 +65,29 @@ 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()
self._set_transpile_options = set()
self._set_run_options = set()
self._set_analysis_options = set()

# Set analysis
self._analysis = None
if analysis:
self.analysis = analysis
Comment thread
chriseclectic marked this conversation as resolved.
# 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
Comment thread
chriseclectic marked this conversation as resolved.

# Set backend
# This should be called last incase `_set_backend` access any of the
# attributes created during initialization
Expand All @@ -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"""
Comment thread
chriseclectic marked this conversation as resolved.
Outdated
return self._analysis

@analysis.setter
def analysis(self, analysis: Union[BaseAnalysis, None]) -> None:
"""Set the backend for the experiment"""
Comment thread
chriseclectic marked this conversation as resolved.
Outdated
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"""
Expand All @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -238,6 +261,11 @@ def run_analysis(

See :meth:`BaseAnalysis.run` for additional information.

.. deprecated:: 0.2.0

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.

We should also deprecate analysis option from BaseExperiment.run because

  • kind of edge case; if analysis is None and run(analysis=True).
  • now BaseExperiment.analysis returns an instance so arg name is really confusing, we may want to set analysis instance rather than bool.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I could change it to analysis="default" as the kwarg, and allow you to pass in a full on analysis class if you wanted as the runtime option (since this could override the stored analysis class for that execution). And of course then you could still do analysis=None/False to not run analysis.

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
Expand All @@ -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."""
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Comment thread
chriseclectic marked this conversation as resolved.
Outdated

def _postprocess_transpiled_circuits(self, circuits: List[QuantumCircuit], **run_options):
"""Additional post-processing of transpiled circuits before running on backend"""
Expand Down Expand Up @@ -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__)

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.

Is this for backward compatibility? It seems like experiment instance doesn't need to have analysis configuration. For example, user can apply arbitrary analysis after experiment is done, then the instance will be agnostic to following analysis.

@chriseclectic chriseclectic Dec 6, 2021

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don't think this is strictly needed but just left it here to be consistent with previous behavior wrt result DB (otherwise custom analysis options are not saved anywhere there).


metadata["job_metadata"] = [values]

def __json_encode__(self):
"""Convert to format that can be JSON serialized"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
class CompositeExperiment(BaseExperiment):
"""Composite Experiment base class"""

__analysis_class__ = CompositeAnalysis

def __init__(
self,
experiments: List[BaseExperiment],
Expand All @@ -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):
Expand Down
Loading