From fc550fa03ba3173ab6f75d3133551ba8ef82112c Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Tue, 4 May 2021 17:50:21 -0400 Subject: [PATCH 01/11] Rework option handling in base classes * Reworks how experiment, transpiler, backend/run, and analysis options are handled in the base classes to make it similar to how options are used by qiskit backends. * Removes transpiled_circuits method. Transpiled circuits can now be obtained via `transpile(expr.circuits(backend), backend, **expr.transpile_options)` --- qiskit_experiments/base_analysis.py | 69 +++++-- qiskit_experiments/base_experiment.py | 259 ++++++++++++++------------ 2 files changed, 193 insertions(+), 135 deletions(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 5536e07567..65a6edfbc9 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -15,7 +15,9 @@ from abc import ABC, abstractmethod from typing import List, Tuple +import copy +from qiskit.providers.options import Options from qiskit.exceptions import QiskitError from .experiment_data import ExperimentData, AnalysisResult @@ -27,23 +29,51 @@ class BaseAnalysis(ABC): # Expected experiment data container for analysis __experiment_data__ = ExperimentData - def run( - self, - experiment_data: ExperimentData, - save: bool = True, - return_figures: bool = False, - **options, - ): + def __init__(self, **options): + """Initialize a base analysis class + + Args: + options: kwarg options for analysis. + """ + self._options = self._default_options() + self.set_options(**options) + + @classmethod + def _default_options(cls) -> Options: + return Options() + + def set_options(self, **fields): + """Set the analysis options. + + Args: + fields: The fields to update the options + """ + self._options.update_options(**fields) + + @property + def options(self) -> Options: + """Return the analysis options. + + The options of an analysis class are used to provide kwarg values for + the :meth:`run` method. + """ + return self._options + + def run(self, + experiment_data: ExperimentData, + save: bool = True, + return_figures: bool = False, + **options): """Run analysis and update stored ExperimentData with analysis result. Args: experiment_data: the experiment data to analyze. save: if True save analysis results and figures to the :class:`ExperimentData`. - return_figures: if true return a pair of - ``(analysis_results, figures)``, + return_figures: if true return a pair of ``(analysis_results, figures)``, otherwise return only analysis_results. - options: kwarg options for analysis function. + options: additional analysis options. Any values set here will + override the value from :meth:`options` for the current run. Returns: AnalysisResult: the output of the analysis that produces a @@ -63,10 +93,16 @@ def run( f"Invalid experiment data type, expected {self.__experiment_data__.__name__}" f" but received {type(experiment_data).__name__}" ) + + # Get runtime analysis options + analysis_options = copy.copy(self.options) + analysis_options.update_options(**options) + analysis_options = analysis_options.__dict__ + # Run analysis # pylint: disable=broad-except try: - analysis_results, figures = self._run_analysis(experiment_data, **options) + analysis_results, figures = self._run_analysis(experiment_data, **analysis_options) analysis_results["success"] = True except Exception as ex: analysis_results = AnalysisResult(success=False, error_message=ex) @@ -94,12 +130,13 @@ def _run_analysis( Args: experiment_data: the experiment data to analyze. - options: kwarg options for analysis function. + options: additional options for analysis. By default the fields and + values in :meth:`options` are used and any provided values + can override these. Returns: - tuple: A pair ``(analysis_results, figures)`` where - ``analysis_results`` may be a single or list of - AnalysisResult objects, and ``figures`` is a list of any - figures for the experiment. + A pair ``(analysis_results, figures)`` where ``analysis_results`` + may be a single or list of AnalysisResult objects, and ``figures`` + is a list of any figures for the experiment. """ pass diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 3cafeacd19..9a3ab6ea60 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -15,33 +15,20 @@ from abc import ABC, abstractmethod from typing import Union, Iterable, Optional, Tuple, List +import copy from numbers import Integral +from typing import List, Optional, Iterable, Tuple, Union from qiskit import transpile, assemble, QuantumCircuit +from qiskit.providers.options import Options +from qiskit.providers.backend import Backend +from qiskit.providers.basebackend import BaseBackend as LegacyBackend from qiskit.exceptions import QiskitError from qiskit.providers.backend import Backend from qiskit.providers.basebackend import BaseBackend as LegacyBackend from .experiment_data import ExperimentData -_TRANSPILE_OPTIONS = { - "basis_gates", - "coupling_map", - "backend_properties", - "initial_layout", - "layout_method", - "routing_method", - "translation_method", - "scheduling_method", - "instruction_durations", - "dt", - "seed_transpiler", - "optimization_level", - "pass_manager", - "callback", - "output_name", -} - class BaseExperiment(ABC): """Base Experiment class @@ -61,26 +48,14 @@ class BaseExperiment(ABC): # ExperimentData class for experiment __experiment_data__ = ExperimentData - # Custom default transpiler options for experiment subclasses - __transpile_defaults__ = {"optimization_level": 0} - - # Custom default run (assemble) options for experiment subclasses - __run_defaults__ = {} - - def __init__( - self, - qubits: Union[int, Iterable[int]], - experiment_type: Optional[str] = None, - circuit_options: Optional[Iterable[str]] = None, - ): + def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None, **options): """Initialize the experiment object. Args: - qubits: the number of qubits or list of physical qubits - for the experiment. + qubits: the number of qubits or list of physical qubits for + the experiment. experiment_type: Optional, the experiment type string. - circuit_options: Optional, list of kwarg names for - the subclassed `circuit` method. + options: kwarg options for experiment circuits. Raises: QiskitError: if qubits is a list and contains duplicates. @@ -99,12 +74,21 @@ def __init__( print(self._num_qubits, self._physical_qubits) raise QiskitError("Duplicate qubits in physical qubits list.") - # Store options and values - self._circuit_options = set(circuit_options) if circuit_options else set() + # Experiment options + self._options = self._default_options() + self.set_options(**options) + + # Execution and analysis options + self._transpile_options = self._default_transpile_options() + self._backend_options = self._default_backend_options() + self._analysis_options = self._default_analysis_options() + + # Set initial layout from qubits + self._transpile_options.initial_layout = self._physical_qubits def run( self, - backend: "Backend", + backend: Backend, analysis: bool = True, experiment_data: Optional[ExperimentData] = None, **kwargs, @@ -113,31 +97,29 @@ def run( Args: backend: The backend to run the experiment on. - analysis: If True run analysis on experiment data. - experiment_data: Optional, add results to existing experiment data. - If None a new ExperimentData object will be returned. - kwargs: keyword arguments for self.circuit, qiskit.transpile, and backend.run. + analysis: If True run analysis on the experiment data. + experiment_data: Optional, add results to existing + experiment data. If None a new ExperimentData object will be + returned. + kwargs: runtime keyword arguments for backend.run. Returns: - ExperimentData: the experiment data object. + The experiment data object. """ - # NOTE: This method is intended to be overriden by subclasses if required. - # Create new experiment data if experiment_data is None: experiment_data = self.__experiment_data__(self, backend=backend) - # Filter kwargs - run_options = self.__run_defaults__.copy() - circuit_options = {} - for key, value in kwargs.items(): - if key in _TRANSPILE_OPTIONS or key in self._circuit_options: - circuit_options[key] = value - else: - run_options[key] = value - - # Generate and run circuits - circuits = self.transpiled_circuits(backend, **circuit_options) + # Generate and transpile circuits + circuits = self._transpile( + self.circuits(backend), backend, **self.transpile_options.__dict__ + ) + + # Run circuits on backend + run_options = copy.copy(self.backend_options) + run_options.update_options(**kwargs) + run_options = run_options.__dict__ + if isinstance(backend, LegacyBackend): qobj = assemble(circuits, backend=backend, **run_options) job = backend.run(qobj) @@ -150,7 +132,7 @@ def run( # Queue analysis of data for when job is finished if analysis and self.__analysis_class__ is not None: # pylint: disable = not-callable - self.__analysis_class__().run(experiment_data, **kwargs) + self.analysis(**self.analysis_options.__dict__).run(experiment_data) # Return the ExperimentData future return experiment_data @@ -166,94 +148,133 @@ def physical_qubits(self) -> Tuple[int]: return self._physical_qubits @classmethod - def analysis(cls, **kwargs): - """Return the default Analysis class for the experiment. - - Returns: - BaseAnalysis: the analysis object. - - Raises: - QiskitError: if the experiment does not have a defaul - analysis class. - """ + def analysis(cls, **analysis_options) -> "BaseAnalysis": + """Return the default Analysis class for the experiment.""" if cls.__analysis_class__ is None: - raise QiskitError( - f"Experiment {cls.__name__} does not define" " a default Analysis class" - ) + raise QiskitError(f"Experiment {cls.__name__} does not have a default Analysis class") # pylint: disable = not-callable - return cls.__analysis_class__(**kwargs) + return cls.__analysis_class__(**analysis_options) @abstractmethod - def circuits( - self, backend: Optional[Backend] = None, **circuit_options - ) -> List[QuantumCircuit]: + def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: """Return a list of experiment circuits. Args: backend: Optional, a backend object. - circuit_options: kwarg options for the function. Returns: - A list of :class:`QuantumCircuit`. + A list of :class:`QuantumCircuit`s. .. note:: These circuits should be on qubits ``[0, .., N-1]`` for an *N*-qubit experiment. The circuits mapped to physical qubits are obtained via the :meth:`transpiled_circuits` method. """ - # NOTE: Subclasses should override this method with explicit - # kwargs for any circuit options rather than use `**circuit_options`. - # This allows these options to have default values, and be - # documented in the methods docstring for the API docs. + # NOTE: Subclasses should override this method using the `options` + # values for any explicit experiment options that effect circuit + # generation - def transpiled_circuits( - self, backend: Optional[Backend] = None, **kwargs + def _transpile( + self, + circuits: Union[QuantumCircuit, List[QuantumCircuit]], + backend: Optional[Backend] = None, + **transpile_options, ) -> List[QuantumCircuit]: - """Return a list of experiment circuits. + """Custom transpilation of circuits for running on backend. + + Subclasses may modify this method if they need to customize how + transpilation is done, for example to update metadata in the + transpiled circuits. + """ + return transpile(circuits, backend=backend, **transpile_options) + + @classmethod + def _default_options(cls) -> Options: + """Default kwarg options for experiment""" + return Options() + + @property + def options(self) -> Options: + """Return the options for the experiment.""" + return self._options + + def set_options(self, **fields): + """Set the experiment options. Args: - backend: Optional, a backend object to use as the - argument for the :func:`qiskit.transpile` - function. - kwargs: kwarg options for the :meth:`circuits` method, and - :func:`qiskit.transpile` function. + fields: The fields to update the options - Returns: - A list of :class:`QuantumCircuit`. + Raises: + AttributeError: If the field passed in is not a supported options + """ + for field in fields: + if not hasattr(self._options, field): + raise AttributeError( + f"Options field {field} is not valid for {type(self).__name__}" + ) + self._options.update_options(**fields) + + @classmethod + def _default_transpile_options(cls) -> Options: + """Default transpiler options for transpilation of circuits""" + return Options(optimization_level=0) + + @property + def transpile_options(self) -> Options: + """Return the transpiler options for the :meth:`run` method.""" + return self._transpile_options + + def set_transpile_options(self, **fields): + """Set the transpiler options for :meth:`run` method. + + Args: + fields: The fields to update the options Raises: - QiskitError: if an initial layout is specified in the - kwarg options for transpilation. The initial - layout must be generated from the experiment. + QiskitError: if `initial_layout` is one of the fields. + """ + if "initial_layout" in fields: + raise QiskitError( + "Initial layout cannot be specified as a transpile option" + " as it is determined by the experiment physical qubits." + ) + self._transpile_options.update_options(**fields) - .. note:: - These circuits should be on qubits ``[0, .., N-1]`` for an - *N*-qubit experiment. The circuits mapped to physical qubits - are obtained via the :meth:`transpiled_circuits` method. + @classmethod + def _default_backend_options(cls) -> Options: + """Default backend options for running experiment""" + return Options() + + @property + def backend_options(self) -> Options: + """Return the backend options for the :meth:`run` method.""" + return self._backend_options + + def set_backend_options(self, **fields): + """Set the backend options for the :meth:`run` method. + + Args: + fields: The fields to update the options """ - # Filter kwargs to circuit and transpile options - circuit_options = {} - transpile_options = self.__transpile_defaults__.copy() - for key, value in kwargs.items(): - valid_key = False - if key in self._circuit_options: - circuit_options[key] = value - valid_key = True - if key in _TRANSPILE_OPTIONS: - transpile_options[key] = value - valid_key = True - if not valid_key: - raise QiskitError( - f"{key} is not a valid kwarg for" f" {self.circuits} or {transpile}" - ) + self._backend_options.update_options(**fields) - # Generate circuits - circuits = self.circuits(backend=backend, **circuit_options) + @classmethod + def _default_analysis_options(cls) -> Options: + """Default transpiler options for transpilation of circuits""" + # These should typically be set by the analysis class default options + if cls.__analysis_class__: + return cls.__analysis_class__._default_options() + return None - # Transpile circuits - if "initial_layout" in transpile_options: - raise QiskitError("Initial layout must be specified by the Experiement.") - transpile_options["initial_layout"] = self.physical_qubits - circuits = transpile(circuits, backend=backend, **transpile_options) + @property + def analysis_options(self) -> Options: + """Return the analysis options for :meth:`run` analysis.""" + return self._analysis_options - return circuits + def set_analysis_options(self, **fields): + """Set the analysis options for :meth:`run` method. + + Args: + fields: The fields to update the options + """ + self._analysis_options.update_options(**fields) From aabfb62b93379afce79a5b451d2e0ad754330d87 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Tue, 4 May 2021 17:51:33 -0400 Subject: [PATCH 02/11] Update experiments --- .../characterization/t1_experiment.py | 66 +++++++++++-------- .../composite/batch_experiment.py | 4 +- .../composite/composite_analysis.py | 12 +++- .../composite/composite_experiment.py | 13 ++-- .../composite/parallel_experiment.py | 4 +- .../randomized_benchmarking/rb_analysis.py | 18 ++++- .../randomized_benchmarking/rb_experiment.py | 21 ------ 7 files changed, 75 insertions(+), 63 deletions(-) diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index e8dd1822cc..40d4b1980a 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -19,6 +19,7 @@ from qiskit.providers import Backend from qiskit.circuit import QuantumCircuit from qiskit.utils import apply_prefix +from qiskit.providers.options import Options from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.base_analysis import BaseAnalysis @@ -29,9 +30,29 @@ class T1Analysis(BaseAnalysis): - """T1 Experiment result analysis class.""" + """T1 Experiment result analysis class. - # pylint: disable=arguments-differ, unused-argument + Analysis Options: + t1_guess (float): Optional, an initial guess of T1 + amplitude_guess (float): Optional, an initial guess of the coefficient of the exponent + offset_guess (float): Optional, an initial guess of the offset + t1_bounds (list of two floats): Optional, lower bound and upper bound to T1 + amplitude_bounds (list of two floats): Optional, lower bound and upper bound to the amplitude + offset_bounds (list of two floats): Optional, lower bound and upper bound to the offset + """ + + @classmethod + def _default_options(cls): + return Options( + t1_guess=None, + amplitude_guess=None, + offset_guess=None, + t1_bounds=None, + amplitude_bounds=None, + offset_bounds=None, + ) + + # pylint: disable=arguments-differ def _run_analysis( self, experiment_data, @@ -54,23 +75,15 @@ def _run_analysis( amplitude_guess (float): Optional, an initial guess of the coefficient of the exponent offset_guess (float): Optional, an initial guess of the offset - t1_bounds (list of two floats): Optional, lower bound and upper - bound to T1 - amplitude_bounds (list of two floats): Optional, lower bound and - upper bound to the amplitude - offset_bounds (list of two floats): Optional, lower bound and upper - bound to the offset - plot: If True generate a plot of fitted data. - ax: Optional, matplotlib axis to add plot to. - kwargs: Trailing unused function parameters + t1_bounds (list of two floats): Optional, lower bound and upper bound to T1 + amplitude_bounds (list of two floats): Optional, lower bound and upper bound to the amplitude + offset_bounds (list of two floats): Optional, lower bound and upper bound to the offset Returns: The analysis result with the estimated T1 """ - data = experiment_data.data() - unit = data[0]["metadata"]["unit"] - conversion_factor = data[0]["metadata"].get("dt_factor", None) - qubit = data[0]["metadata"]["qubit"] + unit = experiment_data._data[0]["metadata"]["unit"] + conversion_factor = experiment_data._data[0]["metadata"].get("dt_factor", None) if conversion_factor is None: conversion_factor = 1 if unit == "s" else apply_prefix(1, unit) @@ -190,6 +203,10 @@ class T1Experiment(BaseExperiment): __analysis_class__ = T1Analysis + @classmethod + def _default_options(cls) -> Options: + return Options(delays=None, unit="s") + def __init__( self, qubit: int, @@ -210,13 +227,9 @@ def __init__( """ if len(delays) < 3: raise ValueError("T1 experiment: number of delays must be at least 3") + super().__init__([qubit], delays=delays, unit=unit) - self._delays = delays - self._unit = unit - super().__init__([qubit]) - - # pylint: disable=arguments-differ - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self, backend: Optional["Backend"] = None) -> List[QuantumCircuit]: """ Return a list of experiment circuits @@ -229,8 +242,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: Raises: AttributeError: if unit is dt but dt parameter is missing in the backend configuration """ - - if self._unit == "dt": + if self.options.unit == "dt": try: dt_factor = getattr(backend.configuration(), "dt") except AttributeError as no_dt: @@ -238,11 +250,11 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: circuits = [] - for delay in self._delays: + for delay in self.options.delays: circ = QuantumCircuit(1, 1) circ.x(0) circ.barrier(0) - circ.delay(delay, 0, self._unit) + circ.delay(delay, 0, self.options.unit) circ.barrier(0) circ.measure(0, 0) @@ -250,10 +262,10 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: "experiment_type": self._type, "qubit": self.physical_qubits[0], "xval": delay, - "unit": self._unit, + "unit": self.options.unit, } - if self._unit == "dt": + if self.options.unit == "dt": circ.metadata["dt_factor"] = dt_factor circuits.append(circ) diff --git a/qiskit_experiments/composite/batch_experiment.py b/qiskit_experiments/composite/batch_experiment.py index b96c7dc474..c6d0a71ada 100644 --- a/qiskit_experiments/composite/batch_experiment.py +++ b/qiskit_experiments/composite/batch_experiment.py @@ -41,7 +41,7 @@ def __init__(self, experiments): qubits = tuple(self._qubit_map.keys()) super().__init__(experiments, qubits) - def circuits(self, backend=None, **circuit_options): + def circuits(self, backend=None): batch_circuits = [] @@ -51,7 +51,7 @@ def circuits(self, backend=None, **circuit_options): qubit_mapping = None else: qubit_mapping = [self._qubit_map[qubit] for qubit in expr.physical_qubits] - for circuit in expr.circuits(**circuit_options): + for circuit in expr.circuits(backend): # Update metadata circuit.metadata = { "experiment_type": self._type, diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 265c3d9c03..b388804971 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -40,8 +40,16 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): QiskitError: if analysis is attempted on non-composite experiment data. """ - # Run analysis for sub-experiments and add sub-experiment metadata - # as result of batch experiment + if not isinstance(experiment_data, CompositeExperimentData): + raise QiskitError("CompositeAnalysis must be run on CompositeExperimentData.") + + # Run analysis for sub-experiments + for expr, expr_data in zip( + experiment_data._experiment._experiments, experiment_data._composite_expdata + ): + expr.analysis(**expr.analysis_options.__dict__).run(expr_data, **options) + + # Add sub-experiment metadata as result of batch experiment # Note: if Analysis results had ID's these should be included here # rather than just the sub-experiment IDs sub_types = [] diff --git a/qiskit_experiments/composite/composite_experiment.py b/qiskit_experiments/composite/composite_experiment.py index 0256e1f59c..70b1583b2d 100644 --- a/qiskit_experiments/composite/composite_experiment.py +++ b/qiskit_experiments/composite/composite_experiment.py @@ -26,7 +26,7 @@ class CompositeExperiment(BaseExperiment): __analysis_class__ = CompositeAnalysis __experiment_data__ = CompositeExperimentData - def __init__(self, experiments, qubits, experiment_type=None, circuit_options=None): + def __init__(self, experiments, qubits, experiment_type=None): """Initialize the composite experiment object. Args: @@ -34,16 +34,13 @@ def __init__(self, experiments, qubits, experiment_type=None, circuit_options=No qubits (int or Iterable[int]): the number of qubits or list of physical qubits for the experiment. experiment_type (str): Optional, composite experiment subclass name. - circuit_options (str): Optional, Optional, dictionary of allowed - kwargs and default values for the `circuit` - method. """ self._experiments = experiments self._num_experiments = len(experiments) - super().__init__(qubits, experiment_type=experiment_type, circuit_options=circuit_options) + super().__init__(qubits, experiment_type=experiment_type) @abstractmethod - def circuits(self, backend=None, **circuit_options): + def circuits(self, backend=None): pass @property @@ -55,6 +52,6 @@ def component_experiment(self, index): """Return the component Experiment object""" return self._experiments[index] - def component_analysis(self, index, **kwargs): + def component_analysis(self, index, **analysis_options): """Return the component experiment Analysis object""" - return self.component_experiment(index).analysis(**kwargs) + return self.component_experiment(index).analysis(**analysis_options) diff --git a/qiskit_experiments/composite/parallel_experiment.py b/qiskit_experiments/composite/parallel_experiment.py index 152709df27..f6a280350d 100644 --- a/qiskit_experiments/composite/parallel_experiment.py +++ b/qiskit_experiments/composite/parallel_experiment.py @@ -32,7 +32,7 @@ def __init__(self, experiments): qubits += exp.physical_qubits super().__init__(experiments, qubits) - def circuits(self, backend=None, **circuit_options): + def circuits(self, backend=None): sub_circuits = [] sub_qubits = [] @@ -42,7 +42,7 @@ def circuits(self, backend=None, **circuit_options): # Generate data for combination for expr in self._experiments: # Add subcircuits - circs = expr.circuits(**circuit_options) + circs = expr.circuits(backend) sub_circuits.append(circs) sub_size.append(len(circs)) diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index 61d16b8140..916b819fad 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -14,7 +14,9 @@ """ from typing import Optional, List +import numpy as np +from qiskit.providers.options import Options from qiskit_experiments.base_analysis import BaseAnalysis, ExperimentData from qiskit_experiments.analysis.curve_fitting import curve_fit, process_curve_data from qiskit_experiments.analysis.data_processing import ( @@ -30,7 +32,21 @@ class RBAnalysis(BaseAnalysis): - """RB Analysis class.""" + """RB Analysis class. + + Analysis Options: + p0: Optional, initial parameter values for curve_fit. + plot: If True generate a plot of fitted data. + ax: Optional, matplotlib axis to add plot to. + """ + + @classmethod + def _default_options(cls): + return Options( + p0=None, + plot=True, + ax=None, + ) # pylint: disable = arguments-differ, invalid-name def _run_analysis( diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index 21f22232f1..154783384e 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -78,27 +78,6 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: circuits += self._sample_circuits(self._lengths, seed=self._rng) return circuits - def transpiled_circuits( - self, backend: Optional[Backend] = None, **kwargs - ) -> List[QuantumCircuit]: - """Return a list of transpiled RB circuits. - - Args: - backend: Optional, a backend object to use as the - argument for the :func:`qiskit.transpile` function. - kwargs: kwarg options for the :func:`qiskit.transpile` function. - - Returns: - A list of :class:`QuantumCircuit`. - - Raises: - QiskitError: if an initial layout is specified in the - kwarg options for transpilation. The initial - layout must be generated from the experiment. - """ - circuits = super().transpiled_circuits(backend=backend, **kwargs) - return circuits - def _sample_circuits( self, lengths: Iterable[int], seed: Optional[Union[int, Generator]] = None ) -> List[QuantumCircuit]: From e111c590bd8ab092550e10efc362f3bbba3e3f82 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Thu, 6 May 2021 12:37:40 -0400 Subject: [PATCH 03/11] Add more documentation --- qiskit_experiments/base_experiment.py | 13 +++++++++-- .../characterization/t1_experiment.py | 8 ++++++- .../randomized_benchmarking/rb_analysis.py | 1 - .../randomized_benchmarking/rb_experiment.py | 23 ++++++++++++------- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 9a3ab6ea60..daa5b2d0cd 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -191,6 +191,10 @@ def _transpile( @classmethod def _default_options(cls) -> Options: """Default kwarg options for experiment""" + # Experiment subclasses should override this method to return + # an `Options` object containing all the supported options for + # that experiment and their default values. Only options listed + # here can be modified later by the `set_options` method. return Options() @property @@ -217,6 +221,9 @@ def set_options(self, **fields): @classmethod def _default_transpile_options(cls) -> Options: """Default transpiler options for transpilation of circuits""" + # Experiment subclasses can override this method if they need + # to set specific transpiler options defaults for running the + # experiment. return Options(optimization_level=0) @property @@ -260,8 +267,10 @@ def set_backend_options(self, **fields): @classmethod def _default_analysis_options(cls) -> Options: - """Default transpiler options for transpilation of circuits""" - # These should typically be set by the analysis class default 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 None diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index 40d4b1980a..0819c284e4 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -199,7 +199,13 @@ def _format_plot(cls, ax, analysis_result, qubit=None, add_label=True): class T1Experiment(BaseExperiment): - """T1 experiment class""" + """T1 experiment class. + + Experiment Options: + delays: delay times of the experiments + unit: Optional, unit of the delay times. Supported units are + 's', 'ms', 'us', 'ns', 'ps', 'dt'. + """ __analysis_class__ = T1Analysis diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index 916b819fad..50b7a45a00 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -14,7 +14,6 @@ """ from typing import Optional, List -import numpy as np from qiskit.providers.options import Options from qiskit_experiments.base_analysis import BaseAnalysis, ExperimentData diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index 154783384e..b63e841449 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -20,6 +20,7 @@ from qiskit import QuantumCircuit from qiskit.providers import Backend from qiskit.quantum_info import Clifford +from qiskit.providers.options import Options from qiskit_experiments.base_experiment import BaseExperiment from .rb_analysis import RBAnalysis @@ -27,7 +28,12 @@ class RBExperiment(BaseExperiment): - """RB Experiment class""" + """RB Experiment class. + + Experiment Options: + lengths: A list of RB sequences lengths. + num_samples: number of samples to generate for each sequence length. + """ # Analysis class for experiment __analysis_class__ = RBAnalysis @@ -46,8 +52,7 @@ def __init__( qubits: the number of qubits or list of physical qubits for the experiment. lengths: A list of RB sequences lengths. - num_samples: number of samples to generate for each - sequence length + num_samples: number of samples to generate for each sequence length. seed: Seed or generator object for random number generation. If None default_rng will be used. full_sampling: If True all Cliffords are independently sampled for @@ -59,11 +64,13 @@ def __init__( self._rng = default_rng(seed=seed) else: self._rng = seed - self._lengths = list(lengths) - self._num_samples = num_samples self._full_sampling = full_sampling self._clifford_utils = CliffordUtils() - super().__init__(qubits) + super().__init__(qubits, lengths=list(lengths), num_samples=num_samples) + + @classmethod + def _default_options(cls): + return Options(lengths=None, num_samples=None) # pylint: disable = arguments-differ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: @@ -74,8 +81,8 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: A list of :class:`QuantumCircuit`. """ circuits = [] - for _ in range(self._num_samples): - circuits += self._sample_circuits(self._lengths, seed=self._rng) + for _ in range(self.options.num_samples): + circuits += self._sample_circuits(self.options.lengths, seed=self._rng) return circuits def _sample_circuits( From 17c03af450e1e9a0cc1cbb862d37a409d1d15041 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Fri, 7 May 2021 11:29:53 -0400 Subject: [PATCH 04/11] Simply analysis options * Remove `set_options` from `BaseAnalysis` * Add `run_analysis` method to `BaseExperiment` that uses experiment `analysis_options`. * Rename `backend_options` to `run_options` --- qiskit_experiments/base_analysis.py | 63 +++++--------- qiskit_experiments/base_experiment.py | 82 +++++++++++++------ .../characterization/t1_experiment.py | 7 +- .../composite/composite_analysis.py | 2 +- .../randomized_benchmarking/rb_experiment.py | 15 +++- 5 files changed, 94 insertions(+), 75 deletions(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 65a6edfbc9..cd94cf6957 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -24,56 +24,34 @@ class BaseAnalysis(ABC): - """Base Analysis class for analyzing Experiment data.""" + """Base Analysis class for analyzing Experiment data. + + 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 combined with all other option kwargs in the + run method and passed to the `_run_analysis` function. + """ # Expected experiment data container for analysis __experiment_data__ = ExperimentData - def __init__(self, **options): - """Initialize a base analysis class - - Args: - options: kwarg options for analysis. - """ - self._options = self._default_options() - self.set_options(**options) - @classmethod def _default_options(cls) -> Options: return Options() - def set_options(self, **fields): - """Set the analysis options. - - Args: - fields: The fields to update the options - """ - self._options.update_options(**fields) - - @property - def options(self) -> Options: - """Return the analysis options. - - The options of an analysis class are used to provide kwarg values for - the :meth:`run` method. - """ - return self._options - - def run(self, - experiment_data: ExperimentData, - save: bool = True, - return_figures: bool = False, - **options): - """Run analysis and update stored ExperimentData with analysis result. + def run(self, experiment_data, save=True, return_figures=False, **options): + """Run analysis and update ExperimentData with analysis result. Args: - experiment_data: the experiment data to analyze. - save: if True save analysis results and figures to the - :class:`ExperimentData`. - return_figures: if true return a pair of ``(analysis_results, figures)``, - otherwise return only analysis_results. - options: additional analysis options. Any values set here will - override the value from :meth:`options` for the current run. + experiment_data (ExperimentData): the experiment data to analyze. + save (bool): if True save analysis results and figures to the + :class:`ExperimentData`. + return_figures (bool): if true return a pair of + ``(analysis_results, figures)``, + otherwise return only analysis_results. + options: additional analysis options. See class documentation for + supported options. Returns: AnalysisResult: the output of the analysis that produces a @@ -93,9 +71,8 @@ def run(self, f"Invalid experiment data type, expected {self.__experiment_data__.__name__}" f" but received {type(experiment_data).__name__}" ) - - # Get runtime analysis options - analysis_options = copy.copy(self.options) + # Get analysis options + analysis_options = self._default_options() analysis_options.update_options(**options) analysis_options = analysis_options.__dict__ diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index daa5b2d0cd..9dc8097683 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -48,14 +48,13 @@ class BaseExperiment(ABC): # ExperimentData class for experiment __experiment_data__ = ExperimentData - def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None, **options): + def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None): """Initialize the experiment object. Args: qubits: the number of qubits or list of physical qubits for the experiment. experiment_type: Optional, the experiment type string. - options: kwarg options for experiment circuits. Raises: QiskitError: if qubits is a list and contains duplicates. @@ -76,11 +75,8 @@ def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None, # Experiment options self._options = self._default_options() - self.set_options(**options) - - # Execution and analysis options self._transpile_options = self._default_transpile_options() - self._backend_options = self._default_backend_options() + self._run_options = self._default_run_options() self._analysis_options = self._default_analysis_options() # Set initial layout from qubits @@ -91,7 +87,7 @@ def run( backend: Backend, analysis: bool = True, experiment_data: Optional[ExperimentData] = None, - **kwargs, + **run_options, ) -> ExperimentData: """Run an experiment and perform analysis. @@ -101,7 +97,7 @@ def run( experiment_data: Optional, add results to existing experiment data. If None a new ExperimentData object will be returned. - kwargs: runtime keyword arguments for backend.run. + run_options: backend runtime options used for circuit execution. Returns: The experiment data object. @@ -116,27 +112,61 @@ def run( ) # Run circuits on backend - run_options = copy.copy(self.backend_options) - run_options.update_options(**kwargs) - run_options = run_options.__dict__ + run_opts = copy.copy(self.run_options) + run_opts.update_options(**run_options) + run_opts = run_opts.__dict__ if isinstance(backend, LegacyBackend): - qobj = assemble(circuits, backend=backend, **run_options) + qobj = assemble(circuits, backend=backend, **run_opts) job = backend.run(qobj) else: - job = backend.run(circuits, **run_options) + job = backend.run(circuits, **run_opts) # Add Job to ExperimentData experiment_data.add_data(job) # Queue analysis of data for when job is finished if analysis and self.__analysis_class__ is not None: - # pylint: disable = not-callable - self.analysis(**self.analysis_options.__dict__).run(experiment_data) + self.run_analysis(experiment_data) # Return the ExperimentData future return experiment_data + def run_analysis( + self, experiment_data, save=True, return_figures=False, **options + ) -> ExperimentData: + """Run analysis and update ExperimentData with analysis result. + + Args: + experiment_data (ExperimentData): the experiment data to analyze. + save (bool): if True save analysis results and figures to the + :class:`ExperimentData`. + return_figures (bool): if true return a pair of + ``(analysis_results, figures)``, + otherwise return only analysis_results. + options: additional analysis options. Any values set here will + override the value from :meth:`analysis_options` + for the current run. + + Returns: + The updated experiment data containing the analysis results and figures. + + Raises: + QiskitError: if experiment_data container is not valid for analysis. + """ + if self.__analysis_class__ is None: + raise QiskitError(f"Experiment {self._type} does not have a default Analysis class") + + # 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_class__() + analysis.run(experiment_data, **analysis_options) + return experiment_data + @property def num_qubits(self) -> int: """Return the number of qubits for this experiment.""" @@ -148,12 +178,12 @@ def physical_qubits(self) -> Tuple[int]: return self._physical_qubits @classmethod - def analysis(cls, **analysis_options) -> "BaseAnalysis": + def analysis(cls, **kwargs) -> "BaseAnalysis": """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__(**analysis_options) + return cls.__analysis_class__(**kwargs) @abstractmethod def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: @@ -248,22 +278,22 @@ def set_transpile_options(self, **fields): self._transpile_options.update_options(**fields) @classmethod - def _default_backend_options(cls) -> Options: - """Default backend options for running experiment""" + def _default_run_options(cls) -> Options: + """Default options values for the experiment :meth:`run` method.""" return Options() @property - def backend_options(self) -> Options: - """Return the backend options for the :meth:`run` method.""" - return self._backend_options + def run_options(self) -> Options: + """Return options values for the experiment :meth:`run` method.""" + return self._run_options - def set_backend_options(self, **fields): - """Set the backend options for the :meth:`run` method. + def set_run_options(self, **fields): + """Set options values for the experiment :meth:`run` method. Args: fields: The fields to update the options """ - self._backend_options.update_options(**fields) + self._run_options.update_options(**fields) @classmethod def _default_analysis_options(cls) -> Options: @@ -273,7 +303,7 @@ def _default_analysis_options(cls) -> Options: # from the Analysis subclass `_default_options` values. if cls.__analysis_class__: return cls.__analysis_class__._default_options() - return None + return Options() @property def analysis_options(self) -> Options: diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index 0819c284e4..8a24f305b1 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -233,7 +233,12 @@ def __init__( """ if len(delays) < 3: raise ValueError("T1 experiment: number of delays must be at least 3") - super().__init__([qubit], delays=delays, unit=unit) + + # Initialize base experiment + super().__init__([qubit]) + + # Set experiment options + self.set_options(delays=delays, unit=unit) def circuits(self, backend: Optional["Backend"] = None) -> List[QuantumCircuit]: """ diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index b388804971..d96765e27c 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -47,7 +47,7 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): for expr, expr_data in zip( experiment_data._experiment._experiments, experiment_data._composite_expdata ): - expr.analysis(**expr.analysis_options.__dict__).run(expr_data, **options) + expr.run_analysis(expr_data, **options) # Add sub-experiment metadata as result of batch experiment # Note: if Analysis results had ID's these should be included here diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index b63e841449..96b9fb393f 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -29,7 +29,7 @@ class RBExperiment(BaseExperiment): """RB Experiment class. - + Experiment Options: lengths: A list of RB sequences lengths. num_samples: number of samples to generate for each sequence length. @@ -60,13 +60,20 @@ def __init__( sequences are constructed by appending additional Clifford samples to shorter sequences. """ + # Initialize base experiment + super().__init__(qubits) + + # Set configurable options + self.set_options(lengths=list(lengths), num_samples=num_samples) + + # Set fixed options + self._full_sampling = full_sampling + self._clifford_utils = CliffordUtils() + if not isinstance(seed, Generator): self._rng = default_rng(seed=seed) else: self._rng = seed - self._full_sampling = full_sampling - self._clifford_utils = CliffordUtils() - super().__init__(qubits, lengths=list(lengths), num_samples=num_samples) @classmethod def _default_options(cls): From 86ed278c21c23c12756e96ab51569edca110605a Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Fri, 7 May 2021 11:37:59 -0400 Subject: [PATCH 05/11] Fixup `run_analysis` --- qiskit_experiments/base_experiment.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 9dc8097683..c32f02645c 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -132,18 +132,11 @@ def run( # Return the ExperimentData future return experiment_data - def run_analysis( - self, experiment_data, save=True, return_figures=False, **options - ) -> ExperimentData: + def run_analysis(self, experiment_data, **options) -> ExperimentData: """Run analysis and update ExperimentData with analysis result. Args: experiment_data (ExperimentData): the experiment data to analyze. - save (bool): if True save analysis results and figures to the - :class:`ExperimentData`. - return_figures (bool): if true return a pair of - ``(analysis_results, figures)``, - otherwise return only analysis_results. options: additional analysis options. Any values set here will override the value from :meth:`analysis_options` for the current run. @@ -163,8 +156,9 @@ def run_analysis( analysis_options = analysis_options.__dict__ # Run analysis + # pylint: disable = not-callable analysis = self.__analysis_class__() - analysis.run(experiment_data, **analysis_options) + analysis.run(experiment_data, save=True, return_figures=False, **analysis_options) return experiment_data @property From df283aa30c827bfaba07cc6b8d0ea4bae0046f47 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Thu, 20 May 2021 10:30:28 -0400 Subject: [PATCH 06/11] Fix rebase / doc errors --- qiskit_experiments/analysis/plotting.py | 2 +- qiskit_experiments/base_analysis.py | 34 +++++++++------ .../characterization/t1_experiment.py | 43 ++++++++++++------- .../composite/composite_analysis.py | 3 +- .../randomized_benchmarking/rb_analysis.py | 20 ++++----- test/data_processing/fake_experiment.py | 2 +- 6 files changed, 59 insertions(+), 45 deletions(-) diff --git a/qiskit_experiments/analysis/plotting.py b/qiskit_experiments/analysis/plotting.py index b9f4150977..fdaf0733ef 100644 --- a/qiskit_experiments/analysis/plotting.py +++ b/qiskit_experiments/analysis/plotting.py @@ -35,7 +35,7 @@ def plot_curve_fit( ): """Generate plot of a curve fit analysis result. - Wraps ``matplotlib.pyplot.plot``. + Wraps :func:`matplotlib.pyplot.plot`. Args: func: the fit function for curve_fit. diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index cd94cf6957..a534edc185 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -15,12 +15,14 @@ from abc import ABC, abstractmethod from typing import List, Tuple -import copy from qiskit.providers.options import Options from qiskit.exceptions import QiskitError -from .experiment_data import ExperimentData, AnalysisResult +from qiskit_experiments.experiment_data import ExperimentData, AnalysisResult + +# pylint: disable = unused-import +from qiskit_experiments.matplotlib import pyplot class BaseAnalysis(ABC): @@ -40,25 +42,29 @@ class BaseAnalysis(ABC): def _default_options(cls) -> Options: return Options() - def run(self, experiment_data, save=True, return_figures=False, **options): + def run( + self, + experiment_data: ExperimentData, + save: bool = True, + return_figures: bool = False, + **options, + ): """Run analysis and update ExperimentData with analysis result. Args: - experiment_data (ExperimentData): the experiment data to analyze. - save (bool): if True save analysis results and figures to the - :class:`ExperimentData`. - return_figures (bool): if true return a pair of - ``(analysis_results, figures)``, - otherwise return only analysis_results. + experiment_data: the experiment data to analyze. + save: if True save analysis results and figures to the + :class:`ExperimentData`. + return_figures: if true return a pair of + ``(analysis_results, figures)``, + otherwise return only analysis_results. options: additional analysis options. See class documentation for supported options. Returns: - AnalysisResult: the output of the analysis that produces a - single result. List[AnalysisResult]: the output for analysis that produces multiple results. - tuple: If ``return_figures=True`` the output is a pair + Tuple: If ``return_figures=True`` the output is a pair ``(analysis_results, figures)`` where ``analysis_results`` may be a single or list of :class:`AnalysisResult` objects, and ``figures`` may be None, a single figure, or a list of figures. @@ -101,8 +107,8 @@ def run(self, experiment_data, save=True, return_figures=False, **options): @abstractmethod def _run_analysis( - self, data: ExperimentData, **options - ) -> Tuple[List[AnalysisResult], List["matplotlib.figure.Figure"]]: + self, experiment_data: ExperimentData, **options + ) -> Tuple[List[AnalysisResult], List["pyplot.Figure"]]: """Run analysis on circuit data. Args: diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index 8a24f305b1..38c8398871 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -33,12 +33,17 @@ class T1Analysis(BaseAnalysis): """T1 Experiment result analysis class. Analysis Options: - t1_guess (float): Optional, an initial guess of T1 - amplitude_guess (float): Optional, an initial guess of the coefficient of the exponent - offset_guess (float): Optional, an initial guess of the offset - t1_bounds (list of two floats): Optional, lower bound and upper bound to T1 - amplitude_bounds (list of two floats): Optional, lower bound and upper bound to the amplitude - offset_bounds (list of two floats): Optional, lower bound and upper bound to the offset + + * t1_guess (float): Optional, an initial guess of T1. + * amplitude_guess (float): Optional, an initial guess of the + coefficient of the exponent. + * offset_guess (float): Optional, an initial guess of the offset. + * t1_bounds (list of two floats): Optional, lower bound and upper + bound to T1. + * amplitude_bounds (list of two floats): Optional, lower bound and upper + bound to the amplitude. + * offset_bounds (list of two floats): Optional, lower bound and + upper bound to the offset. """ @classmethod @@ -64,7 +69,6 @@ def _run_analysis( offset_bounds=None, plot=True, ax=None, - **kwargs, ) -> Tuple[AnalysisResult, List["matplotlib.figure.Figure"]]: """ Calculate T1 @@ -73,17 +77,24 @@ def _run_analysis( experiment_data (ExperimentData): the experiment data to analyze t1_guess (float): Optional, an initial guess of T1 amplitude_guess (float): Optional, an initial guess of the coefficient - of the exponent + of the exponent offset_guess (float): Optional, an initial guess of the offset t1_bounds (list of two floats): Optional, lower bound and upper bound to T1 - amplitude_bounds (list of two floats): Optional, lower bound and upper bound to the amplitude - offset_bounds (list of two floats): Optional, lower bound and upper bound to the offset + amplitude_bounds (list of two floats): Optional, lower bound and upper + bound to the amplitude + offset_bounds (list of two floats): Optional, lower bound and upper + bound to the offset + plot (bool): Generator plot of exponential fit. + ax (AxesSubplot): Optional, axes to add figure to. Returns: The analysis result with the estimated T1 """ - unit = experiment_data._data[0]["metadata"]["unit"] - conversion_factor = experiment_data._data[0]["metadata"].get("dt_factor", None) + data = experiment_data.data() + unit = data[0]["metadata"]["unit"] + conversion_factor = data[0]["metadata"].get("dt_factor", None) + qubit = data[0]["metadata"]["qubit"] + if conversion_factor is None: conversion_factor = 1 if unit == "s" else apply_prefix(1, unit) @@ -202,9 +213,9 @@ class T1Experiment(BaseExperiment): """T1 experiment class. Experiment Options: - delays: delay times of the experiments - unit: Optional, unit of the delay times. Supported units are - 's', 'ms', 'us', 'ns', 'ps', 'dt'. + * delays: delay times of the experiments + * unit: Optional, unit of the delay times. Supported units are + 's', 'ms', 'us', 'ns', 'ps', 'dt'. """ __analysis_class__ = T1Analysis @@ -240,7 +251,7 @@ def __init__( # Set experiment options self.set_options(delays=delays, unit=unit) - def circuits(self, backend: Optional["Backend"] = None) -> List[QuantumCircuit]: + def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: """ Return a list of experiment circuits diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index d96765e27c..33258c78bd 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -13,6 +13,7 @@ Composite Experiment Analysis class. """ +from qiskit.exceptions import QiskitError from qiskit_experiments.base_analysis import BaseAnalysis, AnalysisResult from .composite_experiment_data import CompositeExperimentData @@ -45,7 +46,7 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): # Run analysis for sub-experiments for expr, expr_data in zip( - experiment_data._experiment._experiments, experiment_data._composite_expdata + experiment_data._experiment._experiments, experiment_data._components ): expr.run_analysis(expr_data, **options) diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index 50b7a45a00..45b21e0063 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -16,18 +16,14 @@ from typing import Optional, List from qiskit.providers.options import Options -from qiskit_experiments.base_analysis import BaseAnalysis, ExperimentData +from qiskit_experiments.experiment_data import ExperimentData +from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.analysis.curve_fitting import curve_fit, process_curve_data from qiskit_experiments.analysis.data_processing import ( level2_probability, mean_xy_data, ) -from qiskit_experiments.analysis.plotting import ( - HAS_MATPLOTLIB, - plot_curve_fit, - plot_scatter, - plot_errorbar, -) +from qiskit_experiments.analysis import plotting class RBAnalysis(BaseAnalysis): @@ -53,7 +49,7 @@ def _run_analysis( experiment_data: ExperimentData, p0: Optional[List[float]] = None, plot: bool = True, - ax: Optional["AxesSubplot"] = None, + ax: Optional["plotting.pyplot.AxesSubplot"] = None, ): """Run analysis on circuit data. Args: @@ -95,10 +91,10 @@ def fit_fun(x, a, alpha, b): analysis_result["EPC"] = scale * (1 - popt[1]) analysis_result["EPC_err"] = scale * popt_err[1] / popt[1] - if plot and HAS_MATPLOTLIB: - ax = plot_curve_fit(fit_fun, analysis_result, ax=ax) - ax = plot_scatter(x_raw, y_raw, ax=ax) - ax = plot_errorbar(xdata, ydata, ydata_sigma, ax=ax) + if plot and plotting.HAS_MATPLOTLIB: + ax = plotting.plot_curve_fit(fit_fun, analysis_result, ax=ax) + ax = plotting.plot_scatter(x_raw, y_raw, ax=ax) + ax = plotting.plot_errorbar(xdata, ydata, ydata_sigma, ax=ax) self._format_plot(ax, analysis_result) figures = [ax.get_figure()] else: diff --git a/test/data_processing/fake_experiment.py b/test/data_processing/fake_experiment.py index 03f96d12f8..d925c8d68e 100644 --- a/test/data_processing/fake_experiment.py +++ b/test/data_processing/fake_experiment.py @@ -25,7 +25,7 @@ def __init__(self): self._type = None super().__init__((0,), "fake_test_experiment") - def circuits(self, backend=None, **circuit_options): + def circuits(self, backend=None): """Fake circuits.""" return [] From d21cd454a8fab7e33bacaccd736ded6875712ae7 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Thu, 20 May 2021 10:34:36 -0400 Subject: [PATCH 07/11] Remove _transpile from BaseExperiment --- qiskit_experiments/base_experiment.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index c32f02645c..c2d1395f0d 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -14,18 +14,15 @@ """ from abc import ABC, abstractmethod -from typing import Union, Iterable, Optional, Tuple, List +from typing import Iterable, Optional, Tuple, List import copy from numbers import Integral -from typing import List, Optional, Iterable, Tuple, Union from qiskit import transpile, assemble, QuantumCircuit from qiskit.providers.options import Options from qiskit.providers.backend import Backend from qiskit.providers.basebackend import BaseBackend as LegacyBackend from qiskit.exceptions import QiskitError -from qiskit.providers.backend import Backend -from qiskit.providers.basebackend import BaseBackend as LegacyBackend from .experiment_data import ExperimentData @@ -107,9 +104,7 @@ def run( experiment_data = self.__experiment_data__(self, backend=backend) # Generate and transpile circuits - circuits = self._transpile( - self.circuits(backend), backend, **self.transpile_options.__dict__ - ) + circuits = transpile(self.circuits(backend), backend, **self.transpile_options.__dict__) # Run circuits on backend run_opts = copy.copy(self.run_options) @@ -172,7 +167,7 @@ def physical_qubits(self) -> Tuple[int]: return self._physical_qubits @classmethod - def analysis(cls, **kwargs) -> "BaseAnalysis": + def analysis(cls, **kwargs): """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") @@ -187,7 +182,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: backend: Optional, a backend object. Returns: - A list of :class:`QuantumCircuit`s. + A list of :class:`QuantumCircuit`. .. note:: These circuits should be on qubits ``[0, .., N-1]`` for an @@ -198,20 +193,6 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: # values for any explicit experiment options that effect circuit # generation - def _transpile( - self, - circuits: Union[QuantumCircuit, List[QuantumCircuit]], - backend: Optional[Backend] = None, - **transpile_options, - ) -> List[QuantumCircuit]: - """Custom transpilation of circuits for running on backend. - - Subclasses may modify this method if they need to customize how - transpilation is done, for example to update metadata in the - transpiled circuits. - """ - return transpile(circuits, backend=backend, **transpile_options) - @classmethod def _default_options(cls) -> Options: """Default kwarg options for experiment""" From ae949ecf09941f22acc8ba06c288b7029522a206 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Fri, 21 May 2021 13:06:28 -0400 Subject: [PATCH 08/11] Address review comments * Rename Experiment.options to Experiment.experiment_options. * Add some more comments --- qiskit_experiments/base_analysis.py | 8 +++++++- qiskit_experiments/base_experiment.py | 18 +++++++++--------- .../characterization/t1_experiment.py | 14 +++++++------- .../randomized_benchmarking/rb_experiment.py | 6 +++--- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index a534edc185..6105ca7088 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -28,10 +28,16 @@ class BaseAnalysis(ABC): """Base Analysis class for analyzing Experiment data. + The data produced by experiments (i.e. subclasses of BaseExperiment) + are analyzed with subclasses of BaseExperiment. The analysis is + typically run after the data has been gathered by the experiment. + 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 combined with all other option kwargs in the + default values will be combined with all other option kwargs in the run method and passed to the `_run_analysis` function. """ diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index c2d1395f0d..4c9469e468 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -71,7 +71,7 @@ def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None) raise QiskitError("Duplicate qubits in physical qubits list.") # Experiment options - self._options = self._default_options() + 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() @@ -194,7 +194,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: # generation @classmethod - def _default_options(cls) -> Options: + def _default_experiment_options(cls) -> Options: """Default kwarg options for experiment""" # Experiment subclasses should override this method to return # an `Options` object containing all the supported options for @@ -203,11 +203,11 @@ def _default_options(cls) -> Options: return Options() @property - def options(self) -> Options: + def experiment_options(self) -> Options: """Return the options for the experiment.""" - return self._options + return self._experiment_options - def set_options(self, **fields): + def set_experiment_options(self, **fields): """Set the experiment options. Args: @@ -217,18 +217,18 @@ def set_options(self, **fields): AttributeError: If the field passed in is not a supported options """ for field in fields: - if not hasattr(self._options, field): + if not hasattr(self._experiment_options, field): raise AttributeError( f"Options field {field} is not valid for {type(self).__name__}" ) - self._options.update_options(**fields) + self._experiment_options.update_options(**fields) @classmethod def _default_transpile_options(cls) -> Options: """Default transpiler options for transpilation of circuits""" # Experiment subclasses can override this method if they need - # to set specific transpiler options defaults for running the - # experiment. + # to set specific default transpiler options to transpile the + # experiment circuits. return Options(optimization_level=0) @property diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index 38c8398871..cfabf3018c 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -221,7 +221,7 @@ class T1Experiment(BaseExperiment): __analysis_class__ = T1Analysis @classmethod - def _default_options(cls) -> Options: + def _default_experiment_options(cls) -> Options: return Options(delays=None, unit="s") def __init__( @@ -249,7 +249,7 @@ def __init__( super().__init__([qubit]) # Set experiment options - self.set_options(delays=delays, unit=unit) + self.set_experiment_options(delays=delays, unit=unit) def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: """ @@ -264,7 +264,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: Raises: AttributeError: if unit is dt but dt parameter is missing in the backend configuration """ - if self.options.unit == "dt": + if self.experiment_options.unit == "dt": try: dt_factor = getattr(backend.configuration(), "dt") except AttributeError as no_dt: @@ -272,11 +272,11 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: circuits = [] - for delay in self.options.delays: + for delay in self.experiment_options.delays: circ = QuantumCircuit(1, 1) circ.x(0) circ.barrier(0) - circ.delay(delay, 0, self.options.unit) + circ.delay(delay, 0, self.experiment_options.unit) circ.barrier(0) circ.measure(0, 0) @@ -284,10 +284,10 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: "experiment_type": self._type, "qubit": self.physical_qubits[0], "xval": delay, - "unit": self.options.unit, + "unit": self.experiment_options.unit, } - if self.options.unit == "dt": + if self.experiment_options.unit == "dt": circ.metadata["dt_factor"] = dt_factor circuits.append(circ) diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index 96b9fb393f..e57aab330e 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -64,7 +64,7 @@ def __init__( super().__init__(qubits) # Set configurable options - self.set_options(lengths=list(lengths), num_samples=num_samples) + self.set_experiment_options(lengths=list(lengths), num_samples=num_samples) # Set fixed options self._full_sampling = full_sampling @@ -88,8 +88,8 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: A list of :class:`QuantumCircuit`. """ circuits = [] - for _ in range(self.options.num_samples): - circuits += self._sample_circuits(self.options.lengths, seed=self._rng) + for _ in range(self.experiment_options.num_samples): + circuits += self._sample_circuits(self.experiment_options.lengths, seed=self._rng) return circuits def _sample_circuits( From a89fd47fc02f01a080791443fe80fe78df900234 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Wed, 26 May 2021 14:22:04 -0400 Subject: [PATCH 09/11] Fix RB experiment --- .../randomized_benchmarking/clifford_utils.py | 36 ++++++------------- .../randomized_benchmarking/rb_experiment.py | 2 +- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/qiskit_experiments/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/randomized_benchmarking/clifford_utils.py index 8965630956..adfd607a0c 100644 --- a/qiskit_experiments/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/randomized_benchmarking/clifford_utils.py @@ -52,20 +52,6 @@ def _define(self): self.definition = qc -def v(self, q): - """Apply V to q.""" - return self.append(VGate(), [q], []) - - -def w(self, q): - """Apply W to q.""" - return self.append(WGate(), [q], []) - - -QuantumCircuit.v = v -QuantumCircuit.v = w - - class CliffordUtils: """Utilities for generating 1 and 2 qubit clifford circuits and elements""" @@ -143,9 +129,9 @@ def clifford_1_qubit_circuit(self, num): if i == 1: qc.h(0) if j == 1: - qc.v(0) + qc.append(VGate(), [0]) if j == 2: - qc.w(0) + qc.append(WGate(), [0]) if p == 1: qc.x(0) if p == 2: @@ -170,13 +156,13 @@ def clifford_2_qubit_circuit(self, num): if i1 == 1: qc.h(1) if j0 == 1: - qc.v(0) + qc.append(VGate(), [0]) if j0 == 2: - qc.w(0) + qc.append(WGate(), [0]) if j1 == 1: - qc.v(1) + qc.append(VGate(), [1]) if j1 == 2: - qc.w(1) + qc.append(WGate(), [1]) if form in (1, 2, 3): qc.cx(0, 1) if form in (2, 3): @@ -185,14 +171,14 @@ def clifford_2_qubit_circuit(self, num): qc.cx(0, 1) if form in (1, 2): if k0 == 1: - qc.v(0) + qc.append(VGate(), [0]) if k0 == 2: - qc.w(0) + qc.append(WGate(), [0]) if k1 == 1: - qc.v(1) + qc.append(VGate(), [1]) if k1 == 2: - qc.v(1) - qc.v(1) + qc.append(VGate(), [1]) + qc.append(VGate(), [1]) if p0 == 1: qc.x(0) if p0 == 2: diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index e57aab330e..476b3f7975 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -76,7 +76,7 @@ def __init__( self._rng = seed @classmethod - def _default_options(cls): + def _default_experiment_options(cls): return Options(lengths=None, num_samples=None) # pylint: disable = arguments-differ From c5479e3f3187427fd08b571c83028956226ed61c Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Wed, 26 May 2021 14:25:46 -0400 Subject: [PATCH 10/11] Fix T2Star experiment --- .../characterization/__init__.py | 4 +- .../characterization/t2star_experiment.py | 106 +++++++++--------- test/test_t2star.py | 25 +++-- 3 files changed, 67 insertions(+), 68 deletions(-) diff --git a/qiskit_experiments/characterization/__init__.py b/qiskit_experiments/characterization/__init__.py index 7c39b7db9e..053799bacb 100644 --- a/qiskit_experiments/characterization/__init__.py +++ b/qiskit_experiments/characterization/__init__.py @@ -23,6 +23,7 @@ :toctree: ../stubs/ T1Experiment + T2StarExperiment Analysis @@ -32,6 +33,7 @@ :toctree: ../stubs/ T1Analysis + T2StarAnalysis """ from .t1_experiment import T1Experiment, T1Analysis -from .t2star_experiment import T2StarExperiment +from .t2star_experiment import T2StarExperiment, T2StarAnalysis diff --git a/qiskit_experiments/characterization/t2star_experiment.py b/qiskit_experiments/characterization/t2star_experiment.py index 7331536c8f..cc580b8fde 100644 --- a/qiskit_experiments/characterization/t2star_experiment.py +++ b/qiskit_experiments/characterization/t2star_experiment.py @@ -17,8 +17,10 @@ import numpy as np import qiskit +from qiskit.providers import Backend from qiskit.circuit import QuantumCircuit from qiskit.utils import apply_prefix +from qiskit.providers.options import Options from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.base_analysis import BaseAnalysis, AnalysisResult from qiskit_experiments.analysis.curve_fitting import curve_fit, process_curve_data @@ -30,49 +32,46 @@ class T2StarAnalysis(BaseAnalysis): """T2Star Experiment result analysis class.""" - def __init__( - self, - ): - self._conversion_factor = None + @classmethod + def _default_options(cls): + return Options(user_p0=None, user_bounds=None) # pylint: disable=arguments-differ, unused-argument def _run_analysis( self, experiment_data: ExperimentData, - user_p0: Dict[str, float], - user_bounds: Tuple[List[float], List[float]], + user_p0: Optional[Dict[str, float]] = None, + user_bounds: Optional[Tuple[List[float], List[float]]] = None, plot: bool = True, ax: Optional["AxesSubplot"] = None, **kwargs, ) -> Tuple[AnalysisResult, List["matplotlib.figure.Figure"]]: - r""" - Calculate T2Star experiment - The probability of measuring `+` is assumed to be of the form - .. math:: - f(t) = a\mathrm{e}^{-t / T_2^*}\cos(2\pi freq t + \phi) + b + r"""Calculate T2Star experiment. + + The probability of measuring `+` is assumed to be of the form + :math:`f(t) = a\mathrm{e}^{-t / T_2^*}\cos(2\pi freq t + \phi) + b` for unknown parameters :math:`a, b, freq, \phi, T_2^*`. - Args: - experiment_data (ExperimentData): the experiment data to analyze - user_p0: contains initial values given by the user, for the - fit parameters :math:`(a, T_2^*, freq, \phi, b)` - User_bounds: lower and upper bounds on the parameters in p0, - given by the user. - The first tuple is the lower bounds, - The second tuple is the upper bounds. - For both params, the order is :math:`a, T_2^*, freq, \phi, b`. - plot: if True, create the plot, otherwise, do not create the plot. - ax: the plot object - **kwargs: additional parameters - Returns: - The analysis result with the estimated :math:`T_2^*` and 'freq' (frequency) - The graph of the function. + Args: + experiment_data (ExperimentData): the experiment data to analyze + user_p0: contains initial values given by the user, for the + fit parameters :math:`(a, T_2^*, freq, \phi, b)` + User_bounds: lower and upper bounds on the parameters in p0, + given by the user. + The first tuple is the lower bounds, + The second tuple is the upper bounds. + For both params, the order is :math:`a, T_2^*, freq, \phi, b`. + plot: if True, create the plot, otherwise, do not create the plot. + ax: the plot object + **kwargs: additional parameters for curve fit. + + Returns: + The analysis result with the estimated :math:`T_2^*` and 'freq' (frequency) + The graph of the function. """ def osc_fit_fun(x, a, t2star, freq, phi, c): - """ - Decay cosine fit function - """ + """Decay cosine fit function""" return a * np.exp(-x / t2star) * np.cos(2 * np.pi * freq * x + phi) + c def _format_plot(ax, unit): @@ -84,17 +83,19 @@ def _format_plot(ax, unit): # implementation of _run_analysis unit = experiment_data._data[0]["metadata"]["unit"] - self._conversion_factor = experiment_data._data[0]["metadata"].get("dt_factor", None) - if self._conversion_factor is None: - self._conversion_factor = 1 if unit == "s" else apply_prefix(1, unit) + conversion_factor = experiment_data._data[0]["metadata"].get("dt_factor", None) + if conversion_factor is None: + conversion_factor = 1 if unit == "s" else apply_prefix(1, unit) xdata, ydata, sigma = process_curve_data( experiment_data._data, lambda datum: level2_probability(datum, "0") ) - si_xdata = xdata * self._conversion_factor + si_xdata = xdata * conversion_factor t2star_estimate = np.mean(si_xdata) - p0, bounds = self._t2star_default_params(user_p0, user_bounds, t2star_input=t2star_estimate) + p0, bounds = self._t2star_default_params( + conversion_factor, user_p0, user_bounds, t2star_estimate + ) fit_result = curve_fit( osc_fit_fun, si_xdata, ydata, p0=list(p0.values()), sigma=sigma, bounds=bounds ) @@ -125,39 +126,35 @@ def _format_plot(ax, unit): analysis_result["fit"]["circuit_unit"] = unit if unit == "dt": - analysis_result["fit"]["dt"] = self._conversion_factor + analysis_result["fit"]["dt"] = conversion_factor return analysis_result, figures def _t2star_default_params( self, - user_p0, - user_bounds, - t2star_input: float, + conversion_factor, + user_p0=None, + user_bounds=None, + t2star_input=None, ) -> Tuple[List[float], Tuple[List[float]]]: - """ - Default fit parameters for oscillation data + """Default fit parameters for oscillation data. + Note that :math:`T_2^*` and 'freq' units are converted to 'sec' and will be output in 'sec'. - Args: - t2star_input: default for t2star if p0==None - Returns: - Fit guessed parameters: either from the input (if given) or - else assign default values. """ if user_p0 is None: a = 0.5 - t2star = t2star_input * self._conversion_factor + t2star = t2star_input * conversion_factor freq = 0.1 phi = 0.0 b = 0.5 else: a = user_p0["A"] t2star = user_p0["t2star"] - t2star *= self._conversion_factor + t2star *= conversion_factor freq = user_p0["f"] phi = user_p0["phi"] b = user_p0["B"] - freq /= self._conversion_factor + freq /= conversion_factor p0 = {"a_guess": a, "t2star": t2star, "f_guess": freq, "phi_guess": phi, "b_guess": b} if user_bounds is None: a_bounds = [-0.5, 1.5] @@ -200,7 +197,6 @@ def __init__( osc_freq: float = 0.0, experiment_type: Optional[str] = None, ): - """Initialize the T2Star experiment class. Args: @@ -220,18 +216,18 @@ def __init__( self._osc_freq = osc_freq super().__init__([qubit], experiment_type) - def circuits( - self, backend: Optional["Backend"] = None, **circuit_options - ) -> List[QuantumCircuit]: - """ - Return a list of experiment circuits + def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + """Return a list of experiment circuits. + Each circuit consists of a Hadamard gate, followed by a fixed delay, a phase gate (with a linear phase), and an additional Hadamard gate. + Args: backend: Optional, a backend object - circuit_options: from base class, empty here + Returns: The experiment circuits + Raises: AttributeError: if unit is dt but dt parameter is missing in the backend configuration """ diff --git a/test/test_t2star.py b/test/test_t2star.py index 4b23f14d28..7b08bfb5ec 100644 --- a/test/test_t2star.py +++ b/test/test_t2star.py @@ -175,6 +175,15 @@ def test_t2star_run_end2end(self): ] exp = T2StarExperiment(qubit, delays, unit=unit) + exp.set_analysis_options( + user_p0={ + "A": 0.5, + "t2star": estimated_t2star, + "f": estimated_freq, + "phi": 0, + "B": 0.5, + } + ) backend = T2starBackend( p0={ @@ -193,20 +202,14 @@ def test_t2star_run_end2end(self): dt_factor = getattr(backend._configuration, "dt") # run circuits - result = exp.run( + + expdata = exp.run( backend=backend, - user_p0={ - "A": 0.5, - "t2star": estimated_t2star, - "f": estimated_freq, - "phi": 0, - "B": 0.5, - }, - user_bounds=None, # plot=False, instruction_durations=instruction_durations, shots=2000, - ).analysis_result(0) + ) + result = expdata.analysis_result(0) self.assertAlmostEqual( result["t2star_value"], estimated_t2star * dt_factor, @@ -244,8 +247,6 @@ def test_t2star_parallel(self): backend = T2starBackend(p0) res = par_exp.run( backend=backend, - user_p0=None, - user_bounds=None, # plot=False, shots=1000, ) From 58ee4d4d447c742e7f77b02a819e6d5d2bf7667a Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Wed, 26 May 2021 14:29:18 -0400 Subject: [PATCH 11/11] Remove kwargs from Experiment.analysis class meth --- qiskit_experiments/base_experiment.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 4c9469e468..016b48fde4 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -142,17 +142,13 @@ def run_analysis(self, experiment_data, **options) -> ExperimentData: Raises: QiskitError: if experiment_data container is not valid for analysis. """ - if self.__analysis_class__ is None: - raise QiskitError(f"Experiment {self._type} does not have a default Analysis class") - # Get analysis options analysis_options = copy.copy(self.analysis_options) analysis_options.update_options(**options) analysis_options = analysis_options.__dict__ # Run analysis - # pylint: disable = not-callable - analysis = self.__analysis_class__() + analysis = self.analysis() analysis.run(experiment_data, save=True, return_figures=False, **analysis_options) return experiment_data @@ -167,12 +163,12 @@ def physical_qubits(self) -> Tuple[int]: return self._physical_qubits @classmethod - def analysis(cls, **kwargs): + 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__(**kwargs) + return cls.__analysis_class__() @abstractmethod def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: