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 5536e07567..6105ca7088 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -16,17 +16,38 @@ from abc import ABC, abstractmethod from typing import List, Tuple +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): - """Base Analysis class for analyzing Experiment data.""" + """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 be 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 + @classmethod + def _default_options(cls) -> Options: + return Options() + def run( self, experiment_data: ExperimentData, @@ -34,7 +55,7 @@ def run( return_figures: bool = False, **options, ): - """Run analysis and update stored ExperimentData with analysis result. + """Run analysis and update ExperimentData with analysis result. Args: experiment_data: the experiment data to analyze. @@ -43,14 +64,13 @@ def run( 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. 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. @@ -63,10 +83,15 @@ def run( f"Invalid experiment data type, expected {self.__experiment_data__.__name__}" f" but received {type(experiment_data).__name__}" ) + # Get analysis options + analysis_options = self._default_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) @@ -88,18 +113,19 @@ def run( @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: 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..016b48fde4 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -14,34 +14,18 @@ """ 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 qiskit import transpile, assemble, QuantumCircuit -from qiskit.exceptions import QiskitError +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 .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 +45,13 @@ 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): """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. Raises: QiskitError: if qubits is a list and contains duplicates. @@ -99,62 +70,88 @@ 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._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() + + # 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, + **run_options, ) -> ExperimentData: """Run an experiment and perform analysis. 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. + run_options: backend runtime options used for circuit execution. 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 = transpile(self.circuits(backend), backend, **self.transpile_options.__dict__) + + # Run circuits on backend + 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_class__().run(experiment_data, **kwargs) + self.run_analysis(experiment_data) # Return the ExperimentData future return experiment_data + 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. + 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. + """ + # 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() + analysis.run(experiment_data, save=True, return_figures=False, **analysis_options) + return experiment_data + @property def num_qubits(self) -> int: """Return the number of qubits for this experiment.""" @@ -166,32 +163,19 @@ 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): + """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__() @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`. @@ -201,59 +185,106 @@ def circuits( *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. - - def transpiled_circuits( - self, backend: Optional[Backend] = None, **kwargs - ) -> List[QuantumCircuit]: - """Return a list of experiment circuits. + # NOTE: Subclasses should override this method using the `options` + # values for any explicit experiment options that effect circuit + # generation + + @classmethod + 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 + # that experiment and their default values. Only options listed + # here can be modified later by the `set_options` method. + return Options() + + @property + def experiment_options(self) -> Options: + """Return the options for the experiment.""" + return self._experiment_options + + def set_experiment_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._experiment_options, field): + raise AttributeError( + f"Options field {field} is not valid for {type(self).__name__}" + ) + 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 default transpiler options to transpile the + # experiment 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_run_options(cls) -> Options: + """Default options values for the experiment :meth:`run` method.""" + return Options() + + @property + def run_options(self) -> Options: + """Return options values for the experiment :meth:`run` method.""" + return self._run_options + + def set_run_options(self, **fields): + """Set options values for the experiment :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._run_options.update_options(**fields) - # Generate circuits - circuits = self.circuits(backend=backend, **circuit_options) + @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() - # 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) 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/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index e8dd1822cc..cfabf3018c 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,34 @@ class T1Analysis(BaseAnalysis): - """T1 Experiment result analysis class.""" + """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. + """ - # pylint: disable=arguments-differ, unused-argument + @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, @@ -43,7 +69,6 @@ def _run_analysis( offset_bounds=None, plot=True, ax=None, - **kwargs, ) -> Tuple[AnalysisResult, List["matplotlib.figure.Figure"]]: """ Calculate T1 @@ -52,17 +77,15 @@ 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 + 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 + 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 @@ -71,6 +94,7 @@ def _run_analysis( 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) @@ -186,10 +210,20 @@ 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 + @classmethod + def _default_experiment_options(cls) -> Options: + return Options(delays=None, unit="s") + def __init__( self, qubit: int, @@ -211,11 +245,12 @@ def __init__( if len(delays) < 3: raise ValueError("T1 experiment: number of delays must be at least 3") - self._delays = delays - self._unit = unit + # Initialize base experiment super().__init__([qubit]) - # pylint: disable=arguments-differ + # Set experiment options + self.set_experiment_options(delays=delays, unit=unit) + def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: """ Return a list of experiment circuits @@ -229,8 +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._unit == "dt": + if self.experiment_options.unit == "dt": try: dt_factor = getattr(backend.configuration(), "dt") except AttributeError as no_dt: @@ -238,11 +272,11 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: circuits = [] - for delay in self._delays: + for delay in self.experiment_options.delays: circ = QuantumCircuit(1, 1) circ.x(0) circ.barrier(0) - circ.delay(delay, 0, self._unit) + circ.delay(delay, 0, self.experiment_options.unit) circ.barrier(0) circ.measure(0, 0) @@ -250,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._unit, + "unit": self.experiment_options.unit, } - if self._unit == "dt": + if self.experiment_options.unit == "dt": circ.metadata["dt_factor"] = dt_factor circuits.append(circ) 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/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..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 @@ -40,8 +41,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._components + ): + 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 # 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/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_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index 61d16b8140..45b21e0063 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -15,22 +15,33 @@ from typing import Optional, List -from qiskit_experiments.base_analysis import BaseAnalysis, ExperimentData +from qiskit.providers.options import Options +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): - """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( @@ -38,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: @@ -80,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/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index 21f22232f1..476b3f7975 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 @@ -55,15 +60,24 @@ def __init__( sequences are constructed by appending additional Clifford samples to shorter sequences. """ + # Initialize base experiment + super().__init__(qubits) + + # Set configurable options + self.set_experiment_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._lengths = list(lengths) - self._num_samples = num_samples - self._full_sampling = full_sampling - self._clifford_utils = CliffordUtils() - super().__init__(qubits) + + @classmethod + def _default_experiment_options(cls): + return Options(lengths=None, num_samples=None) # pylint: disable = arguments-differ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: @@ -74,29 +88,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) - 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) + for _ in range(self.experiment_options.num_samples): + circuits += self._sample_circuits(self.experiment_options.lengths, seed=self._rng) return circuits def _sample_circuits( 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 [] 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, )