From eb60945f8dd5d92670fce0071cdb399f7c0e74b4 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 13 May 2021 18:39:46 +0900 Subject: [PATCH 01/46] initial commit model based experiment --- qiskit_experiments/test/__init__.py | 15 +++ qiskit_experiments/test/mock_experiment.py | 103 +++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 qiskit_experiments/test/__init__.py create mode 100644 qiskit_experiments/test/mock_experiment.py diff --git a/qiskit_experiments/test/__init__.py b/qiskit_experiments/test/__init__.py new file mode 100644 index 0000000000..7b7b33381f --- /dev/null +++ b/qiskit_experiments/test/__init__.py @@ -0,0 +1,15 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Test tools for experiment. +""" diff --git a/qiskit_experiments/test/mock_experiment.py b/qiskit_experiments/test/mock_experiment.py new file mode 100644 index 0000000000..f6aa42fe2c --- /dev/null +++ b/qiskit_experiments/test/mock_experiment.py @@ -0,0 +1,103 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Standard RB analysis class. +""" + +from qiskit_experiments.base_experiment import BaseExperiment +from qiskit_experiments.experiment_data import ExperimentData +from qiskit_experiments.analysis import CurveAnalysis, SeriesDef +from typing import Tuple, Optional +import numpy as np + + +class FakeExperiment(BaseExperiment): + """A fake experiment class.""" + + def __init__(self, qubits=(0,)): + super().__init__(qubits=qubits, experiment_type="fake_experiment") + + def circuits(self, backend=None, **circuit_options): + return [] + + +def curve_model_based_level2_probability_experiment( + experiment: BaseExperiment, + xvals: np.ndarray, + target_params: np.ndarray, + outcome_labels: Tuple[str, str], + shots: Optional[int] = 1024 +) -> ExperimentData: + """Simulate fake experiment data with curve analysis fit model. + + Args: + experiment: Target experiment class. + xvals: Values to scan. + target_params: Parameters used to generate sampling data with the fit function. + outcome_labels: Two strings tuple of out come label in count dictionary. + The first and second label should be assigned to success and failure, respectively. + shots: Number of shot to generate count data. + + Returns: + A fake experiment data. + """ + analysis = experiment.__analysis_class__ + + if isinstance(analysis, CurveAnalysis): + raise Exception("The attached analysis is not CurveAnalysis subclass. " + "This function cannot simulate output data.") + + x_key = analysis.__x_key__ + series = analysis.__series__ + fit_funcs = analysis.__fit_funcs__ + param_names = analysis.__param_names__ + + if series is None: + series = [ + SeriesDef( + name='default', + param_names=[f"p{idx}" for idx in range(len(target_params))], + fit_func_index=0, + filter_kwargs=dict() + ) + ] + + data = [] + for curve_properties in series: + fit_func = fit_funcs[curve_properties.fit_func_index] + params = [] + for param_name in curve_properties.param_names: + param_index = param_names.index(param_name) + params.append(param_index) + y_values = fit_func(xvals, *params) + counts = np.asarray(y_values * shots, dtype=int) + + for xi, count in zip(xvals, counts): + metadata = { + x_key: xi, + "qubits": experiment._physical_qubits, + "experiment_type": experiment._type + } + metadata.update(**series.filter_kwargs) + + data.append( + { + "counts": {outcome_labels[0]: shots - count, outcome_labels[1]: count}, + "metadata": metadata + } + ) + + expdata = ExperimentData(experiment=experiment) + for datum in data: + expdata.add_data(datum) + + return expdata From c291e598860490ac08a3a78626fd06053d394c11 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 13 May 2021 18:45:11 +0900 Subject: [PATCH 02/46] Revert "initial commit model based experiment" This reverts commit eb60945f8dd5d92670fce0071cdb399f7c0e74b4. --- qiskit_experiments/test/__init__.py | 15 --- qiskit_experiments/test/mock_experiment.py | 103 --------------------- 2 files changed, 118 deletions(-) delete mode 100644 qiskit_experiments/test/__init__.py delete mode 100644 qiskit_experiments/test/mock_experiment.py diff --git a/qiskit_experiments/test/__init__.py b/qiskit_experiments/test/__init__.py deleted file mode 100644 index 7b7b33381f..0000000000 --- a/qiskit_experiments/test/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Test tools for experiment. -""" diff --git a/qiskit_experiments/test/mock_experiment.py b/qiskit_experiments/test/mock_experiment.py deleted file mode 100644 index f6aa42fe2c..0000000000 --- a/qiskit_experiments/test/mock_experiment.py +++ /dev/null @@ -1,103 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Standard RB analysis class. -""" - -from qiskit_experiments.base_experiment import BaseExperiment -from qiskit_experiments.experiment_data import ExperimentData -from qiskit_experiments.analysis import CurveAnalysis, SeriesDef -from typing import Tuple, Optional -import numpy as np - - -class FakeExperiment(BaseExperiment): - """A fake experiment class.""" - - def __init__(self, qubits=(0,)): - super().__init__(qubits=qubits, experiment_type="fake_experiment") - - def circuits(self, backend=None, **circuit_options): - return [] - - -def curve_model_based_level2_probability_experiment( - experiment: BaseExperiment, - xvals: np.ndarray, - target_params: np.ndarray, - outcome_labels: Tuple[str, str], - shots: Optional[int] = 1024 -) -> ExperimentData: - """Simulate fake experiment data with curve analysis fit model. - - Args: - experiment: Target experiment class. - xvals: Values to scan. - target_params: Parameters used to generate sampling data with the fit function. - outcome_labels: Two strings tuple of out come label in count dictionary. - The first and second label should be assigned to success and failure, respectively. - shots: Number of shot to generate count data. - - Returns: - A fake experiment data. - """ - analysis = experiment.__analysis_class__ - - if isinstance(analysis, CurveAnalysis): - raise Exception("The attached analysis is not CurveAnalysis subclass. " - "This function cannot simulate output data.") - - x_key = analysis.__x_key__ - series = analysis.__series__ - fit_funcs = analysis.__fit_funcs__ - param_names = analysis.__param_names__ - - if series is None: - series = [ - SeriesDef( - name='default', - param_names=[f"p{idx}" for idx in range(len(target_params))], - fit_func_index=0, - filter_kwargs=dict() - ) - ] - - data = [] - for curve_properties in series: - fit_func = fit_funcs[curve_properties.fit_func_index] - params = [] - for param_name in curve_properties.param_names: - param_index = param_names.index(param_name) - params.append(param_index) - y_values = fit_func(xvals, *params) - counts = np.asarray(y_values * shots, dtype=int) - - for xi, count in zip(xvals, counts): - metadata = { - x_key: xi, - "qubits": experiment._physical_qubits, - "experiment_type": experiment._type - } - metadata.update(**series.filter_kwargs) - - data.append( - { - "counts": {outcome_labels[0]: shots - count, outcome_labels[1]: count}, - "metadata": metadata - } - ) - - expdata = ExperimentData(experiment=experiment) - for datum in data: - expdata.add_data(datum) - - return expdata From bed94818f8e71c5f696ae57aa6282ce0a5d7be87 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 17 Jun 2021 01:51:26 +0900 Subject: [PATCH 03/46] wip options field --- qiskit_experiments/__init__.py | 2 +- qiskit_experiments/analysis/curve_analysis.py | 157 +++++++++++------- qiskit_experiments/base_analysis.py | 10 +- qiskit_experiments/base_experiment.py | 25 ++- qiskit_experiments/options_field.py | 58 +++++++ 5 files changed, 184 insertions(+), 68 deletions(-) create mode 100644 qiskit_experiments/options_field.py diff --git a/qiskit_experiments/__init__.py b/qiskit_experiments/__init__.py index 9f1c6fa3c2..09781ebef3 100644 --- a/qiskit_experiments/__init__.py +++ b/qiskit_experiments/__init__.py @@ -51,7 +51,7 @@ # Base Classes from .experiment_data import ExperimentData, AnalysisResult from .base_analysis import BaseAnalysis -from .base_experiment import BaseExperiment +from .base_experiment import BaseExperiment, OptionsField # Experiment modules from . import composite diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 7e10e9751c..77683f6450 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -26,6 +26,7 @@ from qiskit_experiments.analysis.curve_fitting import multi_curve_fit, CurveAnalysisResult from qiskit_experiments.analysis.data_processing import probability from qiskit_experiments.analysis.utils import get_opt_value, get_opt_error +from qiskit_experiments.options_field import OptionsField from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.data_processing import DataProcessor from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -242,64 +243,104 @@ def __init__(self): setattr(self, f"__{key}", None) @classmethod - def _default_options(cls): - """Return default analysis options. - - Options: - curve_fitter: A callback function to perform fitting with formatted data. - This function should have signature: - - .. code-block:: - - def curve_fitter( - funcs: List[Callable], - series: ndarray, - xdata: ndarray, - ydata: ndarray, - p0: ndarray, - sigma: Optional[ndarray], - weights: Optional[ndarray], - bounds: Optional[ - Union[Dict[str, Tuple[float, float]], Tuple[ndarray, ndarray]] - ], - ) -> CurveAnalysisResult: - - See :func:`~qiskit_experiment.analysis.multi_curve_fit` for example. - data_processor: A callback function to format experiment data. - This function should have signature: - - .. code-block:: - - def data_processor(data: Dict[str, Any]) -> Tuple[float, float] - - This can be a :class:`~qiskit_experiment.data_processing.DataProcessor` - instance that defines the `self.__call__` method. - normalization: Set ``True`` to normalize y values within range [-1, 1]. - p0: Array-like or dictionary of initial parameters. - bounds: Array-like or dictionary of (min, max) tuple of fit parameter boundaries. - x_key: Circuit metadata key representing a scanned value. - plot: Set ``True`` to create figure for fit result. - axis: Optional. A matplotlib axis object to draw. - xlabel: X label of fit result figure. - ylabel: Y label of fit result figure. - fit_reports: Mapping of fit parameters and representation in the fit report. - return_data_points: Set ``True`` to return formatted XY data. - """ - return Options( - curve_fitter=multi_curve_fit, - data_processor=probability(outcome="1"), - normalization=False, - p0=None, - bounds=None, - x_key="xval", - plot=True, - axis=None, - xlabel=None, - ylabel=None, - ylim=None, - fit_reports=None, - return_data_points=False, - ) + def _default_options(cls) -> Dict[str, OptionsField]: + """Return default analysis options.""" + options = { + "curve_fitter": OptionsField( + default=multi_curve_fit, + annotation=Callable, + description=""" +A callback function to perform fitting with formatted data. +This function should have signature: + +.. code-block:: + + def curve_fitter( + funcs: List[Callable], + series: ndarray, + xdata: ndarray, + ydata: ndarray, + p0: ndarray, + sigma: Optional[ndarray], + weights: Optional[ndarray], + bounds: Optional[ + Union[Dict[str, Tuple[float, float]], Tuple[ndarray, ndarray]] + ], + ) -> CurveAnalysisResult: + +See :func:`~qiskit_experiment.analysis.multi_curve_fit` for example. + """, + ), + "data_processor": OptionsField( + default=probability(outcome="1"), + annotation=Union[Callable, DataProcessor], + description=""" +A callback function to format experiment data. +This function should have signature: + +.. code-block:: + + def data_processor(data: Dict[str, Any]) -> Tuple[float, float] + +This can be a :class:`~qiskit_experiment.data_processing.DataProcessor` +instance that defines the `self.__call__` method. + """, + ), + "p0": OptionsField( + default=None, + annotation=Dict[str, float], + description="Dictionary of initial parameters. Keys are parameter names.", + ), + "bounds": OptionsField( + default=None, + annotation=Dict[str, Tuple[float, float]], + description="Dictionary of (min, max) tuple of fit parameter boundaries. \ + Keys are parameter names.", + ), + "x_key": OptionsField( + default="xval", + annotation=str, + description="Circuit metadata key representing a scanned value.", + ), + "plot": OptionsField( + default=True, + annotation=bool, + description="Set ``True`` to create figure for fit result.", + ), + "axis": OptionsField( + default=None, + annotation="matplotlib.axes._subplots.AxesSubplot", + description="Optional. A matplotlib axis object to draw.", + ), + "xlabel": OptionsField( + default=None, + annotation=str, + description="X label of the fit result figure.", + ), + "ylabel": OptionsField( + default=None, + annotation=str, + description="Y label of the fit result figure.", + ), + "ylim": OptionsField( + default=None, + annotation=Tuple[float, float], + description="Y axis limit of the fit result figure.", + ), + "fit_reports": OptionsField( + default=None, + annotation=Dict[str, str], + description="Mapping of fit parameters and representation in the fit report. \ + If nothing specified, fit report will not be shown.", + ), + "return_data_points": OptionsField( + default=False, + annotation=bool, + description="Set ``True`` to return arrays of measured data points." + ), + } + + return options def _create_figures(self, analysis_results: CurveAnalysisResult) -> List["Figure"]: """Create new figures with the fit result and raw data. diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index f5abc3244c..61d3663af3 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -14,13 +14,13 @@ """ from abc import ABC, abstractmethod -from typing import List, Tuple +from typing import List, Tuple, Dict from qiskit.exceptions import QiskitError -from qiskit.providers.options import Options from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ExperimentData, AnalysisResult +from qiskit_experiments.options_field import OptionsField, _to_options class BaseAnalysis(ABC): @@ -43,8 +43,8 @@ class BaseAnalysis(ABC): __experiment_data__ = ExperimentData @classmethod - def _default_options(cls) -> Options: - return Options() + def _default_options(cls) -> Dict[str, OptionsField]: + return dict() def run( self, @@ -82,7 +82,7 @@ def run( f" but received {type(experiment_data).__name__}" ) # Get analysis options - analysis_options = self._default_options() + analysis_options = _to_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 016b48fde4..f1034ea182 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -13,18 +13,19 @@ Base Experiment class. """ -from abc import ABC, abstractmethod -from typing import Iterable, Optional, Tuple, List import copy +from abc import ABC, abstractmethod from numbers import Integral +from typing import Iterable, Optional, Tuple, List from qiskit import transpile, assemble, QuantumCircuit -from qiskit.providers.options import Options +from qiskit.exceptions import QiskitError from qiskit.providers.backend import Backend from qiskit.providers.basebackend import BaseBackend as LegacyBackend -from qiskit.exceptions import QiskitError +from qiskit.providers.options import Options from .experiment_data import ExperimentData +from .options_field import _compile_docstring, _compile_annotations class BaseExperiment(ABC): @@ -45,6 +46,22 @@ class BaseExperiment(ABC): # ExperimentData class for experiment __experiment_data__ = ExperimentData + # pylint: disable=unused-argument + def __new__(cls, qubits: Iterable[int], experiment_type: Optional[str] = None): + """Override help of options setter based on information provided by subclasses.""" + obj = object.__new__(cls) + + # Override set analysis option method help. + obj.set_analysis_options.__doc__ = _compile_docstring( + header="Return the analysis options for :meth:`run` analysis.", + fields=cls.__analysis_class__._default_options(), + ) + obj.set_analysis_options.__annotations__ = _compile_annotations( + fields=cls.__analysis_class__._default_options() + ) + + return obj + def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None): """Initialize the experiment object. diff --git a/qiskit_experiments/options_field.py b/qiskit_experiments/options_field.py new file mode 100644 index 0000000000..ce29cb34a4 --- /dev/null +++ b/qiskit_experiments/options_field.py @@ -0,0 +1,58 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Description of field of experiment options. +""" + +import dataclasses +from typing import Any, Type, Dict +from qiskit.providers.options import Options + + +@dataclasses.dataclass +class OptionsField: + default: Any + annotation: Type + description: str + + +def _compile_docstring(header: str, fields: Dict[str, OptionsField]): + + __indent__ = " " + + docstring = f"{header}\n\n" + docstring += "Args:\n" + + for field_name, field in fields.items(): + docstring += __indent__ + docstring += f"{field_name}: {field.description}. Defaults to {repr(field.default)}.\n" + + return docstring + + +def _compile_annotations(fields: Dict[str, OptionsField]): + + annotations = dict() + + for field_name, field in fields.items(): + annotations[field_name] = field.annotation + + return annotations + + +def _to_options(fields: Dict[str, OptionsField]) -> Options: + + default_options = dict() + for field_name, field in fields.items(): + default_options[field_name] = field.default + + return Options(**default_options) From d473d5bd5eb95de2b43dffb8ac2968e03a99138b Mon Sep 17 00:00:00 2001 From: knzwnao Date: Fri, 18 Jun 2021 02:31:03 +0900 Subject: [PATCH 04/46] fix bug --- qiskit_experiments/__init__.py | 2 +- qiskit_experiments/analysis/curve_analysis.py | 55 +++-------- qiskit_experiments/base_analysis.py | 4 +- qiskit_experiments/base_experiment.py | 20 +--- qiskit_experiments/options_field.py | 92 +++++++++++++++++-- .../interleaved_rb_analysis.py | 12 ++- .../interleaved_rb_experiment.py | 2 + .../randomized_benchmarking/rb_analysis.py | 12 ++- .../randomized_benchmarking/rb_experiment.py | 2 + 9 files changed, 119 insertions(+), 82 deletions(-) diff --git a/qiskit_experiments/__init__.py b/qiskit_experiments/__init__.py index 09781ebef3..9f1c6fa3c2 100644 --- a/qiskit_experiments/__init__.py +++ b/qiskit_experiments/__init__.py @@ -51,7 +51,7 @@ # Base Classes from .experiment_data import ExperimentData, AnalysisResult from .base_analysis import BaseAnalysis -from .base_experiment import BaseExperiment, OptionsField +from .base_experiment import BaseExperiment # Experiment modules from . import composite diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 77683f6450..c07de25809 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -20,7 +20,6 @@ from typing import Any, Dict, List, Tuple, Callable, Union, Optional import numpy as np -from qiskit.providers.options import Options from qiskit_experiments.analysis import plotting from qiskit_experiments.analysis.curve_fitting import multi_curve_fit, CurveAnalysisResult @@ -249,94 +248,64 @@ def _default_options(cls) -> Dict[str, OptionsField]: "curve_fitter": OptionsField( default=multi_curve_fit, annotation=Callable, - description=""" -A callback function to perform fitting with formatted data. -This function should have signature: - -.. code-block:: - - def curve_fitter( - funcs: List[Callable], - series: ndarray, - xdata: ndarray, - ydata: ndarray, - p0: ndarray, - sigma: Optional[ndarray], - weights: Optional[ndarray], - bounds: Optional[ - Union[Dict[str, Tuple[float, float]], Tuple[ndarray, ndarray]] - ], - ) -> CurveAnalysisResult: - -See :func:`~qiskit_experiment.analysis.multi_curve_fit` for example. - """, + description="test", ), "data_processor": OptionsField( default=probability(outcome="1"), annotation=Union[Callable, DataProcessor], - description=""" -A callback function to format experiment data. -This function should have signature: - -.. code-block:: - - def data_processor(data: Dict[str, Any]) -> Tuple[float, float] - -This can be a :class:`~qiskit_experiment.data_processing.DataProcessor` -instance that defines the `self.__call__` method. - """, + description="test", ), "p0": OptionsField( default=None, annotation=Dict[str, float], - description="Dictionary of initial parameters. Keys are parameter names.", + description="Dictionary of initial parameters. Keys are parameter names", ), "bounds": OptionsField( default=None, annotation=Dict[str, Tuple[float, float]], description="Dictionary of (min, max) tuple of fit parameter boundaries. \ - Keys are parameter names.", +Keys are parameter names", ), "x_key": OptionsField( default="xval", annotation=str, - description="Circuit metadata key representing a scanned value.", + description="Circuit metadata key representing a scanned value", ), "plot": OptionsField( default=True, annotation=bool, - description="Set ``True`` to create figure for fit result.", + description="Set ``True`` to create figure for fit result", ), "axis": OptionsField( default=None, annotation="matplotlib.axes._subplots.AxesSubplot", - description="Optional. A matplotlib axis object to draw.", + description="Optional. A matplotlib axis object to draw", ), "xlabel": OptionsField( default=None, annotation=str, - description="X label of the fit result figure.", + description="X label of the fit result figure", ), "ylabel": OptionsField( default=None, annotation=str, - description="Y label of the fit result figure.", + description="Y label of the fit result figure", ), "ylim": OptionsField( default=None, annotation=Tuple[float, float], - description="Y axis limit of the fit result figure.", + description="Y axis limit of the fit result figure", ), "fit_reports": OptionsField( default=None, annotation=Dict[str, str], description="Mapping of fit parameters and representation in the fit report. \ - If nothing specified, fit report will not be shown.", +If nothing specified, fit report will not be shown", ), "return_data_points": OptionsField( default=False, annotation=bool, - description="Set ``True`` to return arrays of measured data points." + description="Set ``True`` to return arrays of measured data points" ), } diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 61d3663af3..7d7507b06a 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -20,7 +20,7 @@ from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ExperimentData, AnalysisResult -from qiskit_experiments.options_field import OptionsField, _to_options +from qiskit_experiments.options_field import OptionsField, to_options class BaseAnalysis(ABC): @@ -82,7 +82,7 @@ def run( f" but received {type(experiment_data).__name__}" ) # Get analysis options - analysis_options = _to_options(self._default_options()) + analysis_options = to_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 f1034ea182..fee77165bc 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -25,7 +25,7 @@ from qiskit.providers.options import Options from .experiment_data import ExperimentData -from .options_field import _compile_docstring, _compile_annotations +from .options_field import to_options class BaseExperiment(ABC): @@ -46,22 +46,6 @@ class BaseExperiment(ABC): # ExperimentData class for experiment __experiment_data__ = ExperimentData - # pylint: disable=unused-argument - def __new__(cls, qubits: Iterable[int], experiment_type: Optional[str] = None): - """Override help of options setter based on information provided by subclasses.""" - obj = object.__new__(cls) - - # Override set analysis option method help. - obj.set_analysis_options.__doc__ = _compile_docstring( - header="Return the analysis options for :meth:`run` analysis.", - fields=cls.__analysis_class__._default_options(), - ) - obj.set_analysis_options.__annotations__ = _compile_annotations( - fields=cls.__analysis_class__._default_options() - ) - - return obj - def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None): """Initialize the experiment object. @@ -290,7 +274,7 @@ def _default_analysis_options(cls) -> Options: # 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 to_options(cls.__analysis_class__._default_options()) return Options() @property diff --git a/qiskit_experiments/options_field.py b/qiskit_experiments/options_field.py index ce29cb34a4..a6c9aefc66 100644 --- a/qiskit_experiments/options_field.py +++ b/qiskit_experiments/options_field.py @@ -14,7 +14,9 @@ """ import dataclasses -from typing import Any, Type, Dict +import functools +from typing import Any, Type, Dict, Callable + from qiskit.providers.options import Options @@ -25,34 +27,104 @@ class OptionsField: description: str -def _compile_docstring(header: str, fields: Dict[str, OptionsField]): +def _compile_docstring( + header: str, + fields: Dict[str, OptionsField], +) -> str: + """Dynamically generates docstring based on information provided by ``OptionsField``s. - __indent__ = " " + Args: + header: Header string of the method docstring. + fields: List of ``OptionsField`` object. - docstring = f"{header}\n\n" - docstring += "Args:\n" + Returns: + Method docstring tailored for subclasses. + """ + _indent = " " + options = "" for field_name, field in fields.items(): - docstring += __indent__ - docstring += f"{field_name}: {field.description}. Defaults to {repr(field.default)}.\n" + if isinstance(field.default, Callable): + default_obj = f":py:func:`{field.default.__name__}`" + else: + default_obj = f":py:obj:`{field.default}`" + + options += _indent + f"{field_name}: {field.description} (Default: {default_obj}).\n" + + docstring = f"""{header} + +This method is always called with at least one of following keyword arguments. + +Args: +{options} + +.. note:: + + You can define arbitrary field with this method. + If you specify a field name not defined in above list, + the name-value pair is passed as ``**kwargs``. + If the target API does not support the keyword, you may fail in execution. + +""" return docstring -def _compile_annotations(fields: Dict[str, OptionsField]): +def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: + """Dynamically generate method annotation based on information provided by ``OptionsField``s. + + Args: + fields: List of ``OptionsField`` object. + Returns: + Dictionary of field name and type annotation. + """ annotations = dict() for field_name, field in fields.items(): - annotations[field_name] = field.annotation + if not isinstance(field.annotation, str): + annotations[field_name] = field.annotation return annotations -def _to_options(fields: Dict[str, OptionsField]) -> Options: +def to_options(fields: Dict[str, OptionsField]) -> Options: + """Converts a list of ``OptionsField`` into ``Options`` object. + + Args: + fields: List of ``OptionsField`` object to convert. + + Returns: + ``Options`` that filled with ``.default`` value of ``OptionsField``. + """ + if isinstance(fields, Options): + return fields default_options = dict() for field_name, field in fields.items(): default_options[field_name] = field.default return Options(**default_options) + + +def create_options_docs(experiment): + """A class decorator that overrides the docstring and annotation of option setters.""" + + # experiment.set_analysis_options directly calls base class method. + # Thus we cannot directly override __doc__ attribute. + + @functools.wraps(experiment.set_analysis_options) + def set_analysis_options(self, **fields): + self._analysis_options.update_options(**fields) + + set_analysis_options.__doc__ = _compile_docstring( + header="Set the analysis options for :meth:`run` method.", + fields=experiment.__analysis_class__._default_options(), + ) + set_analysis_options.__annotations__ = _compile_annotations( + fields=experiment.__analysis_class__._default_options() + ) + + setattr(experiment, set_analysis_options.__name__, set_analysis_options) + + return experiment diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index babdb11bef..2a464f30af 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -123,14 +123,20 @@ def _default_options(cls): descriptions of analysis options. """ default_options = super()._default_options() - default_options.p0 = {"a": None, "alpha": None, "alpha_c": None, "b": None} - default_options.bounds = { + + # update default values + default_options["p0"].default = {"a": None, "alpha": None, "alpha_c": None, "b": None} + default_options["bounds"].default = { "a": (0.0, 1.0), "alpha": (0.0, 1.0), "alpha_c": (0.0, 1.0), "b": (0.0, 1.0), } - default_options.fit_reports = {"alpha": "\u03B1", "alpha_c": "\u03B1$_c$", "EPC": "EPC"} + default_options["fit_reports"].default = { + "alpha": "\u03B1", + "alpha_c": "\u03B1$_c$", + "EPC": "EPC", + } return default_options diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py index b2932d994c..7150144505 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py @@ -22,8 +22,10 @@ from .rb_experiment import RBExperiment from .interleaved_rb_analysis import InterleavedRBAnalysis +from qiskit_experiments.options_field import create_options_docs +@create_options_docs class InterleavedRBExperiment(RBExperiment): """Interleaved RB Experiment class""" diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index f42f7359da..06bb76acf9 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -80,11 +80,13 @@ def _default_options(cls): descriptions of analysis options. """ default_options = super()._default_options() - default_options.p0 = {"a": None, "alpha": None, "b": None} - default_options.bounds = {"a": (0.0, 1.0), "alpha": (0.0, 1.0), "b": (0.0, 1.0)} - default_options.xlabel = "Clifford Length" - default_options.ylabel = "P(0)" - default_options.fit_reports = {"alpha": "\u03B1", "EPC": "EPC"} + + # update default values + default_options["p0"].default = {"a": None, "alpha": None, "b": None} + default_options["bounds"].default = {"a": (0.0, 1.0), "alpha": (0.0, 1.0), "b": (0.0, 1.0)} + default_options["xlabel"].default = "Clifford Length" + default_options["ylabel"].default = "P(0)" + default_options["fit_reports"].default = {"alpha": "\u03B1", "EPC": "EPC"} return default_options diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index b253705103..4cd79e3f19 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -25,10 +25,12 @@ from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.analysis.data_processing import probability +from qiskit_experiments.options_field import create_options_docs from .rb_analysis import RBAnalysis from .clifford_utils import CliffordUtils +@create_options_docs class RBExperiment(BaseExperiment): """RB Experiment class. From 3c911bb759887e50734a43f193e74c147397d701 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sun, 20 Jun 2021 18:16:17 +0900 Subject: [PATCH 05/46] update type handling --- qiskit_experiments/analysis/curve_analysis.py | 50 +++- qiskit_experiments/base_experiment.py | 8 + qiskit_experiments/options_field.py | 236 ++++++++++++++---- 3 files changed, 228 insertions(+), 66 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index c07de25809..ab13df0182 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -248,64 +248,88 @@ def _default_options(cls) -> Dict[str, OptionsField]: "curve_fitter": OptionsField( default=multi_curve_fit, annotation=Callable, - description="test", + description="""A callback function to perform fitting with formatted data. +This function should have signature: + +.. code-block:: + + def curve_fitter( + funcs: List[Callable], + series: ndarray, + xdata: ndarray, + ydata: ndarray, + p0: ndarray, + sigma: Optional[ndarray], + weights: Optional[ndarray], + bounds: Optional[Union[Dict[str, Tuple[float, float]], Tuple[ndarray, ndarray]]], + ) -> CurveAnalysisResult: + +See :py:func:`~qiskit_experiment.analysis.multi_curve_fit` for example.""", ), "data_processor": OptionsField( default=probability(outcome="1"), - annotation=Union[Callable, DataProcessor], - description="test", + annotation=Callable, + description="""A callback function to format experiment data. +This function should have signature: + +.. code-block:: + + def data_processor(data: Dict[str, Any]) -> Tuple[float, float] + +This can be a :py:class:`~qiskit_experiment.data_processing.DataProcessor` instance +or any class instance that defines the ``__call__`` method to be a callable.""", ), "p0": OptionsField( default=None, annotation=Dict[str, float], - description="Dictionary of initial parameters. Keys are parameter names", + description="Dictionary of initial parameters. Keys are parameter names.", ), "bounds": OptionsField( default=None, annotation=Dict[str, Tuple[float, float]], description="Dictionary of (min, max) tuple of fit parameter boundaries. \ -Keys are parameter names", +Keys are parameter names.", ), "x_key": OptionsField( default="xval", annotation=str, - description="Circuit metadata key representing a scanned value", + description="Circuit metadata key representing a scanned value.", ), "plot": OptionsField( default=True, annotation=bool, - description="Set ``True`` to create figure for fit result", + description="Set ``True`` to create figure for fit result.", ), "axis": OptionsField( default=None, annotation="matplotlib.axes._subplots.AxesSubplot", - description="Optional. A matplotlib axis object to draw", + description="Optional. A matplotlib axis object to draw.", ), "xlabel": OptionsField( default=None, annotation=str, - description="X label of the fit result figure", + description="X label of the fit result figure.", ), "ylabel": OptionsField( default=None, annotation=str, - description="Y label of the fit result figure", + description="Y label of the fit result figure.", ), "ylim": OptionsField( default=None, annotation=Tuple[float, float], - description="Y axis limit of the fit result figure", + description="Y axis limit of the fit result figure.", ), "fit_reports": OptionsField( default=None, annotation=Dict[str, str], description="Mapping of fit parameters and representation in the fit report. \ -If nothing specified, fit report will not be shown", +If nothing specified, fit report will not be shown.", ), "return_data_points": OptionsField( default=False, annotation=bool, - description="Set ``True`` to return arrays of measured data points" + description="Set ``True`` to return arrays of measured data points." ), } diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index fee77165bc..71152d0929 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -207,6 +207,10 @@ def experiment_options(self) -> Options: def set_experiment_options(self, **fields): """Set the experiment options. + .. note:: + + This docstring is automatically overridden by subclasses. + Args: fields: The fields to update the options @@ -284,6 +288,10 @@ def analysis_options(self) -> Options: def set_analysis_options(self, **fields): """Set the analysis options for :meth:`run` method. + + .. note:: + + This docstring is automatically overridden by subclasses. Args: fields: The fields to update the options diff --git a/qiskit_experiments/options_field.py b/qiskit_experiments/options_field.py index a6c9aefc66..638a8513cd 100644 --- a/qiskit_experiments/options_field.py +++ b/qiskit_experiments/options_field.py @@ -15,59 +15,157 @@ import dataclasses import functools -from typing import Any, Type, Dict, Callable +from types import FunctionType +from typing import Any, Type, Dict, Optional, List +from qiskit.exceptions import QiskitError from qiskit.providers.options import Options @dataclasses.dataclass class OptionsField: - default: Any - annotation: Type - description: str + """A data container to describe a single entry in options.""" + # Default value + default: Any -def _compile_docstring( - header: str, - fields: Dict[str, OptionsField], -) -> str: - """Dynamically generates docstring based on information provided by ``OptionsField``s. - - Args: - header: Header string of the method docstring. - fields: List of ``OptionsField`` object. + # Type annotation + annotation: Type - Returns: - Method docstring tailored for subclasses. - """ - _indent = " " + # Docstring description of the entry + description: str - options = "" - for field_name, field in fields.items(): - if isinstance(field.default, Callable): - default_obj = f":py:func:`{field.default.__name__}`" + # Set True if this is not a default option + extra_option: bool = False + + +class _OptionMethodDocstringMaker: + """Facade of method docstring writer.""" + @classmethod + def make_docstring( + cls, + header: str, + fields: Dict[str, OptionsField], + notes: Optional[str] = None, + raises: Optional[Dict[str, str]] = None + ) -> str: + """Create method docstring. + + This mutably update method docstring. + + Args: + header: Short string for method docstring header to write Args section. + fields: Dictionary of argument name and ``OptionsField``. + notes: Additional text block for notation. + raises: Dictionary of error name and descriptions to write Raises section. + + Returns: + Automatically generated docstring. + """ + try: + writer = _OptionMethodDocstringWriter() + writer.header(header) + arg_elems = [ + (key, field.annotation, field.description, field.default) + for key, field in fields.items() + ] + writer.args(*list(zip(*arg_elems))) + if raises: + writer.raises(*list(zip(*raises.items()))) + if notes: + writer.note(notes) + except Exception as ex: + raise QiskitError( + f"Auto docstring failed due to following error: {ex}" + ) from ex + return writer.docstring + + +class _OptionMethodDocstringWriter: + """Actual docstring writer.""" + __indent__ = " " + + def __init__(self): + self.docstring = "" + + def header(self, header: str): + """Output header.""" + self.docstring += f"{header}\n\n" + + def args( + self, + argnames: List[str], + annotations: List[Any], + descriptions: List[str], + defaults: List[str] + ): + """Output argument section.""" + self.docstring += "Args:\n" + for argname, annotation, description, default in \ + zip(argnames, annotations, descriptions, defaults): + self.docstring += self.__indent__ + + # write argument name + self.docstring += f"{argname} (:py:obj:`{self._parse_type(annotation)}`): " + + # write multi line description of argument + for line in description.split("\n"): + self.docstring += f"{line}\n" + self.docstring += self.__indent__ * 2 + + # write default value + if isinstance(default, FunctionType): + default_obj = f":py:func:`~{default.__module__}.{default.__name__}`" + else: + default_obj = f":py:obj:`{default}`" + self.docstring += f"(Default: {default_obj})\n" + self.docstring += "\n" + + def note(self, note: str): + """Output note section.""" + self.docstring += ".. note::\n\n" + for line in note.split("\n"): + self.docstring += self.__indent__ + self.docstring += f"{line}\n" + self.docstring += "\n" + + def raises(self, error_kinds: List[str], descriptions: List[str]): + """Output raises section.""" + self.docstring += "Raises:\n" + for error_kind, description in zip(error_kinds, descriptions): + self.docstring += self.__indent__ + self.docstring += f"{error_kind}: {description}\n" + self.docstring += "\n" + + def _parse_type(self, type_obj: Any): + """Convert type alias to string.""" + if isinstance(type_obj, str): + # forward reference + return type_obj + + module = type_obj.__module__ + + if module == "builtins": + return type_obj.__name__ + elif module == "typing": + # type name + if hasattr(type_obj, "_name") and type_obj._name: + # _SpecialForm or special=True + name = type_obj._name + else: + # _GenericAlias and special=False + if type_obj.__origin__: + name = self._parse_type(type_obj.__origin__) + else: + name = type_obj.__name__ + # arguments + if hasattr(type_obj, "__args__") and type_obj.__args__: + args = [self._parse_type(arg) for arg in type_obj.__args__] + return f"{name}[{', '.join(args)}]" + else: + return name else: - default_obj = f":py:obj:`{field.default}`" - - options += _indent + f"{field_name}: {field.description} (Default: {default_obj}).\n" - - docstring = f"""{header} - -This method is always called with at least one of following keyword arguments. - -Args: -{options} - -.. note:: - - You can define arbitrary field with this method. - If you specify a field name not defined in above list, - the name-value pair is passed as ``**kwargs``. - If the target API does not support the keyword, you may fail in execution. - -""" - - return docstring + return f":py:class`{module}.{type_obj.__name__}`" def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: @@ -88,6 +186,28 @@ def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: return annotations +def _copy_method(experiment, method_name: str) -> FunctionType: + """A helper function to duplicate base calss method. + + Args: + experiment: Base class to get a method. + method_name: Name of method to copy. + + Returns: + Duplicated function object. + """ + base_method = getattr(experiment, method_name) + + new_method = FunctionType( + code=base_method.__code__, + globals=base_method.__globals__, + name=base_method.__name__, + argdefs=base_method.__defaults__, + closure=base_method.__closure__, + ) + return functools.update_wrapper(wrapper=new_method, wrapped=base_method) + + def to_options(fields: Dict[str, OptionsField]) -> Options: """Converts a list of ``OptionsField`` into ``Options`` object. @@ -112,19 +232,29 @@ def create_options_docs(experiment): # experiment.set_analysis_options directly calls base class method. # Thus we cannot directly override __doc__ attribute. + fields = experiment.__analysis_class__._default_options() + + method = _copy_method(experiment, "set_analysis_options") + method.__annotations__ = _compile_annotations(fields=fields) + method.__doc__ = _OptionMethodDocstringMaker.make_docstring( + header=f"Set the analysis options for :meth:`run_analysis` method.", + fields=fields, + notes="""You can define arbitrary field with this method. +If you specify a field name not defined in above list, +the name-value pair is passed as ``**kwargs``. +If your ``curve_fitter`` API does not support the keyword, you may fail in analysis. +""", + ) + + setattr(experiment, "set_analysis_options", method) + + # experiment.set_experiment_options directly calls base class method. + # Thus we cannot directly override __doc__ attribute. + + + - @functools.wraps(experiment.set_analysis_options) - def set_analysis_options(self, **fields): - self._analysis_options.update_options(**fields) - set_analysis_options.__doc__ = _compile_docstring( - header="Set the analysis options for :meth:`run` method.", - fields=experiment.__analysis_class__._default_options(), - ) - set_analysis_options.__annotations__ = _compile_annotations( - fields=experiment.__analysis_class__._default_options() - ) - setattr(experiment, set_analysis_options.__name__, set_analysis_options) return experiment From df55eec2192a0dc566e40120b08dedb5e52450f3 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sun, 20 Jun 2021 18:17:41 +0900 Subject: [PATCH 06/46] black --- qiskit_experiments/analysis/curve_analysis.py | 2 +- qiskit_experiments/base_experiment.py | 2 +- qiskit_experiments/options_field.py | 37 ++++++++----------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index ab13df0182..85c2602e2f 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -329,7 +329,7 @@ def data_processor(data: Dict[str, Any]) -> Tuple[float, float] "return_data_points": OptionsField( default=False, annotation=bool, - description="Set ``True`` to return arrays of measured data points." + description="Set ``True`` to return arrays of measured data points.", ), } diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 71152d0929..5d5ed4977f 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -288,7 +288,7 @@ def analysis_options(self) -> Options: def set_analysis_options(self, **fields): """Set the analysis options for :meth:`run` method. - + .. note:: This docstring is automatically overridden by subclasses. diff --git a/qiskit_experiments/options_field.py b/qiskit_experiments/options_field.py index 638a8513cd..0cf3d207a4 100644 --- a/qiskit_experiments/options_field.py +++ b/qiskit_experiments/options_field.py @@ -41,13 +41,14 @@ class OptionsField: class _OptionMethodDocstringMaker: """Facade of method docstring writer.""" + @classmethod def make_docstring( - cls, - header: str, - fields: Dict[str, OptionsField], - notes: Optional[str] = None, - raises: Optional[Dict[str, str]] = None + cls, + header: str, + fields: Dict[str, OptionsField], + notes: Optional[str] = None, + raises: Optional[Dict[str, str]] = None, ) -> str: """Create method docstring. @@ -75,14 +76,13 @@ def make_docstring( if notes: writer.note(notes) except Exception as ex: - raise QiskitError( - f"Auto docstring failed due to following error: {ex}" - ) from ex + raise QiskitError(f"Auto docstring failed due to following error: {ex}") from ex return writer.docstring class _OptionMethodDocstringWriter: """Actual docstring writer.""" + __indent__ = " " def __init__(self): @@ -93,16 +93,17 @@ def header(self, header: str): self.docstring += f"{header}\n\n" def args( - self, - argnames: List[str], - annotations: List[Any], - descriptions: List[str], - defaults: List[str] + self, + argnames: List[str], + annotations: List[Any], + descriptions: List[str], + defaults: List[str], ): """Output argument section.""" self.docstring += "Args:\n" - for argname, annotation, description, default in \ - zip(argnames, annotations, descriptions, defaults): + for argname, annotation, description, default in zip( + argnames, annotations, descriptions, defaults + ): self.docstring += self.__indent__ # write argument name @@ -251,10 +252,4 @@ def create_options_docs(experiment): # experiment.set_experiment_options directly calls base class method. # Thus we cannot directly override __doc__ attribute. - - - - - - return experiment From eb525bc69709600a86243b2d06c6550f27fe4c5e Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 22 Jun 2021 16:16:43 +0900 Subject: [PATCH 07/46] add experiment options update and class docstring update --- qiskit_experiments/analysis/curve_analysis.py | 5 +- qiskit_experiments/base_analysis.py | 2 +- qiskit_experiments/base_experiment.py | 20 +- .../characterization/qubit_spectroscopy.py | 92 +++++---- .../{options_field.py => options_autodoc.py} | 174 +++++++++++++----- .../interleaved_rb_analysis.py | 6 +- .../interleaved_rb_experiment.py | 4 +- .../randomized_benchmarking/rb_analysis.py | 6 +- .../randomized_benchmarking/rb_experiment.py | 28 ++- 9 files changed, 219 insertions(+), 118 deletions(-) rename qiskit_experiments/{options_field.py => options_autodoc.py} (55%) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 85c2602e2f..8aa370d7db 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -17,6 +17,7 @@ import dataclasses import inspect +from abc import ABC from typing import Any, Dict, List, Tuple, Callable, Union, Optional import numpy as np @@ -25,12 +26,12 @@ from qiskit_experiments.analysis.curve_fitting import multi_curve_fit, CurveAnalysisResult from qiskit_experiments.analysis.data_processing import probability from qiskit_experiments.analysis.utils import get_opt_value, get_opt_error -from qiskit_experiments.options_field import OptionsField from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.data_processing import DataProcessor from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import AnalysisResult, ExperimentData +from qiskit_experiments.options_autodoc import OptionsField @dataclasses.dataclass(frozen=True) @@ -56,7 +57,7 @@ class CurveData: metadata: np.ndarray = None -class CurveAnalysis(BaseAnalysis): +class CurveAnalysis(BaseAnalysis, ABC): """A base class for curve fit type analysis. The subclasses can override class attributes to define the behavior of diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 7d7507b06a..37cde36a54 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -20,7 +20,7 @@ from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ExperimentData, AnalysisResult -from qiskit_experiments.options_field import OptionsField, to_options +from qiskit_experiments.options_autodoc import OptionsField, to_options class BaseAnalysis(ABC): diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 5d5ed4977f..4d4ead5c3e 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -16,7 +16,7 @@ import copy from abc import ABC, abstractmethod from numbers import Integral -from typing import Iterable, Optional, Tuple, List +from typing import Iterable, Optional, Tuple, List, Dict from qiskit import transpile, assemble, QuantumCircuit from qiskit.exceptions import QiskitError @@ -25,7 +25,7 @@ from qiskit.providers.options import Options from .experiment_data import ExperimentData -from .options_field import to_options +from .options_autodoc import to_options, OptionsField class BaseExperiment(ABC): @@ -72,10 +72,10 @@ def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None) raise QiskitError("Duplicate qubits in physical qubits list.") # Experiment options - self._experiment_options = self._default_experiment_options() + self._experiment_options = to_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() + self._analysis_options = to_options(self._default_analysis_options()) # Set initial layout from qubits self._transpile_options.initial_layout = self._physical_qubits @@ -191,13 +191,13 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: # generation @classmethod - def _default_experiment_options(cls) -> Options: + def _default_experiment_options(cls) -> Dict[str, OptionsField]: """Default kwarg options for experiment""" # Experiment subclasses should override this method to return - # an `Options` object containing all the supported options for + # an dictionary of OptionsField 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() + return dict() @property def experiment_options(self) -> Options: @@ -272,14 +272,14 @@ def set_run_options(self, **fields): self._run_options.update_options(**fields) @classmethod - def _default_analysis_options(cls) -> Options: + def _default_analysis_options(cls) -> Dict[str, OptionsField]: """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 to_options(cls.__analysis_class__._default_options()) - return Options() + return cls.__analysis_class__._default_options() + return dict() @property def analysis_options(self) -> Options: diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 34acf7814d..8f49819eaf 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -31,6 +31,7 @@ get_opt_value, get_opt_error, ) +from qiskit_experiments.options_autodoc import OptionsField, create_experiment_docs from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.data_processing.processor_library import get_to_signal_processor @@ -80,17 +81,19 @@ class SpectroscopyAnalysis(CurveAnalysis): ] @classmethod - def _default_options(cls): - """Return default data processing options. - - See :meth:`~qiskit_experiment.analysis.CurveAnalysis._default_options` for - descriptions of analysis options. - """ + def _default_options(cls) -> Dict[str, OptionsField]: + """Return default options.""" default_options = super()._default_options() - default_options.p0 = {"a": None, "sigma": None, "freq": None, "b": None} - default_options.bounds = {"a": None, "sigma": None, "freq": None, "b": None} - default_options.fit_reports = {"freq": "frequency"} - default_options.normalization = True + default_options["p0"].default = {"a": None, "sigma": None, "freq": None, "b": None} + default_options["bounds"].default = {"a": None, "sigma": None, "freq": None, "b": None} + default_options["fit_reports"].default = {"freq": "frequency"} + + default_options["normalization"] = OptionsField( + default=True, + annotation=bool, + description="Set ``True`` to normalize measurement data. Usually applied to \ +Kerneled (level1) measurement data.", + ) return default_options @@ -176,22 +179,27 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR return analysis_result +@create_experiment_docs class QubitSpectroscopy(BaseExperiment): """Class that runs spectroscopy by sweeping the qubit frequency. - The circuits produced by spectroscopy, i.e. + Overview + The circuits produced by spectroscopy, i.e. + + .. parsed-literal:: - .. parsed-literal:: + ┌────────────┐ ░ ┌─┐ + q_0: ┤ Spec(freq) ├─░─┤M├ + └────────────┘ ░ └╥┘ + measure: 1/══════════════════╩═ + 0 - ┌────────────┐ ░ ┌─┐ - q_0: ┤ Spec(freq) ├─░─┤M├ - └────────────┘ ░ └╥┘ - measure: 1/══════════════════╩═ - 0 + have a spectroscopy pulse-schedule embedded in a spectroscopy gate. The + pulse-schedule consists of a set frequency instruction followed by a GaussianSquare + pulse. A list of circuits is generated, each with a different frequency "freq". - have a spectroscopy pulse-schedule embedded in a spectroscopy gate. The - pulse-schedule consists of a set frequency instruction followed by a GaussianSquare - pulse. A list of circuits is generated, each with a different frequency "freq". + A spectroscopy experiment run by setting the frequency of the qubit drive. + The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. """ __analysis_class__ = SpectroscopyAnalysis @@ -208,14 +216,35 @@ def _default_run_options(cls) -> Options: ) @classmethod - def _default_experiment_options(cls) -> Options: + def _default_experiment_options(cls) -> Dict[str, OptionsField]: """Default option values used for the spectroscopy pulse.""" - return Options( - amp=0.1, - duration=1024, - sigma=256, - width=0, - ) + return { + "amp": OptionsField( + default=0.1, + annotation=float, + description="Amplitude of spectroscopy pulse. Usually weak power pulse is used to \ +suppress broadening of observed peaks.", + ), + "duration": OptionsField( + default=1024, + annotation=int, + description="Duration of spectroscopy pulse. This may need to satisfy the \ +hardware waveform memory constraint. The default value is represented in units of dt.", + ), + "sigma": OptionsField( + default=256, + annotation=Union[int, float], + description="Sigma of Gaussian rising and falling edges. This value should be \ +sufficiently smaller than the duration, otherwise waveform is distorted. \ +The default value is represented in units of dt." + ), + "width": OptionsField( + default=0, + annotation=Union[int, float], + description="Width of the flat-top part of the Gaussian square envelope of \ +spectroscopy pulse. Set width=0 to use Gaussian pulse.", + ), + } def __init__( self, @@ -224,14 +253,7 @@ def __init__( unit: Optional[str] = "Hz", absolute: bool = True, ): - """ - A spectroscopy experiment run by setting the frequency of the qubit drive. - The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. - The spectroscopy pulse has the following parameters: - - amp: The amplitude of the pulse must be between 0 and 1, the default is 0.1. - - duration: The duration of the spectroscopy pulse in samples, the default is 1000 samples. - - sigma: The standard deviation of the pulse, the default is duration / 4. - - width: The width of the flat-top in the pulse, the default is 0, i.e. a Gaussian. + """Create new experiment. Args: qubit: The qubit on which to run spectroscopy. diff --git a/qiskit_experiments/options_field.py b/qiskit_experiments/options_autodoc.py similarity index 55% rename from qiskit_experiments/options_field.py rename to qiskit_experiments/options_autodoc.py index 0cf3d207a4..1941993ecb 100644 --- a/qiskit_experiments/options_field.py +++ b/qiskit_experiments/options_autodoc.py @@ -16,7 +16,7 @@ import dataclasses import functools from types import FunctionType -from typing import Any, Type, Dict, Optional, List +import typing from qiskit.exceptions import QiskitError from qiskit.providers.options import Options @@ -27,10 +27,10 @@ class OptionsField: """A data container to describe a single entry in options.""" # Default value - default: Any + default: typing.Any # Type annotation - annotation: Type + annotation: typing.Type # Docstring description of the entry description: str @@ -39,6 +39,41 @@ class OptionsField: extra_option: bool = False +class _ExperimentDocstringMaker: + """Facade of class docstring writer.""" + + @classmethod + def make_docsrting( + cls, + experiment, + analysis_fields: typing.Dict[str, OptionsField], + experiment_fields: typing.Dict[str, OptionsField] + ) -> str: + """Create extra class docstring for options.""" + try: + writer = _DocstringWriter() + writer.custom_section( + section="Analysis Class", + section_note=f":py:class:`~{experiment.__analysis_class__.__module__}.\ +{experiment.__analysis_class__.__name__}`" + ) + writer.custom_section_options( + fields=experiment_fields, + section="Experiment Options", + section_note="Experiment options to generate circuits. Options can be updated \ +with :meth:`set_experiment_options` method. See docstring of the method for details.", + ) + writer.custom_section_options( + fields=analysis_fields, + section="Analysis Options", + section_note="Analysis options to perform result analysis. Options can be updated \ +with :meth:`set_analysis_options` method. See docstring of the method for details.", + ) + except Exception as ex: + raise QiskitError(f"Auto docstring failed due to following error: {ex}") from ex + return writer.docstring + + class _OptionMethodDocstringMaker: """Facade of method docstring writer.""" @@ -46,9 +81,9 @@ class _OptionMethodDocstringMaker: def make_docstring( cls, header: str, - fields: Dict[str, OptionsField], - notes: Optional[str] = None, - raises: Optional[Dict[str, str]] = None, + fields: typing.Dict[str, OptionsField], + notes: typing.Optional[str] = None, + raises: typing.Optional[typing.Dict[str, str]] = None, ) -> str: """Create method docstring. @@ -64,13 +99,9 @@ def make_docstring( Automatically generated docstring. """ try: - writer = _OptionMethodDocstringWriter() + writer = _DocstringWriter() writer.header(header) - arg_elems = [ - (key, field.annotation, field.description, field.default) - for key, field in fields.items() - ] - writer.args(*list(zip(*arg_elems))) + writer.args(fields) if raises: writer.raises(*list(zip(*raises.items()))) if notes: @@ -80,9 +111,8 @@ def make_docstring( return writer.docstring -class _OptionMethodDocstringWriter: - """Actual docstring writer.""" - +class _DocstringWriter: + """Docstring writer.""" __indent__ = " " def __init__(self): @@ -92,36 +122,53 @@ def header(self, header: str): """Output header.""" self.docstring += f"{header}\n\n" - def args( - self, - argnames: List[str], - annotations: List[Any], - descriptions: List[str], - defaults: List[str], - ): + def args(self, fields: typing.Dict[str, OptionsField]): """Output argument section.""" self.docstring += "Args:\n" - for argname, annotation, description, default in zip( - argnames, annotations, descriptions, defaults - ): - self.docstring += self.__indent__ - - # write argument name - self.docstring += f"{argname} (:py:obj:`{self._parse_type(annotation)}`): " - - # write multi line description of argument - for line in description.split("\n"): - self.docstring += f"{line}\n" - self.docstring += self.__indent__ * 2 + for arg_name, field in fields.items(): + # parse type + arg_str_type = f":py:obj:`{self._parse_type(field.annotation)}`" + # write multi line description + arg_description = "" + for line in field.description.split("\n"): + arg_description += f"{line}\n" + arg_description += self.__indent__ * 2 # write default value - if isinstance(default, FunctionType): - default_obj = f":py:func:`~{default.__module__}.{default.__name__}`" + if isinstance(field.default, FunctionType): + default_obj = f":py:func:`~{field.default.__module__}.{field.default.__name__}`" else: - default_obj = f":py:obj:`{default}`" - self.docstring += f"(Default: {default_obj})\n" + default_obj = f":py:obj:`{field.default}`" + arg_description += f"(Default: {default_obj})" + self.docstring += self.__indent__ + self.docstring += f"{arg_name} ({arg_str_type}): {arg_description}\n" + self.docstring += "\n" + + def custom_section_options( + self, + fields: typing.Dict[str, OptionsField], + section: str, + section_note: typing.Optional[str] = "" + ): + """Output custom section for options.""" + self.docstring += f"{section_note}\n\n" + self.docstring += f"{section}\n" + + for arg_name, field in fields.items(): + arg_str_type = f":py:obj:`{self._parse_type(field.annotation)}`" + arg_description = field.description.split('\n')[0] + # write multi line description + self.docstring += self.__indent__ + self.docstring += f"- **{arg_name}** ({arg_str_type}): {arg_description}\n" self.docstring += "\n" + def custom_section(self, section: str, section_note: str): + """Output arbitrary custom section.""" + self.docstring += f"{section}\n" + self.docstring += self.__indent__ + self.docstring += section_note + self.docstring += "\n\n" + def note(self, note: str): """Output note section.""" self.docstring += ".. note::\n\n" @@ -130,7 +177,7 @@ def note(self, note: str): self.docstring += f"{line}\n" self.docstring += "\n" - def raises(self, error_kinds: List[str], descriptions: List[str]): + def raises(self, error_kinds: typing.List[str], descriptions: typing.List[str]): """Output raises section.""" self.docstring += "Raises:\n" for error_kind, description in zip(error_kinds, descriptions): @@ -138,7 +185,7 @@ def raises(self, error_kinds: List[str], descriptions: List[str]): self.docstring += f"{error_kind}: {description}\n" self.docstring += "\n" - def _parse_type(self, type_obj: Any): + def _parse_type(self, type_obj: typing.Any): """Convert type alias to string.""" if isinstance(type_obj, str): # forward reference @@ -155,10 +202,11 @@ def _parse_type(self, type_obj: Any): name = type_obj._name else: # _GenericAlias and special=False - if type_obj.__origin__: - name = self._parse_type(type_obj.__origin__) + type_repr = repr(type_obj).replace("typing.", "") + if type_repr in typing.__all__: + name = type_repr else: - name = type_obj.__name__ + name = self._parse_type(type_obj.__origin__) # arguments if hasattr(type_obj, "__args__") and type_obj.__args__: args = [self._parse_type(arg) for arg in type_obj.__args__] @@ -169,7 +217,7 @@ def _parse_type(self, type_obj: Any): return f":py:class`{module}.{type_obj.__name__}`" -def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: +def _compile_annotations(fields: typing.Dict[str, OptionsField]) -> typing.Dict[str, typing.Any]: """Dynamically generate method annotation based on information provided by ``OptionsField``s. Args: @@ -209,7 +257,7 @@ def _copy_method(experiment, method_name: str) -> FunctionType: return functools.update_wrapper(wrapper=new_method, wrapped=base_method) -def to_options(fields: Dict[str, OptionsField]) -> Options: +def to_options(fields: typing.Dict[str, OptionsField]) -> Options: """Converts a list of ``OptionsField`` into ``Options`` object. Args: @@ -228,28 +276,54 @@ def to_options(fields: Dict[str, OptionsField]) -> Options: return Options(**default_options) -def create_options_docs(experiment): +def create_experiment_docs(experiment): """A class decorator that overrides the docstring and annotation of option setters.""" # experiment.set_analysis_options directly calls base class method. # Thus we cannot directly override __doc__ attribute. - fields = experiment.__analysis_class__._default_options() + analysis_fields = experiment.__analysis_class__._default_options() method = _copy_method(experiment, "set_analysis_options") - method.__annotations__ = _compile_annotations(fields=fields) + method.__annotations__ = _compile_annotations(fields=analysis_fields) method.__doc__ = _OptionMethodDocstringMaker.make_docstring( header=f"Set the analysis options for :meth:`run_analysis` method.", - fields=fields, + fields=analysis_fields, notes="""You can define arbitrary field with this method. If you specify a field name not defined in above list, the name-value pair is passed as ``**kwargs``. If your ``curve_fitter`` API does not support the keyword, you may fail in analysis. """, ) - setattr(experiment, "set_analysis_options", method) # experiment.set_experiment_options directly calls base class method. # Thus we cannot directly override __doc__ attribute. + experiment_fields = experiment._default_experiment_options() + + method = _copy_method(experiment, "set_experiment_options") + method.__annotations__ = _compile_annotations(fields=experiment_fields) + method.__doc__ = _OptionMethodDocstringMaker.make_docstring( + header=f"Set the experiment options. These options are consumed for generating \ +the experiment circuits.", + fields=experiment_fields, + raises={"AttributeError": "If the field passed in is not a supported options"}, + ) + setattr(experiment, "set_experiment_options", method) + + extra_class_docs = _ExperimentDocstringMaker.make_docsrting( + experiment=experiment, + analysis_fields=analysis_fields, + experiment_fields=experiment_fields, + ) + experiment.__doc__ += f"\n\n{extra_class_docs}" return experiment + + +def create_analysis_docs(analysis): + """A class decorator that overrides the docstring.""" + analysis_fields = analysis._default_options() + + + + diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index 2a464f30af..8dff6b3f9b 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -117,11 +117,7 @@ class InterleavedRBAnalysis(RBAnalysis): @classmethod def _default_options(cls): - """Return default data processing options. - - See :meth:`~qiskit_experiment.analysis.CurveAnalysis._default_options` for - descriptions of analysis options. - """ + """Return default options.""" default_options = super()._default_options() # update default values diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py index 7150144505..65bad588ea 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py @@ -22,10 +22,10 @@ from .rb_experiment import RBExperiment from .interleaved_rb_analysis import InterleavedRBAnalysis -from qiskit_experiments.options_field import create_options_docs +from qiskit_experiments.options_autodoc import create_experiment_docs -@create_options_docs +@create_experiment_docs class InterleavedRBExperiment(RBExperiment): """Interleaved RB Experiment class""" diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index 06bb76acf9..a6103ff4a4 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -74,11 +74,7 @@ class RBAnalysis(CurveAnalysis): @classmethod def _default_options(cls): - """Return default options. - - See :meth:`~qiskit_experiment.analysis.CurveAnalysis._default_options` for - descriptions of analysis options. - """ + """Return default options.""" default_options = super()._default_options() # update default values diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index 4cd79e3f19..2ffa6529ad 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -16,21 +16,19 @@ import numpy as np from numpy.random import Generator, default_rng - from qiskit import QuantumCircuit +from qiskit.circuit import Gate from qiskit.providers import Backend from qiskit.quantum_info import Clifford -from qiskit.providers.options import Options -from qiskit.circuit import Gate -from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.analysis.data_processing import probability -from qiskit_experiments.options_field import create_options_docs -from .rb_analysis import RBAnalysis +from qiskit_experiments.base_experiment import BaseExperiment +from qiskit_experiments.options_autodoc import OptionsField, create_experiment_docs from .clifford_utils import CliffordUtils +from .rb_analysis import RBAnalysis -@create_options_docs +@create_experiment_docs class RBExperiment(BaseExperiment): """RB Experiment class. @@ -82,7 +80,21 @@ def __init__( @classmethod def _default_experiment_options(cls): - return Options(lengths=None, num_samples=None) + return { + "lengths": OptionsField( + default=None, + annotation=Iterable[int], + description="Array of integer values representing a number of Clifford gate N per \ +RB sequence. This value should be chosen based on expected decay curve. If the maximum length is \ +too short, confidence interval of fit will become poor.", + ), + "num_samples": OptionsField( + default=None, + annotation=int, + description="Number of RB sequence per Clifford length. M random sequences are \ +generated for a length N.", + ), + } # pylint: disable = arguments-differ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: From bc2f3ed7b6d2fedf4d24a33e9d16f407f4354c76 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 23 Jun 2021 21:39:10 +0900 Subject: [PATCH 08/46] cleanup facades and make autodoc module a package --- qiskit_experiments/analysis/curve_analysis.py | 2 +- qiskit_experiments/autodocs/__init__.py | 25 ++ qiskit_experiments/autodocs/descriptions.py | 102 ++++++ .../autodocs/experiment_docs.py | 88 +++++ .../autodocs/set_option_docs.py | 123 +++++++ qiskit_experiments/autodocs/writer.py | 171 +++++++++ qiskit_experiments/base_analysis.py | 2 +- qiskit_experiments/base_experiment.py | 10 +- .../characterization/qubit_spectroscopy.py | 39 ++- qiskit_experiments/options_autodoc.py | 329 ------------------ .../interleaved_rb_experiment.py | 14 +- .../randomized_benchmarking/rb_experiment.py | 37 +- 12 files changed, 582 insertions(+), 360 deletions(-) create mode 100644 qiskit_experiments/autodocs/__init__.py create mode 100644 qiskit_experiments/autodocs/descriptions.py create mode 100644 qiskit_experiments/autodocs/experiment_docs.py create mode 100644 qiskit_experiments/autodocs/set_option_docs.py create mode 100644 qiskit_experiments/autodocs/writer.py delete mode 100644 qiskit_experiments/options_autodoc.py diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 8aa370d7db..fa232b153e 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -31,7 +31,7 @@ from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import AnalysisResult, ExperimentData -from qiskit_experiments.options_autodoc import OptionsField +from qiskit_experiments.autodocs import OptionsField @dataclasses.dataclass(frozen=True) diff --git a/qiskit_experiments/autodocs/__init__.py b/qiskit_experiments/autodocs/__init__.py new file mode 100644 index 0000000000..16dce6305f --- /dev/null +++ b/qiskit_experiments/autodocs/__init__.py @@ -0,0 +1,25 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Auto docstring generation package for Qiskit expeirments. + +This tool guarantees the standardized docstring among the experiments and removes +variation of docstring quality depending on the coders. +In addition, this provides auto docstring generation of experiment options, considering +the inheritance of experiment classes, i.e. a documentation for options in a parent class +also appears in all subclasses. This drastically reduces the overhead of docstring management, +while providing users with the standardized and high quality documentation. +""" + +from .experiment_docs import auto_experiment_documentation +from .set_option_docs import auto_options_method_documentation +from .descriptions import OptionsField, Reference, to_options diff --git a/qiskit_experiments/autodocs/descriptions.py b/qiskit_experiments/autodocs/descriptions.py new file mode 100644 index 0000000000..09c2739912 --- /dev/null +++ b/qiskit_experiments/autodocs/descriptions.py @@ -0,0 +1,102 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Description of options. +""" + +import dataclasses +import typing +from qiskit.providers import Options + + +@dataclasses.dataclass +class OptionsField: + """A data container to describe a single entry in options.""" + + # Default value + default: typing.Any + + # Type annotation + annotation: typing.Type + + # Docstring description of the entry + description: str + + # Set True if this is not a default option + is_extra: bool = False + + +@dataclasses.dataclass +class Reference: + """A data class to describe reference.""" + + # Article title + title: typing.Optional[str] = None + + # Author + authors: typing.Optional[str] = None + + # Journal info + journal_info: typing.Optional[str] = None + + # Open Access + open_access_link: typing.Optional[str] = None + + +def _parse_annotation(_type: typing.Any) -> str: + """A helper function to convert type object into string.""" + + if isinstance(_type, str): + # forward reference + return _type + + module = _type.__module__ + + if module == "builtins": + return _type.__name__ + elif module == "typing": + # type representation + name = getattr(_type, "_name", None) + if name is None: + # _GenericAlias and special=False + type_repr = repr(_type).replace("typing.", "") + if type_repr in typing.__all__: + name = type_repr + else: + name = _parse_annotation(_type.__origin__) + # arguments + if hasattr(_type, "__args__") and _type.__args__: + args = [_parse_annotation(arg) for arg in _type.__args__] + return f"{name}[{', '.join(args)}]" + else: + return name + else: + return f":py:class:`~{module}.{_type.__name__}`" + + +def to_options(fields: typing.Dict[str, OptionsField]) -> Options: + """Converts a dictionary of ``OptionsField`` into ``Options`` object. + + Args: + fields: List of ``OptionsField`` object to convert. + + Returns: + ``Options`` that filled with ``.default`` value of ``OptionsField``. + """ + if isinstance(fields, Options): + return fields + + default_options = dict() + for field_name, field in fields.items(): + default_options[field_name] = field.default + + return Options(**default_options) diff --git a/qiskit_experiments/autodocs/experiment_docs.py b/qiskit_experiments/autodocs/experiment_docs.py new file mode 100644 index 0000000000..a8efef995a --- /dev/null +++ b/qiskit_experiments/autodocs/experiment_docs.py @@ -0,0 +1,88 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Documentation for experiment. +""" +from typing import Optional, Dict, List + +from qiskit.exceptions import QiskitError + +from qiskit_experiments.base_experiment import BaseExperiment +from .descriptions import OptionsField, Reference +from .writer import _DocstringWriter, _DocstringMaker + + +class StandardExperimentDocstring(_DocstringMaker): + """A facade class to write standard experiment docstring.""" + + @classmethod + def make_docstring( + cls, + analysis_options: Dict[str, OptionsField], + experiment_options: Dict[str, OptionsField], + analysis: str, + overview: Optional[str] = None, + example: Optional[str] = None, + references: Optional[List[Reference]] = None, + note: Optional[str] = None, + warning: Optional[str] = None, + tutorial: Optional[str] = None, + ) -> str: + try: + writer = _DocstringWriter() + if warning: + writer.write_warning(warning) + if overview: + writer.write_section(overview, "Overview") + if example: + writer.write_example(example) + writer.write_lines("This experiment uses following analysis class.") + writer.write_section(f":py:class:`~{analysis}`", "Analysis Class Reference") + writer.write_lines("Experiment options to generate circuits. \ +Options can be updated with :py:meth:`set_experiment_options`. \ +See method documentation for details.") + writer.write_options_as_sections(experiment_options, "Experiment Options") + writer.write_lines("Analysis options to run the analysis class. \ +Options can be updated with :py:meth:`set_analysis_options`. \ +See method documentation for details.") + writer.write_options_as_sections(analysis_options, "Analysis Options") + if references: + writer.write_references(references) + if note: + writer.write_note(note) + if tutorial: + writer.write_tutorial_link(tutorial) + except Exception as ex: + raise QiskitError(f"Auto docstring generation failed with the error: {ex}") + return writer.docstring + + +def auto_experiment_documentation(style: _DocstringMaker = StandardExperimentDocstring): + """A class decorator that overrides experiment class docstring.""" + def decorator(experiment: BaseExperiment): + analysis = experiment.__analysis_class__ + + exp_docs = style.make_docstring( + analysis_options=experiment.__analysis_class__._default_options(), + experiment_options=experiment._default_experiment_options(), + analysis=f"{analysis.__module__}.{analysis.__name__}", + overview=getattr(experiment, "__doc_overview__", None), + example=getattr(experiment, "__doc_example__", None), + references=getattr(experiment, "__doc_references__", None), + note=getattr(experiment, "__doc_note__", None), + warning=getattr(experiment, "__doc_warning__", None), + tutorial=getattr(experiment, "__doc_tutorial__", None), + ) + experiment.__doc__ += f"\n\n{exp_docs}" + + return experiment + return decorator diff --git a/qiskit_experiments/autodocs/set_option_docs.py b/qiskit_experiments/autodocs/set_option_docs.py new file mode 100644 index 0000000000..51d35b960a --- /dev/null +++ b/qiskit_experiments/autodocs/set_option_docs.py @@ -0,0 +1,123 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Documentation for set option methods. +""" +import functools +from types import FunctionType +from typing import Optional, Dict, Any + +from qiskit.exceptions import QiskitError + +from qiskit_experiments.base_experiment import BaseExperiment +from .descriptions import OptionsField +from .writer import _DocstringWriter, _DocstringMaker + + +class StandardSetOptionsDocstring(_DocstringMaker): + """A facade class to write standard set options docstring.""" + + @classmethod + def make_docstring( + cls, + description: str, + options: Dict[str, OptionsField], + note: Optional[str] = None, + raises: Optional[Dict[str, str]] = None, + ) -> str: + try: + writer = _DocstringWriter() + writer.write_lines(description) + writer.write_options_as_args(options) + if note: + writer.write_note(note) + if raises: + writer.write_raises(*list(zip(*raises.items()))) + except Exception as ex: + raise QiskitError(f"Auto docstring generation failed with the error: {ex}") + return writer.docstring + + +def _copy_method(experiment: BaseExperiment, method_name: str) -> FunctionType: + """A helper function to duplicate base class method. + + Note that calling set options method will access to the base class method. + If we override attribute, the change will propagate through the all subclass attributes. + This function prevent this by copying the base class method. + + Args: + experiment: Base class to get a method. + method_name: Name of method to copy. + + Returns: + Duplicated function object. + """ + base_method = getattr(experiment, method_name) + + new_method = FunctionType( + code=base_method.__code__, + globals=base_method.__globals__, + name=base_method.__name__, + argdefs=base_method.__defaults__, + closure=base_method.__closure__, + ) + return functools.update_wrapper(wrapper=new_method, wrapped=base_method) + + +def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: + """Dynamically generate method annotation based on information provided by ``OptionsField``s. + + Args: + fields: A dictionary of ``OptionsField`` object. + + Returns: + Dictionary of field name and type annotation. + """ + annotations = dict() + + for field_name, field in fields.items(): + if not isinstance(field.annotation, str): + annotations[field_name] = field.annotation + + return annotations + + +def auto_options_method_documentation(style: _DocstringMaker = StandardSetOptionsDocstring): + """A class decorator that overrides set options method docstring.""" + def decorator(experiment: BaseExperiment): + analysis_options = experiment.__analysis_class__._default_options() + experiment_options = experiment._default_experiment_options() + + # update analysis options setter + analysis_setter = _copy_method(experiment, "set_analysis_options") + analysis_setter.__annotations__ = _compile_annotations(analysis_options) + analysis_setter.__doc__ = style.make_docstring( + description="Set the analysis options for :py:meth:`run_analysis` method.", + options=analysis_options, + note="Here you can set arbitrary parameter, even if it is not listed. \ +Such option is passed as a keyword argument to the analysis fitter functions (if exist). \ +The execution may fail if the function API doesn't support extra keyword arguments.", + ) + setattr(experiment, "set_analysis_options", analysis_setter) + + # update experiment options setter + experiment_setter = _copy_method(experiment, "set_experiment_options") + experiment_setter.__annotations__ = _compile_annotations(experiment_options) + experiment_setter.__doc__ = style.make_docstring( + description="Set the analysis options for :py:meth:`run` method.", + options=experiment_options, + raises={"AttributeError": "If the field passed in is not a supported options."} + ) + setattr(experiment, "set_experiment_options", experiment_setter) + + return experiment + return decorator diff --git a/qiskit_experiments/autodocs/writer.py b/qiskit_experiments/autodocs/writer.py new file mode 100644 index 0000000000..dc5bb3187a --- /dev/null +++ b/qiskit_experiments/autodocs/writer.py @@ -0,0 +1,171 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Docstring writer. This module takes facade pattern to implement the functionality. +""" +import typing +from types import FunctionType + +from .descriptions import OptionsField, Reference, _parse_annotation +from abc import abstractclassmethod + + +class _DocstringMaker: + """A base facade class to write docstring.""" + + @abstractclassmethod + def make_docstring(cls, *args, **kwargs) -> str: + """Write a docstring.""" + pass + + +class _DocstringWriter: + """A docstring writer.""" + __indent__ = " " + + def __init__(self): + self.docstring = "" + + def write_header(self, header: str): + """Write header.""" + self.docstring += f"{header}\n\n" + + def write_options_as_args(self, fields: typing.Dict[str, OptionsField]): + """Write option descriptions as an argument section. + + This can write multi line description for each option. + This style is used for detailed summary for the set method docstring. + Extra fields (non-default options) are also shown. + """ + def _write_field(_arg_name, _field): + + # parse type + arg_str_type = f":py:obj:`{_parse_annotation(_field.annotation)}`" + + # write multi line description + arg_description = self._write_multi_line(_field.description, self.__indent__ * 2) + + # write default value + default = _field.default + if default is not None: + # format representation + if isinstance(_field.default, FunctionType): + default_str = f":py:func:`~{default.__module__}.{default.__name__}`" + else: + default_str = f":py:obj:`{default}`" + arg_description += self.__indent__ * 2 + arg_description += f"(Default: {default_str})" + self.docstring += self.__indent__ + self.docstring += f"{_arg_name} ({arg_str_type}):\n{arg_description}\n" + + self.docstring += "Parameters:\n" + extra_fields = dict() + for arg_name, field in fields.items(): + if field.is_extra: + extra_fields[arg_name] = field + continue + _write_field(arg_name, field) + self.docstring += "\n" + + if extra_fields: + self.docstring += "Other Parameters:\n" + for arg_name, field in extra_fields.items(): + _write_field(arg_name, field) + self.docstring += "\n" + + def write_options_as_sections(self, fields: typing.Dict[str, OptionsField], section: str): + """Write option descriptions as a custom section. + + This writes only the first line of description, if multiple lines exist. + This style is mainly used for the short summary (options are itemized). + """ + self.docstring += f"{section}\n" + + for arg_name, field in fields.items(): + if field.is_extra: + continue + arg_str_type = f":py:obj:`{_parse_annotation(field.annotation)}`" + arg_description = field.description.split('\n')[0] + # write multi line description + self.docstring += self.__indent__ + self.docstring += f"- **{arg_name}** ({arg_str_type}): {arg_description}\n" + self.docstring += "\n" + + def write_lines(self, text_block: str): + """Write text without section.""" + self.docstring += self._write_multi_line(text_block) + self.docstring += "\n" + + def write_example(self, text_block: str): + """Write error descriptions.""" + self.docstring += "Example:\n" + self.docstring += self._write_multi_line(text_block, self.__indent__) + self.docstring += "\n" + + def write_raises(self, error_kinds: typing.List[str], descriptions: typing.List[str]): + """Write error descriptions.""" + self.docstring += "Raises:\n" + for error_kind, description in zip(error_kinds, descriptions): + self.docstring += self.__indent__ + self.docstring += f"{error_kind}: {description}\n" + self.docstring += "\n" + + def write_section(self, text_block: str, section: str): + """Write new user defined section.""" + self.docstring += f"{section}\n" + self.docstring += self._write_multi_line(text_block, self.__indent__) + self.docstring += "\n\n" + + def write_note(self, text_block: str): + """Write note.""" + self.docstring += "Note:\n" + self.docstring += self._write_multi_line(text_block, self.__indent__) + self.docstring += "\n" + + def write_warning(self, text_block: str): + """Write warning.""" + self.docstring += "Warning:\n" + self.docstring += self._write_multi_line(text_block, self.__indent__) + self.docstring += "\n" + + def write_references(self, refs: typing.List[Reference]): + """Write references.""" + self.docstring += "References:\n" + for idx, ref in enumerate(refs): + ref_repr = [] + if ref.authors: + ref_repr.append(f"{ref.authors}") + if ref.title: + ref_repr.append(f"`{ref.title}`") + if ref.journal_info: + ref_repr.append(f"{ref.journal_info}") + if ref.open_access_link: + ref_repr.append(f"`open access <{ref.open_access_link}>`_") + self.docstring += self.__indent__ + self.docstring += f"- [{idx + 1}] {', '.join(ref_repr)}\n" + self.docstring += "\n" + + def write_tutorial_link(self, link: str): + self.docstring += "See Also:\n" + self.docstring += self.__indent__ + self.docstring += f"- `Qiskit Experiment Tutorial <{link}>`_\n" + self.docstring += "\n" + + @staticmethod + def _write_multi_line(text_block: str, indent: typing.Optional[str] = None) -> str: + """A util method to write multi line text with indentation.""" + indented_text = "" + for line in text_block.split("\n"): + if indent is not None: + indented_text += indent + indented_text += f"{line}\n" + return indented_text diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 37cde36a54..77a21ddd0d 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -20,7 +20,7 @@ from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ExperimentData, AnalysisResult -from qiskit_experiments.options_autodoc import OptionsField, to_options +from qiskit_experiments.autodocs import OptionsField, to_options class BaseAnalysis(ABC): diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 4d4ead5c3e..0971e17cc4 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -25,7 +25,7 @@ from qiskit.providers.options import Options from .experiment_data import ExperimentData -from .options_autodoc import to_options, OptionsField +from .autodocs import to_options, OptionsField class BaseExperiment(ABC): @@ -40,6 +40,14 @@ class BaseExperiment(ABC): experiment (Default: ExperimentData). """ + # Documentation sections + __doc_overview__ = None + __doc_example__ = None + __doc_references__ = None + __doc_note__ = None + __doc_warning__ = None + __doc_tutorial__ = None + # Analysis class for experiment __analysis_class__ = None diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 8f49819eaf..58806efbf7 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -31,7 +31,11 @@ get_opt_value, get_opt_error, ) -from qiskit_experiments.options_autodoc import OptionsField, create_experiment_docs +from qiskit_experiments.autodocs import ( + OptionsField, + auto_experiment_documentation, + auto_options_method_documentation, +) from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.data_processing.processor_library import get_to_signal_processor @@ -179,28 +183,27 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR return analysis_result -@create_experiment_docs +@auto_experiment_documentation() +@auto_options_method_documentation() class QubitSpectroscopy(BaseExperiment): - """Class that runs spectroscopy by sweeping the qubit frequency. - - Overview - The circuits produced by spectroscopy, i.e. + """Class that runs spectroscopy by sweeping the qubit frequency.""" - .. parsed-literal:: + __doc_overview__ = """The circuits produced by spectroscopy, i.e. + +.. parsed-literal:: - ┌────────────┐ ░ ┌─┐ - q_0: ┤ Spec(freq) ├─░─┤M├ - └────────────┘ ░ └╥┘ - measure: 1/══════════════════╩═ - 0 + ┌────────────┐ ░ ┌─┐ + q_0: ┤ Spec(freq) ├─░─┤M├ + └────────────┘ ░ └╥┘ + measure: 1/══════════════════╩═ + 0 - have a spectroscopy pulse-schedule embedded in a spectroscopy gate. The - pulse-schedule consists of a set frequency instruction followed by a GaussianSquare - pulse. A list of circuits is generated, each with a different frequency "freq". +have a spectroscopy pulse-schedule embedded in a spectroscopy gate. The +pulse-schedule consists of a set frequency instruction followed by a GaussianSquare +pulse. A list of circuits is generated, each with a different frequency "freq". - A spectroscopy experiment run by setting the frequency of the qubit drive. - The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. - """ +A spectroscopy experiment run by setting the frequency of the qubit drive. +The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time.""" __analysis_class__ = SpectroscopyAnalysis diff --git a/qiskit_experiments/options_autodoc.py b/qiskit_experiments/options_autodoc.py deleted file mode 100644 index 1941993ecb..0000000000 --- a/qiskit_experiments/options_autodoc.py +++ /dev/null @@ -1,329 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Description of field of experiment options. -""" - -import dataclasses -import functools -from types import FunctionType -import typing - -from qiskit.exceptions import QiskitError -from qiskit.providers.options import Options - - -@dataclasses.dataclass -class OptionsField: - """A data container to describe a single entry in options.""" - - # Default value - default: typing.Any - - # Type annotation - annotation: typing.Type - - # Docstring description of the entry - description: str - - # Set True if this is not a default option - extra_option: bool = False - - -class _ExperimentDocstringMaker: - """Facade of class docstring writer.""" - - @classmethod - def make_docsrting( - cls, - experiment, - analysis_fields: typing.Dict[str, OptionsField], - experiment_fields: typing.Dict[str, OptionsField] - ) -> str: - """Create extra class docstring for options.""" - try: - writer = _DocstringWriter() - writer.custom_section( - section="Analysis Class", - section_note=f":py:class:`~{experiment.__analysis_class__.__module__}.\ -{experiment.__analysis_class__.__name__}`" - ) - writer.custom_section_options( - fields=experiment_fields, - section="Experiment Options", - section_note="Experiment options to generate circuits. Options can be updated \ -with :meth:`set_experiment_options` method. See docstring of the method for details.", - ) - writer.custom_section_options( - fields=analysis_fields, - section="Analysis Options", - section_note="Analysis options to perform result analysis. Options can be updated \ -with :meth:`set_analysis_options` method. See docstring of the method for details.", - ) - except Exception as ex: - raise QiskitError(f"Auto docstring failed due to following error: {ex}") from ex - return writer.docstring - - -class _OptionMethodDocstringMaker: - """Facade of method docstring writer.""" - - @classmethod - def make_docstring( - cls, - header: str, - fields: typing.Dict[str, OptionsField], - notes: typing.Optional[str] = None, - raises: typing.Optional[typing.Dict[str, str]] = None, - ) -> str: - """Create method docstring. - - This mutably update method docstring. - - Args: - header: Short string for method docstring header to write Args section. - fields: Dictionary of argument name and ``OptionsField``. - notes: Additional text block for notation. - raises: Dictionary of error name and descriptions to write Raises section. - - Returns: - Automatically generated docstring. - """ - try: - writer = _DocstringWriter() - writer.header(header) - writer.args(fields) - if raises: - writer.raises(*list(zip(*raises.items()))) - if notes: - writer.note(notes) - except Exception as ex: - raise QiskitError(f"Auto docstring failed due to following error: {ex}") from ex - return writer.docstring - - -class _DocstringWriter: - """Docstring writer.""" - __indent__ = " " - - def __init__(self): - self.docstring = "" - - def header(self, header: str): - """Output header.""" - self.docstring += f"{header}\n\n" - - def args(self, fields: typing.Dict[str, OptionsField]): - """Output argument section.""" - self.docstring += "Args:\n" - - for arg_name, field in fields.items(): - # parse type - arg_str_type = f":py:obj:`{self._parse_type(field.annotation)}`" - # write multi line description - arg_description = "" - for line in field.description.split("\n"): - arg_description += f"{line}\n" - arg_description += self.__indent__ * 2 - # write default value - if isinstance(field.default, FunctionType): - default_obj = f":py:func:`~{field.default.__module__}.{field.default.__name__}`" - else: - default_obj = f":py:obj:`{field.default}`" - arg_description += f"(Default: {default_obj})" - self.docstring += self.__indent__ - self.docstring += f"{arg_name} ({arg_str_type}): {arg_description}\n" - self.docstring += "\n" - - def custom_section_options( - self, - fields: typing.Dict[str, OptionsField], - section: str, - section_note: typing.Optional[str] = "" - ): - """Output custom section for options.""" - self.docstring += f"{section_note}\n\n" - self.docstring += f"{section}\n" - - for arg_name, field in fields.items(): - arg_str_type = f":py:obj:`{self._parse_type(field.annotation)}`" - arg_description = field.description.split('\n')[0] - # write multi line description - self.docstring += self.__indent__ - self.docstring += f"- **{arg_name}** ({arg_str_type}): {arg_description}\n" - self.docstring += "\n" - - def custom_section(self, section: str, section_note: str): - """Output arbitrary custom section.""" - self.docstring += f"{section}\n" - self.docstring += self.__indent__ - self.docstring += section_note - self.docstring += "\n\n" - - def note(self, note: str): - """Output note section.""" - self.docstring += ".. note::\n\n" - for line in note.split("\n"): - self.docstring += self.__indent__ - self.docstring += f"{line}\n" - self.docstring += "\n" - - def raises(self, error_kinds: typing.List[str], descriptions: typing.List[str]): - """Output raises section.""" - self.docstring += "Raises:\n" - for error_kind, description in zip(error_kinds, descriptions): - self.docstring += self.__indent__ - self.docstring += f"{error_kind}: {description}\n" - self.docstring += "\n" - - def _parse_type(self, type_obj: typing.Any): - """Convert type alias to string.""" - if isinstance(type_obj, str): - # forward reference - return type_obj - - module = type_obj.__module__ - - if module == "builtins": - return type_obj.__name__ - elif module == "typing": - # type name - if hasattr(type_obj, "_name") and type_obj._name: - # _SpecialForm or special=True - name = type_obj._name - else: - # _GenericAlias and special=False - type_repr = repr(type_obj).replace("typing.", "") - if type_repr in typing.__all__: - name = type_repr - else: - name = self._parse_type(type_obj.__origin__) - # arguments - if hasattr(type_obj, "__args__") and type_obj.__args__: - args = [self._parse_type(arg) for arg in type_obj.__args__] - return f"{name}[{', '.join(args)}]" - else: - return name - else: - return f":py:class`{module}.{type_obj.__name__}`" - - -def _compile_annotations(fields: typing.Dict[str, OptionsField]) -> typing.Dict[str, typing.Any]: - """Dynamically generate method annotation based on information provided by ``OptionsField``s. - - Args: - fields: List of ``OptionsField`` object. - - Returns: - Dictionary of field name and type annotation. - """ - annotations = dict() - - for field_name, field in fields.items(): - if not isinstance(field.annotation, str): - annotations[field_name] = field.annotation - - return annotations - - -def _copy_method(experiment, method_name: str) -> FunctionType: - """A helper function to duplicate base calss method. - - Args: - experiment: Base class to get a method. - method_name: Name of method to copy. - - Returns: - Duplicated function object. - """ - base_method = getattr(experiment, method_name) - - new_method = FunctionType( - code=base_method.__code__, - globals=base_method.__globals__, - name=base_method.__name__, - argdefs=base_method.__defaults__, - closure=base_method.__closure__, - ) - return functools.update_wrapper(wrapper=new_method, wrapped=base_method) - - -def to_options(fields: typing.Dict[str, OptionsField]) -> Options: - """Converts a list of ``OptionsField`` into ``Options`` object. - - Args: - fields: List of ``OptionsField`` object to convert. - - Returns: - ``Options`` that filled with ``.default`` value of ``OptionsField``. - """ - if isinstance(fields, Options): - return fields - - default_options = dict() - for field_name, field in fields.items(): - default_options[field_name] = field.default - - return Options(**default_options) - - -def create_experiment_docs(experiment): - """A class decorator that overrides the docstring and annotation of option setters.""" - - # experiment.set_analysis_options directly calls base class method. - # Thus we cannot directly override __doc__ attribute. - analysis_fields = experiment.__analysis_class__._default_options() - - method = _copy_method(experiment, "set_analysis_options") - method.__annotations__ = _compile_annotations(fields=analysis_fields) - method.__doc__ = _OptionMethodDocstringMaker.make_docstring( - header=f"Set the analysis options for :meth:`run_analysis` method.", - fields=analysis_fields, - notes="""You can define arbitrary field with this method. -If you specify a field name not defined in above list, -the name-value pair is passed as ``**kwargs``. -If your ``curve_fitter`` API does not support the keyword, you may fail in analysis. -""", - ) - setattr(experiment, "set_analysis_options", method) - - # experiment.set_experiment_options directly calls base class method. - # Thus we cannot directly override __doc__ attribute. - experiment_fields = experiment._default_experiment_options() - - method = _copy_method(experiment, "set_experiment_options") - method.__annotations__ = _compile_annotations(fields=experiment_fields) - method.__doc__ = _OptionMethodDocstringMaker.make_docstring( - header=f"Set the experiment options. These options are consumed for generating \ -the experiment circuits.", - fields=experiment_fields, - raises={"AttributeError": "If the field passed in is not a supported options"}, - ) - setattr(experiment, "set_experiment_options", method) - - extra_class_docs = _ExperimentDocstringMaker.make_docsrting( - experiment=experiment, - analysis_fields=analysis_fields, - experiment_fields=experiment_fields, - ) - experiment.__doc__ += f"\n\n{extra_class_docs}" - - return experiment - - -def create_analysis_docs(analysis): - """A class decorator that overrides the docstring.""" - analysis_fields = analysis._default_options() - - - - diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py index 65bad588ea..90cecec75c 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py @@ -22,13 +22,21 @@ from .rb_experiment import RBExperiment from .interleaved_rb_analysis import InterleavedRBAnalysis -from qiskit_experiments.options_autodoc import create_experiment_docs +from qiskit_experiments.autodocs import ( + auto_experiment_documentation, + auto_options_method_documentation, +) -@create_experiment_docs +@auto_experiment_documentation() +@auto_options_method_documentation() class InterleavedRBExperiment(RBExperiment): """Interleaved RB Experiment class""" + __doc_overview__ = """ + + """ + # Analysis class for experiment __analysis_class__ = InterleavedRBAnalysis @@ -41,7 +49,7 @@ def __init__( seed: Optional[Union[int, Generator]] = None, full_sampling: bool = False, ): - """Interleaved randomized benchmarking experiment. + """Create new experiment. Args: interleaved_element: the element to interleave, diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index 2ffa6529ad..a9584fc2d3 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -23,19 +23,42 @@ from qiskit_experiments.analysis.data_processing import probability from qiskit_experiments.base_experiment import BaseExperiment -from qiskit_experiments.options_autodoc import OptionsField, create_experiment_docs +from qiskit_experiments.autodocs import ( + OptionsField, + Reference, + auto_experiment_documentation, + auto_options_method_documentation, +) from .clifford_utils import CliffordUtils from .rb_analysis import RBAnalysis -@create_experiment_docs +@auto_experiment_documentation() +@auto_options_method_documentation() class RBExperiment(BaseExperiment): - """RB Experiment class. + """Randomized benchmarking.""" - Experiment Options: - lengths: A list of RB sequences lengths. - num_samples: number of samples to generate for each sequence length. - """ + __doc_overview__ = """A randomized benchmarking (RB) is a scalable and robust algorithm +for benchmarking the full set of Clifford gates by a single parameter using the +randomization technique [1]. + +The RB sequences consist of random Clifford elements chosen uniformly from the Clifford group on +n-qubits, including a computed reversal element, that should return the qubits to the +initial state. + +Averaging over K random realizations of the sequence, we can find the averaged sequence fidelity, +or error per Clifford (EPC).""" + + __doc_tutorial__ = "https://github.com/Qiskit/qiskit-experiments/blob/main/docs/tutorials/\ +rb_example.ipynb" + + __doc_references__ = [ + Reference( + title="Robust randomized benchmarking of quantum processes", + authors="Easwar Magesan, J. M. Gambetta, and Joseph Emerson", + open_access_link="https://arxiv.org/abs/1009.3639", + ) + ] # Analysis class for experiment __analysis_class__ = RBAnalysis From 5f832a840f425b20bc4ef782dd7b5ebce46c6bb2 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 23 Jun 2021 22:50:24 +0900 Subject: [PATCH 09/46] wip analysis documentation --- qiskit_experiments/autodocs/__init__.py | 1 + qiskit_experiments/autodocs/analysis_docs.py | 126 ++++++++++++++++++ qiskit_experiments/autodocs/descriptions.py | 17 +++ .../autodocs/experiment_docs.py | 20 +-- .../autodocs/set_option_docs.py | 5 +- qiskit_experiments/autodocs/writer.py | 51 ++++++- qiskit_experiments/base_analysis.py | 8 ++ .../interleaved_rb_experiment.py | 4 - 8 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 qiskit_experiments/autodocs/analysis_docs.py diff --git a/qiskit_experiments/autodocs/__init__.py b/qiskit_experiments/autodocs/__init__.py index 16dce6305f..7b26ac4d6e 100644 --- a/qiskit_experiments/autodocs/__init__.py +++ b/qiskit_experiments/autodocs/__init__.py @@ -22,4 +22,5 @@ from .experiment_docs import auto_experiment_documentation from .set_option_docs import auto_options_method_documentation +from .analysis_docs import auto_analysis_documentation from .descriptions import OptionsField, Reference, to_options diff --git a/qiskit_experiments/autodocs/analysis_docs.py b/qiskit_experiments/autodocs/analysis_docs.py new file mode 100644 index 0000000000..58d93e86b5 --- /dev/null +++ b/qiskit_experiments/autodocs/analysis_docs.py @@ -0,0 +1,126 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Documentation for analysis class. +""" +import re +from typing import Optional, Dict, List + +from qiskit.exceptions import QiskitError + +from .descriptions import OptionsField, Reference, CurveFitParameter +from .writer import _DocstringWriter, _CurveFitDocstringWriter, _DocstringMaker + + +class StandardAnalysisDocstring(_DocstringMaker): + """A facade class to write standard analysis docstring.""" + + @classmethod + def make_docstring( + cls, + default_options: Dict[str, OptionsField], + overview: Optional[str] = None, + example: Optional[str] = None, + references: Optional[List[Reference]] = None, + note: Optional[str] = None, + warning: Optional[str] = None, + tutorial: Optional[str] = None, + ) -> str: + try: + writer = _DocstringWriter() + if warning: + writer.write_warning(warning) + if overview: + writer.write_section(overview, "Overview") + if example: + writer.write_example(example) + writer.write_lines("Default class options. These options are automatically set \ +when :py:meth:`run` method is called.") + writer.write_options_as_sections(default_options, "Default Options") + if references: + writer.write_references(references) + if note: + writer.write_note(note) + if tutorial: + writer.write_tutorial_link(tutorial) + except Exception as ex: + raise QiskitError(f"Auto docstring generation failed with the error: {ex}") + return writer.docstring + + +class CurveAnalysisDocstring(_DocstringMaker): + """A facade class to write curve analysis docstring.""" + + @classmethod + def make_docstring( + cls, + default_options: Dict[str, OptionsField], + overview: Optional[str] = None, + equations: Optional[List[str]] = None, + fit_params: Optional[List[CurveFitParameter]] = None, + example: Optional[str] = None, + references: Optional[List[Reference]] = None, + note: Optional[str] = None, + warning: Optional[str] = None, + tutorial: Optional[str] = None, + ) -> str: + try: + writer = _CurveFitDocstringWriter() + if warning: + writer.write_warning(warning) + if overview: + writer.write_section(overview, "Overview") + if equations: + if isinstance(equations, str): + equations = [equations] + writer.write_fit_models(equations) + if fit_params: + writer.write_fit_parameter(fit_params) + writer.write_initial_guess(fit_params) + writer.write_bounds(fit_params) + if example: + writer.write_example(example) + writer.write_lines("Default class options. These options are automatically set \ +when :py:meth:`run` method is called.") + writer.write_options_as_sections(default_options, "Default Options") + if references: + writer.write_references(references) + if note: + writer.write_note(note) + if tutorial: + writer.write_tutorial_link(tutorial) + except Exception as ex: + raise QiskitError(f"Auto docstring generation failed with the error: {ex}") + return writer.docstring + + +def auto_analysis_documentation(style: _DocstringMaker = StandardAnalysisDocstring): + """A class decorator that overrides analysis class docstring.""" + def decorator(analysis: "BaseAnalysis"): + regex = r"__doc_(?P\S+)__" + + kwargs = {} + for attribute in dir(analysis): + match = re.match(regex, attribute) + if match: + arg = match["kwarg"] + kwargs[arg] = getattr(analysis, arg) + + exp_docs = style.make_docstring( + default_options=analysis._default_options(), + analysis=f"{analysis.__module__}.{analysis.__name__}", + **kwargs + ) + analysis.__doc__ += f"\n\n{exp_docs}" + + return analysis + return decorator diff --git a/qiskit_experiments/autodocs/descriptions.py b/qiskit_experiments/autodocs/descriptions.py index 09c2739912..a2852a9f3b 100644 --- a/qiskit_experiments/autodocs/descriptions.py +++ b/qiskit_experiments/autodocs/descriptions.py @@ -52,6 +52,23 @@ class Reference: open_access_link: typing.Optional[str] = None +@dataclasses.dataclass +class CurveFitParameter: + """A data class to describe fit parameter.""" + + # Name of fit parameter + name: str + + # Description about the parameter + description: str + + # How initial guess is calculated + initial_guess: str + + # How bounds are calculated + bounds: str + + def _parse_annotation(_type: typing.Any) -> str: """A helper function to convert type object into string.""" diff --git a/qiskit_experiments/autodocs/experiment_docs.py b/qiskit_experiments/autodocs/experiment_docs.py index a8efef995a..2be2f66938 100644 --- a/qiskit_experiments/autodocs/experiment_docs.py +++ b/qiskit_experiments/autodocs/experiment_docs.py @@ -12,11 +12,11 @@ """ Documentation for experiment. """ +import re from typing import Optional, Dict, List from qiskit.exceptions import QiskitError -from qiskit_experiments.base_experiment import BaseExperiment from .descriptions import OptionsField, Reference from .writer import _DocstringWriter, _DocstringMaker @@ -68,19 +68,23 @@ def make_docstring( def auto_experiment_documentation(style: _DocstringMaker = StandardExperimentDocstring): """A class decorator that overrides experiment class docstring.""" - def decorator(experiment: BaseExperiment): + def decorator(experiment: "BaseExperiment"): + regex = r"__doc_(?P\S+)__" + + kwargs = {} + for attribute in dir(experiment): + match = re.match(regex, attribute) + if match: + arg = match["kwarg"] + kwargs[arg] = getattr(experiment, attribute, None) + analysis = experiment.__analysis_class__ exp_docs = style.make_docstring( analysis_options=experiment.__analysis_class__._default_options(), experiment_options=experiment._default_experiment_options(), analysis=f"{analysis.__module__}.{analysis.__name__}", - overview=getattr(experiment, "__doc_overview__", None), - example=getattr(experiment, "__doc_example__", None), - references=getattr(experiment, "__doc_references__", None), - note=getattr(experiment, "__doc_note__", None), - warning=getattr(experiment, "__doc_warning__", None), - tutorial=getattr(experiment, "__doc_tutorial__", None), + **kwargs ) experiment.__doc__ += f"\n\n{exp_docs}" diff --git a/qiskit_experiments/autodocs/set_option_docs.py b/qiskit_experiments/autodocs/set_option_docs.py index 51d35b960a..76b086c1a0 100644 --- a/qiskit_experiments/autodocs/set_option_docs.py +++ b/qiskit_experiments/autodocs/set_option_docs.py @@ -18,7 +18,6 @@ from qiskit.exceptions import QiskitError -from qiskit_experiments.base_experiment import BaseExperiment from .descriptions import OptionsField from .writer import _DocstringWriter, _DocstringMaker @@ -47,7 +46,7 @@ def make_docstring( return writer.docstring -def _copy_method(experiment: BaseExperiment, method_name: str) -> FunctionType: +def _copy_method(experiment: "BaseExperiment", method_name: str) -> FunctionType: """A helper function to duplicate base class method. Note that calling set options method will access to the base class method. @@ -93,7 +92,7 @@ def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: def auto_options_method_documentation(style: _DocstringMaker = StandardSetOptionsDocstring): """A class decorator that overrides set options method docstring.""" - def decorator(experiment: BaseExperiment): + def decorator(experiment: "BaseExperiment"): analysis_options = experiment.__analysis_class__._default_options() experiment_options = experiment._default_experiment_options() diff --git a/qiskit_experiments/autodocs/writer.py b/qiskit_experiments/autodocs/writer.py index dc5bb3187a..a01b292a27 100644 --- a/qiskit_experiments/autodocs/writer.py +++ b/qiskit_experiments/autodocs/writer.py @@ -15,8 +15,9 @@ import typing from types import FunctionType -from .descriptions import OptionsField, Reference, _parse_annotation +from .descriptions import OptionsField, Reference, CurveFitParameter, _parse_annotation from abc import abstractclassmethod +from qiskit.exceptions import QiskitError class _DocstringMaker: @@ -169,3 +170,51 @@ def _write_multi_line(text_block: str, indent: typing.Optional[str] = None) -> s indented_text += indent indented_text += f"{line}\n" return indented_text + + +class _CurveFitDocstringWriter(_DocstringWriter): + + def write_fit_parameter(self, fit_params: typing.List[CurveFitParameter]): + """Write fit parameters.""" + self.docstring += "Fit Parameters\n" + + for fit_param in fit_params: + self.docstring += self.__indent__ + self.docstring += f":math:`{fit_param.name}`: {fit_param.description}\n" + self.docstring += "\n" + + def write_initial_guess(self, fit_params: typing.List[CurveFitParameter]): + """Write initial guess estimation method.""" + self.docstring += "Initial Guess\n" + + for fit_param in fit_params: + self.docstring += self.__indent__ + self.docstring += f":math`{fit_param.name}`: {fit_param.initial_guess}\n" + self.docstring += "\n" + + def write_bounds(self, fit_params: typing.List[CurveFitParameter]): + """Write fit parameter bound.""" + self.docstring += "Parameter Boundaries\n" + + for fit_param in fit_params: + self.docstring += self.__indent__ + self.docstring += f":math`{fit_param.name}`: {fit_param.bounds}\n" + self.docstring += "\n" + + def write_fit_models(self, equations: typing.List[str]): + """Write fitting models.""" + self.docstring += "Fit Model\n\n" + self.docstring += ".. math::\n\n" + + if len(equations) > 1: + for equation in equations: + self.docstring += self.__indent__ * 2 + try: + lh, rh = equation.split("=") + except ValueError: + raise QiskitError(f"Equation {equation} is not a valid form.") + self.docstring += f"{lh} &= {rh}\n" + else: + self.docstring += self.__indent__ * 2 + self.docstring += f"{equations[0]}\n" + self.docstring += "\n" diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 77a21ddd0d..a765dcbd3c 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -39,6 +39,14 @@ class BaseAnalysis(ABC): run method and passed to the `_run_analysis` function. """ + # Documentation sections + __doc_overview__ = None + __doc_example__ = None + __doc_references__ = None + __doc_note__ = None + __doc_warning__ = None + __doc_tutorial__ = None + # Expected experiment data container for analysis __experiment_data__ = ExperimentData diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py index 90cecec75c..7865b375b4 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py @@ -33,10 +33,6 @@ class InterleavedRBExperiment(RBExperiment): """Interleaved RB Experiment class""" - __doc_overview__ = """ - - """ - # Analysis class for experiment __analysis_class__ = InterleavedRBAnalysis From 82c710004d92a071f9bd7156225677afc92fa204 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 24 Jun 2021 02:04:56 +0900 Subject: [PATCH 10/46] update analysis docs and reformat styles. --- qiskit_experiments/analysis/curve_analysis.py | 12 +- qiskit_experiments/autodocs/__init__.py | 28 +++- qiskit_experiments/autodocs/analysis_docs.py | 33 +++-- .../autodocs/experiment_docs.py | 29 ++-- .../autodocs/set_option_docs.py | 11 +- qiskit_experiments/autodocs/writer.py | 37 ++++-- .../characterization/qubit_spectroscopy.py | 103 ++++++++------- .../interleaved_rb_analysis.py | 124 ++++++++++-------- .../interleaved_rb_experiment.py | 8 +- .../randomized_benchmarking/rb_analysis.py | 64 ++++----- .../randomized_benchmarking/rb_experiment.py | 19 +-- 11 files changed, 282 insertions(+), 186 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index fa232b153e..0fbbe32b44 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -201,6 +201,10 @@ class AnalysisExample(CurveAnalysis): https://github.com/Qiskit/qiskit-experiments/issues """ + # extra documentations + __doc_equations__ = None + __doc_fit_params__ = None + #: List[SeriesDef]: List of mapping representing a data series __series__ = None @@ -288,8 +292,8 @@ def data_processor(data: Dict[str, Any]) -> Tuple[float, float] "bounds": OptionsField( default=None, annotation=Dict[str, Tuple[float, float]], - description="Dictionary of (min, max) tuple of fit parameter boundaries. \ -Keys are parameter names.", + description="Dictionary of (min, max) tuple of fit parameter boundaries. " + "Keys are parameter names.", ), "x_key": OptionsField( default="xval", @@ -324,8 +328,8 @@ def data_processor(data: Dict[str, Any]) -> Tuple[float, float] "fit_reports": OptionsField( default=None, annotation=Dict[str, str], - description="Mapping of fit parameters and representation in the fit report. \ -If nothing specified, fit report will not be shown.", + description="Mapping of fit parameters and representation in the fit report. " + "If nothing specified, fit report will not be shown.", ), "return_data_points": OptionsField( default=False, diff --git a/qiskit_experiments/autodocs/__init__.py b/qiskit_experiments/autodocs/__init__.py index 7b26ac4d6e..4c928fc12e 100644 --- a/qiskit_experiments/autodocs/__init__.py +++ b/qiskit_experiments/autodocs/__init__.py @@ -20,7 +20,27 @@ while providing users with the standardized and high quality documentation. """ -from .experiment_docs import auto_experiment_documentation -from .set_option_docs import auto_options_method_documentation -from .analysis_docs import auto_analysis_documentation -from .descriptions import OptionsField, Reference, to_options +import qiskit_experiments.autodocs.analysis_docs +import qiskit_experiments.autodocs.experiment_docs +import qiskit_experiments.autodocs.set_option_docs +from .descriptions import OptionsField, Reference, CurveFitParameter, to_options + + +standard_experiment_documentation = experiment_docs.base_experiment_documentation( + style=experiment_docs.StandardExperimentDocstring +) + + +standard_analysis_documentation = analysis_docs.base_analysis_documentation( + style=analysis_docs.StandardAnalysisDocstring +) + + +curve_analysis_documentation = analysis_docs.base_analysis_documentation( + style=analysis_docs.CurveAnalysisDocstring +) + + +standard_option_documentation = set_option_docs.base_options_method_documentation( + style=set_option_docs.StandardSetOptionsDocstring +) diff --git a/qiskit_experiments/autodocs/analysis_docs.py b/qiskit_experiments/autodocs/analysis_docs.py index 58d93e86b5..5363b883f6 100644 --- a/qiskit_experiments/autodocs/analysis_docs.py +++ b/qiskit_experiments/autodocs/analysis_docs.py @@ -13,7 +13,7 @@ Documentation for analysis class. """ import re -from typing import Optional, Dict, List +from typing import Optional, Dict, List, Type from qiskit.exceptions import QiskitError @@ -43,8 +43,10 @@ def make_docstring( writer.write_section(overview, "Overview") if example: writer.write_example(example) - writer.write_lines("Default class options. These options are automatically set \ -when :py:meth:`run` method is called.") + writer.write_lines( + "Default class options. These options are automatically set " + "when :py:meth:`run` method is called." + ) writer.write_options_as_sections(default_options, "Default Options") if references: writer.write_references(references) @@ -82,15 +84,31 @@ def make_docstring( if equations: if isinstance(equations, str): equations = [equations] + writer.write_lines("This analysis assumes following fit function(s).") writer.write_fit_models(equations) if fit_params: + writer.write_lines( + "The fit model takes following fit parameters." + "These parameters are fit by the ``curve_fitter`` function specified in " + "the analysis options." + ) writer.write_fit_parameter(fit_params) + writer.write_lines( + "The parameter initial guess are generated as follows. " + "If you want to override, you can provide ``p0`` of the analysis options." + ) writer.write_initial_guess(fit_params) + writer.write_lines( + "The parameter boundaries are generated as follows. " + "If you want to override, you can provide ``bounds`` of the analysis options." + ) writer.write_bounds(fit_params) if example: writer.write_example(example) - writer.write_lines("Default class options. These options are automatically set \ -when :py:meth:`run` method is called.") + writer.write_lines( + "Default class options. These options are automatically set " + "when :py:meth:`run` method is called." + ) writer.write_options_as_sections(default_options, "Default Options") if references: writer.write_references(references) @@ -103,7 +121,7 @@ def make_docstring( return writer.docstring -def auto_analysis_documentation(style: _DocstringMaker = StandardAnalysisDocstring): +def base_analysis_documentation(style: Type[_DocstringMaker]): """A class decorator that overrides analysis class docstring.""" def decorator(analysis: "BaseAnalysis"): regex = r"__doc_(?P\S+)__" @@ -113,11 +131,10 @@ def decorator(analysis: "BaseAnalysis"): match = re.match(regex, attribute) if match: arg = match["kwarg"] - kwargs[arg] = getattr(analysis, arg) + kwargs[arg] = getattr(analysis, attribute) exp_docs = style.make_docstring( default_options=analysis._default_options(), - analysis=f"{analysis.__module__}.{analysis.__name__}", **kwargs ) analysis.__doc__ += f"\n\n{exp_docs}" diff --git a/qiskit_experiments/autodocs/experiment_docs.py b/qiskit_experiments/autodocs/experiment_docs.py index 2be2f66938..a4ae2dc5fe 100644 --- a/qiskit_experiments/autodocs/experiment_docs.py +++ b/qiskit_experiments/autodocs/experiment_docs.py @@ -13,7 +13,7 @@ Documentation for experiment. """ import re -from typing import Optional, Dict, List +from typing import Optional, Dict, List, Type from qiskit.exceptions import QiskitError @@ -47,14 +47,23 @@ def make_docstring( writer.write_example(example) writer.write_lines("This experiment uses following analysis class.") writer.write_section(f":py:class:`~{analysis}`", "Analysis Class Reference") - writer.write_lines("Experiment options to generate circuits. \ -Options can be updated with :py:meth:`set_experiment_options`. \ -See method documentation for details.") - writer.write_options_as_sections(experiment_options, "Experiment Options") - writer.write_lines("Analysis options to run the analysis class. \ -Options can be updated with :py:meth:`set_analysis_options`. \ -See method documentation for details.") - writer.write_options_as_sections(analysis_options, "Analysis Options") + writer.write_lines( + "See below configurable experiment options to customize your execution." + ) + writer.write_options_as_sections( + fields=experiment_options, + section="Experiment Options", + text_block="Experiment options to generate circuits. " + "Options can be updated with :py:meth:`set_experiment_options`. " + "See method documentation for details." + ) + writer.write_options_as_sections( + fields=analysis_options, + section="Analysis Options", + text_block="Analysis options to run the analysis class. " + "Options can be updated with :py:meth:`set_analysis_options`. " + "See method documentation for details." + ) if references: writer.write_references(references) if note: @@ -66,7 +75,7 @@ def make_docstring( return writer.docstring -def auto_experiment_documentation(style: _DocstringMaker = StandardExperimentDocstring): +def base_experiment_documentation(style: Type[_DocstringMaker]): """A class decorator that overrides experiment class docstring.""" def decorator(experiment: "BaseExperiment"): regex = r"__doc_(?P\S+)__" diff --git a/qiskit_experiments/autodocs/set_option_docs.py b/qiskit_experiments/autodocs/set_option_docs.py index 76b086c1a0..0e40b13d13 100644 --- a/qiskit_experiments/autodocs/set_option_docs.py +++ b/qiskit_experiments/autodocs/set_option_docs.py @@ -14,7 +14,7 @@ """ import functools from types import FunctionType -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, Type from qiskit.exceptions import QiskitError @@ -90,7 +90,7 @@ def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: return annotations -def auto_options_method_documentation(style: _DocstringMaker = StandardSetOptionsDocstring): +def base_options_method_documentation(style: Type[_DocstringMaker]): """A class decorator that overrides set options method docstring.""" def decorator(experiment: "BaseExperiment"): analysis_options = experiment.__analysis_class__._default_options() @@ -102,9 +102,10 @@ def decorator(experiment: "BaseExperiment"): analysis_setter.__doc__ = style.make_docstring( description="Set the analysis options for :py:meth:`run_analysis` method.", options=analysis_options, - note="Here you can set arbitrary parameter, even if it is not listed. \ -Such option is passed as a keyword argument to the analysis fitter functions (if exist). \ -The execution may fail if the function API doesn't support extra keyword arguments.", + note="Here you can set arbitrary parameter, even if it is not listed. " + "Such option is passed as a keyword argument to the analysis fitter functions " + "(if exist). The execution may fail if the function API doesn't support " + "extra keyword arguments.", ) setattr(experiment, "set_analysis_options", analysis_setter) diff --git a/qiskit_experiments/autodocs/writer.py b/qiskit_experiments/autodocs/writer.py index a01b292a27..37280601a6 100644 --- a/qiskit_experiments/autodocs/writer.py +++ b/qiskit_experiments/autodocs/writer.py @@ -83,13 +83,25 @@ def _write_field(_arg_name, _field): _write_field(arg_name, field) self.docstring += "\n" - def write_options_as_sections(self, fields: typing.Dict[str, OptionsField], section: str): + def write_options_as_sections( + self, + fields: typing.Dict[str, OptionsField], + section: str, + text_block: typing.Optional[str] = None, + ): """Write option descriptions as a custom section. This writes only the first line of description, if multiple lines exist. This style is mainly used for the short summary (options are itemized). + This section will be shown as a drop down box. """ - self.docstring += f"{section}\n" + self.docstring += f".. dropdown:: {section}\n" + self.docstring += self.__indent__ + self.docstring += ":animate: fade-in-slide-down\n\n" + + if text_block: + self.docstring += self._write_multi_line(text_block, self.__indent__) + self.docstring += "\n" for arg_name, field in fields.items(): if field.is_extra: @@ -138,6 +150,12 @@ def write_warning(self, text_block: str): self.docstring += self._write_multi_line(text_block, self.__indent__) self.docstring += "\n" + def write_returns(self, text_block: str): + """Write returns.""" + self.docstring += "Returns:\n" + self.docstring += self._write_multi_line(text_block, self.__indent__) + self.docstring += "\n" + def write_references(self, refs: typing.List[Reference]): """Write references.""" self.docstring += "References:\n" @@ -180,7 +198,7 @@ def write_fit_parameter(self, fit_params: typing.List[CurveFitParameter]): for fit_param in fit_params: self.docstring += self.__indent__ - self.docstring += f":math:`{fit_param.name}`: {fit_param.description}\n" + self.docstring += f"- :math:`{fit_param.name}`: {fit_param.description}\n" self.docstring += "\n" def write_initial_guess(self, fit_params: typing.List[CurveFitParameter]): @@ -189,7 +207,7 @@ def write_initial_guess(self, fit_params: typing.List[CurveFitParameter]): for fit_param in fit_params: self.docstring += self.__indent__ - self.docstring += f":math`{fit_param.name}`: {fit_param.initial_guess}\n" + self.docstring += f"- :math:`{fit_param.name}`: {fit_param.initial_guess}\n" self.docstring += "\n" def write_bounds(self, fit_params: typing.List[CurveFitParameter]): @@ -198,22 +216,25 @@ def write_bounds(self, fit_params: typing.List[CurveFitParameter]): for fit_param in fit_params: self.docstring += self.__indent__ - self.docstring += f":math`{fit_param.name}`: {fit_param.bounds}\n" + self.docstring += f"- :math:`{fit_param.name}`: {fit_param.bounds}\n" self.docstring += "\n" def write_fit_models(self, equations: typing.List[str]): """Write fitting models.""" - self.docstring += "Fit Model\n\n" + self.docstring += "Fit Model\n" + self.docstring += self.__indent__ self.docstring += ".. math::\n\n" if len(equations) > 1: + eqs = [] for equation in equations: - self.docstring += self.__indent__ * 2 try: lh, rh = equation.split("=") except ValueError: raise QiskitError(f"Equation {equation} is not a valid form.") - self.docstring += f"{lh} &= {rh}\n" + eqs.append(f"{self.__indent__ * 2}{lh} &= {rh}") + self.docstring += " \\\\\n".join(eqs) + self.docstring += "\n" else: self.docstring += self.__indent__ * 2 self.docstring += f"{equations[0]}\n" diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 58806efbf7..48d04aa325 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -33,47 +33,52 @@ ) from qiskit_experiments.autodocs import ( OptionsField, - auto_experiment_documentation, - auto_options_method_documentation, + CurveFitParameter, + standard_experiment_documentation, + standard_option_documentation, + curve_analysis_documentation, ) from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.data_processing.processor_library import get_to_signal_processor +@curve_analysis_documentation class SpectroscopyAnalysis(CurveAnalysis): - r"""A class to analyze spectroscopy experiment. - - Overview - This analysis takes only single series. This series is fit by the Gaussian function. - - Fit Model - The fit is based on the following Gaussian function. - - .. math:: - - F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b - - Fit Parameters - - :math:`a`: Peak height. - - :math:`b`: Base line. - - :math:`f`: Center frequency. This is the fit parameter of main interest. - - :math:`\sigma`: Standard deviation of Gaussian function. - - Initial Guesses - - :math:`a`: The maximum signal value with removed baseline. - - :math:`b`: A median value of the signal. - - :math:`f`: A frequency value at the peak (maximum signal). - - :math:`\sigma`: Calculated from FWHM of peak :math:`w` - such that :math:`w / \sqrt{8} \ln{2}`. - - Bounds - - :math:`a`: [-2, 2] scaled with maximum signal value. - - :math:`b`: [-1, 1] scaled with maximum signal value. - - :math:`f`: [min(x), max(x)] of frequency scan range. - - :math:`\sigma`: [0, :math:`\Delta x`] where :math:`\Delta x` - represents frequency scan range. - - """ + """Spectroscopy analysis.""" + + __doc_overview__ = """This analysis uses Gaussian function to find a peak. +Note that this analysis assumes only single peak. +If multiple peaks exist, you'll get a poor chi squared value.""" + + __doc_equations__ = [r"F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b"] + + __doc_fit_params__ = [ + CurveFitParameter( + name="a", + description="Base line.", + initial_guess="The maximum signal value with removed baseline.", + bounds="[-2, 2] scaled with maximum signal value.", + ), + CurveFitParameter( + name="b", + description="Peak height.", + initial_guess="A median value of the signal.", + bounds="[-1, 1] scaled with maximum signal value.", + ), + CurveFitParameter( + name="f", + description="Center frequency. This is the fit parameter of main interest.", + initial_guess="A frequency value at the peak (maximum signal).", + bounds="[min(x), max(x)] of frequency scan range.", + ), + CurveFitParameter( + name=r"\sigma", + description="Standard deviation of Gaussian function.", + initial_guess=r"Calculated from FWHM of peak :math:`w` such that " + r":math:`w / \sqrt{8} \ln{2}`.", + bounds=r"[0, :math:`\Delta x`] where :math:`\Delta x` represents frequency scan range.", + ), + ] __series__ = [ SeriesDef( @@ -95,8 +100,8 @@ def _default_options(cls) -> Dict[str, OptionsField]: default_options["normalization"] = OptionsField( default=True, annotation=bool, - description="Set ``True`` to normalize measurement data. Usually applied to \ -Kerneled (level1) measurement data.", + description="Set ``True`` to normalize measurement data. Usually applied to " + "Kerneled (level1) measurement data.", ) return default_options @@ -183,8 +188,8 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR return analysis_result -@auto_experiment_documentation() -@auto_options_method_documentation() +@standard_experiment_documentation +@standard_option_documentation class QubitSpectroscopy(BaseExperiment): """Class that runs spectroscopy by sweeping the qubit frequency.""" @@ -225,27 +230,29 @@ def _default_experiment_options(cls) -> Dict[str, OptionsField]: "amp": OptionsField( default=0.1, annotation=float, - description="Amplitude of spectroscopy pulse. Usually weak power pulse is used to \ -suppress broadening of observed peaks.", + description="Amplitude of spectroscopy pulse. Usually weak power pulse is used to " + "suppress broadening of observed peaks.", ), "duration": OptionsField( default=1024, annotation=int, - description="Duration of spectroscopy pulse. This may need to satisfy the \ -hardware waveform memory constraint. The default value is represented in units of dt.", + description="Duration of spectroscopy pulse. This may need to satisfy the " + "hardware waveform memory constraint. " + "The default value is represented in units of dt.", ), "sigma": OptionsField( default=256, annotation=Union[int, float], - description="Sigma of Gaussian rising and falling edges. This value should be \ -sufficiently smaller than the duration, otherwise waveform is distorted. \ -The default value is represented in units of dt." + description="Sigma of Gaussian rising and falling edges. This value should be " + "sufficiently smaller than the duration, " + "otherwise waveform is distorted. " + "The default value is represented in units of dt." ), "width": OptionsField( default=0, annotation=Union[int, float], - description="Width of the flat-top part of the Gaussian square envelope of \ -spectroscopy pulse. Set width=0 to use Gaussian pulse.", + description="Width of the flat-top part of the Gaussian square envelope of " + "spectroscopy pulse. Set width=0 to use Gaussian pulse.", ), } diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index 8dff6b3f9b..f6f84bed91 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -23,76 +23,92 @@ get_opt_value, get_opt_error, ) +from qiskit_experiments.autodocs import ( + Reference, + CurveFitParameter, + curve_analysis_documentation, +) from .rb_analysis import RBAnalysis +@curve_analysis_documentation class InterleavedRBAnalysis(RBAnalysis): - r"""A class to analyze interleaved randomized benchmarking experiment. - - Overview - This analysis takes only two series for standard and interleaved RB curve fitting. - From the fit :math:`\alpha` and :math:`\alpha_c` value this analysis estimates - the error per Clifford (EPC) of interleaved gate. - - The EPC estimate is obtained using the equation - + """Interleaved randomized benchmarking analysis.""" - .. math:: + __doc_overview__ = r"""This analysis takes two series for standard and +interleaved RB curve fitting. - r_{\mathcal{C}}^{\text{est}} = - \frac{\left(d-1\right)\left(1-\alpha_{\overline{\mathcal{C}}}/\alpha\right)}{d} +From the fit :math:`\alpha` and :math:`\alpha_c` value this analysis estimates +the error per Clifford (EPC) of interleaved gate. - The error bounds are given by +The EPC estimate is obtained using the equation - .. math:: +.. math:: - E = \min\left\{ - \begin{array}{c} - \frac{\left(d-1\right)\left[\left|\alpha-\alpha_{\overline{\mathcal{C}}}\right| - +\left(1-\alpha\right)\right]}{d} \\ - \frac{2\left(d^{2}-1\right)\left(1-\alpha\right)} - {\alpha d^{2}}+\frac{4\sqrt{1-\alpha}\sqrt{d^{2}-1}}{\alpha} - \end{array} - \right. + r_{\mathcal{C}}^{\text{est}} = + \frac{\left(d-1\right)\left(1-\alpha_{\overline{\mathcal{C}}}/\alpha\right)}{d} - See the reference[1] for more details. +The error bounds are given by +.. math:: + E = \min\left\{ + \begin{array}{c} + \frac{\left(d-1\right)\left[\left|\alpha-\alpha_{\overline{\mathcal{C}}}\right| + +\left(1-\alpha\right)\right]}{d} \\ + \frac{2\left(d^{2}-1\right)\left(1-\alpha\right)} + {\alpha d^{2}}+\frac{4\sqrt{1-\alpha}\sqrt{d^{2}-1}}{\alpha} + \end{array} + \right. - Fit Model - The fit is based on the following decay functions. +See [1] for more details.""" - .. math:: - - F_1(x_1) &= a \alpha^{x_1} + b ... {\rm standard RB} \\ - F_2(x_2) &= a (\alpha_c \alpha)^{x_2} + b ... {\rm interleaved RB} - - Fit Parameters - - :math:`a`: Height of decay curve. - - :math:`b`: Base line. - - :math:`\alpha`: Depolarizing parameter. - - :math:`\alpha_c`: Ratio of the depolarizing parameter of - interleaved RB to standard RB curve. - - Initial Guesses - - :math:`a`: Determined by the average :math:`a` of the standard and interleaved RB. - - :math:`b`: Determined by the average :math:`b` of the standard and interleaved RB. - Usually equivalent to :math:`(1/2)**n` where :math:`n` is number of qubit. - - :math:`\alpha`: Determined by the slope of :math:`(y_1 - b)**(-x_1)` of the first and the - second data point of the standard RB. - - :math:`\alpha_c`: Estimate :math:`\alpha' = \alpha_c * \alpha` from the - interleaved RB curve, then divide this by the initial guess of :math:`\alpha`. + __doc_equations__ = [ + r"F_1(x_1) = a \alpha^{x_1} + b", + r"F_2(x_2) = a (\alpha_c \alpha)^{x_2} + b", + ] - Bounds - - :math:`a`: [0, 1] - - :math:`b`: [0, 1] - - :math:`\alpha`: [0, 1] - - :math:`\alpha_c`: [0, 1] + __doc_fit_params__ = [ + CurveFitParameter( + name="a", + description="Height of decay curve.", + initial_guess=r"Average :math:`a` of the standard and interleaved RB.", + bounds="[0, 1]", + ), + CurveFitParameter( + name="b", + description="Base line.", + initial_guess=r"Average :math:`b` of the standard and interleaved RB. " + r"Usually equivalent to :math:`(1/2)^n` where :math:`n` is number " + "of qubit.", + bounds="[0, 1]", + ), + CurveFitParameter( + name=r"\alpha", + description="Depolarizing parameter.", + initial_guess=r"The slope of :math:`(y_1 - b)^{-x_1}` of the first and the " + "second data point of the standard RB.", + bounds="[0, 1]", + ), + CurveFitParameter( + name=r"\alpha_c", + description="Ratio of the depolarizing parameter of " + "interleaved RB to standard RB curve.", + initial_guess=r"Estimate :math:`\alpha' = \alpha_c \alpha` from the " + "interleaved RB curve, then divide this by " + r"the initial guess of :math:`\alpha`.", + bounds="[0, 1]", + ), + ] - References - [1] "Efficient measurement of quantum gate error by interleaved randomized benchmarking" - (arXiv:1203.4550). - """ + __doc_references__ = [ + Reference( + title="Efficient measurement of quantum gate error by " + "interleaved randomized benchmarking", + authors="Easwar Magesan, et. al.", + open_access_link="https://arxiv.org/abs/1203.4550", + ), + ] __series__ = [ SeriesDef( diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py index 7865b375b4..77441a2864 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py @@ -23,13 +23,13 @@ from .rb_experiment import RBExperiment from .interleaved_rb_analysis import InterleavedRBAnalysis from qiskit_experiments.autodocs import ( - auto_experiment_documentation, - auto_options_method_documentation, + standard_experiment_documentation, + standard_option_documentation, ) -@auto_experiment_documentation() -@auto_options_method_documentation() +@standard_experiment_documentation +@standard_option_documentation class InterleavedRBExperiment(RBExperiment): """Interleaved RB Experiment class""" diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index a6103ff4a4..1529c754c0 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -27,41 +27,41 @@ get_opt_error, ) from qiskit_experiments.analysis.data_processing import multi_mean_xy_data +from qiskit_experiments.autodocs import CurveFitParameter, curve_analysis_documentation +@curve_analysis_documentation class RBAnalysis(CurveAnalysis): - r"""A class to analyze randomized benchmarking experiments. - - Overview - This analysis takes only single series. - This series is fit by the exponential decay function. - From the fit :math:`\alpha` value this analysis estimates the error per Clifford (EPC). - - Fit Model - The fit is based on the following decay function. - - .. math:: - - F(x) = a \alpha^x + b - - Fit Parameters - - :math:`a`: Height of decay curve. - - :math:`b`: Base line. - - :math:`\alpha`: Depolarizing parameter. This is the fit parameter of main interest. - - Initial Guesses - - :math:`a`: Determined by :math:`(y_0 - b) / \alpha^x_0` - where :math:`b` and :math:`\alpha` are initial guesses. - - :math:`b`: Determined by :math:`(1/2)^n` where :math:`n` is the number of qubit. - - :math:`\alpha`: Determined by the slope of :math:`(y - b)^{-x}` of the first and the - second data point. - - Bounds - - :math:`a`: [0, 1] - - :math:`b`: [0, 1] - - :math:`\alpha`: [0, 1] - - """ + """Randomized benchmarking analysis.""" + + __doc_overview__ = r"""This analysis takes only single series. +This curve is fit by the exponential decay function. +From the fit :math:`\alpha` value this analysis estimates the error per Clifford (EPC).""" + + __doc_equations__ = [r"F(x) = a \alpha^x + b"] + + __doc_fit_params__ = [ + CurveFitParameter( + name="a", + description="Height of decay curve.", + initial_guess=r"Determined by :math:`(y_0 - b) / \alpha^{x_0}`, " + r"where :math:`b` and :math:`\alpha` are initial guesses.", + bounds="[0, 1]", + ), + CurveFitParameter( + name="b", + description="Base line.", + initial_guess=r"Determined by :math:`(1/2)^n` where :math:`n` is the number of qubit.", + bounds="[0, 1]", + ), + CurveFitParameter( + name=r"\alpha", + description="Depolarizing parameter. This is the fit parameter of main interest.", + initial_guess=r"Determined by the slope of :math:`(y - b)^{-x}` of the first and " + "the second data point.", + bounds="[0, 1]", + ), + ] __series__ = [ SeriesDef( diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index a9584fc2d3..56cc228235 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -26,15 +26,15 @@ from qiskit_experiments.autodocs import ( OptionsField, Reference, - auto_experiment_documentation, - auto_options_method_documentation, + standard_experiment_documentation, + standard_option_documentation, ) from .clifford_utils import CliffordUtils from .rb_analysis import RBAnalysis -@auto_experiment_documentation() -@auto_options_method_documentation() +@standard_experiment_documentation +@standard_option_documentation class RBExperiment(BaseExperiment): """Randomized benchmarking.""" @@ -107,15 +107,16 @@ def _default_experiment_options(cls): "lengths": OptionsField( default=None, annotation=Iterable[int], - description="Array of integer values representing a number of Clifford gate N per \ -RB sequence. This value should be chosen based on expected decay curve. If the maximum length is \ -too short, confidence interval of fit will become poor.", + description="Array of integer values representing a number of Clifford gate N " + "per RB sequence. This value should be chosen based on " + "expected decay curve. If the maximum length is " + "too short, confidence interval of fit will become poor.", ), "num_samples": OptionsField( default=None, annotation=int, - description="Number of RB sequence per Clifford length. M random sequences are \ -generated for a length N.", + description="Number of RB sequence per Clifford length. M random sequences are " + "generated for a length N.", ), } From 6fbfb0357d5423bce784d5332763f15cd02dee68 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 24 Jun 2021 02:14:37 +0900 Subject: [PATCH 11/46] black and typing --- qiskit_experiments/analysis/curve_analysis.py | 9 ++-- qiskit_experiments/autodocs/analysis_docs.py | 43 +++++++++---------- qiskit_experiments/autodocs/descriptions.py | 2 +- .../autodocs/experiment_docs.py | 32 +++++++------- .../autodocs/set_option_docs.py | 20 +++++---- qiskit_experiments/autodocs/writer.py | 13 +++--- qiskit_experiments/base_analysis.py | 7 +-- qiskit_experiments/base_experiment.py | 4 +- .../characterization/qubit_spectroscopy.py | 25 ++++++----- .../interleaved_rb_analysis.py | 18 ++++---- .../randomized_benchmarking/rb_analysis.py | 13 ++++-- .../randomized_benchmarking/rb_experiment.py | 14 +++--- 12 files changed, 106 insertions(+), 94 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 0fbbe32b44..d21a68dd86 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -21,17 +21,18 @@ from typing import Any, Dict, List, Tuple, Callable, Union, Optional import numpy as np +from qiskit.providers import Options from qiskit_experiments.analysis import plotting from qiskit_experiments.analysis.curve_fitting import multi_curve_fit, CurveAnalysisResult from qiskit_experiments.analysis.data_processing import probability from qiskit_experiments.analysis.utils import get_opt_value, get_opt_error +from qiskit_experiments.autodocs import OptionsField from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.data_processing import DataProcessor from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import AnalysisResult, ExperimentData -from qiskit_experiments.autodocs import OptionsField @dataclasses.dataclass(frozen=True) @@ -247,7 +248,7 @@ def __init__(self): setattr(self, f"__{key}", None) @classmethod - def _default_options(cls) -> Dict[str, OptionsField]: + def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: """Return default analysis options.""" options = { "curve_fitter": OptionsField( @@ -293,7 +294,7 @@ def data_processor(data: Dict[str, Any]) -> Tuple[float, float] default=None, annotation=Dict[str, Tuple[float, float]], description="Dictionary of (min, max) tuple of fit parameter boundaries. " - "Keys are parameter names.", + "Keys are parameter names.", ), "x_key": OptionsField( default="xval", @@ -329,7 +330,7 @@ def data_processor(data: Dict[str, Any]) -> Tuple[float, float] default=None, annotation=Dict[str, str], description="Mapping of fit parameters and representation in the fit report. " - "If nothing specified, fit report will not be shown.", + "If nothing specified, fit report will not be shown.", ), "return_data_points": OptionsField( default=False, diff --git a/qiskit_experiments/autodocs/analysis_docs.py b/qiskit_experiments/autodocs/analysis_docs.py index 5363b883f6..fe6aa44d60 100644 --- a/qiskit_experiments/autodocs/analysis_docs.py +++ b/qiskit_experiments/autodocs/analysis_docs.py @@ -26,14 +26,14 @@ class StandardAnalysisDocstring(_DocstringMaker): @classmethod def make_docstring( - cls, - default_options: Dict[str, OptionsField], - overview: Optional[str] = None, - example: Optional[str] = None, - references: Optional[List[Reference]] = None, - note: Optional[str] = None, - warning: Optional[str] = None, - tutorial: Optional[str] = None, + cls, + default_options: Dict[str, OptionsField], + overview: Optional[str] = None, + example: Optional[str] = None, + references: Optional[List[Reference]] = None, + note: Optional[str] = None, + warning: Optional[str] = None, + tutorial: Optional[str] = None, ) -> str: try: writer = _DocstringWriter() @@ -64,16 +64,16 @@ class CurveAnalysisDocstring(_DocstringMaker): @classmethod def make_docstring( - cls, - default_options: Dict[str, OptionsField], - overview: Optional[str] = None, - equations: Optional[List[str]] = None, - fit_params: Optional[List[CurveFitParameter]] = None, - example: Optional[str] = None, - references: Optional[List[Reference]] = None, - note: Optional[str] = None, - warning: Optional[str] = None, - tutorial: Optional[str] = None, + cls, + default_options: Dict[str, OptionsField], + overview: Optional[str] = None, + equations: Optional[List[str]] = None, + fit_params: Optional[List[CurveFitParameter]] = None, + example: Optional[str] = None, + references: Optional[List[Reference]] = None, + note: Optional[str] = None, + warning: Optional[str] = None, + tutorial: Optional[str] = None, ) -> str: try: writer = _CurveFitDocstringWriter() @@ -123,6 +123,7 @@ def make_docstring( def base_analysis_documentation(style: Type[_DocstringMaker]): """A class decorator that overrides analysis class docstring.""" + def decorator(analysis: "BaseAnalysis"): regex = r"__doc_(?P\S+)__" @@ -133,11 +134,9 @@ def decorator(analysis: "BaseAnalysis"): arg = match["kwarg"] kwargs[arg] = getattr(analysis, attribute) - exp_docs = style.make_docstring( - default_options=analysis._default_options(), - **kwargs - ) + exp_docs = style.make_docstring(default_options=analysis._default_options(), **kwargs) analysis.__doc__ += f"\n\n{exp_docs}" return analysis + return decorator diff --git a/qiskit_experiments/autodocs/descriptions.py b/qiskit_experiments/autodocs/descriptions.py index a2852a9f3b..bfab3517f4 100644 --- a/qiskit_experiments/autodocs/descriptions.py +++ b/qiskit_experiments/autodocs/descriptions.py @@ -100,7 +100,7 @@ def _parse_annotation(_type: typing.Any) -> str: return f":py:class:`~{module}.{_type.__name__}`" -def to_options(fields: typing.Dict[str, OptionsField]) -> Options: +def to_options(fields: typing.Union[Options, typing.Dict[str, OptionsField]]) -> Options: """Converts a dictionary of ``OptionsField`` into ``Options`` object. Args: diff --git a/qiskit_experiments/autodocs/experiment_docs.py b/qiskit_experiments/autodocs/experiment_docs.py index a4ae2dc5fe..dae4673d67 100644 --- a/qiskit_experiments/autodocs/experiment_docs.py +++ b/qiskit_experiments/autodocs/experiment_docs.py @@ -26,16 +26,16 @@ class StandardExperimentDocstring(_DocstringMaker): @classmethod def make_docstring( - cls, - analysis_options: Dict[str, OptionsField], - experiment_options: Dict[str, OptionsField], - analysis: str, - overview: Optional[str] = None, - example: Optional[str] = None, - references: Optional[List[Reference]] = None, - note: Optional[str] = None, - warning: Optional[str] = None, - tutorial: Optional[str] = None, + cls, + analysis_options: Dict[str, OptionsField], + experiment_options: Dict[str, OptionsField], + analysis: str, + overview: Optional[str] = None, + example: Optional[str] = None, + references: Optional[List[Reference]] = None, + note: Optional[str] = None, + warning: Optional[str] = None, + tutorial: Optional[str] = None, ) -> str: try: writer = _DocstringWriter() @@ -54,15 +54,15 @@ def make_docstring( fields=experiment_options, section="Experiment Options", text_block="Experiment options to generate circuits. " - "Options can be updated with :py:meth:`set_experiment_options`. " - "See method documentation for details." + "Options can be updated with :py:meth:`set_experiment_options`. " + "See method documentation for details.", ) writer.write_options_as_sections( fields=analysis_options, section="Analysis Options", text_block="Analysis options to run the analysis class. " - "Options can be updated with :py:meth:`set_analysis_options`. " - "See method documentation for details." + "Options can be updated with :py:meth:`set_analysis_options`. " + "See method documentation for details.", ) if references: writer.write_references(references) @@ -77,6 +77,7 @@ def make_docstring( def base_experiment_documentation(style: Type[_DocstringMaker]): """A class decorator that overrides experiment class docstring.""" + def decorator(experiment: "BaseExperiment"): regex = r"__doc_(?P\S+)__" @@ -93,9 +94,10 @@ def decorator(experiment: "BaseExperiment"): analysis_options=experiment.__analysis_class__._default_options(), experiment_options=experiment._default_experiment_options(), analysis=f"{analysis.__module__}.{analysis.__name__}", - **kwargs + **kwargs, ) experiment.__doc__ += f"\n\n{exp_docs}" return experiment + return decorator diff --git a/qiskit_experiments/autodocs/set_option_docs.py b/qiskit_experiments/autodocs/set_option_docs.py index 0e40b13d13..a03399e563 100644 --- a/qiskit_experiments/autodocs/set_option_docs.py +++ b/qiskit_experiments/autodocs/set_option_docs.py @@ -27,11 +27,11 @@ class StandardSetOptionsDocstring(_DocstringMaker): @classmethod def make_docstring( - cls, - description: str, - options: Dict[str, OptionsField], - note: Optional[str] = None, - raises: Optional[Dict[str, str]] = None, + cls, + description: str, + options: Dict[str, OptionsField], + note: Optional[str] = None, + raises: Optional[Dict[str, str]] = None, ) -> str: try: writer = _DocstringWriter() @@ -92,6 +92,7 @@ def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: def base_options_method_documentation(style: Type[_DocstringMaker]): """A class decorator that overrides set options method docstring.""" + def decorator(experiment: "BaseExperiment"): analysis_options = experiment.__analysis_class__._default_options() experiment_options = experiment._default_experiment_options() @@ -103,9 +104,9 @@ def decorator(experiment: "BaseExperiment"): description="Set the analysis options for :py:meth:`run_analysis` method.", options=analysis_options, note="Here you can set arbitrary parameter, even if it is not listed. " - "Such option is passed as a keyword argument to the analysis fitter functions " - "(if exist). The execution may fail if the function API doesn't support " - "extra keyword arguments.", + "Such option is passed as a keyword argument to the analysis fitter functions " + "(if exist). The execution may fail if the function API doesn't support " + "extra keyword arguments.", ) setattr(experiment, "set_analysis_options", analysis_setter) @@ -115,9 +116,10 @@ def decorator(experiment: "BaseExperiment"): experiment_setter.__doc__ = style.make_docstring( description="Set the analysis options for :py:meth:`run` method.", options=experiment_options, - raises={"AttributeError": "If the field passed in is not a supported options."} + raises={"AttributeError": "If the field passed in is not a supported options."}, ) setattr(experiment, "set_experiment_options", experiment_setter) return experiment + return decorator diff --git a/qiskit_experiments/autodocs/writer.py b/qiskit_experiments/autodocs/writer.py index 37280601a6..d75a34a1d9 100644 --- a/qiskit_experiments/autodocs/writer.py +++ b/qiskit_experiments/autodocs/writer.py @@ -31,6 +31,7 @@ def make_docstring(cls, *args, **kwargs) -> str: class _DocstringWriter: """A docstring writer.""" + __indent__ = " " def __init__(self): @@ -47,6 +48,7 @@ def write_options_as_args(self, fields: typing.Dict[str, OptionsField]): This style is used for detailed summary for the set method docstring. Extra fields (non-default options) are also shown. """ + def _write_field(_arg_name, _field): # parse type @@ -84,10 +86,10 @@ def _write_field(_arg_name, _field): self.docstring += "\n" def write_options_as_sections( - self, - fields: typing.Dict[str, OptionsField], - section: str, - text_block: typing.Optional[str] = None, + self, + fields: typing.Dict[str, OptionsField], + section: str, + text_block: typing.Optional[str] = None, ): """Write option descriptions as a custom section. @@ -107,7 +109,7 @@ def write_options_as_sections( if field.is_extra: continue arg_str_type = f":py:obj:`{_parse_annotation(field.annotation)}`" - arg_description = field.description.split('\n')[0] + arg_description = field.description.split("\n")[0] # write multi line description self.docstring += self.__indent__ self.docstring += f"- **{arg_name}** ({arg_str_type}): {arg_description}\n" @@ -191,7 +193,6 @@ def _write_multi_line(text_block: str, indent: typing.Optional[str] = None) -> s class _CurveFitDocstringWriter(_DocstringWriter): - def write_fit_parameter(self, fit_params: typing.List[CurveFitParameter]): """Write fit parameters.""" self.docstring += "Fit Parameters\n" diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index a765dcbd3c..5ee5439641 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -14,13 +14,14 @@ """ from abc import ABC, abstractmethod -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Union from qiskit.exceptions import QiskitError +from qiskit.providers import Options +from qiskit_experiments.autodocs import OptionsField, to_options from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ExperimentData, AnalysisResult -from qiskit_experiments.autodocs import OptionsField, to_options class BaseAnalysis(ABC): @@ -51,7 +52,7 @@ class BaseAnalysis(ABC): __experiment_data__ = ExperimentData @classmethod - def _default_options(cls) -> Dict[str, OptionsField]: + def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: return dict() def run( diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 0971e17cc4..95b8c170a8 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -16,7 +16,7 @@ import copy from abc import ABC, abstractmethod from numbers import Integral -from typing import Iterable, Optional, Tuple, List, Dict +from typing import Iterable, Optional, Tuple, List, Dict, Union from qiskit import transpile, assemble, QuantumCircuit from qiskit.exceptions import QiskitError @@ -199,7 +199,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: # generation @classmethod - def _default_experiment_options(cls) -> Dict[str, OptionsField]: + def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: """Default kwarg options for experiment""" # Experiment subclasses should override this method to return # an dictionary of OptionsField object containing all the supported options for diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 48d04aa325..60ee3b9c6a 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -19,8 +19,7 @@ from qiskit import QuantumCircuit from qiskit.circuit import Gate, Parameter from qiskit.exceptions import QiskitError -from qiskit.providers import Backend -from qiskit.providers.options import Options +from qiskit.providers import Backend, Options from qiskit.qobj.utils import MeasLevel from qiskit_experiments.analysis import ( @@ -75,7 +74,7 @@ class SpectroscopyAnalysis(CurveAnalysis): name=r"\sigma", description="Standard deviation of Gaussian function.", initial_guess=r"Calculated from FWHM of peak :math:`w` such that " - r":math:`w / \sqrt{8} \ln{2}`.", + r":math:`w / \sqrt{8} \ln{2}`.", bounds=r"[0, :math:`\Delta x`] where :math:`\Delta x` represents frequency scan range.", ), ] @@ -90,7 +89,7 @@ class SpectroscopyAnalysis(CurveAnalysis): ] @classmethod - def _default_options(cls) -> Dict[str, OptionsField]: + def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: """Return default options.""" default_options = super()._default_options() default_options["p0"].default = {"a": None, "sigma": None, "freq": None, "b": None} @@ -101,7 +100,7 @@ def _default_options(cls) -> Dict[str, OptionsField]: default=True, annotation=bool, description="Set ``True`` to normalize measurement data. Usually applied to " - "Kerneled (level1) measurement data.", + "Kerneled (level1) measurement data.", ) return default_options @@ -224,35 +223,35 @@ def _default_run_options(cls) -> Options: ) @classmethod - def _default_experiment_options(cls) -> Dict[str, OptionsField]: + def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: """Default option values used for the spectroscopy pulse.""" return { "amp": OptionsField( default=0.1, annotation=float, description="Amplitude of spectroscopy pulse. Usually weak power pulse is used to " - "suppress broadening of observed peaks.", + "suppress broadening of observed peaks.", ), "duration": OptionsField( default=1024, annotation=int, description="Duration of spectroscopy pulse. This may need to satisfy the " - "hardware waveform memory constraint. " - "The default value is represented in units of dt.", + "hardware waveform memory constraint. " + "The default value is represented in units of dt.", ), "sigma": OptionsField( default=256, annotation=Union[int, float], description="Sigma of Gaussian rising and falling edges. This value should be " - "sufficiently smaller than the duration, " - "otherwise waveform is distorted. " - "The default value is represented in units of dt." + "sufficiently smaller than the duration, " + "otherwise waveform is distorted. " + "The default value is represented in units of dt.", ), "width": OptionsField( default=0, annotation=Union[int, float], description="Width of the flat-top part of the Gaussian square envelope of " - "spectroscopy pulse. Set width=0 to use Gaussian pulse.", + "spectroscopy pulse. Set width=0 to use Gaussian pulse.", ), } diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index f6f84bed91..bb17e3161a 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -15,6 +15,7 @@ from typing import List, Dict, Any, Union import numpy as np +from qiskit.providers import Options from qiskit_experiments.analysis import ( CurveAnalysisResult, @@ -25,6 +26,7 @@ ) from qiskit_experiments.autodocs import ( Reference, + OptionsField, CurveFitParameter, curve_analysis_documentation, ) @@ -79,24 +81,24 @@ class InterleavedRBAnalysis(RBAnalysis): name="b", description="Base line.", initial_guess=r"Average :math:`b` of the standard and interleaved RB. " - r"Usually equivalent to :math:`(1/2)^n` where :math:`n` is number " - "of qubit.", + r"Usually equivalent to :math:`(1/2)^n` where :math:`n` is number " + "of qubit.", bounds="[0, 1]", ), CurveFitParameter( name=r"\alpha", description="Depolarizing parameter.", initial_guess=r"The slope of :math:`(y_1 - b)^{-x_1}` of the first and the " - "second data point of the standard RB.", + "second data point of the standard RB.", bounds="[0, 1]", ), CurveFitParameter( name=r"\alpha_c", description="Ratio of the depolarizing parameter of " - "interleaved RB to standard RB curve.", + "interleaved RB to standard RB curve.", initial_guess=r"Estimate :math:`\alpha' = \alpha_c \alpha` from the " - "interleaved RB curve, then divide this by " - r"the initial guess of :math:`\alpha`.", + "interleaved RB curve, then divide this by " + r"the initial guess of :math:`\alpha`.", bounds="[0, 1]", ), ] @@ -104,7 +106,7 @@ class InterleavedRBAnalysis(RBAnalysis): __doc_references__ = [ Reference( title="Efficient measurement of quantum gate error by " - "interleaved randomized benchmarking", + "interleaved randomized benchmarking", authors="Easwar Magesan, et. al.", open_access_link="https://arxiv.org/abs/1203.4550", ), @@ -132,7 +134,7 @@ class InterleavedRBAnalysis(RBAnalysis): ] @classmethod - def _default_options(cls): + def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: """Return default options.""" default_options = super()._default_options() diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index 1529c754c0..0b9264960e 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -16,6 +16,7 @@ from typing import List, Dict, Any, Union import numpy as np +from qiskit.providers import Options from qiskit_experiments.analysis import ( CurveAnalysis, @@ -27,7 +28,11 @@ get_opt_error, ) from qiskit_experiments.analysis.data_processing import multi_mean_xy_data -from qiskit_experiments.autodocs import CurveFitParameter, curve_analysis_documentation +from qiskit_experiments.autodocs import ( + OptionsField, + CurveFitParameter, + curve_analysis_documentation, +) @curve_analysis_documentation @@ -45,7 +50,7 @@ class RBAnalysis(CurveAnalysis): name="a", description="Height of decay curve.", initial_guess=r"Determined by :math:`(y_0 - b) / \alpha^{x_0}`, " - r"where :math:`b` and :math:`\alpha` are initial guesses.", + r"where :math:`b` and :math:`\alpha` are initial guesses.", bounds="[0, 1]", ), CurveFitParameter( @@ -58,7 +63,7 @@ class RBAnalysis(CurveAnalysis): name=r"\alpha", description="Depolarizing parameter. This is the fit parameter of main interest.", initial_guess=r"Determined by the slope of :math:`(y - b)^{-x}` of the first and " - "the second data point.", + "the second data point.", bounds="[0, 1]", ), ] @@ -73,7 +78,7 @@ class RBAnalysis(CurveAnalysis): ] @classmethod - def _default_options(cls): + def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: """Return default options.""" default_options = super()._default_options() diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index 56cc228235..bb8757be03 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -12,13 +12,13 @@ """ Standard RB Experiment class. """ -from typing import Union, Iterable, Optional, List +from typing import Union, Iterable, Optional, List, Dict import numpy as np from numpy.random import Generator, default_rng from qiskit import QuantumCircuit from qiskit.circuit import Gate -from qiskit.providers import Backend +from qiskit.providers import Backend, Options from qiskit.quantum_info import Clifford from qiskit_experiments.analysis.data_processing import probability @@ -102,21 +102,21 @@ def __init__( self._rng = seed @classmethod - def _default_experiment_options(cls): + def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: return { "lengths": OptionsField( default=None, annotation=Iterable[int], description="Array of integer values representing a number of Clifford gate N " - "per RB sequence. This value should be chosen based on " - "expected decay curve. If the maximum length is " - "too short, confidence interval of fit will become poor.", + "per RB sequence. This value should be chosen based on " + "expected decay curve. If the maximum length is " + "too short, confidence interval of fit will become poor.", ), "num_samples": OptionsField( default=None, annotation=int, description="Number of RB sequence per Clifford length. M random sequences are " - "generated for a length N.", + "generated for a length N.", ), } From 85ad101b8c7bff159c11ab48de29284f5c39353d Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 24 Jun 2021 03:26:38 +0900 Subject: [PATCH 12/46] wip lint --- qiskit_experiments/analysis/curve_analysis.py | 7 ++++++- qiskit_experiments/autodocs/__init__.py | 6 ++---- qiskit_experiments/autodocs/analysis_docs.py | 6 ++++-- .../autodocs/experiment_docs.py | 3 ++- .../autodocs/set_option_docs.py | 5 +++-- qiskit_experiments/autodocs/writer.py | 21 ++++++++++++------- qiskit_experiments/base_analysis.py | 2 +- qiskit_experiments/base_experiment.py | 2 +- .../characterization/qubit_spectroscopy.py | 4 ++-- .../interleaved_rb_experiment.py | 9 ++++---- .../randomized_benchmarking/rb_analysis.py | 1 + .../randomized_benchmarking/rb_experiment.py | 4 ++-- 12 files changed, 41 insertions(+), 29 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index d21a68dd86..3f16e755d7 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -244,7 +244,12 @@ def __init__(self): self.__processed_data_set = list() # Add expected options to instance variable so that every method can access to. - for key in self._default_options().__dict__: + default_options = self._default_options() + + if not isinstance(default_options, dict): + default_options = default_options.__dict__ + + for key in default_options: setattr(self, f"__{key}", None) @classmethod diff --git a/qiskit_experiments/autodocs/__init__.py b/qiskit_experiments/autodocs/__init__.py index 4c928fc12e..26469000fd 100644 --- a/qiskit_experiments/autodocs/__init__.py +++ b/qiskit_experiments/autodocs/__init__.py @@ -20,9 +20,7 @@ while providing users with the standardized and high quality documentation. """ -import qiskit_experiments.autodocs.analysis_docs -import qiskit_experiments.autodocs.experiment_docs -import qiskit_experiments.autodocs.set_option_docs +from . import analysis_docs, experiment_docs, set_option_docs from .descriptions import OptionsField, Reference, CurveFitParameter, to_options @@ -41,6 +39,6 @@ ) -standard_option_documentation = set_option_docs.base_options_method_documentation( +standard_options_documentation = set_option_docs.base_options_documentation( style=set_option_docs.StandardSetOptionsDocstring ) diff --git a/qiskit_experiments/autodocs/analysis_docs.py b/qiskit_experiments/autodocs/analysis_docs.py index fe6aa44d60..653c656f09 100644 --- a/qiskit_experiments/autodocs/analysis_docs.py +++ b/qiskit_experiments/autodocs/analysis_docs.py @@ -24,6 +24,7 @@ class StandardAnalysisDocstring(_DocstringMaker): """A facade class to write standard analysis docstring.""" + # pylint: disable=arguments-differ @classmethod def make_docstring( cls, @@ -55,13 +56,14 @@ def make_docstring( if tutorial: writer.write_tutorial_link(tutorial) except Exception as ex: - raise QiskitError(f"Auto docstring generation failed with the error: {ex}") + raise QiskitError(f"Auto docstring generation failed with the error: {ex}") from ex return writer.docstring class CurveAnalysisDocstring(_DocstringMaker): """A facade class to write curve analysis docstring.""" + # pylint: disable=arguments-differ @classmethod def make_docstring( cls, @@ -117,7 +119,7 @@ def make_docstring( if tutorial: writer.write_tutorial_link(tutorial) except Exception as ex: - raise QiskitError(f"Auto docstring generation failed with the error: {ex}") + raise QiskitError(f"Auto docstring generation failed with the error: {ex}") from ex return writer.docstring diff --git a/qiskit_experiments/autodocs/experiment_docs.py b/qiskit_experiments/autodocs/experiment_docs.py index dae4673d67..ed75821659 100644 --- a/qiskit_experiments/autodocs/experiment_docs.py +++ b/qiskit_experiments/autodocs/experiment_docs.py @@ -24,6 +24,7 @@ class StandardExperimentDocstring(_DocstringMaker): """A facade class to write standard experiment docstring.""" + # pylint: disable=arguments-differ @classmethod def make_docstring( cls, @@ -71,7 +72,7 @@ def make_docstring( if tutorial: writer.write_tutorial_link(tutorial) except Exception as ex: - raise QiskitError(f"Auto docstring generation failed with the error: {ex}") + raise QiskitError(f"Auto docstring generation failed with the error: {ex}") from ex return writer.docstring diff --git a/qiskit_experiments/autodocs/set_option_docs.py b/qiskit_experiments/autodocs/set_option_docs.py index a03399e563..22bc2f5116 100644 --- a/qiskit_experiments/autodocs/set_option_docs.py +++ b/qiskit_experiments/autodocs/set_option_docs.py @@ -25,6 +25,7 @@ class StandardSetOptionsDocstring(_DocstringMaker): """A facade class to write standard set options docstring.""" + # pylint: disable=arguments-differ @classmethod def make_docstring( cls, @@ -42,7 +43,7 @@ def make_docstring( if raises: writer.write_raises(*list(zip(*raises.items()))) except Exception as ex: - raise QiskitError(f"Auto docstring generation failed with the error: {ex}") + raise QiskitError(f"Auto docstring generation failed with the error: {ex}") from ex return writer.docstring @@ -90,7 +91,7 @@ def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: return annotations -def base_options_method_documentation(style: Type[_DocstringMaker]): +def base_options_documentation(style: Type[_DocstringMaker]): """A class decorator that overrides set options method docstring.""" def decorator(experiment: "BaseExperiment"): diff --git a/qiskit_experiments/autodocs/writer.py b/qiskit_experiments/autodocs/writer.py index d75a34a1d9..c75157626c 100644 --- a/qiskit_experiments/autodocs/writer.py +++ b/qiskit_experiments/autodocs/writer.py @@ -13,17 +13,19 @@ Docstring writer. This module takes facade pattern to implement the functionality. """ import typing +from abc import ABC, abstractmethod from types import FunctionType -from .descriptions import OptionsField, Reference, CurveFitParameter, _parse_annotation -from abc import abstractclassmethod from qiskit.exceptions import QiskitError +from .descriptions import OptionsField, Reference, CurveFitParameter, _parse_annotation + -class _DocstringMaker: +class _DocstringMaker(ABC): """A base facade class to write docstring.""" - @abstractclassmethod + @classmethod + @abstractmethod def make_docstring(cls, *args, **kwargs) -> str: """Write a docstring.""" pass @@ -138,7 +140,7 @@ def write_section(self, text_block: str, section: str): """Write new user defined section.""" self.docstring += f"{section}\n" self.docstring += self._write_multi_line(text_block, self.__indent__) - self.docstring += "\n\n" + self.docstring += "\n" def write_note(self, text_block: str): """Write note.""" @@ -176,6 +178,7 @@ def write_references(self, refs: typing.List[Reference]): self.docstring += "\n" def write_tutorial_link(self, link: str): + """Write link to tutorial website.""" self.docstring += "See Also:\n" self.docstring += self.__indent__ self.docstring += f"- `Qiskit Experiment Tutorial <{link}>`_\n" @@ -186,13 +189,15 @@ def _write_multi_line(text_block: str, indent: typing.Optional[str] = None) -> s """A util method to write multi line text with indentation.""" indented_text = "" for line in text_block.split("\n"): - if indent is not None: + if len(line) > 0 and indent is not None: indented_text += indent indented_text += f"{line}\n" return indented_text class _CurveFitDocstringWriter(_DocstringWriter): + """A docstring writer supporting fit model descriptions.""" + def write_fit_parameter(self, fit_params: typing.List[CurveFitParameter]): """Write fit parameters.""" self.docstring += "Fit Parameters\n" @@ -231,8 +236,8 @@ def write_fit_models(self, equations: typing.List[str]): for equation in equations: try: lh, rh = equation.split("=") - except ValueError: - raise QiskitError(f"Equation {equation} is not a valid form.") + except ValueError as ex: + raise QiskitError(f"Equation {equation} is not a valid form.") from ex eqs.append(f"{self.__indent__ * 2}{lh} &= {rh}") self.docstring += " \\\\\n".join(eqs) self.docstring += "\n" diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 5ee5439641..0ea6f98dc0 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -53,7 +53,7 @@ class BaseAnalysis(ABC): @classmethod def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: - return dict() + return Options() def run( self, diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 95b8c170a8..d38b54016b 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -205,7 +205,7 @@ def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: # an dictionary of OptionsField 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 dict() + return Options() @property def experiment_options(self) -> Options: diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 60ee3b9c6a..ee5c040521 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -34,7 +34,7 @@ OptionsField, CurveFitParameter, standard_experiment_documentation, - standard_option_documentation, + standard_options_documentation, curve_analysis_documentation, ) from qiskit_experiments.base_experiment import BaseExperiment @@ -188,7 +188,7 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR @standard_experiment_documentation -@standard_option_documentation +@standard_options_documentation class QubitSpectroscopy(BaseExperiment): """Class that runs spectroscopy by sweeping the qubit frequency.""" diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py index 77441a2864..def8e26a2d 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py @@ -15,21 +15,20 @@ from typing import Union, Iterable, Optional, List from numpy.random import Generator - from qiskit import QuantumCircuit from qiskit.circuit import Instruction from qiskit.quantum_info import Clifford -from .rb_experiment import RBExperiment -from .interleaved_rb_analysis import InterleavedRBAnalysis from qiskit_experiments.autodocs import ( standard_experiment_documentation, - standard_option_documentation, + standard_options_documentation, ) +from .interleaved_rb_analysis import InterleavedRBAnalysis +from .rb_experiment import RBExperiment @standard_experiment_documentation -@standard_option_documentation +@standard_options_documentation class InterleavedRBExperiment(RBExperiment): """Interleaved RB Experiment class""" diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index 0b9264960e..f4f97846eb 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -40,6 +40,7 @@ class RBAnalysis(CurveAnalysis): """Randomized benchmarking analysis.""" __doc_overview__ = r"""This analysis takes only single series. + This curve is fit by the exponential decay function. From the fit :math:`\alpha` value this analysis estimates the error per Clifford (EPC).""" diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index bb8757be03..c836a19dfe 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -27,14 +27,14 @@ OptionsField, Reference, standard_experiment_documentation, - standard_option_documentation, + standard_options_documentation, ) from .clifford_utils import CliffordUtils from .rb_analysis import RBAnalysis @standard_experiment_documentation -@standard_option_documentation +@standard_options_documentation class RBExperiment(BaseExperiment): """Randomized benchmarking.""" From dc141600217b3adc77b93e7ad0373c47f77c2799 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 24 Jun 2021 12:29:06 +0900 Subject: [PATCH 13/46] replace LF --- qiskit_experiments/autodocs/analysis_docs.py | 5 +- .../autodocs/experiment_docs.py | 5 +- qiskit_experiments/autodocs/writer.py | 137 +++++++++--------- 3 files changed, 75 insertions(+), 72 deletions(-) diff --git a/qiskit_experiments/autodocs/analysis_docs.py b/qiskit_experiments/autodocs/analysis_docs.py index 653c656f09..05fac2a5cd 100644 --- a/qiskit_experiments/autodocs/analysis_docs.py +++ b/qiskit_experiments/autodocs/analysis_docs.py @@ -12,6 +12,7 @@ """ Documentation for analysis class. """ +import os import re from typing import Optional, Dict, List, Type @@ -137,7 +138,9 @@ def decorator(analysis: "BaseAnalysis"): kwargs[arg] = getattr(analysis, attribute) exp_docs = style.make_docstring(default_options=analysis._default_options(), **kwargs) - analysis.__doc__ += f"\n\n{exp_docs}" + analysis.__doc__ += os.linesep + analysis.__doc__ += os.linesep + analysis.__doc__ += exp_docs return analysis diff --git a/qiskit_experiments/autodocs/experiment_docs.py b/qiskit_experiments/autodocs/experiment_docs.py index ed75821659..56610436ad 100644 --- a/qiskit_experiments/autodocs/experiment_docs.py +++ b/qiskit_experiments/autodocs/experiment_docs.py @@ -12,6 +12,7 @@ """ Documentation for experiment. """ +import os import re from typing import Optional, Dict, List, Type @@ -97,7 +98,9 @@ def decorator(experiment: "BaseExperiment"): analysis=f"{analysis.__module__}.{analysis.__name__}", **kwargs, ) - experiment.__doc__ += f"\n\n{exp_docs}" + experiment.__doc__ += os.linesep + experiment.__doc__ += os.linesep + experiment.__doc__ += exp_docs return experiment diff --git a/qiskit_experiments/autodocs/writer.py b/qiskit_experiments/autodocs/writer.py index c75157626c..007924b1f9 100644 --- a/qiskit_experiments/autodocs/writer.py +++ b/qiskit_experiments/autodocs/writer.py @@ -12,6 +12,7 @@ """ Docstring writer. This module takes facade pattern to implement the functionality. """ +import os import typing from abc import ABC, abstractmethod from types import FunctionType @@ -41,7 +42,8 @@ def __init__(self): def write_header(self, header: str): """Write header.""" - self.docstring += f"{header}\n\n" + self._write_line(header) + self.docstring += os.linesep def write_options_as_args(self, fields: typing.Dict[str, OptionsField]): """Write option descriptions as an argument section. @@ -53,13 +55,9 @@ def write_options_as_args(self, fields: typing.Dict[str, OptionsField]): def _write_field(_arg_name, _field): - # parse type arg_str_type = f":py:obj:`{_parse_annotation(_field.annotation)}`" - - # write multi line description - arg_description = self._write_multi_line(_field.description, self.__indent__ * 2) - - # write default value + self._write_line(f"{_arg_name} ({arg_str_type}):") + self._write_multi_line(_field.description, self.__indent__ * 2) default = _field.default if default is not None: # format representation @@ -67,25 +65,23 @@ def _write_field(_arg_name, _field): default_str = f":py:func:`~{default.__module__}.{default.__name__}`" else: default_str = f":py:obj:`{default}`" - arg_description += self.__indent__ * 2 - arg_description += f"(Default: {default_str})" - self.docstring += self.__indent__ - self.docstring += f"{_arg_name} ({arg_str_type}):\n{arg_description}\n" + self.docstring += self.__indent__ * 2 + self._write_line(f"(Default: {default_str})") - self.docstring += "Parameters:\n" + self._write_line("Parameters:") extra_fields = dict() for arg_name, field in fields.items(): if field.is_extra: extra_fields[arg_name] = field continue _write_field(arg_name, field) - self.docstring += "\n" + self.docstring += os.linesep if extra_fields: - self.docstring += "Other Parameters:\n" + self._write_line("Other Parameters:") for arg_name, field in extra_fields.items(): _write_field(arg_name, field) - self.docstring += "\n" + self.docstring += os.linesep def write_options_as_sections( self, @@ -99,70 +95,70 @@ def write_options_as_sections( This style is mainly used for the short summary (options are itemized). This section will be shown as a drop down box. """ - self.docstring += f".. dropdown:: {section}\n" + self._write_line(f".. dropdown:: {section}") self.docstring += self.__indent__ - self.docstring += ":animate: fade-in-slide-down\n\n" + self._write_line(":animate: fade-in-slide-down") + self.docstring += os.linesep if text_block: - self.docstring += self._write_multi_line(text_block, self.__indent__) - self.docstring += "\n" + self._write_multi_line(text_block, self.__indent__) for arg_name, field in fields.items(): if field.is_extra: continue arg_str_type = f":py:obj:`{_parse_annotation(field.annotation)}`" - arg_description = field.description.split("\n")[0] + arg_description = field.description.split(os.linesep)[0] # write multi line description self.docstring += self.__indent__ - self.docstring += f"- **{arg_name}** ({arg_str_type}): {arg_description}\n" - self.docstring += "\n" + self._write_line(f"- **{arg_name}** ({arg_str_type}): {arg_description}") + self.docstring += os.linesep def write_lines(self, text_block: str): """Write text without section.""" - self.docstring += self._write_multi_line(text_block) - self.docstring += "\n" + self._write_multi_line(text_block) + self.docstring += os.linesep def write_example(self, text_block: str): """Write error descriptions.""" - self.docstring += "Example:\n" - self.docstring += self._write_multi_line(text_block, self.__indent__) - self.docstring += "\n" + self._write_line("Example:") + self._write_multi_line(text_block, self.__indent__) + self.docstring += os.linesep def write_raises(self, error_kinds: typing.List[str], descriptions: typing.List[str]): """Write error descriptions.""" - self.docstring += "Raises:\n" + self._write_line("Raises:") for error_kind, description in zip(error_kinds, descriptions): self.docstring += self.__indent__ - self.docstring += f"{error_kind}: {description}\n" - self.docstring += "\n" + self._write_line(f"{error_kind}: {description}") + self.docstring += os.linesep def write_section(self, text_block: str, section: str): """Write new user defined section.""" - self.docstring += f"{section}\n" - self.docstring += self._write_multi_line(text_block, self.__indent__) - self.docstring += "\n" + self._write_line(f"{section}") + self._write_multi_line(text_block, self.__indent__) + self.docstring += os.linesep def write_note(self, text_block: str): """Write note.""" - self.docstring += "Note:\n" - self.docstring += self._write_multi_line(text_block, self.__indent__) - self.docstring += "\n" + self._write_line("Note:") + self._write_multi_line(text_block, self.__indent__) + self.docstring += os.linesep def write_warning(self, text_block: str): """Write warning.""" - self.docstring += "Warning:\n" - self.docstring += self._write_multi_line(text_block, self.__indent__) - self.docstring += "\n" + self._write_line("Warning:") + self._write_multi_line(text_block, self.__indent__) + self.docstring += os.linesep def write_returns(self, text_block: str): """Write returns.""" - self.docstring += "Returns:\n" - self.docstring += self._write_multi_line(text_block, self.__indent__) - self.docstring += "\n" + self._write_line("Returns:") + self._write_multi_line(text_block, self.__indent__) + self.docstring += os.linesep def write_references(self, refs: typing.List[Reference]): """Write references.""" - self.docstring += "References:\n" + self._write_line("References:") for idx, ref in enumerate(refs): ref_repr = [] if ref.authors: @@ -174,25 +170,27 @@ def write_references(self, refs: typing.List[Reference]): if ref.open_access_link: ref_repr.append(f"`open access <{ref.open_access_link}>`_") self.docstring += self.__indent__ - self.docstring += f"- [{idx + 1}] {', '.join(ref_repr)}\n" - self.docstring += "\n" + self._write_line(f"- [{idx + 1}] {', '.join(ref_repr)}") + self.docstring += os.linesep def write_tutorial_link(self, link: str): """Write link to tutorial website.""" - self.docstring += "See Also:\n" + self._write_line("See Also:") self.docstring += self.__indent__ - self.docstring += f"- `Qiskit Experiment Tutorial <{link}>`_\n" - self.docstring += "\n" + self._write_line(f"- `Qiskit Experiment Tutorial <{link}>`_") + self.docstring += os.linesep - @staticmethod - def _write_multi_line(text_block: str, indent: typing.Optional[str] = None) -> str: + def _write_multi_line(self, text_block: str, indent: typing.Optional[str] = None): """A util method to write multi line text with indentation.""" indented_text = "" - for line in text_block.split("\n"): + for line in text_block.split(os.linesep): if len(line) > 0 and indent is not None: indented_text += indent - indented_text += f"{line}\n" - return indented_text + self._write_line(line.rstrip()) + + def _write_line(self, text: str): + """A helper function to write single line.""" + self.docstring += text.rstrip() + os.linesep class _CurveFitDocstringWriter(_DocstringWriter): @@ -200,36 +198,35 @@ class _CurveFitDocstringWriter(_DocstringWriter): def write_fit_parameter(self, fit_params: typing.List[CurveFitParameter]): """Write fit parameters.""" - self.docstring += "Fit Parameters\n" - + self._write_line("Fit Parameters") for fit_param in fit_params: self.docstring += self.__indent__ - self.docstring += f"- :math:`{fit_param.name}`: {fit_param.description}\n" - self.docstring += "\n" + self._write_line(f"- :math:`{fit_param.name}`: {fit_param.description}") + self.docstring += os.linesep def write_initial_guess(self, fit_params: typing.List[CurveFitParameter]): """Write initial guess estimation method.""" - self.docstring += "Initial Guess\n" - + self._write_line("Initial Guess") for fit_param in fit_params: self.docstring += self.__indent__ - self.docstring += f"- :math:`{fit_param.name}`: {fit_param.initial_guess}\n" - self.docstring += "\n" + self._write_line(f"- :math:`{fit_param.name}`: {fit_param.initial_guess}") + self.docstring += os.linesep def write_bounds(self, fit_params: typing.List[CurveFitParameter]): """Write fit parameter bound.""" - self.docstring += "Parameter Boundaries\n" + self._write_line("Parameter Boundaries") for fit_param in fit_params: self.docstring += self.__indent__ - self.docstring += f"- :math:`{fit_param.name}`: {fit_param.bounds}\n" - self.docstring += "\n" + self._write_line(f"- :math:`{fit_param.name}`: {fit_param.bounds}") + self.docstring += os.linesep def write_fit_models(self, equations: typing.List[str]): """Write fitting models.""" - self.docstring += "Fit Model\n" + self._write_line("Fit Model") self.docstring += self.__indent__ - self.docstring += ".. math::\n\n" + self._write_line(".. math::") + self.docstring += os.linesep if len(equations) > 1: eqs = [] @@ -239,9 +236,9 @@ def write_fit_models(self, equations: typing.List[str]): except ValueError as ex: raise QiskitError(f"Equation {equation} is not a valid form.") from ex eqs.append(f"{self.__indent__ * 2}{lh} &= {rh}") - self.docstring += " \\\\\n".join(eqs) - self.docstring += "\n" + self.docstring += f" \\\\{os.linesep}".join(eqs) + self.docstring += os.linesep else: self.docstring += self.__indent__ * 2 - self.docstring += f"{equations[0]}\n" - self.docstring += "\n" + self._write_line(equations[0]) + self.docstring += os.linesep From b30459dcf77b4f776c84cb6b53a7fb220e4ab7d9 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 24 Jun 2021 13:14:33 +0900 Subject: [PATCH 14/46] strip LFs of input text --- qiskit_experiments/autodocs/writer.py | 15 +++++++++++---- .../characterization/qubit_spectroscopy.py | 13 +++++++++---- .../interleaved_rb_analysis.py | 7 ++++--- .../randomized_benchmarking/rb_analysis.py | 6 ++++-- .../randomized_benchmarking/rb_experiment.py | 6 ++++-- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/qiskit_experiments/autodocs/writer.py b/qiskit_experiments/autodocs/writer.py index 007924b1f9..114a86f884 100644 --- a/qiskit_experiments/autodocs/writer.py +++ b/qiskit_experiments/autodocs/writer.py @@ -54,13 +54,12 @@ def write_options_as_args(self, fields: typing.Dict[str, OptionsField]): """ def _write_field(_arg_name, _field): - arg_str_type = f":py:obj:`{_parse_annotation(_field.annotation)}`" + self.docstring += self.__indent__ self._write_line(f"{_arg_name} ({arg_str_type}):") self._write_multi_line(_field.description, self.__indent__ * 2) default = _field.default if default is not None: - # format representation if isinstance(_field.default, FunctionType): default_str = f":py:func:`~{default.__module__}.{default.__name__}`" else: @@ -102,6 +101,7 @@ def write_options_as_sections( if text_block: self._write_multi_line(text_block, self.__indent__) + self.docstring += os.linesep for arg_name, field in fields.items(): if field.is_extra: @@ -182,14 +182,21 @@ def write_tutorial_link(self, link: str): def _write_multi_line(self, text_block: str, indent: typing.Optional[str] = None): """A util method to write multi line text with indentation.""" - indented_text = "" + text_block = text_block.strip(os.linesep) + for line in text_block.split(os.linesep): + indented_text = "" if len(line) > 0 and indent is not None: indented_text += indent - self._write_line(line.rstrip()) + # for C0303 trailing whitespace + indented_text += line.rstrip() + + self._write_line(indented_text) def _write_line(self, text: str): """A helper function to write single line.""" + text = text.strip(os.linesep) + self.docstring += text.rstrip() + os.linesep diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index ee5c040521..a932bfaaeb 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -45,9 +45,12 @@ class SpectroscopyAnalysis(CurveAnalysis): """Spectroscopy analysis.""" - __doc_overview__ = """This analysis uses Gaussian function to find a peak. + __doc_overview__ = """ +This analysis uses Gaussian function to find a peak. + Note that this analysis assumes only single peak. -If multiple peaks exist, you'll get a poor chi squared value.""" +If multiple peaks exist, you'll get a poor chi squared value. +""" __doc_equations__ = [r"F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b"] @@ -192,7 +195,8 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR class QubitSpectroscopy(BaseExperiment): """Class that runs spectroscopy by sweeping the qubit frequency.""" - __doc_overview__ = """The circuits produced by spectroscopy, i.e. + __doc_overview__ = """ +The circuits produced by spectroscopy, i.e. .. parsed-literal:: @@ -207,7 +211,8 @@ class QubitSpectroscopy(BaseExperiment): pulse. A list of circuits is generated, each with a different frequency "freq". A spectroscopy experiment run by setting the frequency of the qubit drive. -The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time.""" +The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. +""" __analysis_class__ = SpectroscopyAnalysis diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index bb17e3161a..ae7f83981b 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -37,8 +37,8 @@ class InterleavedRBAnalysis(RBAnalysis): """Interleaved randomized benchmarking analysis.""" - __doc_overview__ = r"""This analysis takes two series for standard and -interleaved RB curve fitting. + __doc_overview__ = r""" +This analysis takes two series for standard and interleaved RB curve fitting. From the fit :math:`\alpha` and :math:`\alpha_c` value this analysis estimates the error per Clifford (EPC) of interleaved gate. @@ -63,7 +63,8 @@ class InterleavedRBAnalysis(RBAnalysis): \end{array} \right. -See [1] for more details.""" +See [1] for more details. +""" __doc_equations__ = [ r"F_1(x_1) = a \alpha^{x_1} + b", diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index f4f97846eb..7f9b968dae 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -39,10 +39,12 @@ class RBAnalysis(CurveAnalysis): """Randomized benchmarking analysis.""" - __doc_overview__ = r"""This analysis takes only single series. + __doc_overview__ = r""" +This analysis takes only single series. This curve is fit by the exponential decay function. -From the fit :math:`\alpha` value this analysis estimates the error per Clifford (EPC).""" +From the fit :math:`\alpha` value this analysis estimates the error per Clifford (EPC). +""" __doc_equations__ = [r"F(x) = a \alpha^x + b"] diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index c836a19dfe..ee477784a9 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -38,7 +38,8 @@ class RBExperiment(BaseExperiment): """Randomized benchmarking.""" - __doc_overview__ = """A randomized benchmarking (RB) is a scalable and robust algorithm + __doc_overview__ = """ +A randomized benchmarking (RB) is a scalable and robust algorithm for benchmarking the full set of Clifford gates by a single parameter using the randomization technique [1]. @@ -47,7 +48,8 @@ class RBExperiment(BaseExperiment): initial state. Averaging over K random realizations of the sequence, we can find the averaged sequence fidelity, -or error per Clifford (EPC).""" +or error per Clifford (EPC). +""" __doc_tutorial__ = "https://github.com/Qiskit/qiskit-experiments/blob/main/docs/tutorials/\ rb_example.ipynb" From 402c1237d0f5d35160376179142b03ed77726201 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 24 Jun 2021 13:21:46 +0900 Subject: [PATCH 15/46] update irb docstring --- .../randomized_benchmarking/interleaved_rb_experiment.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py index def8e26a2d..38e0a1332d 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py @@ -32,6 +32,12 @@ class InterleavedRBExperiment(RBExperiment): """Interleaved RB Experiment class""" + __doc_overview__ = None + + __doc_references__ = None + + __doc_tutorial__ = None + # Analysis class for experiment __analysis_class__ = InterleavedRBAnalysis From 60de870f1b9f66808f76536d8171e97c53985fd5 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 24 Jun 2021 13:25:44 +0900 Subject: [PATCH 16/46] ignore lint --- qiskit_experiments/analysis/curve_analysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 3f16e755d7..24675bd9c0 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -246,7 +246,8 @@ def __init__(self): # Add expected options to instance variable so that every method can access to. default_options = self._default_options() - if not isinstance(default_options, dict): + if isinstance(default_options, Options): + # pylint: disable=no-member default_options = default_options.__dict__ for key in default_options: From a1acf25fc9f535e7cb323abe3597339be75e4513 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 24 Jun 2021 14:37:15 +0900 Subject: [PATCH 17/46] unittest --- qiskit_experiments/analysis/curve_analysis.py | 7 +- qiskit_experiments/autodocs/analysis_docs.py | 31 ++- .../autodocs/experiment_docs.py | 41 ++- .../autodocs/set_option_docs.py | 41 +-- qiskit_experiments/autodocs/writer.py | 9 + test/test_autodocs.py | 252 ++++++++++++++++++ 6 files changed, 336 insertions(+), 45 deletions(-) create mode 100644 test/test_autodocs.py diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 24675bd9c0..41f8425703 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -260,7 +260,9 @@ def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: "curve_fitter": OptionsField( default=multi_curve_fit, annotation=Callable, - description="""A callback function to perform fitting with formatted data. + description="""\ +A callback function to perform fitting with formatted data. + This function should have signature: .. code-block:: @@ -281,7 +283,8 @@ def curve_fitter( "data_processor": OptionsField( default=probability(outcome="1"), annotation=Callable, - description="""A callback function to format experiment data. + description="""\ +A callback function to format experiment data. This function should have signature: .. code-block:: diff --git a/qiskit_experiments/autodocs/analysis_docs.py b/qiskit_experiments/autodocs/analysis_docs.py index 05fac2a5cd..34707d7352 100644 --- a/qiskit_experiments/autodocs/analysis_docs.py +++ b/qiskit_experiments/autodocs/analysis_docs.py @@ -17,6 +17,7 @@ from typing import Optional, Dict, List, Type from qiskit.exceptions import QiskitError +from qiskit.providers import Options from .descriptions import OptionsField, Reference, CurveFitParameter from .writer import _DocstringWriter, _CurveFitDocstringWriter, _DocstringMaker @@ -45,11 +46,16 @@ def make_docstring( writer.write_section(overview, "Overview") if example: writer.write_example(example) - writer.write_lines( - "Default class options. These options are automatically set " - "when :py:meth:`run` method is called." - ) - writer.write_options_as_sections(default_options, "Default Options") + if not isinstance(default_options, Options): + writer.write_lines( + "Default class options. These options are automatically set " + "when :py:meth:`run` method is called." + ) + writer.write_options_as_sections(default_options, "Default Options") + else: + writer.write_lines( + "Documentation for default options are not provided for this class." + ) if references: writer.write_references(references) if note: @@ -108,11 +114,16 @@ def make_docstring( writer.write_bounds(fit_params) if example: writer.write_example(example) - writer.write_lines( - "Default class options. These options are automatically set " - "when :py:meth:`run` method is called." - ) - writer.write_options_as_sections(default_options, "Default Options") + if not isinstance(default_options, Options): + writer.write_lines( + "Default class options. These options are automatically set " + "when :py:meth:`run` method is called." + ) + writer.write_options_as_sections(default_options, "Default Options") + else: + writer.write_lines( + "Documentation for default options are not provided for this class." + ) if references: writer.write_references(references) if note: diff --git a/qiskit_experiments/autodocs/experiment_docs.py b/qiskit_experiments/autodocs/experiment_docs.py index 56610436ad..68d79b0829 100644 --- a/qiskit_experiments/autodocs/experiment_docs.py +++ b/qiskit_experiments/autodocs/experiment_docs.py @@ -17,6 +17,7 @@ from typing import Optional, Dict, List, Type from qiskit.exceptions import QiskitError +from qiskit.providers import Options from .descriptions import OptionsField, Reference from .writer import _DocstringWriter, _DocstringMaker @@ -52,20 +53,32 @@ def make_docstring( writer.write_lines( "See below configurable experiment options to customize your execution." ) - writer.write_options_as_sections( - fields=experiment_options, - section="Experiment Options", - text_block="Experiment options to generate circuits. " - "Options can be updated with :py:meth:`set_experiment_options`. " - "See method documentation for details.", - ) - writer.write_options_as_sections( - fields=analysis_options, - section="Analysis Options", - text_block="Analysis options to run the analysis class. " - "Options can be updated with :py:meth:`set_analysis_options`. " - "See method documentation for details.", - ) + if not isinstance(experiment_options, Options): + writer.write_options_as_sections( + fields=experiment_options, + section="Experiment Options", + text_block="Experiment options to generate circuits. " + "Options can be updated with :py:meth:`set_experiment_options`. " + "See method documentation for details.", + ) + else: + writer.write_dropdown_section( + text_block="Documentation for experiment options are not provided.", + section="Experiment Options", + ) + if not isinstance(analysis_options, Options): + writer.write_options_as_sections( + fields=analysis_options, + section="Analysis Options", + text_block="Analysis options to run the analysis class. " + "Options can be updated with :py:meth:`set_analysis_options`. " + "See method documentation for details.", + ) + else: + writer.write_dropdown_section( + text_block="Documentation for analysis options are not provided.", + section="Analysis Options", + ) if references: writer.write_references(references) if note: diff --git a/qiskit_experiments/autodocs/set_option_docs.py b/qiskit_experiments/autodocs/set_option_docs.py index 22bc2f5116..2be8633631 100644 --- a/qiskit_experiments/autodocs/set_option_docs.py +++ b/qiskit_experiments/autodocs/set_option_docs.py @@ -17,6 +17,7 @@ from typing import Optional, Dict, Any, Type from qiskit.exceptions import QiskitError +from qiskit.providers import Options from .descriptions import OptionsField from .writer import _DocstringWriter, _DocstringMaker @@ -99,27 +100,29 @@ def decorator(experiment: "BaseExperiment"): experiment_options = experiment._default_experiment_options() # update analysis options setter - analysis_setter = _copy_method(experiment, "set_analysis_options") - analysis_setter.__annotations__ = _compile_annotations(analysis_options) - analysis_setter.__doc__ = style.make_docstring( - description="Set the analysis options for :py:meth:`run_analysis` method.", - options=analysis_options, - note="Here you can set arbitrary parameter, even if it is not listed. " - "Such option is passed as a keyword argument to the analysis fitter functions " - "(if exist). The execution may fail if the function API doesn't support " - "extra keyword arguments.", - ) - setattr(experiment, "set_analysis_options", analysis_setter) + if not isinstance(analysis_options, Options): + analysis_setter = _copy_method(experiment, "set_analysis_options") + analysis_setter.__annotations__ = _compile_annotations(analysis_options) + analysis_setter.__doc__ = style.make_docstring( + description="Set the analysis options for :py:meth:`run_analysis` method.", + options=analysis_options, + note="Here you can set arbitrary parameter, even if it is not listed. " + "Such option is passed as a keyword argument to the analysis fitter functions " + "(if exist). The execution may fail if the function API doesn't support " + "extra keyword arguments.", + ) + setattr(experiment, "set_analysis_options", analysis_setter) # update experiment options setter - experiment_setter = _copy_method(experiment, "set_experiment_options") - experiment_setter.__annotations__ = _compile_annotations(experiment_options) - experiment_setter.__doc__ = style.make_docstring( - description="Set the analysis options for :py:meth:`run` method.", - options=experiment_options, - raises={"AttributeError": "If the field passed in is not a supported options."}, - ) - setattr(experiment, "set_experiment_options", experiment_setter) + if not isinstance(experiment_options, Options): + experiment_setter = _copy_method(experiment, "set_experiment_options") + experiment_setter.__annotations__ = _compile_annotations(experiment_options) + experiment_setter.__doc__ = style.make_docstring( + description="Set the analysis options for :py:meth:`run` method.", + options=experiment_options, + raises={"AttributeError": "If the field passed in is not a supported options."}, + ) + setattr(experiment, "set_experiment_options", experiment_setter) return experiment diff --git a/qiskit_experiments/autodocs/writer.py b/qiskit_experiments/autodocs/writer.py index 114a86f884..f37bbd6f19 100644 --- a/qiskit_experiments/autodocs/writer.py +++ b/qiskit_experiments/autodocs/writer.py @@ -138,6 +138,15 @@ def write_section(self, text_block: str, section: str): self._write_multi_line(text_block, self.__indent__) self.docstring += os.linesep + def write_dropdown_section(self, text_block: str, section: str): + """Write new user defined section as drop down box.""" + self._write_line(f".. dropdown:: {section}") + self.docstring += self.__indent__ + self._write_line(":animate: fade-in-slide-down") + self.docstring += os.linesep + self._write_multi_line(text_block, self.__indent__) + self.docstring += os.linesep + def write_note(self, text_block: str): """Write note.""" self._write_line("Note:") diff --git a/test/test_autodocs.py b/test/test_autodocs.py new file mode 100644 index 0000000000..4972cc38f4 --- /dev/null +++ b/test/test_autodocs.py @@ -0,0 +1,252 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test version string generation.""" + +import typing + +from qiskit.test import QiskitTestCase + +from qiskit_experiments.autodocs import ( + Reference, + OptionsField, + standard_experiment_documentation, + standard_options_documentation, +) +from qiskit_experiments.base_analysis import BaseAnalysis +from qiskit_experiments.base_experiment import BaseExperiment + + +# +# Note that this mock class shows how to write docstring with this framework +# + + +class MockExperiment(BaseExperiment): + """Very simple description of class. You can write details in the overview section.""" + + # this is recommended to fill + __doc_overview__ = """ +Test auto documentation of overview section. + +This can be multi-line. +Line feeds in front and end of the text block are removed. + + +""" + + # if the experiment options are complicated perhaps you can show example codes + __doc_example__ = """ +This is the place to show example code. + +For example: + +.. code-block:: + + from qiskit_experiments import MyExperiment + + exp = MyExperiment(**options).run(my_backend) + +You can write arbitrary code block or jupyter execute here. +""" + + # this is optional + __doc_note__ = """ +This is a place to write some notification to users. + +This appears as bounded box in the documentation. +""" + + # this is optional + __doc_warning__ = """ +This is a place to write warnings if exist. +This block will appear at the top of the documentation. + +For example, if the module is still under active development, or being deprecated, +you can communicate these things with users by using this section. +""" + + # this is recommended to fill if exist + __doc_tutorial__ = "https://my_tutorial.com/this_is_my_experiment" + + # this is recommended to fill if exist + # the article number is automatically generated by this list index + 1 + # so you can cite this number as, for example, [1] from the overview section + # The fields of Reference instance are all optional but you should fill at least one + __doc_references__ = [ + Reference( + title="My scientific paper title or title of website.", + authors="Author names.", + journal_info="Qiskit Experiment publications (2021)", + open_access_link="https://my_journal_website.com/this_is_my_journal_page", + ) + ] + + __analysis_class__ = BaseAnalysis + + @classmethod + def _default_experiment_options(cls): + return { + "option1": OptionsField( + description="This is option1.", + annotation=typing.Union[str, int], + default=5, + ), + "option2": OptionsField( + description="""\ +This can be multi line text block if we need to show code block. + +The first line should be without line feed, otherwise empty sentence +will be shown in the option summary in class docstring. +Full text is only shown in the set method docstring. + +.. code-block: + + option2 = {"sub_field1": 1, "sub_field2": 2} + +You can show code block like this.""", + annotation=typing.Dict[str, typing.Any], + default={"sub_field1": 10, "sub_field2": 20}, + ), + "option3": OptionsField( + description="If you want show a long text without line feed, " + "you can split the text like this.", + annotation=BaseExperiment, + default=None, + ), + } + + def circuits(self, backend=None): + pass + + +class TestAutodocs(QiskitTestCase): + """Test for auto documentation.""" + + def test_class_docstring(self): + """Test if class docstring is correctly generated.""" + # pylint: disable=invalid-name + self.maxDiff = None + + DocumentedExperiment = standard_experiment_documentation(MockExperiment) + ref_docs = """\ +Very simple description of class. You can write details in the overview section. + +Warning: + This is a place to write warnings if exist. + This block will appear at the top of the documentation. + + For example, if the module is still under active development, or being deprecated, + you can communicate these things with users by using this section. + +Overview + Test auto documentation of overview section. + + This can be multi-line. + Line feeds in front and end of the text block are removed. + +Example: + This is the place to show example code. + + For example: + + .. code-block:: + + from qiskit_experiments import MyExperiment + + exp = MyExperiment(**options).run(my_backend) + + You can write arbitrary code block or jupyter execute here. + +This experiment uses following analysis class. + +Analysis Class Reference + :py:class:`~qiskit_experiments.base_analysis.BaseAnalysis` + +See below configurable experiment options to customize your execution. + +.. dropdown:: Experiment Options + :animate: fade-in-slide-down + + Experiment options to generate circuits. Options can be updated with :py:meth:`set_experiment_options`. See method documentation for details. + + - **option1** (:py:obj:`Union[str, int]`): This is option1. + - **option2** (:py:obj:`Dict[str, Any]`): This can be multi line text block if we need to show code block. + - **option3** (:py:obj:`:py:class:`~qiskit_experiments.base_experiment.BaseExperiment``): If you want show a long text without line feed, you can split the text like this. + +.. dropdown:: Analysis Options + :animate: fade-in-slide-down + + Documentation for analysis options are not provided. + +References: + - [1] Author names., `My scientific paper title or title of website.`, Qiskit Experiment publications (2021), `open access `_ + +Note: + This is a place to write some notification to users. + + This appears as bounded box in the documentation. + +See Also: + - `Qiskit Experiment Tutorial `_ + +""" + + self.assertEqual(ref_docs, DocumentedExperiment.__doc__) + + def test_option_method_docstring(self): + """Test if method docstring and annotations are correctly generated.""" + # pylint: disable=invalid-name + self.maxDiff = None + + DocumentedExperiment = standard_options_documentation(MockExperiment) + + ref_docs_experiment_options = """\ +Set the analysis options for :py:meth:`run` method. + +Parameters: + option1 (:py:obj:`Union[str, int]`): + This is option1. + (Default: :py:obj:`5`) + option2 (:py:obj:`Dict[str, Any]`): + This can be multi line text block if we need to show code block. + + The first line should be without line feed, otherwise empty sentence + will be shown in the option summary in class docstring. + Full text is only shown in the set method docstring. + + .. code-block: + + option2 = {"sub_field1": 1, "sub_field2": 2} + + You can show code block like this. + (Default: :py:obj:`{'sub_field1': 10, 'sub_field2': 20}`) + option3 (:py:obj:`:py:class:`~qiskit_experiments.base_experiment.BaseExperiment``): + If you want show a long text without line feed, you can split the text like this. + +Raises: + AttributeError: If the field passed in is not a supported options. + +""" + + self.assertEqual( + DocumentedExperiment.set_experiment_options.__doc__, ref_docs_experiment_options + ) + + ref_annotation = { + "option1": typing.Union[str, int], + "option2": typing.Dict[str, typing.Any], + "option3": BaseExperiment, + } + self.assertDictEqual( + DocumentedExperiment.set_experiment_options.__annotations__, ref_annotation + ) From 4d6490e4e8919af182e8275afd8c5b4a357f38ee Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:13:15 +0900 Subject: [PATCH 18/46] Update qiskit_experiments/analysis/curve_analysis.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/analysis/curve_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 41f8425703..eacba2f6d8 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -284,7 +284,7 @@ def curve_fitter( default=probability(outcome="1"), annotation=Callable, description="""\ -A callback function to format experiment data. +A callback function to extract and process experiment data making it fit-ready. This function should have signature: .. code-block:: From dfb350f9aa4d67faf1d47c1a09c717463e35ad65 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:19:42 +0900 Subject: [PATCH 19/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index a932bfaaeb..c8f19889d8 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -46,7 +46,7 @@ class SpectroscopyAnalysis(CurveAnalysis): """Spectroscopy analysis.""" __doc_overview__ = """ -This analysis uses Gaussian function to find a peak. +This analysis uses a Gaussian function to find a peak. Note that this analysis assumes only single peak. If multiple peaks exist, you'll get a poor chi squared value. From 96b0e81f54c6efd51b313cc73062e68b8c93f00f Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:20:05 +0900 Subject: [PATCH 20/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index c8f19889d8..e519d09dcd 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -48,8 +48,8 @@ class SpectroscopyAnalysis(CurveAnalysis): __doc_overview__ = """ This analysis uses a Gaussian function to find a peak. -Note that this analysis assumes only single peak. -If multiple peaks exist, you'll get a poor chi squared value. +Note that this analysis assumes only a single peak. +If multiple peaks exist, you will get a poor reduced chi squared value. """ __doc_equations__ = [r"F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b"] From 97236e60aaec64e22d07773de232dc69bfbd2cca Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:20:16 +0900 Subject: [PATCH 21/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index e519d09dcd..85d6130f9d 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -59,7 +59,7 @@ class SpectroscopyAnalysis(CurveAnalysis): name="a", description="Base line.", initial_guess="The maximum signal value with removed baseline.", - bounds="[-2, 2] scaled with maximum signal value.", + bounds="[-2, 2] scaled to the maximum absolute signal value.", ), CurveFitParameter( name="b", From 94f731e621b27d80b42c993681d42ef42a65a57f Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:20:26 +0900 Subject: [PATCH 22/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 85d6130f9d..6556996619 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -65,7 +65,7 @@ class SpectroscopyAnalysis(CurveAnalysis): name="b", description="Peak height.", initial_guess="A median value of the signal.", - bounds="[-1, 1] scaled with maximum signal value.", + bounds="[-1, 1] scaled to the maximum absolute signal value.", ), CurveFitParameter( name="f", From af6437a0d1cdbd25ef7140dc7e19a5ad12a81a14 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:20:40 +0900 Subject: [PATCH 23/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 6556996619..53c35b5f7f 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -69,7 +69,7 @@ class SpectroscopyAnalysis(CurveAnalysis): ), CurveFitParameter( name="f", - description="Center frequency. This is the fit parameter of main interest.", + description="Center frequency. This is the fit parameter of interest.", initial_guess="A frequency value at the peak (maximum signal).", bounds="[min(x), max(x)] of frequency scan range.", ), From 8e1645a486b8419c088f522c2b8a3245eff84ef3 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:20:57 +0900 Subject: [PATCH 24/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 53c35b5f7f..e6c8a14e88 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -70,7 +70,7 @@ class SpectroscopyAnalysis(CurveAnalysis): CurveFitParameter( name="f", description="Center frequency. This is the fit parameter of interest.", - initial_guess="A frequency value at the peak (maximum signal).", + initial_guess="The frequency value for which the absolute signal is maximum.", bounds="[min(x), max(x)] of frequency scan range.", ), CurveFitParameter( From b7e4a7d6d3b3e4760e5d9ce3f7352ff66024fc68 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:21:07 +0900 Subject: [PATCH 25/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index e6c8a14e88..f620974970 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -78,7 +78,7 @@ class SpectroscopyAnalysis(CurveAnalysis): description="Standard deviation of Gaussian function.", initial_guess=r"Calculated from FWHM of peak :math:`w` such that " r":math:`w / \sqrt{8} \ln{2}`.", - bounds=r"[0, :math:`\Delta x`] where :math:`\Delta x` represents frequency scan range.", + bounds=r"[0, :math:`\Delta x`] where :math:`\Delta x` represents the scanned frequency range.", ), ] From 166dec5d531184b05649033f584e76aef2cec3b7 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:21:23 +0900 Subject: [PATCH 26/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index f620974970..d7f4dbaa3e 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -234,8 +234,8 @@ def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: "amp": OptionsField( default=0.1, annotation=float, - description="Amplitude of spectroscopy pulse. Usually weak power pulse is used to " - "suppress broadening of observed peaks.", + description="The amplitude of the spectroscopy pulse. Usually a pulse with a weak power is used to " + "suppress any broadening in the observed peaks.", ), "duration": OptionsField( default=1024, From 310dc1e74a1b0f6e0f57a46acdeaf941063b6120 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:21:46 +0900 Subject: [PATCH 27/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index d7f4dbaa3e..34c32b6be8 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -240,8 +240,8 @@ def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: "duration": OptionsField( default=1024, annotation=int, - description="Duration of spectroscopy pulse. This may need to satisfy the " - "hardware waveform memory constraint. " + description="The duration of the spectroscopy pulse which must satisfy any " + "hardware waveform memory constraints. " "The default value is represented in units of dt.", ), "sigma": OptionsField( From ee553b83e975c43736c14b4fa48846d96ff0de52 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:22:18 +0900 Subject: [PATCH 28/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 34c32b6be8..4e0e7bb1ad 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -247,9 +247,7 @@ def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: "sigma": OptionsField( default=256, annotation=Union[int, float], - description="Sigma of Gaussian rising and falling edges. This value should be " - "sufficiently smaller than the duration, " - "otherwise waveform is distorted. " + description="Sigma of Gaussian rising and falling edges." "The default value is represented in units of dt.", ), "width": OptionsField( From 482b43bcd4df7b07676dd403a33ea692df26a7e0 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 30 Jun 2021 16:22:27 +0900 Subject: [PATCH 29/46] Update qiskit_experiments/characterization/qubit_spectroscopy.py Co-authored-by: Daniel Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/characterization/qubit_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 4e0e7bb1ad..0713d5ab8c 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -265,7 +265,7 @@ def __init__( unit: Optional[str] = "Hz", absolute: bool = True, ): - """Create new experiment. + """Create a new experiment. Args: qubit: The qubit on which to run spectroscopy. From 8b1ac231ad2dceee14a1505af8db5152e07ca840 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 14 Jul 2021 04:15:01 +0900 Subject: [PATCH 30/46] Sphinx autodoc extension for documentation --- docs/_ext/autodoc_analysis.py | 69 ++++ docs/_ext/autodoc_experiment.py | 81 +++++ docs/_ext/autoref.py | 105 ++++++ docs/_templates/autosummary/analysis.rst | 49 +++ docs/_templates/autosummary/experiment.rst | 49 +++ docs/conf.py | 12 +- qiskit_experiments/analysis/curve_analysis.py | 129 ++------ .../characterization/__init__.py | 20 +- .../characterization/qubit_spectroscopy.py | 207 ++++++------ qiskit_experiments/documentation/__init__.py | 21 ++ .../documentation/example/__init__.py | 54 ++++ .../example/example_experiment.py | 183 +++++++++++ qiskit_experiments/documentation/formatter.py | 177 ++++++++++ .../documentation/section_parsers.py | 91 ++++++ qiskit_experiments/documentation/styles.py | 304 ++++++++++++++++++ qiskit_experiments/documentation/utils.py | 148 +++++++++ .../interleaved_rb_analysis.py | 6 +- .../randomized_benchmarking/rb_analysis.py | 11 +- requirements-dev.txt | 1 + 19 files changed, 1510 insertions(+), 207 deletions(-) create mode 100644 docs/_ext/autodoc_analysis.py create mode 100644 docs/_ext/autodoc_experiment.py create mode 100644 docs/_ext/autoref.py create mode 100644 docs/_templates/autosummary/analysis.rst create mode 100644 docs/_templates/autosummary/experiment.rst create mode 100644 qiskit_experiments/documentation/__init__.py create mode 100644 qiskit_experiments/documentation/example/__init__.py create mode 100644 qiskit_experiments/documentation/example/example_experiment.py create mode 100644 qiskit_experiments/documentation/formatter.py create mode 100644 qiskit_experiments/documentation/section_parsers.py create mode 100644 qiskit_experiments/documentation/styles.py create mode 100644 qiskit_experiments/documentation/utils.py diff --git a/docs/_ext/autodoc_analysis.py b/docs/_ext/autodoc_analysis.py new file mode 100644 index 0000000000..3170bba450 --- /dev/null +++ b/docs/_ext/autodoc_analysis.py @@ -0,0 +1,69 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Documentation extension for analysis class. +""" + + +from typing import Any + +from sphinx.application import Sphinx +from sphinx.ext.autodoc import ClassDocumenter + +from qiskit_experiments import BaseAnalysis +from qiskit_experiments.documentation.styles import AnalysisDocstring + + +class AnalysisDocumenter(ClassDocumenter): + """Sphinx extension for the custom documentation of the standard analysis class.""" + + objtype = "analysis" + directivetype = 'class' + priority = 10 + ClassDocumenter.priority + option_spec = dict(ClassDocumenter.option_spec) + + @classmethod + def can_document_member( + cls, member: Any, membername: str, isattr: bool, parent: Any + ) -> bool: + return isinstance(member, BaseAnalysis) + + def add_content(self, more_content: Any, no_docstring: bool = False) -> None: + sourcename = self.get_sourcename() + + # analysis class doesn't have explicit init method. + class_doc = self.get_doc()[0] + + # format experiment documentation into the analysis style + class_doc_parser = AnalysisDocstring( + target_cls=self.object, + docstring_lines=class_doc, + config=self.env.app.config, + indent=self.content_indent, + ) + + # write introduction + for i, line in enumerate(self.process_doc(class_doc_parser.generate_class_docs())): + self.add_line(line, sourcename, i) + self.add_line("", sourcename) + + # method and attributes + if more_content: + for line, src in zip(more_content.data, more_content.items): + self.add_line(line, src[0], src[1]) + + +def setup(app: Sphinx): + existing_documenter = app.registry.documenters.get(AnalysisDocumenter.objtype) + if existing_documenter is None or not issubclass(existing_documenter, AnalysisDocumenter): + app.add_autodocumenter(AnalysisDocumenter, override=True) diff --git a/docs/_ext/autodoc_experiment.py b/docs/_ext/autodoc_experiment.py new file mode 100644 index 0000000000..455238fc9a --- /dev/null +++ b/docs/_ext/autodoc_experiment.py @@ -0,0 +1,81 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Documentation extension for experiment class. +""" + +from typing import Any + +from qiskit.exceptions import QiskitError +from sphinx.application import Sphinx +from sphinx.ext.autodoc import ClassDocumenter + +from qiskit_experiments import BaseExperiment +from qiskit_experiments.documentation.styles import ExperimentDocstring + + +class ExperimentDocumenter(ClassDocumenter): + """Sphinx extension for the custom documentation of the standard experiment class.""" + + objtype = "experiment" + directivetype = 'class' + priority = 10 + ClassDocumenter.priority + option_spec = dict(ClassDocumenter.option_spec) + + @classmethod + def can_document_member( + cls, member: Any, membername: str, isattr: bool, parent: Any + ) -> bool: + return isinstance(member, BaseExperiment) + + def add_content(self, more_content: Any, no_docstring: bool = False) -> None: + sourcename = self.get_sourcename() + + try: + class_doc, init_doc = self.get_doc() + except ValueError: + raise QiskitError( + f"Documentation of {self.name} doesn't match with the expected format." + "Please run sphinx build without using the experiment template." + ) + + # format experiment documentation into the experiment style + class_doc_parser = ExperimentDocstring( + target_cls=self.object, + docstring_lines=class_doc, + config=self.env.app.config, + indent=self.content_indent, + ) + + # write introduction + for i, line in enumerate(self.process_doc(class_doc_parser.generate_class_docs())): + self.add_line(line, sourcename, i) + self.add_line("", sourcename) + + # write init method documentation + self.add_line(".. rubric:: Initialization", sourcename) + self.add_line("", sourcename) + for i, line in enumerate(self.process_doc([init_doc])): + self.add_line(line, sourcename, i) + self.add_line("", sourcename) + + # method and attributes + if more_content: + for line, src in zip(more_content.data, more_content.items): + self.add_line(line, src[0], src[1]) + + +def setup(app: Sphinx): + existing_documenter = app.registry.documenters.get(ExperimentDocumenter.objtype) + if existing_documenter is None or not issubclass(existing_documenter, ExperimentDocumenter): + app.add_autodocumenter(ExperimentDocumenter, override=True) diff --git a/docs/_ext/autoref.py b/docs/_ext/autoref.py new file mode 100644 index 0000000000..8e12611533 --- /dev/null +++ b/docs/_ext/autoref.py @@ -0,0 +1,105 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Helper directive to generate reference in convenient form. +""" +import arxiv + +from docutils import nodes +from docutils.parsers.rst import Directive +from sphinx.application import Sphinx + + +class WebSite(Directive): + """A custom helper directive for showing website link. + + This can be used, for example, + + .. code-block:: + + .. ref_website:: qiskit-experiments, https://github.com/Qiskit/qiskit-experiments + + """ + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + + def run(self): + try: + name, url = self.arguments[0].split(",") + except ValueError: + raise ValueError( + f"{self.arguments[0]} is invalid website directive format. " + "Name and URL should be separated by a single comma." + ) + + link_name = nodes.paragraph(text=f"{name} ") + link_name += nodes.reference(text="(open)", refuri=url) + + return [link_name] + + +class Arxiv(Directive): + """A custom helper directive for generating journal information from ArXiv id. + + This directive takes two arguments + + - Arbitrary reference name (no white space should be included) + + - ArXiv ID + + This can be used, for example, + + .. code-block:: + + .. ref_arxiv:: qasm3-paper 2104.14722 + + If an article is not found, no journal information will be shown. + + """ + required_arguments = 2 + optional_arguments = 0 + final_argument_whitespace = False + + def run(self): + + # search ArXiv database + try: + search = arxiv.Search(id_list=[self.arguments[1]]) + paper = next(search.results()) + except Exception: + return [] + + # generate journal link nodes + ret_node = nodes.paragraph() + + journal = "" + if paper.journal_ref: + journal += f", {paper.journal_ref}, " + if paper.doi: + journal += f"doi: {paper.doi}" + + ret_node += nodes.Text(f"[{self.arguments[0]}] ") + ret_node += nodes.Text(", ".join([author.name for author in paper.authors]) + ", ") + ret_node += nodes.emphasis(text=f"{paper.title}") + if journal: + ret_node += nodes.Text(journal) + ret_node += nodes.Text(" ") + ret_node += nodes.reference(text="(open)", refuri=paper.pdf_url) + + return [ret_node] + + +def setup(app: Sphinx): + app.add_directive("ref_arxiv", Arxiv) + app.add_directive("ref_website", WebSite) diff --git a/docs/_templates/autosummary/analysis.rst b/docs/_templates/autosummary/analysis.rst new file mode 100644 index 0000000000..222df215af --- /dev/null +++ b/docs/_templates/autosummary/analysis.rst @@ -0,0 +1,49 @@ +{% if referencefile %} +.. include:: {{ referencefile }} +{% endif %} + +{{ objname }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autoanalysis:: {{ objname }} + :no-members: + :no-inherited-members: + :no-special-members: + + {% block attributes_summary %} + {% if attributes %} + + .. rubric:: Attributes + + .. autosummary:: + :toctree: ../stubs/ + {% for item in all_attributes %} + {%- if not item.startswith('_') %} + {{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block methods_summary %} + {% if methods %} + + .. rubric:: Methods + + .. autosummary:: + :toctree: ../stubs/ + {% for item in all_methods %} + {%- if not item.startswith('_') or item in ['__call__', '__mul__', '__getitem__', '__len__'] %} + {{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% for item in inherited_members %} + {%- if item in ['__call__', '__mul__', '__getitem__', '__len__'] %} + {{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + + {% endif %} + {% endblock %} diff --git a/docs/_templates/autosummary/experiment.rst b/docs/_templates/autosummary/experiment.rst new file mode 100644 index 0000000000..01800ea10b --- /dev/null +++ b/docs/_templates/autosummary/experiment.rst @@ -0,0 +1,49 @@ +{% if referencefile %} +.. include:: {{ referencefile }} +{% endif %} + +{{ objname }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autoexperiment:: {{ objname }} + :no-members: + :no-inherited-members: + :no-special-members: + + {% block attributes_summary %} + {% if attributes %} + + .. rubric:: Attributes + + .. autosummary:: + :toctree: ../stubs/ + {% for item in all_attributes %} + {%- if not item.startswith('_') %} + {{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block methods_summary %} + {% if methods %} + + .. rubric:: Methods + + .. autosummary:: + :toctree: ../stubs/ + {% for item in all_methods %} + {%- if not item.startswith('_') or item in ['__call__', '__mul__', '__getitem__', '__len__'] %} + {{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% for item in inherited_members %} + {%- if item in ['__call__', '__mul__', '__getitem__', '__len__'] %} + {{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + + {% endif %} + {% endblock %} diff --git a/docs/conf.py b/docs/conf.py index 79c501e4fd..9fed00b22b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,9 +23,10 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys +sys.path.insert(0, os.path.abspath('.')) +sys.path.append(os.path.abspath("./_ext")) """ Sphinx documentation builder @@ -37,7 +38,7 @@ os.environ['QISKIT_DOCS'] = 'TRUE' # -- Project information ----------------------------------------------------- -project = 'Qiskit ODE Solvers' +project = 'Qiskit Experiments' copyright = '2021, Qiskit Development Team' # pylint: disable=redefined-builtin author = 'Qiskit Development Team' @@ -92,6 +93,9 @@ 'sphinx_panels', 'sphinx.ext.intersphinx', 'nbsphinx', + 'autoref', + 'autodoc_experiment', + 'autodoc_analysis', ] html_static_path = ['_static'] templates_path = ['_templates'] diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 41f8425703..d8afa2b129 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -255,100 +255,41 @@ def __init__(self): @classmethod def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: - """Return default analysis options.""" - options = { - "curve_fitter": OptionsField( - default=multi_curve_fit, - annotation=Callable, - description="""\ -A callback function to perform fitting with formatted data. - -This function should have signature: - -.. code-block:: - - def curve_fitter( - funcs: List[Callable], - series: ndarray, - xdata: ndarray, - ydata: ndarray, - p0: ndarray, - sigma: Optional[ndarray], - weights: Optional[ndarray], - bounds: Optional[Union[Dict[str, Tuple[float, float]], Tuple[ndarray, ndarray]]], - ) -> CurveAnalysisResult: - -See :py:func:`~qiskit_experiment.analysis.multi_curve_fit` for example.""", - ), - "data_processor": OptionsField( - default=probability(outcome="1"), - annotation=Callable, - description="""\ -A callback function to format experiment data. -This function should have signature: - -.. code-block:: - - def data_processor(data: Dict[str, Any]) -> Tuple[float, float] - -This can be a :py:class:`~qiskit_experiment.data_processing.DataProcessor` instance -or any class instance that defines the ``__call__`` method to be a callable.""", - ), - "p0": OptionsField( - default=None, - annotation=Dict[str, float], - description="Dictionary of initial parameters. Keys are parameter names.", - ), - "bounds": OptionsField( - default=None, - annotation=Dict[str, Tuple[float, float]], - description="Dictionary of (min, max) tuple of fit parameter boundaries. " - "Keys are parameter names.", - ), - "x_key": OptionsField( - default="xval", - annotation=str, - description="Circuit metadata key representing a scanned value.", - ), - "plot": OptionsField( - default=True, - annotation=bool, - description="Set ``True`` to create figure for fit result.", - ), - "axis": OptionsField( - default=None, - annotation="matplotlib.axes._subplots.AxesSubplot", - description="Optional. A matplotlib axis object to draw.", - ), - "xlabel": OptionsField( - default=None, - annotation=str, - description="X label of the fit result figure.", - ), - "ylabel": OptionsField( - default=None, - annotation=str, - description="Y label of the fit result figure.", - ), - "ylim": OptionsField( - default=None, - annotation=Tuple[float, float], - description="Y axis limit of the fit result figure.", - ), - "fit_reports": OptionsField( - default=None, - annotation=Dict[str, str], - description="Mapping of fit parameters and representation in the fit report. " - "If nothing specified, fit report will not be shown.", - ), - "return_data_points": OptionsField( - default=False, - annotation=bool, - description="Set ``True`` to return arrays of measured data points.", - ), - } - - return options + """Return default analysis options. + + Analysis Options: + curve_fitter (Callable): A callback function to perform fitting with formatted data. + See :py:func:`~qiskit_experiments.analysis.multi_curve_fit` for example. + data_processor (Callable): A callback function to format experiment data. + This can be a :py:class:`~qiskit_experiments.data_processing.DataProcessor` + instance or any class instance that defines the ``__call__`` method. + p0 (Dict[str, float]): Dictionary of initial parameters. Keys are parameter names. + bounds (Dict[str, Tuple[float, float]]): Dictionary of (min, max) tuple of + fit parameter boundaries. Keys are parameter names. + x_key (str): Circuit metadata key representing a scanned value. + plot (bool): Set ``True`` to create figure for fit result. + axis (AxesSubplot): Optional. A matplotlib axis object to draw. + xlabel (str): X label of the fit result figure. + ylabel (str): Y label of the fit result figure. + ylim (Tuple[float, float]): Y axis limit of the fit result figure. + fit_reports (Dict[str, str]): Mapping of fit parameters and representation + in the fit report. If nothing specified, fit report will not be shown. + return_data_points (bool): Set ``True`` to return arrays of measured data points. + """ + return Options( + curve_fitter=multi_curve_fit, + data_processor=probability(outcome="1"), + p0=None, + bounds=None, + x_key="xval", + plot=True, + axis=None, + xlabel=None, + ylabel=None, + ylim=None, + fit_reports=None, + return_data_points=False, + ) def _create_figures(self, analysis_results: CurveAnalysisResult) -> List["Figure"]: """Create new figures with the fit result and raw data. diff --git a/qiskit_experiments/characterization/__init__.py b/qiskit_experiments/characterization/__init__.py index 3fcb3e7178..b1f2b59ca5 100644 --- a/qiskit_experiments/characterization/__init__.py +++ b/qiskit_experiments/characterization/__init__.py @@ -17,6 +17,24 @@ .. currentmodule:: qiskit_experiments.characterization +Experiments (auto) +================== +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/experiment.rst + + QubitSpectroscopy + + +Analysis (auto) +=============== +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/analysis.rst + + SpectroscopyAnalysis + + Experiments =========== .. autosummary:: @@ -24,7 +42,6 @@ T1Experiment T2StarExperiment - QubitSpectroscopy Analysis @@ -35,7 +52,6 @@ T1Analysis T2StarAnalysis - SpectroscopyAnalysis """ from .t1_experiment import T1Experiment, T1Analysis from .qubit_spectroscopy import QubitSpectroscopy, SpectroscopyAnalysis diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index a932bfaaeb..1b5993fbef 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -41,47 +41,47 @@ from qiskit_experiments.data_processing.processor_library import get_to_signal_processor -@curve_analysis_documentation class SpectroscopyAnalysis(CurveAnalysis): - """Spectroscopy analysis.""" - - __doc_overview__ = """ -This analysis uses Gaussian function to find a peak. - -Note that this analysis assumes only single peak. -If multiple peaks exist, you'll get a poor chi squared value. -""" - - __doc_equations__ = [r"F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b"] - - __doc_fit_params__ = [ - CurveFitParameter( - name="a", - description="Base line.", - initial_guess="The maximum signal value with removed baseline.", - bounds="[-2, 2] scaled with maximum signal value.", - ), - CurveFitParameter( - name="b", - description="Peak height.", - initial_guess="A median value of the signal.", - bounds="[-1, 1] scaled with maximum signal value.", - ), - CurveFitParameter( - name="f", - description="Center frequency. This is the fit parameter of main interest.", - initial_guess="A frequency value at the peak (maximum signal).", - bounds="[min(x), max(x)] of frequency scan range.", - ), - CurveFitParameter( - name=r"\sigma", - description="Standard deviation of Gaussian function.", - initial_guess=r"Calculated from FWHM of peak :math:`w` such that " - r":math:`w / \sqrt{8} \ln{2}`.", - bounds=r"[0, :math:`\Delta x`] where :math:`\Delta x` represents frequency scan range.", - ), - ] + r"""Spectroscopy analysis. + + # section: overview + + This analysis uses Gaussian function to find a peak. + + Note that this analysis assumes only single peak. + If multiple peaks exist, you'll get a poor chi squared value. + + # section: fit_model + + .. math:: + + F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b + + # section: fit_parameters + + defpar a: + desc: Base line. + init_guess: The maximum signal value with removed baseline. + bounds: [-2, 2] scaled with maximum signal value. + defpar b: + desc: Peak height. + init_guess: A median value of the signal. + bounds: [-1, 1] scaled with maximum signal value. + + defpar f: + desc: Center frequency. This is the fit parameter of main interest. + init_guess: A frequency value at the peak (maximum signal). + bounds: [min(x), max(x)] of frequency scan range. + + defpar \sigma: + desc: Standard deviation of Gaussian function. + init_guess: Calculated from FWHM of peak :math:`w` such that + :math:`w / \sqrt{8} \ln{2}`. + bounds: [0, :math:`\Delta x`] where :math:`\Delta x` represents + frequency scan range. + + """ __series__ = [ SeriesDef( fit_func=lambda x, a, sigma, freq, b: fit_function.gaussian( @@ -93,18 +93,17 @@ class SpectroscopyAnalysis(CurveAnalysis): @classmethod def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: - """Return default options.""" + """Default analysis options. + + Analysis Options: + normalization (bool): Set ``True`` to normalize measurement data. Usually applied to + Kerneled (level1) measurement data. + """ default_options = super()._default_options() - default_options["p0"].default = {"a": None, "sigma": None, "freq": None, "b": None} - default_options["bounds"].default = {"a": None, "sigma": None, "freq": None, "b": None} - default_options["fit_reports"].default = {"freq": "frequency"} - - default_options["normalization"] = OptionsField( - default=True, - annotation=bool, - description="Set ``True`` to normalize measurement data. Usually applied to " - "Kerneled (level1) measurement data.", - ) + default_options.p0 = {"a": None, "sigma": None, "freq": None, "b": None} + default_options.bounds = {"a": None, "sigma": None, "freq": None, "b": None} + default_options.fit_reports = {"freq": "frequency"} + default_options.normalization = True return default_options @@ -190,29 +189,51 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR return analysis_result -@standard_experiment_documentation -@standard_options_documentation class QubitSpectroscopy(BaseExperiment): - """Class that runs spectroscopy by sweeping the qubit frequency.""" + """Class that runs spectroscopy by sweeping the qubit frequency. + + # section: overview + + The circuits produced by spectroscopy, i.e. + + .. parsed-literal:: - __doc_overview__ = """ -The circuits produced by spectroscopy, i.e. - -.. parsed-literal:: + ┌────────────┐ ░ ┌─┐ + q_0: ┤ Spec(freq) ├─░─┤M├ + └────────────┘ ░ └╥┘ + measure: 1/══════════════════╩═ + 0 - ┌────────────┐ ░ ┌─┐ - q_0: ┤ Spec(freq) ├─░─┤M├ - └────────────┘ ░ └╥┘ - measure: 1/══════════════════╩═ - 0 + have a spectroscopy pulse-schedule embedded in a spectroscopy gate. The + pulse-schedule consists of a set frequency instruction followed by a GaussianSquare + pulse. A list of circuits is generated, each with a different frequency "freq". -have a spectroscopy pulse-schedule embedded in a spectroscopy gate. The -pulse-schedule consists of a set frequency instruction followed by a GaussianSquare -pulse. A list of circuits is generated, each with a different frequency "freq". + A spectroscopy experiment run by setting the frequency of the qubit drive. + The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. -A spectroscopy experiment run by setting the frequency of the qubit drive. -The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. -""" + # section: warning + + Test warning + + # section: note + + Test note + + # section: example + + Test example + + # section: reference + + .. ref_arxiv:: Alexander2020 2004.06755 + .. ref_arxiv:: Shelly2021 2007.08532 + .. ref_arxiv:: Qasm3 2104.14722 + + # section: tutorial + + .. ref_website:: Qiskit Experiment Tutorial, https://quantum-computing.ibm.com/ + + """ __analysis_class__ = SpectroscopyAnalysis @@ -229,36 +250,26 @@ def _default_run_options(cls) -> Options: @classmethod def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: - """Default option values used for the spectroscopy pulse.""" - return { - "amp": OptionsField( - default=0.1, - annotation=float, - description="Amplitude of spectroscopy pulse. Usually weak power pulse is used to " - "suppress broadening of observed peaks.", - ), - "duration": OptionsField( - default=1024, - annotation=int, - description="Duration of spectroscopy pulse. This may need to satisfy the " - "hardware waveform memory constraint. " - "The default value is represented in units of dt.", - ), - "sigma": OptionsField( - default=256, - annotation=Union[int, float], - description="Sigma of Gaussian rising and falling edges. This value should be " - "sufficiently smaller than the duration, " - "otherwise waveform is distorted. " - "The default value is represented in units of dt.", - ), - "width": OptionsField( - default=0, - annotation=Union[int, float], - description="Width of the flat-top part of the Gaussian square envelope of " - "spectroscopy pulse. Set width=0 to use Gaussian pulse.", - ), - } + """Default experiment options. + + Experiment Options: + amp (float): Amplitude of spectroscopy pulse. Usually weak power pulse is used to + suppress broadening of observed peaks. + duration (int): Duration of spectroscopy pulse. + This may need to satisfy the hardware waveform memory constraint. + The default value is represented in units of dt. + sigma (Union[int, float]): Sigma of Gaussian rising and falling edges. + This value should be sufficiently smaller than the duration, + otherwise waveform is distorted. The default value is represented in units of dt. + width (Union[int, float]) Width of the flat-top part of the Gaussian square + envelope of spectroscopy pulse. Set width=0 to use Gaussian pulse. + """ + return Options( + amp=0.1, + duration=1024, + sigma=256, + width=0, + ) def __init__( self, diff --git a/qiskit_experiments/documentation/__init__.py b/qiskit_experiments/documentation/__init__.py new file mode 100644 index 0000000000..d502194c0f --- /dev/null +++ b/qiskit_experiments/documentation/__init__.py @@ -0,0 +1,21 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Automatic documentation package. + +.. warning:: + + This module uses requirements-dev packages related to Sphinx and journal search. + Thus this should not be imported by the top-level module. + This module is not used except for the Sphinx documentation build. + +""" diff --git a/qiskit_experiments/documentation/example/__init__.py b/qiskit_experiments/documentation/example/__init__.py new file mode 100644 index 0000000000..d2dd855a01 --- /dev/null +++ b/qiskit_experiments/documentation/example/__init__.py @@ -0,0 +1,54 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +======================================================================= +Example Documentation (:mod:`qiskit_experiments.documentation.example`) +======================================================================= + +.. currentmodule:: qiskit_experiments.documentation.example + + +.. warning:: + + This module is just an example for documentation. Do not import. + +.. note:: + + Under the autosummary directive you need to set template to trigger custom documentation. + + +Experiments +=========== +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/experiment.rst + + ExampleDocExperiment + +Analysis +======== +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/analysis.rst + + ExampleDocAnalysis + ExampleDocCurveAnalysis + +""" + +import warnings + +warnings.warn( + "This module dosen't implement actual experiment. " + "This is just a mock module to show the schema of experiment documentation." +) diff --git a/qiskit_experiments/documentation/example/example_experiment.py b/qiskit_experiments/documentation/example/example_experiment.py new file mode 100644 index 0000000000..2ea928a13e --- /dev/null +++ b/qiskit_experiments/documentation/example/example_experiment.py @@ -0,0 +1,183 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +from qiskit.providers import Options + +from qiskit_experiments.analysis import CurveAnalysis +from qiskit_experiments.base_experiment import BaseExperiment + + +class DocumentedCurveAnalysis(CurveAnalysis): + r"""One line summary of this class. This is shown in the top level contains list. + + # section: overview + + Overview of this experiment. It is recommended to write this section. + Here you can explain technical aspect of fit algorithm or fit model. + Standard reStructuredText directives can be used. + No section level indent is necessary. + + You can use following sections + + - ``warning`` + - ``note`` + - ``example`` + - ``reference`` + - ``tutorial`` + + See :class:`DocumentedExperiment` for description of these sections. + In addition to above sections, analysis template provides following extra sections. + + # section: fit_mode: + + Here you can describe your fitting model. + Standard reStructuredText directives can be used. For example: + + .. math:: + + F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b + + enables you to use the Latex syntax to write your equation. + + # section: fit_parameters + + Here you can explain fit parameter details. + This section provides a special syntax to describe details of each parameter. + Documentation except for this syntax will be just ignored. + + defpar a: + desc: Description of parameter :math:`a`. + init_guess: Here you can describe how this analysis estimate initial guess of + parameter :math:`a`. + bounds: Here you can describe how this analysis bounds parameter :math:`a` value + during the fit. + + defpar b: + desc: Description of parameter :math:`b`. + init_guess: Here you can describe how this analysis estimate initial guess of + parameter :math:`b`. + bounds: Here you can describe how this analysis bounds parameter :math:`b` value + during the fit. + + Note that you cannot write text block (i.e. bullet lines, math mode, parsed literal, ...) + in the ``defpar`` syntax items. These are a single line description of parameters. + You can write multiple ``defpar`` block for each fitting parameter. + + It would be nice if parameter names conform to the parameter key values appearing in the + analysis result. For example, if fit model defines the parameter :math:`\sigma` and + this appears as ``eta`` in the result, user cannot find correspondence of these parameters. + + """ + + +class DocumentedExperiment(BaseExperiment): + """One line summary of this class. This is shown in the top level contains list. + + # section: overview + + Overview of this experiment. It is recommended to write this section. + Here you can explain technical aspect of experiment, protocol, etc... + Standard reStructuredText directives can be used. + No section level indent is necessary. + + # section: warning + + Warning about this experiment if exist. + Some functionality is not available or under development, + you should write these details here. + + # section: note + + Notification about this experiment if exist. + + # section: example + + Example code of this experiment. + If this experiment requires user to manage complicated options, + it might be convenient for users to have some code example here. + + You can write code example, for example, as follows + + .. code-block:: python + + import qiskit_experiments + my_experiment = qiskit_experiments.MyExperiment(**options) + + # section: reference + + Currently this supports article reference in ArXiv database. + You can use following helper directive. + + .. ref_arxiv:: Auth2020a 21xx.01xxx + + This directive takes two arguments separated by a whitespace. + The first argument is arbitrary label for this article, which may be used to + refer to this paper from other sections. + Second argument is the ArXiv ID of the paper to refer to. + Once this directive is inserted, Sphinx searches the ArXiv database and + automatically generates a formatted reference sentence with the link to PDF. + + # section: tutorial + + You can refer to the arbitrary web page here. + Following helper directive can be used. + + .. ref_website:: Qiskit Experiment Github, https://github.com/Qiskit/qiskit-experiments + + This directive takes two arguments separated by a comma. + The first argument is arbitrary label shown before the link. Whitespace can be included. + The second argument is the URL of the website to hyperlink. + + """ + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default experiment options. + + .. note:: + + This method documentation should conforms to the below documentation syntax. + Namely, the title should be "Experiment Options" and description should be + written in the Google docstring style. Numpy style is not accepted. + Documentation except for the experiment options will be just ignored, e.g. this note. + If experiment options contains some values from the parent class, + the custom Sphinx parser searches for the parent class method documentation + and automatically generate documentation for all available options. + If there is any missing documentation the Sphinx build will fail. + + Experiment Options: + opt1 (int): Description for the option1. + opt2 (bool): Description for the option2. + opt3 (str): Description for the option3. + + """ + opts = super()._default_experiment_options() + opts.opt1 = 1.0 + opts.opt2 = True + opts.opt3 = "opt3" + + return opts + + def __init__(self, qubit: int): + """Create new experiment. + + .. note:: + + This documentation is shown as-is. + + Args: + qubit: The qubit to run experiment. + """ + super().__init__(qubits=[qubit]) + + def circuits(self, backend = None): + pass diff --git a/qiskit_experiments/documentation/formatter.py b/qiskit_experiments/documentation/formatter.py new file mode 100644 index 0000000000..f1b7fc28a4 --- /dev/null +++ b/qiskit_experiments/documentation/formatter.py @@ -0,0 +1,177 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +A class that formats documentation sections. +""" +from typing import List + + +class DocstringSectionFormatter: + """A class that formats parsed docstring lines. + + This formatter formats sections with Google Style Python Docstrings with + several reStructuredText directives. + """ + + def __init__(self, indent: str): + self.indent = indent + + def format_header(self, lines: List[str]) -> List[str]: + format_lines = lines + format_lines.append("") + + return format_lines + + def format_overview(self, lines: List[str]) -> List[str]: + format_lines = [".. rubric:: Overview", ""] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + def format_reference(self, lines: List[str]) -> List[str]: + format_lines = [".. rubric:: References", ""] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + def format_warning(self, lines: List[str]) -> List[str]: + format_lines = [".. warning::", ""] + for line in lines: + format_lines.append(self.indent + line) + format_lines.append("") + + return format_lines + + def format_example(self, lines: List[str]) -> List[str]: + format_lines = [".. rubric:: Example", ""] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + def format_note(self, lines: List[str]) -> List[str]: + format_lines = [".. note::", ""] + for line in lines: + format_lines.append(self.indent + line) + format_lines.append("") + + return format_lines + + def format_tutorial(self, lines: List[str]) -> List[str]: + format_lines = [".. rubric:: Tutorials", ""] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + +class ExperimentSectionFormatter(DocstringSectionFormatter): + """Formatter for experiment class.""" + + def format_analysis_ref(self, lines: List[str]) -> List[str]: + format_lines = [".. rubric:: Analysis Class Reference", ""] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + def format_experiment_opts(self, lines: List[str]) -> List[str]: + format_lines = [ + ".. rubric:: Experiment Options", + "", + "These options can be set by :py:meth:`set_experiment_options` method.", + "", + ] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + def format_analysis_opts(self, lines: List[str]) -> List[str]: + format_lines = [ + ".. rubric:: Analysis Options", + "", + "These options can be set by :py:meth:`set_analysis_options` method.", + "", + ] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + def format_transpiler_opts(self, lines: List[str]) -> List[str]: + format_lines = [ + ".. rubric:: Transpiler Options", + "", + "This option can be set by :py:meth:`set_transpile_options` method.", + "", + ] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + def format_run_opts(self, lines: List[str]) -> List[str]: + format_lines = [ + ".. rubric:: Backend Run Options", + "", + "This option can be set by :py:meth:`set_run_options` method.", + "", + ] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + +class AnalysisSectionFormatter(DocstringSectionFormatter): + """Formatter for analysis class.""" + + def format_analysis_opts(self, lines: List[str]) -> List[str]: + format_lines = [ + ".. rubric:: Run Options", + "", + "These are the keyword arguments of :py:meth:`run` method.", + "", + ] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + def format_fit_model(self, lines: List[str]) -> List[str]: + format_lines = [ + ".. rubric:: Fit Model", + "", + "This is the curve fitting analysis. ", + "Following equation(s) are used to represent curve(s).", + "", + ] + format_lines.extend(lines) + format_lines.append("") + + return format_lines + + def format_fit_parameters(self, lines: List[str]) -> List[str]: + format_lines = [ + ".. rubric:: Fit Parameters", + "", + "Following fit parameters are estimated during the analysis.", + "", + ] + format_lines.extend(lines) + format_lines.append("") + + return format_lines diff --git a/qiskit_experiments/documentation/section_parsers.py b/qiskit_experiments/documentation/section_parsers.py new file mode 100644 index 0000000000..d07aecc7b5 --- /dev/null +++ b/qiskit_experiments/documentation/section_parsers.py @@ -0,0 +1,91 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Documentation section parsers. +""" + +import re +from typing import List +from .utils import _trim_empty_lines + + +def load_standard_section(docstring_lines: List[str]) -> List[str]: + """Load standard docstring section.""" + return _trim_empty_lines(docstring_lines) + + +def load_fit_parameters(docstring_lines: List[str]) -> List[str]: + """Load fit parameter section.""" + regex_paramdef = re.compile(r"defpar (?P.+):") + + # item finder + description_kind = { + "desc": re.compile(r"desc: (?P.+)"), + "init_guess": re.compile(r"init_guess: (?P.+)"), + "bounds": re.compile(r"bounds: (?P.+)"), + } + + # parse lines + parameter_desc = dict() + current_param = None + current_item = None + for line in docstring_lines: + if not list: + # remove line feed + continue + + # check if line is new parameter definition + match = re.match(regex_paramdef, line) + if match: + current_param = match["param"] + parameter_desc[current_param] = { + "desc": "", + "init_guess": "", + "bounds": "", + } + continue + + # check description + for kind, regex in description_kind.items(): + match = re.search(regex, line) + if match: + current_item = kind + line = match["s"].rstrip() + + # add line if parameter and item are already set + if current_param and current_item: + if parameter_desc[current_param][current_item]: + parameter_desc[current_param][current_item] += " " + line.lstrip() + else: + parameter_desc[current_param][current_item] = line.lstrip() + + section_lines = list() + + def write_description(header: str, kind: str): + section_lines.append(header) + for param, desc in parameter_desc.items(): + if not desc: + section_lines.append( + f" - :math:`{param}`: No description is provided. See source for details." + ) + else: + section_lines.append( + f" - :math:`{param}`: {desc[kind]}" + ) + section_lines.append("") + + write_description("Descriptions", "desc") + write_description("Initial Guess", "init_guess") + write_description("Boundaries", "bounds") + + return _trim_empty_lines(section_lines) diff --git a/qiskit_experiments/documentation/styles.py b/qiskit_experiments/documentation/styles.py new file mode 100644 index 0000000000..31fb2eaaf1 --- /dev/null +++ b/qiskit_experiments/documentation/styles.py @@ -0,0 +1,304 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Documentation extension for experiment class. +""" +import copy +import re +from abc import ABC +from typing import Union, List, Dict + +from sphinx.config import Config as SphinxConfig + +from qiskit_experiments.base_analysis import BaseAnalysis +from qiskit_experiments.base_experiment import BaseExperiment +from .formatter import ExperimentSectionFormatter, AnalysisSectionFormatter +from .utils import _generate_options_documentation, _format_default_options +from .section_parsers import load_standard_section, load_fit_parameters + +section_regex = re.compile(r"# section: (?P\S+)") + + +class QiskitExperimentDocstring(ABC): + """Qiskit Experiment style docstring parser base class.""" + + # mapping of sections supported by this style to parsing method or function + __sections__ = {} + + # section formatter + __formatter__ = None + + def __init__( + self, + target_cls: object, + docstring_lines: Union[str, List[str]], + config: SphinxConfig, + indent: str = "", + ): + """Create new parser and parse formatted docstring.""" + + if isinstance(docstring_lines, str): + lines = docstring_lines.splitlines() + else: + lines = docstring_lines + + self._target_cls = target_cls + self._indent = indent + self._config = config + + self._parsed_lines = self._classify(lines) + + def _classify(self, docstrings: List[str]) -> Dict[str, List[str]]: + """Classify formatted docstring into sections.""" + sectioned_docstrings = dict() + + def add_new_section(section: str, lines: List[str]): + if lines: + parser = self.__sections__[section] + if not parser: + raise KeyError( + f"Section {section} is automatically generated section. " + "This section cannot be overridden by class docstring." + ) + sectioned_docstrings[section] = parser(temp_lines) + + current_section = list(self.__sections__.keys())[0] + temp_lines = list() + for docstring_line in docstrings: + match = re.match(section_regex, docstring_line) + if match: + section_name = match["section_name"] + if section_name in self.__sections__: + # parse previous section + add_new_section(current_section, temp_lines) + # set new section + current_section = section_name + temp_lines.clear() + continue + else: + raise KeyError(f"Section name {section_name} is invalid.") + temp_lines.append(docstring_line) + # parse final section + add_new_section(current_section, temp_lines) + + for section, lines in self._extra_sections().items(): + sectioned_docstrings[section] = lines + + return sectioned_docstrings + + def _extra_sections(self) -> Dict[str, List[str]]: + """Generate extra sections.""" + pass + + def _format(self) -> Dict[str, List[str]]: + """Format each section with predefined formatter.""" + formatter = self.__formatter__(self._indent) + + formatted_sections = {section: None for section in self.__sections__} + for section, lines in self._parsed_lines.items(): + if not lines: + continue + section_formatter = getattr(formatter, f"format_{section}", None) + if section_formatter: + formatted_sections[section] = section_formatter(lines) + else: + formatted_sections[section] = lines + [""] + + return formatted_sections + + def generate_class_docs(self) -> List[List[str]]: + """Output formatted experiment class documentation.""" + formatted_sections = self._format() + + classdoc_lines = [] + for section_lines in formatted_sections.values(): + if section_lines: + classdoc_lines.extend(section_lines) + + return [classdoc_lines] + + +class ExperimentDocstring(QiskitExperimentDocstring): + """Documentation parser for the experiment class introduction.""" + + __sections__ = { + "header": load_standard_section, + "warning": load_standard_section, + "overview": load_standard_section, + "reference": load_standard_section, + "tutorial": load_standard_section, + "analysis_ref": None, + "experiment_opts": None, + "analysis_opts": None, + "transpiler_opts": None, + "run_opts": None, + "example": load_standard_section, + "note": load_standard_section, + } + + __formatter__ = ExperimentSectionFormatter + + def __init__( + self, + target_cls: BaseExperiment, + docstring_lines: Union[str, List[str]], + config: SphinxConfig, + indent: str = "", + ): + """Create new parser and parse formatted docstring.""" + super().__init__(target_cls, docstring_lines, config, indent) + + def _extra_sections(self) -> Dict[str, List[str]]: + """Generate extra sections.""" + parsed_sections = {} + + # add analysis class reference + analysis_class = getattr(self._target_cls, "__analysis_class__", None) + if analysis_class: + analysis_ref = f":py:class:`~{analysis_class.__module__}.{analysis_class.__name__}`" + parsed_sections["analysis_ref"] = [analysis_ref] + + # add experiment option + exp_option_desc = [] + + exp_docs_config = copy.copy(self._config) + exp_docs_config.napoleon_custom_sections = [("experiment options", "args")] + exp_option = _generate_options_documentation( + current_class=self._target_cls, + method_name="_default_experiment_options", + config=exp_docs_config, + indent=self._indent, + ) + if exp_option: + exp_option_desc.extend(exp_option) + exp_option_desc.append("") + exp_option_desc.extend( + _format_default_options( + defaults=self._target_cls._default_experiment_options().__dict__, + indent=self._indent, + ) + ) + else: + exp_option_desc.append("No experiment option available for this experiment.") + + parsed_sections["experiment_opts"] = exp_option_desc + + # add analysis option + analysis_option_desc = [] + + if analysis_class: + analysis_docs_config = copy.copy(self._config) + analysis_docs_config.napoleon_custom_sections = [("analysis options", "args")] + analysis_option = _generate_options_documentation( + current_class=analysis_class, + method_name="_default_options", + config=analysis_docs_config, + indent=self._indent, + ) + + if analysis_option: + analysis_option_desc.extend(analysis_option) + analysis_option_desc.append("") + analysis_option_desc.extend( + _format_default_options( + defaults=analysis_class._default_options().__dict__, + indent=self._indent, + ) + ) + else: + analysis_option_desc.append("No analysis option available for this experiment.") + + parsed_sections["analysis_opts"] = analysis_option_desc + + # add transpiler option + transpiler_option_desc = [ + "This option is used for circuit optimization. ", + "See `Qiskit Transpiler `_ documentation for available options.", + "" + ] + transpiler_option_desc.extend( + _format_default_options( + defaults=self._target_cls._default_transpile_options().__dict__, + indent=self._indent, + ) + ) + + parsed_sections["transpiler_opts"] = transpiler_option_desc + + # add run option + run_option_desc = [ + "This option is used for controlling job execution condition. " + "Note that this option is provider dependent. " + "See provider's backend runner API for available options. " + "See `here `_ for IBM Quantum Service.", + "" + ] + run_option_desc.extend( + _format_default_options( + defaults=self._target_cls._default_run_options().__dict__, + indent=self._indent, + ) + ) + + parsed_sections["run_opts"] = run_option_desc + + return parsed_sections + + +class AnalysisDocstring(QiskitExperimentDocstring): + """Documentation parser for the analysis class introduction.""" + + __sections__ = { + "header": load_standard_section, + "warning": load_standard_section, + "overview": load_standard_section, + "fit_model": load_standard_section, + "fit_parameters": load_fit_parameters, + "reference": load_standard_section, + "tutorial": load_standard_section, + "analysis_opts": None, + "example": load_standard_section, + "note": load_standard_section, + } + + __formatter__ = AnalysisSectionFormatter + + def __init__( + self, + target_cls: BaseAnalysis, + docstring_lines: Union[str, List[str]], + config: SphinxConfig, + indent: str = "", + ): + """Create new parser and parse formatted docstring.""" + super().__init__(target_cls, docstring_lines, config, indent) + + def _extra_sections(self) -> Dict[str, List[str]]: + """Generate extra sections.""" + parsed_sections = {} + + # add analysis option + analysis_docs_config = copy.copy(self._config) + analysis_docs_config.napoleon_custom_sections = [("analysis options", "args")] + analysis_option = _generate_options_documentation( + current_class=self._target_cls, + method_name="_default_options", + config=analysis_docs_config, + indent=self._indent, + ) + if analysis_option: + parsed_sections["analysis_opts"] = analysis_option + + return parsed_sections diff --git a/qiskit_experiments/documentation/utils.py b/qiskit_experiments/documentation/utils.py new file mode 100644 index 0000000000..0014048902 --- /dev/null +++ b/qiskit_experiments/documentation/utils.py @@ -0,0 +1,148 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +import inspect +import re +from typing import List, Tuple, Dict, Any + +from sphinx.config import Config as SphinxConfig +from sphinx.ext.napoleon.docstring import GoogleDocstring +from sphinx.util.docstrings import prepare_docstring + + +def _trim_empty_lines(docstring_lines: List[str]) -> List[str]: + """A helper function to remove redundant line feeds.""" + i_start = 0 + lines_iter = iter(docstring_lines) + while not next(lines_iter): + i_start += 1 + + i_end = len(docstring_lines) + lines_iter = iter(docstring_lines[::-1]) + while not next(lines_iter): + i_end -= 1 + + return docstring_lines[i_start:i_end] + + +def _parse_option_field( + docstring: str, + config: SphinxConfig, + target_args: List[str], + indent: str = "", +) -> Tuple[List[str], List[str]]: + """A helper function to extract descriptions of target arguments.""" + + # use GoogleDocstring parameter parser + experiment_option_parser = GoogleDocstring( + docstring=prepare_docstring(docstring, tabsize=len(indent)), + config=config + ) + parsed_lines = experiment_option_parser.lines() + + # remove redundant descriptions + param_regex = re.compile(r":(param|type) (?P\S+):") + target_params_description = [] + described_params = set() + valid_line = False + for line in parsed_lines: + is_item = re.match(param_regex, line) + if is_item: + if is_item["pname"] in target_args: + valid_line = True + described_params.add(is_item["pname"]) + else: + valid_line = False + if valid_line: + target_params_description.append(line) + + # find missing parameters + missing = set(target_args) - described_params + + return target_params_description, list(missing) + + +def _generate_options_documentation( + current_class: object, + method_name: str, + target_args: List[str] = None, + config: SphinxConfig = None, + indent: str = "", +) -> List[str]: + """Automatically generate documentation from the default options method.""" + + if current_class == object: + # check if no more base class + raise Exception( + f"Option docstring for {', '.join(target_args)} is missing." + ) + + options_docstring_lines = [] + + default_opts = getattr(current_class, method_name, None) + if not default_opts: + # getter option is not defined + return [] + + if not target_args: + target_args = list(default_opts().__dict__.keys()) + + # parse default options method + parsed_lines, target_args = _parse_option_field( + docstring=default_opts.__doc__, + config=config, + target_args=target_args, + indent=indent, + ) + + if target_args: + # parse parent class method docstring if some arg documentation is missing + parent_parsed_lines = _generate_options_documentation( + current_class=inspect.getmro(current_class)[1], + method_name=method_name, + target_args=target_args, + config=config, + indent=indent, + ) + options_docstring_lines.extend(parent_parsed_lines) + + options_docstring_lines.extend(parsed_lines) + + if options_docstring_lines: + return _trim_empty_lines(options_docstring_lines) + + return options_docstring_lines + + +def _format_default_options(defaults: Dict[str, Any], indent: str = "") -> List[str]: + """Format default options to docstring lines.""" + docstring_lines = [ + ".. dropdown:: Default values", + indent + ":animate: fade-in-slide-down", + "", + ] + + if not defaults: + docstring_lines.append(indent + "No default options are set.") + else: + docstring_lines.append(indent + "Following values are set by default.") + docstring_lines.append("") + docstring_lines.append(indent + ".. parsed-literal::") + docstring_lines.append("") + for par, value in defaults.items(): + if callable(value): + value_repr = f"Callable {value.__name__}" + else: + value_repr = repr(value) + docstring_lines.append(indent * 2 + f"{par:<25} := {value_repr}") + + return docstring_lines diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index ae7f83981b..ae454c637c 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -140,14 +140,14 @@ def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: default_options = super()._default_options() # update default values - default_options["p0"].default = {"a": None, "alpha": None, "alpha_c": None, "b": None} - default_options["bounds"].default = { + default_options.p0 = {"a": None, "alpha": None, "alpha_c": None, "b": None} + default_options.bounds = { "a": (0.0, 1.0), "alpha": (0.0, 1.0), "alpha_c": (0.0, 1.0), "b": (0.0, 1.0), } - default_options["fit_reports"].default = { + default_options.fit_reports = { "alpha": "\u03B1", "alpha_c": "\u03B1$_c$", "EPC": "EPC", diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index 7f9b968dae..e35dc5b915 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -35,7 +35,6 @@ ) -@curve_analysis_documentation class RBAnalysis(CurveAnalysis): """Randomized benchmarking analysis.""" @@ -86,11 +85,11 @@ def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: default_options = super()._default_options() # update default values - default_options["p0"].default = {"a": None, "alpha": None, "b": None} - default_options["bounds"].default = {"a": (0.0, 1.0), "alpha": (0.0, 1.0), "b": (0.0, 1.0)} - default_options["xlabel"].default = "Clifford Length" - default_options["ylabel"].default = "P(0)" - default_options["fit_reports"].default = {"alpha": "\u03B1", "EPC": "EPC"} + default_options.p0 = {"a": None, "alpha": None, "b": None} + default_options.bounds = {"a": (0.0, 1.0), "alpha": (0.0, 1.0), "b": (0.0, 1.0)} + default_options.xlabel = "Clifford Length" + default_options.ylabel = "P(0)" + default_options.fit_reports = {"alpha": "\u03B1", "EPC": "EPC"} return default_options diff --git a/requirements-dev.txt b/requirements-dev.txt index cd0c8eb3a5..10ab15adf9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,6 +10,7 @@ pygments>=2.4 reno>=3.2.0 sphinx-panels nbsphinx +arxiv ddt~=1.4.2 qiskit-aer>=0.8.0 From 7d2595251f994eb9251bb8689b883cd0a7fcd765 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 15 Jul 2021 15:53:19 +0900 Subject: [PATCH 31/46] revert experiment modules --- qiskit_experiments/analysis/curve_analysis.py | 7 +- qiskit_experiments/base_analysis.py | 75 ++--- qiskit_experiments/base_experiment.py | 298 ++++++++---------- .../characterization/__init__.py | 20 +- .../characterization/qubit_spectroscopy.py | 129 +++----- .../interleaved_rb_analysis.py | 143 ++++----- .../interleaved_rb_experiment.py | 17 +- .../randomized_benchmarking/rb_analysis.py | 75 ++--- .../randomized_benchmarking/rb_experiment.py | 68 +--- 9 files changed, 301 insertions(+), 531 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index d8afa2b129..091c5f5e7b 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -201,11 +201,6 @@ class AnalysisExample(CurveAnalysis): https://github.com/Qiskit/qiskit-experiments/issues """ - - # extra documentations - __doc_equations__ = None - __doc_fit_params__ = None - #: List[SeriesDef]: List of mapping representing a data series __series__ = None @@ -254,7 +249,7 @@ def __init__(self): setattr(self, f"__{key}", None) @classmethod - def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: + def _default_options(cls) -> Options: """Return default analysis options. Analysis Options: diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 0ea6f98dc0..61cb570296 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -14,47 +14,19 @@ """ from abc import ABC, abstractmethod -from typing import List, Tuple, Dict, Union +from typing import List, Tuple from qiskit.exceptions import QiskitError -from qiskit.providers import Options -from qiskit_experiments.autodocs import OptionsField, to_options -from qiskit_experiments.exceptions import AnalysisError -from qiskit_experiments.experiment_data import ExperimentData, AnalysisResult +from .experiment_data import ExperimentData, AnalysisResult 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 be combined with all other option kwargs in the - run method and passed to the `_run_analysis` function. - """ - - # Documentation sections - __doc_overview__ = None - __doc_example__ = None - __doc_references__ = None - __doc_note__ = None - __doc_warning__ = None - __doc_tutorial__ = None + """Base Analysis class for analyzing Experiment data.""" # Expected experiment data container for analysis __experiment_data__ = ExperimentData - @classmethod - def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: - return Options() - def run( self, experiment_data: ExperimentData, @@ -62,7 +34,7 @@ def run( return_figures: bool = False, **options, ): - """Run analysis and update ExperimentData with analysis result. + """Run analysis and update stored ExperimentData with analysis result. Args: experiment_data: the experiment data to analyze. @@ -71,13 +43,14 @@ def run( 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. + options: kwarg options for analysis function. 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. @@ -90,19 +63,13 @@ def run( f"Invalid experiment data type, expected {self.__experiment_data__.__name__}" f" but received {type(experiment_data).__name__}" ) - # Get analysis options - analysis_options = to_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, **analysis_options) - for res in analysis_results: - if "success" not in res: - res["success"] = True - except AnalysisError as ex: - analysis_results = [AnalysisResult(success=False, error_message=ex)] + analysis_results, figures = self._run_analysis(experiment_data, **options) + analysis_results["success"] = True + except Exception: + analysis_results = AnalysisResult(success=False) figures = None # Save to experiment data @@ -121,22 +88,18 @@ def run( @abstractmethod def _run_analysis( - self, experiment_data: ExperimentData, **options + self, data: ExperimentData, **options ) -> Tuple[List[AnalysisResult], List["matplotlib.figure.Figure"]]: """Run analysis on circuit data. Args: experiment_data: the experiment data to analyze. - options: additional options for analysis. By default the fields and - values in :meth:`options` are used and any provided values - can override these. + options: kwarg options for analysis function. Returns: - 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. - - Raises: - AnalysisError: if the analysis fails. + 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. """ pass diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index d38b54016b..3cafeacd19 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -13,19 +13,34 @@ Base Experiment class. """ -import copy from abc import ABC, abstractmethod +from typing import Union, Iterable, Optional, Tuple, List from numbers import Integral -from typing import Iterable, Optional, Tuple, List, Dict, Union from qiskit import transpile, assemble, QuantumCircuit from qiskit.exceptions import QiskitError from qiskit.providers.backend import Backend from qiskit.providers.basebackend import BaseBackend as LegacyBackend -from qiskit.providers.options import Options from .experiment_data import ExperimentData -from .autodocs import to_options, OptionsField + +_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): @@ -40,27 +55,32 @@ class BaseExperiment(ABC): experiment (Default: ExperimentData). """ - # Documentation sections - __doc_overview__ = None - __doc_example__ = None - __doc_references__ = None - __doc_note__ = None - __doc_warning__ = None - __doc_tutorial__ = None - # Analysis class for experiment __analysis_class__ = None # ExperimentData class for experiment __experiment_data__ = ExperimentData - def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None): + # 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, + ): """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. @@ -79,88 +99,62 @@ def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None) print(self._num_qubits, self._physical_qubits) raise QiskitError("Duplicate qubits in physical qubits list.") - # Experiment options - self._experiment_options = to_options(self._default_experiment_options()) - self._transpile_options = self._default_transpile_options() - self._run_options = self._default_run_options() - self._analysis_options = to_options(self._default_analysis_options()) - - # Set initial layout from qubits - self._transpile_options.initial_layout = self._physical_qubits + # Store options and values + self._circuit_options = set(circuit_options) if circuit_options else set() def run( self, - backend: Backend, + backend: "Backend", analysis: bool = True, experiment_data: Optional[ExperimentData] = None, - **run_options, + **kwargs, ) -> ExperimentData: """Run an experiment and perform analysis. Args: backend: The backend to run the experiment on. - 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. + 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. Returns: - The experiment data object. + ExperimentData: 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) - # 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__ - + # 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) if isinstance(backend, LegacyBackend): - qobj = assemble(circuits, backend=backend, **run_opts) + qobj = assemble(circuits, backend=backend, **run_options) job = backend.run(qobj) else: - job = backend.run(circuits, **run_opts) + job = backend.run(circuits, **run_options) # 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: - self.run_analysis(experiment_data) + # pylint: disable = not-callable + self.__analysis_class__().run(experiment_data, **kwargs) # 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.""" @@ -172,19 +166,32 @@ def physical_qubits(self) -> Tuple[int]: return self._physical_qubits @classmethod - def analysis(cls): - """Return the default Analysis class for the experiment.""" + 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. + """ if cls.__analysis_class__ is None: - raise QiskitError(f"Experiment {cls.__name__} does not have a default Analysis class") + raise QiskitError( + f"Experiment {cls.__name__} does not define" " a default Analysis class" + ) # pylint: disable = not-callable - return cls.__analysis_class__() + return cls.__analysis_class__(**kwargs) @abstractmethod - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits( + self, backend: Optional[Backend] = None, **circuit_options + ) -> 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`. @@ -194,114 +201,59 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: *N*-qubit experiment. The circuits mapped to physical qubits are obtained via the :meth:`transpiled_circuits` method. """ - # 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) -> Union[Options, Dict[str, OptionsField]]: - """Default kwarg options for experiment""" - # Experiment subclasses should override this method to return - # an dictionary of OptionsField 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. - - .. note:: - - This docstring is automatically overridden by subclasses. + # 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. Args: - fields: The fields to update the options + 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. - 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 + Returns: + A list of :class:`QuantumCircuit`. Raises: - 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) - - @classmethod - def _default_run_options(cls) -> Options: - """Default options values for the experiment :meth:`run` method.""" - return Options() + QiskitError: if an initial layout is specified in the + kwarg options for transpilation. The initial + layout must be generated from the experiment. - @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 + .. 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. """ - self._run_options.update_options(**fields) - - @classmethod - def _default_analysis_options(cls) -> Dict[str, OptionsField]: - """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 dict() - - @property - def analysis_options(self) -> Options: - """Return the analysis options for :meth:`run` analysis.""" - return self._analysis_options - - def set_analysis_options(self, **fields): - """Set the analysis options for :meth:`run` method. + # 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}" + ) - .. note:: + # Generate circuits + circuits = self.circuits(backend=backend, **circuit_options) - This docstring is automatically overridden by subclasses. + # 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) - Args: - fields: The fields to update the options - """ - self._analysis_options.update_options(**fields) + return circuits diff --git a/qiskit_experiments/characterization/__init__.py b/qiskit_experiments/characterization/__init__.py index b1f2b59ca5..3fcb3e7178 100644 --- a/qiskit_experiments/characterization/__init__.py +++ b/qiskit_experiments/characterization/__init__.py @@ -17,24 +17,6 @@ .. currentmodule:: qiskit_experiments.characterization -Experiments (auto) -================== -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/experiment.rst - - QubitSpectroscopy - - -Analysis (auto) -=============== -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/analysis.rst - - SpectroscopyAnalysis - - Experiments =========== .. autosummary:: @@ -42,6 +24,7 @@ T1Experiment T2StarExperiment + QubitSpectroscopy Analysis @@ -52,6 +35,7 @@ T1Analysis T2StarAnalysis + SpectroscopyAnalysis """ from .t1_experiment import T1Experiment, T1Analysis from .qubit_spectroscopy import QubitSpectroscopy, SpectroscopyAnalysis diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 1b5993fbef..34acf7814d 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -19,7 +19,8 @@ from qiskit import QuantumCircuit from qiskit.circuit import Gate, Parameter from qiskit.exceptions import QiskitError -from qiskit.providers import Backend, Options +from qiskit.providers import Backend +from qiskit.providers.options import Options from qiskit.qobj.utils import MeasLevel from qiskit_experiments.analysis import ( @@ -30,58 +31,45 @@ get_opt_value, get_opt_error, ) -from qiskit_experiments.autodocs import ( - OptionsField, - CurveFitParameter, - standard_experiment_documentation, - standard_options_documentation, - curve_analysis_documentation, -) from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.data_processing.processor_library import get_to_signal_processor class SpectroscopyAnalysis(CurveAnalysis): - r"""Spectroscopy analysis. - - # section: overview - - This analysis uses Gaussian function to find a peak. + r"""A class to analyze spectroscopy experiment. - Note that this analysis assumes only single peak. - If multiple peaks exist, you'll get a poor chi squared value. + Overview + This analysis takes only single series. This series is fit by the Gaussian function. - # section: fit_model + Fit Model + The fit is based on the following Gaussian function. - .. math:: + .. math:: - F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b + F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b - # section: fit_parameters + Fit Parameters + - :math:`a`: Peak height. + - :math:`b`: Base line. + - :math:`f`: Center frequency. This is the fit parameter of main interest. + - :math:`\sigma`: Standard deviation of Gaussian function. - defpar a: - desc: Base line. - init_guess: The maximum signal value with removed baseline. - bounds: [-2, 2] scaled with maximum signal value. + Initial Guesses + - :math:`a`: The maximum signal value with removed baseline. + - :math:`b`: A median value of the signal. + - :math:`f`: A frequency value at the peak (maximum signal). + - :math:`\sigma`: Calculated from FWHM of peak :math:`w` + such that :math:`w / \sqrt{8} \ln{2}`. - defpar b: - desc: Peak height. - init_guess: A median value of the signal. - bounds: [-1, 1] scaled with maximum signal value. - - defpar f: - desc: Center frequency. This is the fit parameter of main interest. - init_guess: A frequency value at the peak (maximum signal). - bounds: [min(x), max(x)] of frequency scan range. - - defpar \sigma: - desc: Standard deviation of Gaussian function. - init_guess: Calculated from FWHM of peak :math:`w` such that - :math:`w / \sqrt{8} \ln{2}`. - bounds: [0, :math:`\Delta x`] where :math:`\Delta x` represents - frequency scan range. + Bounds + - :math:`a`: [-2, 2] scaled with maximum signal value. + - :math:`b`: [-1, 1] scaled with maximum signal value. + - :math:`f`: [min(x), max(x)] of frequency scan range. + - :math:`\sigma`: [0, :math:`\Delta x`] where :math:`\Delta x` + represents frequency scan range. """ + __series__ = [ SeriesDef( fit_func=lambda x, a, sigma, freq, b: fit_function.gaussian( @@ -92,12 +80,11 @@ class SpectroscopyAnalysis(CurveAnalysis): ] @classmethod - def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: - """Default analysis options. + def _default_options(cls): + """Return default data processing options. - Analysis Options: - normalization (bool): Set ``True`` to normalize measurement data. Usually applied to - Kerneled (level1) measurement data. + See :meth:`~qiskit_experiment.analysis.CurveAnalysis._default_options` for + descriptions of analysis options. """ default_options = super()._default_options() default_options.p0 = {"a": None, "sigma": None, "freq": None, "b": None} @@ -192,8 +179,6 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR class QubitSpectroscopy(BaseExperiment): """Class that runs spectroscopy by sweeping the qubit frequency. - # section: overview - The circuits produced by spectroscopy, i.e. .. parsed-literal:: @@ -207,32 +192,6 @@ class QubitSpectroscopy(BaseExperiment): have a spectroscopy pulse-schedule embedded in a spectroscopy gate. The pulse-schedule consists of a set frequency instruction followed by a GaussianSquare pulse. A list of circuits is generated, each with a different frequency "freq". - - A spectroscopy experiment run by setting the frequency of the qubit drive. - The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. - - # section: warning - - Test warning - - # section: note - - Test note - - # section: example - - Test example - - # section: reference - - .. ref_arxiv:: Alexander2020 2004.06755 - .. ref_arxiv:: Shelly2021 2007.08532 - .. ref_arxiv:: Qasm3 2104.14722 - - # section: tutorial - - .. ref_website:: Qiskit Experiment Tutorial, https://quantum-computing.ibm.com/ - """ __analysis_class__ = SpectroscopyAnalysis @@ -249,21 +208,8 @@ def _default_run_options(cls) -> Options: ) @classmethod - def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: - """Default experiment options. - - Experiment Options: - amp (float): Amplitude of spectroscopy pulse. Usually weak power pulse is used to - suppress broadening of observed peaks. - duration (int): Duration of spectroscopy pulse. - This may need to satisfy the hardware waveform memory constraint. - The default value is represented in units of dt. - sigma (Union[int, float]): Sigma of Gaussian rising and falling edges. - This value should be sufficiently smaller than the duration, - otherwise waveform is distorted. The default value is represented in units of dt. - width (Union[int, float]) Width of the flat-top part of the Gaussian square - envelope of spectroscopy pulse. Set width=0 to use Gaussian pulse. - """ + def _default_experiment_options(cls) -> Options: + """Default option values used for the spectroscopy pulse.""" return Options( amp=0.1, duration=1024, @@ -278,7 +224,14 @@ def __init__( unit: Optional[str] = "Hz", absolute: bool = True, ): - """Create new experiment. + """ + A spectroscopy experiment run by setting the frequency of the qubit drive. + The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. + The spectroscopy pulse has the following parameters: + - amp: The amplitude of the pulse must be between 0 and 1, the default is 0.1. + - duration: The duration of the spectroscopy pulse in samples, the default is 1000 samples. + - sigma: The standard deviation of the pulse, the default is duration / 4. + - width: The width of the flat-top in the pulse, the default is 0, i.e. a Gaussian. Args: qubit: The qubit on which to run spectroscopy. diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index ae454c637c..babdb11bef 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -15,7 +15,6 @@ from typing import List, Dict, Any, Union import numpy as np -from qiskit.providers import Options from qiskit_experiments.analysis import ( CurveAnalysisResult, @@ -24,94 +23,76 @@ get_opt_value, get_opt_error, ) -from qiskit_experiments.autodocs import ( - Reference, - OptionsField, - CurveFitParameter, - curve_analysis_documentation, -) from .rb_analysis import RBAnalysis -@curve_analysis_documentation class InterleavedRBAnalysis(RBAnalysis): - """Interleaved randomized benchmarking analysis.""" + r"""A class to analyze interleaved randomized benchmarking experiment. - __doc_overview__ = r""" -This analysis takes two series for standard and interleaved RB curve fitting. + Overview + This analysis takes only two series for standard and interleaved RB curve fitting. + From the fit :math:`\alpha` and :math:`\alpha_c` value this analysis estimates + the error per Clifford (EPC) of interleaved gate. -From the fit :math:`\alpha` and :math:`\alpha_c` value this analysis estimates -the error per Clifford (EPC) of interleaved gate. + The EPC estimate is obtained using the equation -The EPC estimate is obtained using the equation -.. math:: + .. math:: - r_{\mathcal{C}}^{\text{est}} = - \frac{\left(d-1\right)\left(1-\alpha_{\overline{\mathcal{C}}}/\alpha\right)}{d} + r_{\mathcal{C}}^{\text{est}} = + \frac{\left(d-1\right)\left(1-\alpha_{\overline{\mathcal{C}}}/\alpha\right)}{d} -The error bounds are given by + The error bounds are given by -.. math:: + .. math:: - E = \min\left\{ - \begin{array}{c} - \frac{\left(d-1\right)\left[\left|\alpha-\alpha_{\overline{\mathcal{C}}}\right| - +\left(1-\alpha\right)\right]}{d} \\ - \frac{2\left(d^{2}-1\right)\left(1-\alpha\right)} - {\alpha d^{2}}+\frac{4\sqrt{1-\alpha}\sqrt{d^{2}-1}}{\alpha} - \end{array} - \right. + E = \min\left\{ + \begin{array}{c} + \frac{\left(d-1\right)\left[\left|\alpha-\alpha_{\overline{\mathcal{C}}}\right| + +\left(1-\alpha\right)\right]}{d} \\ + \frac{2\left(d^{2}-1\right)\left(1-\alpha\right)} + {\alpha d^{2}}+\frac{4\sqrt{1-\alpha}\sqrt{d^{2}-1}}{\alpha} + \end{array} + \right. -See [1] for more details. -""" + See the reference[1] for more details. - __doc_equations__ = [ - r"F_1(x_1) = a \alpha^{x_1} + b", - r"F_2(x_2) = a (\alpha_c \alpha)^{x_2} + b", - ] - __doc_fit_params__ = [ - CurveFitParameter( - name="a", - description="Height of decay curve.", - initial_guess=r"Average :math:`a` of the standard and interleaved RB.", - bounds="[0, 1]", - ), - CurveFitParameter( - name="b", - description="Base line.", - initial_guess=r"Average :math:`b` of the standard and interleaved RB. " - r"Usually equivalent to :math:`(1/2)^n` where :math:`n` is number " - "of qubit.", - bounds="[0, 1]", - ), - CurveFitParameter( - name=r"\alpha", - description="Depolarizing parameter.", - initial_guess=r"The slope of :math:`(y_1 - b)^{-x_1}` of the first and the " - "second data point of the standard RB.", - bounds="[0, 1]", - ), - CurveFitParameter( - name=r"\alpha_c", - description="Ratio of the depolarizing parameter of " - "interleaved RB to standard RB curve.", - initial_guess=r"Estimate :math:`\alpha' = \alpha_c \alpha` from the " - "interleaved RB curve, then divide this by " - r"the initial guess of :math:`\alpha`.", - bounds="[0, 1]", - ), - ] - __doc_references__ = [ - Reference( - title="Efficient measurement of quantum gate error by " - "interleaved randomized benchmarking", - authors="Easwar Magesan, et. al.", - open_access_link="https://arxiv.org/abs/1203.4550", - ), - ] + Fit Model + The fit is based on the following decay functions. + + .. math:: + + F_1(x_1) &= a \alpha^{x_1} + b ... {\rm standard RB} \\ + F_2(x_2) &= a (\alpha_c \alpha)^{x_2} + b ... {\rm interleaved RB} + + Fit Parameters + - :math:`a`: Height of decay curve. + - :math:`b`: Base line. + - :math:`\alpha`: Depolarizing parameter. + - :math:`\alpha_c`: Ratio of the depolarizing parameter of + interleaved RB to standard RB curve. + + Initial Guesses + - :math:`a`: Determined by the average :math:`a` of the standard and interleaved RB. + - :math:`b`: Determined by the average :math:`b` of the standard and interleaved RB. + Usually equivalent to :math:`(1/2)**n` where :math:`n` is number of qubit. + - :math:`\alpha`: Determined by the slope of :math:`(y_1 - b)**(-x_1)` of the first and the + second data point of the standard RB. + - :math:`\alpha_c`: Estimate :math:`\alpha' = \alpha_c * \alpha` from the + interleaved RB curve, then divide this by the initial guess of :math:`\alpha`. + + Bounds + - :math:`a`: [0, 1] + - :math:`b`: [0, 1] + - :math:`\alpha`: [0, 1] + - :math:`\alpha_c`: [0, 1] + + References + [1] "Efficient measurement of quantum gate error by interleaved randomized benchmarking" + (arXiv:1203.4550). + """ __series__ = [ SeriesDef( @@ -135,11 +116,13 @@ class InterleavedRBAnalysis(RBAnalysis): ] @classmethod - def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: - """Return default options.""" - default_options = super()._default_options() + def _default_options(cls): + """Return default data processing options. - # update default values + See :meth:`~qiskit_experiment.analysis.CurveAnalysis._default_options` for + descriptions of analysis options. + """ + default_options = super()._default_options() default_options.p0 = {"a": None, "alpha": None, "alpha_c": None, "b": None} default_options.bounds = { "a": (0.0, 1.0), @@ -147,11 +130,7 @@ def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: "alpha_c": (0.0, 1.0), "b": (0.0, 1.0), } - default_options.fit_reports = { - "alpha": "\u03B1", - "alpha_c": "\u03B1$_c$", - "EPC": "EPC", - } + default_options.fit_reports = {"alpha": "\u03B1", "alpha_c": "\u03B1$_c$", "EPC": "EPC"} return default_options diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py index 38e0a1332d..b2932d994c 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_experiment.py @@ -15,29 +15,18 @@ from typing import Union, Iterable, Optional, List from numpy.random import Generator + from qiskit import QuantumCircuit from qiskit.circuit import Instruction from qiskit.quantum_info import Clifford -from qiskit_experiments.autodocs import ( - standard_experiment_documentation, - standard_options_documentation, -) -from .interleaved_rb_analysis import InterleavedRBAnalysis from .rb_experiment import RBExperiment +from .interleaved_rb_analysis import InterleavedRBAnalysis -@standard_experiment_documentation -@standard_options_documentation class InterleavedRBExperiment(RBExperiment): """Interleaved RB Experiment class""" - __doc_overview__ = None - - __doc_references__ = None - - __doc_tutorial__ = None - # Analysis class for experiment __analysis_class__ = InterleavedRBAnalysis @@ -50,7 +39,7 @@ def __init__( seed: Optional[Union[int, Generator]] = None, full_sampling: bool = False, ): - """Create new experiment. + """Interleaved randomized benchmarking experiment. Args: interleaved_element: the element to interleave, diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index e35dc5b915..f42f7359da 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -16,7 +16,6 @@ from typing import List, Dict, Any, Union import numpy as np -from qiskit.providers import Options from qiskit_experiments.analysis import ( CurveAnalysis, @@ -28,47 +27,41 @@ get_opt_error, ) from qiskit_experiments.analysis.data_processing import multi_mean_xy_data -from qiskit_experiments.autodocs import ( - OptionsField, - CurveFitParameter, - curve_analysis_documentation, -) class RBAnalysis(CurveAnalysis): - """Randomized benchmarking analysis.""" + r"""A class to analyze randomized benchmarking experiments. - __doc_overview__ = r""" -This analysis takes only single series. + Overview + This analysis takes only single series. + This series is fit by the exponential decay function. + From the fit :math:`\alpha` value this analysis estimates the error per Clifford (EPC). -This curve is fit by the exponential decay function. -From the fit :math:`\alpha` value this analysis estimates the error per Clifford (EPC). -""" + Fit Model + The fit is based on the following decay function. - __doc_equations__ = [r"F(x) = a \alpha^x + b"] - - __doc_fit_params__ = [ - CurveFitParameter( - name="a", - description="Height of decay curve.", - initial_guess=r"Determined by :math:`(y_0 - b) / \alpha^{x_0}`, " - r"where :math:`b` and :math:`\alpha` are initial guesses.", - bounds="[0, 1]", - ), - CurveFitParameter( - name="b", - description="Base line.", - initial_guess=r"Determined by :math:`(1/2)^n` where :math:`n` is the number of qubit.", - bounds="[0, 1]", - ), - CurveFitParameter( - name=r"\alpha", - description="Depolarizing parameter. This is the fit parameter of main interest.", - initial_guess=r"Determined by the slope of :math:`(y - b)^{-x}` of the first and " - "the second data point.", - bounds="[0, 1]", - ), - ] + .. math:: + + F(x) = a \alpha^x + b + + Fit Parameters + - :math:`a`: Height of decay curve. + - :math:`b`: Base line. + - :math:`\alpha`: Depolarizing parameter. This is the fit parameter of main interest. + + Initial Guesses + - :math:`a`: Determined by :math:`(y_0 - b) / \alpha^x_0` + where :math:`b` and :math:`\alpha` are initial guesses. + - :math:`b`: Determined by :math:`(1/2)^n` where :math:`n` is the number of qubit. + - :math:`\alpha`: Determined by the slope of :math:`(y - b)^{-x}` of the first and the + second data point. + + Bounds + - :math:`a`: [0, 1] + - :math:`b`: [0, 1] + - :math:`\alpha`: [0, 1] + + """ __series__ = [ SeriesDef( @@ -80,11 +73,13 @@ class RBAnalysis(CurveAnalysis): ] @classmethod - def _default_options(cls) -> Union[Options, Dict[str, OptionsField]]: - """Return default options.""" - default_options = super()._default_options() + def _default_options(cls): + """Return default options. - # update default values + See :meth:`~qiskit_experiment.analysis.CurveAnalysis._default_options` for + descriptions of analysis options. + """ + default_options = super()._default_options() default_options.p0 = {"a": None, "alpha": None, "b": None} default_options.bounds = {"a": (0.0, 1.0), "alpha": (0.0, 1.0), "b": (0.0, 1.0)} default_options.xlabel = "Clifford Length" diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index ee477784a9..b253705103 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -12,55 +12,30 @@ """ Standard RB Experiment class. """ -from typing import Union, Iterable, Optional, List, Dict +from typing import Union, Iterable, Optional, List import numpy as np from numpy.random import Generator, default_rng + from qiskit import QuantumCircuit -from qiskit.circuit import Gate -from qiskit.providers import Backend, Options +from qiskit.providers import Backend from qiskit.quantum_info import Clifford +from qiskit.providers.options import Options +from qiskit.circuit import Gate -from qiskit_experiments.analysis.data_processing import probability from qiskit_experiments.base_experiment import BaseExperiment -from qiskit_experiments.autodocs import ( - OptionsField, - Reference, - standard_experiment_documentation, - standard_options_documentation, -) -from .clifford_utils import CliffordUtils +from qiskit_experiments.analysis.data_processing import probability from .rb_analysis import RBAnalysis +from .clifford_utils import CliffordUtils -@standard_experiment_documentation -@standard_options_documentation class RBExperiment(BaseExperiment): - """Randomized benchmarking.""" - - __doc_overview__ = """ -A randomized benchmarking (RB) is a scalable and robust algorithm -for benchmarking the full set of Clifford gates by a single parameter using the -randomization technique [1]. - -The RB sequences consist of random Clifford elements chosen uniformly from the Clifford group on -n-qubits, including a computed reversal element, that should return the qubits to the -initial state. - -Averaging over K random realizations of the sequence, we can find the averaged sequence fidelity, -or error per Clifford (EPC). -""" - - __doc_tutorial__ = "https://github.com/Qiskit/qiskit-experiments/blob/main/docs/tutorials/\ -rb_example.ipynb" + """RB Experiment class. - __doc_references__ = [ - Reference( - title="Robust randomized benchmarking of quantum processes", - authors="Easwar Magesan, J. M. Gambetta, and Joseph Emerson", - open_access_link="https://arxiv.org/abs/1009.3639", - ) - ] + 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 @@ -104,23 +79,8 @@ def __init__( self._rng = seed @classmethod - def _default_experiment_options(cls) -> Union[Options, Dict[str, OptionsField]]: - return { - "lengths": OptionsField( - default=None, - annotation=Iterable[int], - description="Array of integer values representing a number of Clifford gate N " - "per RB sequence. This value should be chosen based on " - "expected decay curve. If the maximum length is " - "too short, confidence interval of fit will become poor.", - ), - "num_samples": OptionsField( - default=None, - annotation=int, - description="Number of RB sequence per Clifford length. M random sequences are " - "generated for a length N.", - ), - } + 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]: From b6a0abdaa92208dd4e781bb1edf0c7c0c96a2215 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 15 Jul 2021 17:14:45 +0900 Subject: [PATCH 32/46] remove old autodoc module --- qiskit_experiments/autodocs/__init__.py | 44 --- qiskit_experiments/autodocs/analysis_docs.py | 158 ----------- qiskit_experiments/autodocs/descriptions.py | 119 -------- .../autodocs/experiment_docs.py | 120 -------- .../autodocs/set_option_docs.py | 129 --------- qiskit_experiments/autodocs/writer.py | 260 ------------------ .../documentation/example/__init__.py | 6 +- test/test_autodocs.py | 252 ----------------- 8 files changed, 3 insertions(+), 1085 deletions(-) delete mode 100644 qiskit_experiments/autodocs/__init__.py delete mode 100644 qiskit_experiments/autodocs/analysis_docs.py delete mode 100644 qiskit_experiments/autodocs/descriptions.py delete mode 100644 qiskit_experiments/autodocs/experiment_docs.py delete mode 100644 qiskit_experiments/autodocs/set_option_docs.py delete mode 100644 qiskit_experiments/autodocs/writer.py delete mode 100644 test/test_autodocs.py diff --git a/qiskit_experiments/autodocs/__init__.py b/qiskit_experiments/autodocs/__init__.py deleted file mode 100644 index 26469000fd..0000000000 --- a/qiskit_experiments/autodocs/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Auto docstring generation package for Qiskit expeirments. - -This tool guarantees the standardized docstring among the experiments and removes -variation of docstring quality depending on the coders. -In addition, this provides auto docstring generation of experiment options, considering -the inheritance of experiment classes, i.e. a documentation for options in a parent class -also appears in all subclasses. This drastically reduces the overhead of docstring management, -while providing users with the standardized and high quality documentation. -""" - -from . import analysis_docs, experiment_docs, set_option_docs -from .descriptions import OptionsField, Reference, CurveFitParameter, to_options - - -standard_experiment_documentation = experiment_docs.base_experiment_documentation( - style=experiment_docs.StandardExperimentDocstring -) - - -standard_analysis_documentation = analysis_docs.base_analysis_documentation( - style=analysis_docs.StandardAnalysisDocstring -) - - -curve_analysis_documentation = analysis_docs.base_analysis_documentation( - style=analysis_docs.CurveAnalysisDocstring -) - - -standard_options_documentation = set_option_docs.base_options_documentation( - style=set_option_docs.StandardSetOptionsDocstring -) diff --git a/qiskit_experiments/autodocs/analysis_docs.py b/qiskit_experiments/autodocs/analysis_docs.py deleted file mode 100644 index 34707d7352..0000000000 --- a/qiskit_experiments/autodocs/analysis_docs.py +++ /dev/null @@ -1,158 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Documentation for analysis class. -""" -import os -import re -from typing import Optional, Dict, List, Type - -from qiskit.exceptions import QiskitError -from qiskit.providers import Options - -from .descriptions import OptionsField, Reference, CurveFitParameter -from .writer import _DocstringWriter, _CurveFitDocstringWriter, _DocstringMaker - - -class StandardAnalysisDocstring(_DocstringMaker): - """A facade class to write standard analysis docstring.""" - - # pylint: disable=arguments-differ - @classmethod - def make_docstring( - cls, - default_options: Dict[str, OptionsField], - overview: Optional[str] = None, - example: Optional[str] = None, - references: Optional[List[Reference]] = None, - note: Optional[str] = None, - warning: Optional[str] = None, - tutorial: Optional[str] = None, - ) -> str: - try: - writer = _DocstringWriter() - if warning: - writer.write_warning(warning) - if overview: - writer.write_section(overview, "Overview") - if example: - writer.write_example(example) - if not isinstance(default_options, Options): - writer.write_lines( - "Default class options. These options are automatically set " - "when :py:meth:`run` method is called." - ) - writer.write_options_as_sections(default_options, "Default Options") - else: - writer.write_lines( - "Documentation for default options are not provided for this class." - ) - if references: - writer.write_references(references) - if note: - writer.write_note(note) - if tutorial: - writer.write_tutorial_link(tutorial) - except Exception as ex: - raise QiskitError(f"Auto docstring generation failed with the error: {ex}") from ex - return writer.docstring - - -class CurveAnalysisDocstring(_DocstringMaker): - """A facade class to write curve analysis docstring.""" - - # pylint: disable=arguments-differ - @classmethod - def make_docstring( - cls, - default_options: Dict[str, OptionsField], - overview: Optional[str] = None, - equations: Optional[List[str]] = None, - fit_params: Optional[List[CurveFitParameter]] = None, - example: Optional[str] = None, - references: Optional[List[Reference]] = None, - note: Optional[str] = None, - warning: Optional[str] = None, - tutorial: Optional[str] = None, - ) -> str: - try: - writer = _CurveFitDocstringWriter() - if warning: - writer.write_warning(warning) - if overview: - writer.write_section(overview, "Overview") - if equations: - if isinstance(equations, str): - equations = [equations] - writer.write_lines("This analysis assumes following fit function(s).") - writer.write_fit_models(equations) - if fit_params: - writer.write_lines( - "The fit model takes following fit parameters." - "These parameters are fit by the ``curve_fitter`` function specified in " - "the analysis options." - ) - writer.write_fit_parameter(fit_params) - writer.write_lines( - "The parameter initial guess are generated as follows. " - "If you want to override, you can provide ``p0`` of the analysis options." - ) - writer.write_initial_guess(fit_params) - writer.write_lines( - "The parameter boundaries are generated as follows. " - "If you want to override, you can provide ``bounds`` of the analysis options." - ) - writer.write_bounds(fit_params) - if example: - writer.write_example(example) - if not isinstance(default_options, Options): - writer.write_lines( - "Default class options. These options are automatically set " - "when :py:meth:`run` method is called." - ) - writer.write_options_as_sections(default_options, "Default Options") - else: - writer.write_lines( - "Documentation for default options are not provided for this class." - ) - if references: - writer.write_references(references) - if note: - writer.write_note(note) - if tutorial: - writer.write_tutorial_link(tutorial) - except Exception as ex: - raise QiskitError(f"Auto docstring generation failed with the error: {ex}") from ex - return writer.docstring - - -def base_analysis_documentation(style: Type[_DocstringMaker]): - """A class decorator that overrides analysis class docstring.""" - - def decorator(analysis: "BaseAnalysis"): - regex = r"__doc_(?P\S+)__" - - kwargs = {} - for attribute in dir(analysis): - match = re.match(regex, attribute) - if match: - arg = match["kwarg"] - kwargs[arg] = getattr(analysis, attribute) - - exp_docs = style.make_docstring(default_options=analysis._default_options(), **kwargs) - analysis.__doc__ += os.linesep - analysis.__doc__ += os.linesep - analysis.__doc__ += exp_docs - - return analysis - - return decorator diff --git a/qiskit_experiments/autodocs/descriptions.py b/qiskit_experiments/autodocs/descriptions.py deleted file mode 100644 index bfab3517f4..0000000000 --- a/qiskit_experiments/autodocs/descriptions.py +++ /dev/null @@ -1,119 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Description of options. -""" - -import dataclasses -import typing -from qiskit.providers import Options - - -@dataclasses.dataclass -class OptionsField: - """A data container to describe a single entry in options.""" - - # Default value - default: typing.Any - - # Type annotation - annotation: typing.Type - - # Docstring description of the entry - description: str - - # Set True if this is not a default option - is_extra: bool = False - - -@dataclasses.dataclass -class Reference: - """A data class to describe reference.""" - - # Article title - title: typing.Optional[str] = None - - # Author - authors: typing.Optional[str] = None - - # Journal info - journal_info: typing.Optional[str] = None - - # Open Access - open_access_link: typing.Optional[str] = None - - -@dataclasses.dataclass -class CurveFitParameter: - """A data class to describe fit parameter.""" - - # Name of fit parameter - name: str - - # Description about the parameter - description: str - - # How initial guess is calculated - initial_guess: str - - # How bounds are calculated - bounds: str - - -def _parse_annotation(_type: typing.Any) -> str: - """A helper function to convert type object into string.""" - - if isinstance(_type, str): - # forward reference - return _type - - module = _type.__module__ - - if module == "builtins": - return _type.__name__ - elif module == "typing": - # type representation - name = getattr(_type, "_name", None) - if name is None: - # _GenericAlias and special=False - type_repr = repr(_type).replace("typing.", "") - if type_repr in typing.__all__: - name = type_repr - else: - name = _parse_annotation(_type.__origin__) - # arguments - if hasattr(_type, "__args__") and _type.__args__: - args = [_parse_annotation(arg) for arg in _type.__args__] - return f"{name}[{', '.join(args)}]" - else: - return name - else: - return f":py:class:`~{module}.{_type.__name__}`" - - -def to_options(fields: typing.Union[Options, typing.Dict[str, OptionsField]]) -> Options: - """Converts a dictionary of ``OptionsField`` into ``Options`` object. - - Args: - fields: List of ``OptionsField`` object to convert. - - Returns: - ``Options`` that filled with ``.default`` value of ``OptionsField``. - """ - if isinstance(fields, Options): - return fields - - default_options = dict() - for field_name, field in fields.items(): - default_options[field_name] = field.default - - return Options(**default_options) diff --git a/qiskit_experiments/autodocs/experiment_docs.py b/qiskit_experiments/autodocs/experiment_docs.py deleted file mode 100644 index 68d79b0829..0000000000 --- a/qiskit_experiments/autodocs/experiment_docs.py +++ /dev/null @@ -1,120 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Documentation for experiment. -""" -import os -import re -from typing import Optional, Dict, List, Type - -from qiskit.exceptions import QiskitError -from qiskit.providers import Options - -from .descriptions import OptionsField, Reference -from .writer import _DocstringWriter, _DocstringMaker - - -class StandardExperimentDocstring(_DocstringMaker): - """A facade class to write standard experiment docstring.""" - - # pylint: disable=arguments-differ - @classmethod - def make_docstring( - cls, - analysis_options: Dict[str, OptionsField], - experiment_options: Dict[str, OptionsField], - analysis: str, - overview: Optional[str] = None, - example: Optional[str] = None, - references: Optional[List[Reference]] = None, - note: Optional[str] = None, - warning: Optional[str] = None, - tutorial: Optional[str] = None, - ) -> str: - try: - writer = _DocstringWriter() - if warning: - writer.write_warning(warning) - if overview: - writer.write_section(overview, "Overview") - if example: - writer.write_example(example) - writer.write_lines("This experiment uses following analysis class.") - writer.write_section(f":py:class:`~{analysis}`", "Analysis Class Reference") - writer.write_lines( - "See below configurable experiment options to customize your execution." - ) - if not isinstance(experiment_options, Options): - writer.write_options_as_sections( - fields=experiment_options, - section="Experiment Options", - text_block="Experiment options to generate circuits. " - "Options can be updated with :py:meth:`set_experiment_options`. " - "See method documentation for details.", - ) - else: - writer.write_dropdown_section( - text_block="Documentation for experiment options are not provided.", - section="Experiment Options", - ) - if not isinstance(analysis_options, Options): - writer.write_options_as_sections( - fields=analysis_options, - section="Analysis Options", - text_block="Analysis options to run the analysis class. " - "Options can be updated with :py:meth:`set_analysis_options`. " - "See method documentation for details.", - ) - else: - writer.write_dropdown_section( - text_block="Documentation for analysis options are not provided.", - section="Analysis Options", - ) - if references: - writer.write_references(references) - if note: - writer.write_note(note) - if tutorial: - writer.write_tutorial_link(tutorial) - except Exception as ex: - raise QiskitError(f"Auto docstring generation failed with the error: {ex}") from ex - return writer.docstring - - -def base_experiment_documentation(style: Type[_DocstringMaker]): - """A class decorator that overrides experiment class docstring.""" - - def decorator(experiment: "BaseExperiment"): - regex = r"__doc_(?P\S+)__" - - kwargs = {} - for attribute in dir(experiment): - match = re.match(regex, attribute) - if match: - arg = match["kwarg"] - kwargs[arg] = getattr(experiment, attribute, None) - - analysis = experiment.__analysis_class__ - - exp_docs = style.make_docstring( - analysis_options=experiment.__analysis_class__._default_options(), - experiment_options=experiment._default_experiment_options(), - analysis=f"{analysis.__module__}.{analysis.__name__}", - **kwargs, - ) - experiment.__doc__ += os.linesep - experiment.__doc__ += os.linesep - experiment.__doc__ += exp_docs - - return experiment - - return decorator diff --git a/qiskit_experiments/autodocs/set_option_docs.py b/qiskit_experiments/autodocs/set_option_docs.py deleted file mode 100644 index 2be8633631..0000000000 --- a/qiskit_experiments/autodocs/set_option_docs.py +++ /dev/null @@ -1,129 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Documentation for set option methods. -""" -import functools -from types import FunctionType -from typing import Optional, Dict, Any, Type - -from qiskit.exceptions import QiskitError -from qiskit.providers import Options - -from .descriptions import OptionsField -from .writer import _DocstringWriter, _DocstringMaker - - -class StandardSetOptionsDocstring(_DocstringMaker): - """A facade class to write standard set options docstring.""" - - # pylint: disable=arguments-differ - @classmethod - def make_docstring( - cls, - description: str, - options: Dict[str, OptionsField], - note: Optional[str] = None, - raises: Optional[Dict[str, str]] = None, - ) -> str: - try: - writer = _DocstringWriter() - writer.write_lines(description) - writer.write_options_as_args(options) - if note: - writer.write_note(note) - if raises: - writer.write_raises(*list(zip(*raises.items()))) - except Exception as ex: - raise QiskitError(f"Auto docstring generation failed with the error: {ex}") from ex - return writer.docstring - - -def _copy_method(experiment: "BaseExperiment", method_name: str) -> FunctionType: - """A helper function to duplicate base class method. - - Note that calling set options method will access to the base class method. - If we override attribute, the change will propagate through the all subclass attributes. - This function prevent this by copying the base class method. - - Args: - experiment: Base class to get a method. - method_name: Name of method to copy. - - Returns: - Duplicated function object. - """ - base_method = getattr(experiment, method_name) - - new_method = FunctionType( - code=base_method.__code__, - globals=base_method.__globals__, - name=base_method.__name__, - argdefs=base_method.__defaults__, - closure=base_method.__closure__, - ) - return functools.update_wrapper(wrapper=new_method, wrapped=base_method) - - -def _compile_annotations(fields: Dict[str, OptionsField]) -> Dict[str, Any]: - """Dynamically generate method annotation based on information provided by ``OptionsField``s. - - Args: - fields: A dictionary of ``OptionsField`` object. - - Returns: - Dictionary of field name and type annotation. - """ - annotations = dict() - - for field_name, field in fields.items(): - if not isinstance(field.annotation, str): - annotations[field_name] = field.annotation - - return annotations - - -def base_options_documentation(style: Type[_DocstringMaker]): - """A class decorator that overrides set options method docstring.""" - - def decorator(experiment: "BaseExperiment"): - analysis_options = experiment.__analysis_class__._default_options() - experiment_options = experiment._default_experiment_options() - - # update analysis options setter - if not isinstance(analysis_options, Options): - analysis_setter = _copy_method(experiment, "set_analysis_options") - analysis_setter.__annotations__ = _compile_annotations(analysis_options) - analysis_setter.__doc__ = style.make_docstring( - description="Set the analysis options for :py:meth:`run_analysis` method.", - options=analysis_options, - note="Here you can set arbitrary parameter, even if it is not listed. " - "Such option is passed as a keyword argument to the analysis fitter functions " - "(if exist). The execution may fail if the function API doesn't support " - "extra keyword arguments.", - ) - setattr(experiment, "set_analysis_options", analysis_setter) - - # update experiment options setter - if not isinstance(experiment_options, Options): - experiment_setter = _copy_method(experiment, "set_experiment_options") - experiment_setter.__annotations__ = _compile_annotations(experiment_options) - experiment_setter.__doc__ = style.make_docstring( - description="Set the analysis options for :py:meth:`run` method.", - options=experiment_options, - raises={"AttributeError": "If the field passed in is not a supported options."}, - ) - setattr(experiment, "set_experiment_options", experiment_setter) - - return experiment - - return decorator diff --git a/qiskit_experiments/autodocs/writer.py b/qiskit_experiments/autodocs/writer.py deleted file mode 100644 index f37bbd6f19..0000000000 --- a/qiskit_experiments/autodocs/writer.py +++ /dev/null @@ -1,260 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Docstring writer. This module takes facade pattern to implement the functionality. -""" -import os -import typing -from abc import ABC, abstractmethod -from types import FunctionType - -from qiskit.exceptions import QiskitError - -from .descriptions import OptionsField, Reference, CurveFitParameter, _parse_annotation - - -class _DocstringMaker(ABC): - """A base facade class to write docstring.""" - - @classmethod - @abstractmethod - def make_docstring(cls, *args, **kwargs) -> str: - """Write a docstring.""" - pass - - -class _DocstringWriter: - """A docstring writer.""" - - __indent__ = " " - - def __init__(self): - self.docstring = "" - - def write_header(self, header: str): - """Write header.""" - self._write_line(header) - self.docstring += os.linesep - - def write_options_as_args(self, fields: typing.Dict[str, OptionsField]): - """Write option descriptions as an argument section. - - This can write multi line description for each option. - This style is used for detailed summary for the set method docstring. - Extra fields (non-default options) are also shown. - """ - - def _write_field(_arg_name, _field): - arg_str_type = f":py:obj:`{_parse_annotation(_field.annotation)}`" - self.docstring += self.__indent__ - self._write_line(f"{_arg_name} ({arg_str_type}):") - self._write_multi_line(_field.description, self.__indent__ * 2) - default = _field.default - if default is not None: - if isinstance(_field.default, FunctionType): - default_str = f":py:func:`~{default.__module__}.{default.__name__}`" - else: - default_str = f":py:obj:`{default}`" - self.docstring += self.__indent__ * 2 - self._write_line(f"(Default: {default_str})") - - self._write_line("Parameters:") - extra_fields = dict() - for arg_name, field in fields.items(): - if field.is_extra: - extra_fields[arg_name] = field - continue - _write_field(arg_name, field) - self.docstring += os.linesep - - if extra_fields: - self._write_line("Other Parameters:") - for arg_name, field in extra_fields.items(): - _write_field(arg_name, field) - self.docstring += os.linesep - - def write_options_as_sections( - self, - fields: typing.Dict[str, OptionsField], - section: str, - text_block: typing.Optional[str] = None, - ): - """Write option descriptions as a custom section. - - This writes only the first line of description, if multiple lines exist. - This style is mainly used for the short summary (options are itemized). - This section will be shown as a drop down box. - """ - self._write_line(f".. dropdown:: {section}") - self.docstring += self.__indent__ - self._write_line(":animate: fade-in-slide-down") - self.docstring += os.linesep - - if text_block: - self._write_multi_line(text_block, self.__indent__) - self.docstring += os.linesep - - for arg_name, field in fields.items(): - if field.is_extra: - continue - arg_str_type = f":py:obj:`{_parse_annotation(field.annotation)}`" - arg_description = field.description.split(os.linesep)[0] - # write multi line description - self.docstring += self.__indent__ - self._write_line(f"- **{arg_name}** ({arg_str_type}): {arg_description}") - self.docstring += os.linesep - - def write_lines(self, text_block: str): - """Write text without section.""" - self._write_multi_line(text_block) - self.docstring += os.linesep - - def write_example(self, text_block: str): - """Write error descriptions.""" - self._write_line("Example:") - self._write_multi_line(text_block, self.__indent__) - self.docstring += os.linesep - - def write_raises(self, error_kinds: typing.List[str], descriptions: typing.List[str]): - """Write error descriptions.""" - self._write_line("Raises:") - for error_kind, description in zip(error_kinds, descriptions): - self.docstring += self.__indent__ - self._write_line(f"{error_kind}: {description}") - self.docstring += os.linesep - - def write_section(self, text_block: str, section: str): - """Write new user defined section.""" - self._write_line(f"{section}") - self._write_multi_line(text_block, self.__indent__) - self.docstring += os.linesep - - def write_dropdown_section(self, text_block: str, section: str): - """Write new user defined section as drop down box.""" - self._write_line(f".. dropdown:: {section}") - self.docstring += self.__indent__ - self._write_line(":animate: fade-in-slide-down") - self.docstring += os.linesep - self._write_multi_line(text_block, self.__indent__) - self.docstring += os.linesep - - def write_note(self, text_block: str): - """Write note.""" - self._write_line("Note:") - self._write_multi_line(text_block, self.__indent__) - self.docstring += os.linesep - - def write_warning(self, text_block: str): - """Write warning.""" - self._write_line("Warning:") - self._write_multi_line(text_block, self.__indent__) - self.docstring += os.linesep - - def write_returns(self, text_block: str): - """Write returns.""" - self._write_line("Returns:") - self._write_multi_line(text_block, self.__indent__) - self.docstring += os.linesep - - def write_references(self, refs: typing.List[Reference]): - """Write references.""" - self._write_line("References:") - for idx, ref in enumerate(refs): - ref_repr = [] - if ref.authors: - ref_repr.append(f"{ref.authors}") - if ref.title: - ref_repr.append(f"`{ref.title}`") - if ref.journal_info: - ref_repr.append(f"{ref.journal_info}") - if ref.open_access_link: - ref_repr.append(f"`open access <{ref.open_access_link}>`_") - self.docstring += self.__indent__ - self._write_line(f"- [{idx + 1}] {', '.join(ref_repr)}") - self.docstring += os.linesep - - def write_tutorial_link(self, link: str): - """Write link to tutorial website.""" - self._write_line("See Also:") - self.docstring += self.__indent__ - self._write_line(f"- `Qiskit Experiment Tutorial <{link}>`_") - self.docstring += os.linesep - - def _write_multi_line(self, text_block: str, indent: typing.Optional[str] = None): - """A util method to write multi line text with indentation.""" - text_block = text_block.strip(os.linesep) - - for line in text_block.split(os.linesep): - indented_text = "" - if len(line) > 0 and indent is not None: - indented_text += indent - # for C0303 trailing whitespace - indented_text += line.rstrip() - - self._write_line(indented_text) - - def _write_line(self, text: str): - """A helper function to write single line.""" - text = text.strip(os.linesep) - - self.docstring += text.rstrip() + os.linesep - - -class _CurveFitDocstringWriter(_DocstringWriter): - """A docstring writer supporting fit model descriptions.""" - - def write_fit_parameter(self, fit_params: typing.List[CurveFitParameter]): - """Write fit parameters.""" - self._write_line("Fit Parameters") - for fit_param in fit_params: - self.docstring += self.__indent__ - self._write_line(f"- :math:`{fit_param.name}`: {fit_param.description}") - self.docstring += os.linesep - - def write_initial_guess(self, fit_params: typing.List[CurveFitParameter]): - """Write initial guess estimation method.""" - self._write_line("Initial Guess") - for fit_param in fit_params: - self.docstring += self.__indent__ - self._write_line(f"- :math:`{fit_param.name}`: {fit_param.initial_guess}") - self.docstring += os.linesep - - def write_bounds(self, fit_params: typing.List[CurveFitParameter]): - """Write fit parameter bound.""" - self._write_line("Parameter Boundaries") - - for fit_param in fit_params: - self.docstring += self.__indent__ - self._write_line(f"- :math:`{fit_param.name}`: {fit_param.bounds}") - self.docstring += os.linesep - - def write_fit_models(self, equations: typing.List[str]): - """Write fitting models.""" - self._write_line("Fit Model") - self.docstring += self.__indent__ - self._write_line(".. math::") - self.docstring += os.linesep - - if len(equations) > 1: - eqs = [] - for equation in equations: - try: - lh, rh = equation.split("=") - except ValueError as ex: - raise QiskitError(f"Equation {equation} is not a valid form.") from ex - eqs.append(f"{self.__indent__ * 2}{lh} &= {rh}") - self.docstring += f" \\\\{os.linesep}".join(eqs) - self.docstring += os.linesep - else: - self.docstring += self.__indent__ * 2 - self._write_line(equations[0]) - self.docstring += os.linesep diff --git a/qiskit_experiments/documentation/example/__init__.py b/qiskit_experiments/documentation/example/__init__.py index d2dd855a01..815328a676 100644 --- a/qiskit_experiments/documentation/example/__init__.py +++ b/qiskit_experiments/documentation/example/__init__.py @@ -33,7 +33,7 @@ :toctree: ../stubs/ :template: autosummary/experiment.rst - ExampleDocExperiment + DocumentedExperiment Analysis ======== @@ -41,12 +41,12 @@ :toctree: ../stubs/ :template: autosummary/analysis.rst - ExampleDocAnalysis - ExampleDocCurveAnalysis + DocumentedCurveAnalysis """ import warnings +from .example_experiment import DocumentedExperiment, DocumentedCurveAnalysis warnings.warn( "This module dosen't implement actual experiment. " diff --git a/test/test_autodocs.py b/test/test_autodocs.py deleted file mode 100644 index 4972cc38f4..0000000000 --- a/test/test_autodocs.py +++ /dev/null @@ -1,252 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test version string generation.""" - -import typing - -from qiskit.test import QiskitTestCase - -from qiskit_experiments.autodocs import ( - Reference, - OptionsField, - standard_experiment_documentation, - standard_options_documentation, -) -from qiskit_experiments.base_analysis import BaseAnalysis -from qiskit_experiments.base_experiment import BaseExperiment - - -# -# Note that this mock class shows how to write docstring with this framework -# - - -class MockExperiment(BaseExperiment): - """Very simple description of class. You can write details in the overview section.""" - - # this is recommended to fill - __doc_overview__ = """ -Test auto documentation of overview section. - -This can be multi-line. -Line feeds in front and end of the text block are removed. - - -""" - - # if the experiment options are complicated perhaps you can show example codes - __doc_example__ = """ -This is the place to show example code. - -For example: - -.. code-block:: - - from qiskit_experiments import MyExperiment - - exp = MyExperiment(**options).run(my_backend) - -You can write arbitrary code block or jupyter execute here. -""" - - # this is optional - __doc_note__ = """ -This is a place to write some notification to users. - -This appears as bounded box in the documentation. -""" - - # this is optional - __doc_warning__ = """ -This is a place to write warnings if exist. -This block will appear at the top of the documentation. - -For example, if the module is still under active development, or being deprecated, -you can communicate these things with users by using this section. -""" - - # this is recommended to fill if exist - __doc_tutorial__ = "https://my_tutorial.com/this_is_my_experiment" - - # this is recommended to fill if exist - # the article number is automatically generated by this list index + 1 - # so you can cite this number as, for example, [1] from the overview section - # The fields of Reference instance are all optional but you should fill at least one - __doc_references__ = [ - Reference( - title="My scientific paper title or title of website.", - authors="Author names.", - journal_info="Qiskit Experiment publications (2021)", - open_access_link="https://my_journal_website.com/this_is_my_journal_page", - ) - ] - - __analysis_class__ = BaseAnalysis - - @classmethod - def _default_experiment_options(cls): - return { - "option1": OptionsField( - description="This is option1.", - annotation=typing.Union[str, int], - default=5, - ), - "option2": OptionsField( - description="""\ -This can be multi line text block if we need to show code block. - -The first line should be without line feed, otherwise empty sentence -will be shown in the option summary in class docstring. -Full text is only shown in the set method docstring. - -.. code-block: - - option2 = {"sub_field1": 1, "sub_field2": 2} - -You can show code block like this.""", - annotation=typing.Dict[str, typing.Any], - default={"sub_field1": 10, "sub_field2": 20}, - ), - "option3": OptionsField( - description="If you want show a long text without line feed, " - "you can split the text like this.", - annotation=BaseExperiment, - default=None, - ), - } - - def circuits(self, backend=None): - pass - - -class TestAutodocs(QiskitTestCase): - """Test for auto documentation.""" - - def test_class_docstring(self): - """Test if class docstring is correctly generated.""" - # pylint: disable=invalid-name - self.maxDiff = None - - DocumentedExperiment = standard_experiment_documentation(MockExperiment) - ref_docs = """\ -Very simple description of class. You can write details in the overview section. - -Warning: - This is a place to write warnings if exist. - This block will appear at the top of the documentation. - - For example, if the module is still under active development, or being deprecated, - you can communicate these things with users by using this section. - -Overview - Test auto documentation of overview section. - - This can be multi-line. - Line feeds in front and end of the text block are removed. - -Example: - This is the place to show example code. - - For example: - - .. code-block:: - - from qiskit_experiments import MyExperiment - - exp = MyExperiment(**options).run(my_backend) - - You can write arbitrary code block or jupyter execute here. - -This experiment uses following analysis class. - -Analysis Class Reference - :py:class:`~qiskit_experiments.base_analysis.BaseAnalysis` - -See below configurable experiment options to customize your execution. - -.. dropdown:: Experiment Options - :animate: fade-in-slide-down - - Experiment options to generate circuits. Options can be updated with :py:meth:`set_experiment_options`. See method documentation for details. - - - **option1** (:py:obj:`Union[str, int]`): This is option1. - - **option2** (:py:obj:`Dict[str, Any]`): This can be multi line text block if we need to show code block. - - **option3** (:py:obj:`:py:class:`~qiskit_experiments.base_experiment.BaseExperiment``): If you want show a long text without line feed, you can split the text like this. - -.. dropdown:: Analysis Options - :animate: fade-in-slide-down - - Documentation for analysis options are not provided. - -References: - - [1] Author names., `My scientific paper title or title of website.`, Qiskit Experiment publications (2021), `open access `_ - -Note: - This is a place to write some notification to users. - - This appears as bounded box in the documentation. - -See Also: - - `Qiskit Experiment Tutorial `_ - -""" - - self.assertEqual(ref_docs, DocumentedExperiment.__doc__) - - def test_option_method_docstring(self): - """Test if method docstring and annotations are correctly generated.""" - # pylint: disable=invalid-name - self.maxDiff = None - - DocumentedExperiment = standard_options_documentation(MockExperiment) - - ref_docs_experiment_options = """\ -Set the analysis options for :py:meth:`run` method. - -Parameters: - option1 (:py:obj:`Union[str, int]`): - This is option1. - (Default: :py:obj:`5`) - option2 (:py:obj:`Dict[str, Any]`): - This can be multi line text block if we need to show code block. - - The first line should be without line feed, otherwise empty sentence - will be shown in the option summary in class docstring. - Full text is only shown in the set method docstring. - - .. code-block: - - option2 = {"sub_field1": 1, "sub_field2": 2} - - You can show code block like this. - (Default: :py:obj:`{'sub_field1': 10, 'sub_field2': 20}`) - option3 (:py:obj:`:py:class:`~qiskit_experiments.base_experiment.BaseExperiment``): - If you want show a long text without line feed, you can split the text like this. - -Raises: - AttributeError: If the field passed in is not a supported options. - -""" - - self.assertEqual( - DocumentedExperiment.set_experiment_options.__doc__, ref_docs_experiment_options - ) - - ref_annotation = { - "option1": typing.Union[str, int], - "option2": typing.Dict[str, typing.Any], - "option3": BaseExperiment, - } - self.assertDictEqual( - DocumentedExperiment.set_experiment_options.__annotations__, ref_annotation - ) From c14c2cc4c5f507b8bbbd1d31e07a67917501d312 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 15 Jul 2021 17:21:57 +0900 Subject: [PATCH 33/46] fix old import --- qiskit_experiments/analysis/curve_analysis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 091c5f5e7b..cd9ac5127b 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -27,7 +27,6 @@ from qiskit_experiments.analysis.curve_fitting import multi_curve_fit, CurveAnalysisResult from qiskit_experiments.analysis.data_processing import probability from qiskit_experiments.analysis.utils import get_opt_value, get_opt_error -from qiskit_experiments.autodocs import OptionsField from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.data_processing import DataProcessor from qiskit_experiments.data_processing.exceptions import DataProcessorError From 5925c26f0ef82c13cc4e04b215a75a06dd0c17e1 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 15 Jul 2021 17:38:00 +0900 Subject: [PATCH 34/46] remove change to base analysis --- qiskit_experiments/base_analysis.py | 64 +++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 61cb570296..f5abc3244c 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -17,16 +17,35 @@ from typing import List, Tuple from qiskit.exceptions import QiskitError +from qiskit.providers.options import Options -from .experiment_data import ExperimentData, AnalysisResult +from qiskit_experiments.exceptions import AnalysisError +from qiskit_experiments.experiment_data import ExperimentData, AnalysisResult 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 +53,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 +62,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,13 +81,19 @@ 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["success"] = True - except Exception: - analysis_results = AnalysisResult(success=False) + analysis_results, figures = self._run_analysis(experiment_data, **analysis_options) + for res in analysis_results: + if "success" not in res: + res["success"] = True + except AnalysisError as ex: + analysis_results = [AnalysisResult(success=False, error_message=ex)] figures = None # Save to experiment data @@ -88,18 +112,22 @@ def run( @abstractmethod def _run_analysis( - self, data: ExperimentData, **options + self, experiment_data: ExperimentData, **options ) -> Tuple[List[AnalysisResult], List["matplotlib.figure.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. + + Raises: + AnalysisError: if the analysis fails. """ pass From 36d4b9ab6c2b76c985c35bebf718bd5a737f8929 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 15 Jul 2021 17:40:45 +0900 Subject: [PATCH 35/46] update docs --- docs/_ext/autoref.py | 6 +++--- .../documentation/example/example_experiment.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/_ext/autoref.py b/docs/_ext/autoref.py index 8e12611533..6a4c42a4ae 100644 --- a/docs/_ext/autoref.py +++ b/docs/_ext/autoref.py @@ -50,13 +50,13 @@ def run(self): class Arxiv(Directive): - """A custom helper directive for generating journal information from ArXiv id. + """A custom helper directive for generating journal information from arXiv id. This directive takes two arguments - Arbitrary reference name (no white space should be included) - - ArXiv ID + - arXiv ID This can be used, for example, @@ -73,7 +73,7 @@ class Arxiv(Directive): def run(self): - # search ArXiv database + # search arXiv database try: search = arxiv.Search(id_list=[self.arguments[1]]) paper = next(search.results()) diff --git a/qiskit_experiments/documentation/example/example_experiment.py b/qiskit_experiments/documentation/example/example_experiment.py index 2ea928a13e..3873ed29db 100644 --- a/qiskit_experiments/documentation/example/example_experiment.py +++ b/qiskit_experiments/documentation/example/example_experiment.py @@ -114,7 +114,7 @@ class DocumentedExperiment(BaseExperiment): # section: reference - Currently this supports article reference in ArXiv database. + Currently this supports article reference in arXiv database. You can use following helper directive. .. ref_arxiv:: Auth2020a 21xx.01xxx @@ -122,8 +122,8 @@ class DocumentedExperiment(BaseExperiment): This directive takes two arguments separated by a whitespace. The first argument is arbitrary label for this article, which may be used to refer to this paper from other sections. - Second argument is the ArXiv ID of the paper to refer to. - Once this directive is inserted, Sphinx searches the ArXiv database and + Second argument is the arXiv ID of the paper to refer to. + Once this directive is inserted, Sphinx searches the arXiv database and automatically generates a formatted reference sentence with the link to PDF. # section: tutorial From fa8a4c0cdcc13bf51d343949692b681d50a2cf9e Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 15 Jul 2021 18:01:21 +0900 Subject: [PATCH 36/46] lint --- .../example/example_experiment.py | 12 +++++-- qiskit_experiments/documentation/formatter.py | 15 ++++++++ .../documentation/section_parsers.py | 4 +-- qiskit_experiments/documentation/styles.py | 36 ++++++++++--------- qiskit_experiments/documentation/utils.py | 10 +++--- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/qiskit_experiments/documentation/example/example_experiment.py b/qiskit_experiments/documentation/example/example_experiment.py index 3873ed29db..e96fa42ee6 100644 --- a/qiskit_experiments/documentation/example/example_experiment.py +++ b/qiskit_experiments/documentation/example/example_experiment.py @@ -9,6 +9,14 @@ # Any modifications or derivative works of this code must retain this # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +""" +Class documentation examples. + +.. warning:: + + This module is just an example for documentation. Do not import. + +""" from qiskit.providers import Options @@ -37,7 +45,7 @@ class DocumentedCurveAnalysis(CurveAnalysis): See :class:`DocumentedExperiment` for description of these sections. In addition to above sections, analysis template provides following extra sections. - # section: fit_mode: + # section: fit_model: Here you can describe your fitting model. Standard reStructuredText directives can be used. For example: @@ -179,5 +187,5 @@ def __init__(self, qubit: int): """ super().__init__(qubits=[qubit]) - def circuits(self, backend = None): + def circuits(self, backend=None): pass diff --git a/qiskit_experiments/documentation/formatter.py b/qiskit_experiments/documentation/formatter.py index f1b7fc28a4..5f1f4b2a04 100644 --- a/qiskit_experiments/documentation/formatter.py +++ b/qiskit_experiments/documentation/formatter.py @@ -27,12 +27,14 @@ def __init__(self, indent: str): self.indent = indent def format_header(self, lines: List[str]) -> List[str]: + """Format header section.""" format_lines = lines format_lines.append("") return format_lines def format_overview(self, lines: List[str]) -> List[str]: + """Format overview section.""" format_lines = [".. rubric:: Overview", ""] format_lines.extend(lines) format_lines.append("") @@ -40,6 +42,7 @@ def format_overview(self, lines: List[str]) -> List[str]: return format_lines def format_reference(self, lines: List[str]) -> List[str]: + """Format reference section.""" format_lines = [".. rubric:: References", ""] format_lines.extend(lines) format_lines.append("") @@ -47,6 +50,7 @@ def format_reference(self, lines: List[str]) -> List[str]: return format_lines def format_warning(self, lines: List[str]) -> List[str]: + """Format warning section.""" format_lines = [".. warning::", ""] for line in lines: format_lines.append(self.indent + line) @@ -55,6 +59,7 @@ def format_warning(self, lines: List[str]) -> List[str]: return format_lines def format_example(self, lines: List[str]) -> List[str]: + """Format example section.""" format_lines = [".. rubric:: Example", ""] format_lines.extend(lines) format_lines.append("") @@ -62,6 +67,7 @@ def format_example(self, lines: List[str]) -> List[str]: return format_lines def format_note(self, lines: List[str]) -> List[str]: + """Format notification section.""" format_lines = [".. note::", ""] for line in lines: format_lines.append(self.indent + line) @@ -70,6 +76,7 @@ def format_note(self, lines: List[str]) -> List[str]: return format_lines def format_tutorial(self, lines: List[str]) -> List[str]: + """Format tutorial section.""" format_lines = [".. rubric:: Tutorials", ""] format_lines.extend(lines) format_lines.append("") @@ -81,6 +88,7 @@ class ExperimentSectionFormatter(DocstringSectionFormatter): """Formatter for experiment class.""" def format_analysis_ref(self, lines: List[str]) -> List[str]: + """Format analysis class reference section.""" format_lines = [".. rubric:: Analysis Class Reference", ""] format_lines.extend(lines) format_lines.append("") @@ -88,6 +96,7 @@ def format_analysis_ref(self, lines: List[str]) -> List[str]: return format_lines def format_experiment_opts(self, lines: List[str]) -> List[str]: + """Format experiment options section.""" format_lines = [ ".. rubric:: Experiment Options", "", @@ -100,6 +109,7 @@ def format_experiment_opts(self, lines: List[str]) -> List[str]: return format_lines def format_analysis_opts(self, lines: List[str]) -> List[str]: + """Format analysis options section.""" format_lines = [ ".. rubric:: Analysis Options", "", @@ -112,6 +122,7 @@ def format_analysis_opts(self, lines: List[str]) -> List[str]: return format_lines def format_transpiler_opts(self, lines: List[str]) -> List[str]: + """Format transpiler options section.""" format_lines = [ ".. rubric:: Transpiler Options", "", @@ -124,6 +135,7 @@ def format_transpiler_opts(self, lines: List[str]) -> List[str]: return format_lines def format_run_opts(self, lines: List[str]) -> List[str]: + """Format run options section.""" format_lines = [ ".. rubric:: Backend Run Options", "", @@ -140,6 +152,7 @@ class AnalysisSectionFormatter(DocstringSectionFormatter): """Formatter for analysis class.""" def format_analysis_opts(self, lines: List[str]) -> List[str]: + """Format analysis options section.""" format_lines = [ ".. rubric:: Run Options", "", @@ -152,6 +165,7 @@ def format_analysis_opts(self, lines: List[str]) -> List[str]: return format_lines def format_fit_model(self, lines: List[str]) -> List[str]: + """Format fit model section.""" format_lines = [ ".. rubric:: Fit Model", "", @@ -165,6 +179,7 @@ def format_fit_model(self, lines: List[str]) -> List[str]: return format_lines def format_fit_parameters(self, lines: List[str]) -> List[str]: + """Format fit parameter section.""" format_lines = [ ".. rubric:: Fit Parameters", "", diff --git a/qiskit_experiments/documentation/section_parsers.py b/qiskit_experiments/documentation/section_parsers.py index d07aecc7b5..0501a20fcf 100644 --- a/qiskit_experiments/documentation/section_parsers.py +++ b/qiskit_experiments/documentation/section_parsers.py @@ -79,9 +79,7 @@ def write_description(header: str, kind: str): f" - :math:`{param}`: No description is provided. See source for details." ) else: - section_lines.append( - f" - :math:`{param}`: {desc[kind]}" - ) + section_lines.append(f" - :math:`{param}`: {desc[kind]}") section_lines.append("") write_description("Descriptions", "desc") diff --git a/qiskit_experiments/documentation/styles.py b/qiskit_experiments/documentation/styles.py index 31fb2eaaf1..55e7e01321 100644 --- a/qiskit_experiments/documentation/styles.py +++ b/qiskit_experiments/documentation/styles.py @@ -22,9 +22,13 @@ from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.base_experiment import BaseExperiment -from .formatter import ExperimentSectionFormatter, AnalysisSectionFormatter -from .utils import _generate_options_documentation, _format_default_options +from .formatter import ( + ExperimentSectionFormatter, + AnalysisSectionFormatter, + DocstringSectionFormatter, +) from .section_parsers import load_standard_section, load_fit_parameters +from .utils import _generate_options_documentation, _format_default_options section_regex = re.compile(r"# section: (?P\S+)") @@ -36,7 +40,7 @@ class QiskitExperimentDocstring(ABC): __sections__ = {} # section formatter - __formatter__ = None + __formatter__ = DocstringSectionFormatter def __init__( self, @@ -84,9 +88,9 @@ def add_new_section(section: str, lines: List[str]): # set new section current_section = section_name temp_lines.clear() - continue else: raise KeyError(f"Section name {section_name} is invalid.") + continue temp_lines.append(docstring_line) # parse final section add_new_section(current_section, temp_lines) @@ -149,11 +153,11 @@ class ExperimentDocstring(QiskitExperimentDocstring): __formatter__ = ExperimentSectionFormatter def __init__( - self, - target_cls: BaseExperiment, - docstring_lines: Union[str, List[str]], - config: SphinxConfig, - indent: str = "", + self, + target_cls: BaseExperiment, + docstring_lines: Union[str, List[str]], + config: SphinxConfig, + indent: str = "", ): """Create new parser and parse formatted docstring.""" super().__init__(target_cls, docstring_lines, config, indent) @@ -225,7 +229,7 @@ def _extra_sections(self) -> Dict[str, List[str]]: "This option is used for circuit optimization. ", "See `Qiskit Transpiler `_ documentation for available options.", - "" + "", ] transpiler_option_desc.extend( _format_default_options( @@ -243,7 +247,7 @@ def _extra_sections(self) -> Dict[str, List[str]]: "See provider's backend runner API for available options. " "See `here `_ for IBM Quantum Service.", - "" + "", ] run_option_desc.extend( _format_default_options( @@ -276,11 +280,11 @@ class AnalysisDocstring(QiskitExperimentDocstring): __formatter__ = AnalysisSectionFormatter def __init__( - self, - target_cls: BaseAnalysis, - docstring_lines: Union[str, List[str]], - config: SphinxConfig, - indent: str = "", + self, + target_cls: BaseAnalysis, + docstring_lines: Union[str, List[str]], + config: SphinxConfig, + indent: str = "", ): """Create new parser and parse formatted docstring.""" super().__init__(target_cls, docstring_lines, config, indent) diff --git a/qiskit_experiments/documentation/utils.py b/qiskit_experiments/documentation/utils.py index 0014048902..5af9e9453b 100644 --- a/qiskit_experiments/documentation/utils.py +++ b/qiskit_experiments/documentation/utils.py @@ -9,6 +9,9 @@ # Any modifications or derivative works of this code must retain this # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +""" +A collection of utilities to generate documentation. +""" import inspect import re @@ -44,8 +47,7 @@ def _parse_option_field( # use GoogleDocstring parameter parser experiment_option_parser = GoogleDocstring( - docstring=prepare_docstring(docstring, tabsize=len(indent)), - config=config + docstring=prepare_docstring(docstring, tabsize=len(indent)), config=config ) parsed_lines = experiment_option_parser.lines() @@ -82,9 +84,7 @@ def _generate_options_documentation( if current_class == object: # check if no more base class - raise Exception( - f"Option docstring for {', '.join(target_args)} is missing." - ) + raise Exception(f"Option docstring for {', '.join(target_args)} is missing.") options_docstring_lines = [] From c8ef173b51d5b5df5f340c7102302f1bfa68fe25 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 15 Jul 2021 18:49:21 +0900 Subject: [PATCH 37/46] add example of analysis default options --- .../example/example_experiment.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/documentation/example/example_experiment.py b/qiskit_experiments/documentation/example/example_experiment.py index e96fa42ee6..99baffc18c 100644 --- a/qiskit_experiments/documentation/example/example_experiment.py +++ b/qiskit_experiments/documentation/example/example_experiment.py @@ -86,6 +86,34 @@ class DocumentedCurveAnalysis(CurveAnalysis): """ + @classmethod + def _default_options(cls) -> Options: + """Default analysis options. + + .. note:: + + This method documentation should conforms to the below documentation syntax. + Namely, the title should be "Analysis Options" and description should be + written in the Google docstring style. Numpy style is not accepted. + Documentation except for the experiment options will be just ignored, e.g. this note. + If experiment options contains some values from the parent class, + the custom Sphinx parser searches for the parent class method documentation + and automatically generate documentation for all available options. + If there is any missing documentation the Sphinx build will fail. + + Analysis Options: + opt1 (int): Description for the option1. + opt2 (bool): Description for the option2. + opt3 (str): Description for the option3. + + """ + opts = super()._default_options() + opts.opt1 = 1.0 + opts.opt2 = True + opts.opt3 = "opt3" + + return opts + class DocumentedExperiment(BaseExperiment): """One line summary of this class. This is shown in the top level contains list. @@ -130,9 +158,9 @@ class DocumentedExperiment(BaseExperiment): This directive takes two arguments separated by a whitespace. The first argument is arbitrary label for this article, which may be used to refer to this paper from other sections. - Second argument is the arXiv ID of the paper to refer to. + Second argument is the arXiv ID of the paper referring to. Once this directive is inserted, Sphinx searches the arXiv database and - automatically generates a formatted reference sentence with the link to PDF. + automatically generates a formatted bibliography with the hyperlink to the online PDF. # section: tutorial From 768f2ff8bfe846653c34e4b931e30d05168ac3a0 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Fri, 16 Jul 2021 01:22:54 +0900 Subject: [PATCH 38/46] move package under docs --- docs/_ext/autodoc_analysis.py | 3 +-- docs/_ext/autodoc_experiment.py | 2 +- .../_ext/experiment_docs}/__init__.py | 9 ++------- .../_ext/experiment_docs}/example/__init__.py | 6 ------ .../_ext/experiment_docs}/example/example_experiment.py | 0 .../_ext/experiment_docs}/formatter.py | 0 .../_ext/experiment_docs}/section_parsers.py | 1 + .../_ext/experiment_docs}/styles.py | 0 .../documentation => docs/_ext/experiment_docs}/utils.py | 0 docs/conf.py | 1 + 10 files changed, 6 insertions(+), 16 deletions(-) rename {qiskit_experiments/documentation => docs/_ext/experiment_docs}/__init__.py (69%) rename {qiskit_experiments/documentation => docs/_ext/experiment_docs}/example/__init__.py (88%) rename {qiskit_experiments/documentation => docs/_ext/experiment_docs}/example/example_experiment.py (100%) rename {qiskit_experiments/documentation => docs/_ext/experiment_docs}/formatter.py (100%) rename {qiskit_experiments/documentation => docs/_ext/experiment_docs}/section_parsers.py (99%) rename {qiskit_experiments/documentation => docs/_ext/experiment_docs}/styles.py (100%) rename {qiskit_experiments/documentation => docs/_ext/experiment_docs}/utils.py (100%) diff --git a/docs/_ext/autodoc_analysis.py b/docs/_ext/autodoc_analysis.py index 3170bba450..a9b53c5a09 100644 --- a/docs/_ext/autodoc_analysis.py +++ b/docs/_ext/autodoc_analysis.py @@ -14,14 +14,13 @@ Documentation extension for analysis class. """ - from typing import Any from sphinx.application import Sphinx from sphinx.ext.autodoc import ClassDocumenter from qiskit_experiments import BaseAnalysis -from qiskit_experiments.documentation.styles import AnalysisDocstring +from .experiment_docs import AnalysisDocstring class AnalysisDocumenter(ClassDocumenter): diff --git a/docs/_ext/autodoc_experiment.py b/docs/_ext/autodoc_experiment.py index 455238fc9a..df155a09d9 100644 --- a/docs/_ext/autodoc_experiment.py +++ b/docs/_ext/autodoc_experiment.py @@ -21,7 +21,7 @@ from sphinx.ext.autodoc import ClassDocumenter from qiskit_experiments import BaseExperiment -from qiskit_experiments.documentation.styles import ExperimentDocstring +from .experiment_docs import ExperimentDocstring class ExperimentDocumenter(ClassDocumenter): diff --git a/qiskit_experiments/documentation/__init__.py b/docs/_ext/experiment_docs/__init__.py similarity index 69% rename from qiskit_experiments/documentation/__init__.py rename to docs/_ext/experiment_docs/__init__.py index d502194c0f..afe585ecaf 100644 --- a/qiskit_experiments/documentation/__init__.py +++ b/docs/_ext/experiment_docs/__init__.py @@ -11,11 +11,6 @@ # that they have been altered from the originals. """ Automatic documentation package. - -.. warning:: - - This module uses requirements-dev packages related to Sphinx and journal search. - Thus this should not be imported by the top-level module. - This module is not used except for the Sphinx documentation build. - """ + +from .styles import ExperimentDocstring, AnalysisDocstring diff --git a/qiskit_experiments/documentation/example/__init__.py b/docs/_ext/experiment_docs/example/__init__.py similarity index 88% rename from qiskit_experiments/documentation/example/__init__.py rename to docs/_ext/experiment_docs/example/__init__.py index 815328a676..fc54e4b2c8 100644 --- a/qiskit_experiments/documentation/example/__init__.py +++ b/docs/_ext/experiment_docs/example/__init__.py @@ -45,10 +45,4 @@ """ -import warnings from .example_experiment import DocumentedExperiment, DocumentedCurveAnalysis - -warnings.warn( - "This module dosen't implement actual experiment. " - "This is just a mock module to show the schema of experiment documentation." -) diff --git a/qiskit_experiments/documentation/example/example_experiment.py b/docs/_ext/experiment_docs/example/example_experiment.py similarity index 100% rename from qiskit_experiments/documentation/example/example_experiment.py rename to docs/_ext/experiment_docs/example/example_experiment.py diff --git a/qiskit_experiments/documentation/formatter.py b/docs/_ext/experiment_docs/formatter.py similarity index 100% rename from qiskit_experiments/documentation/formatter.py rename to docs/_ext/experiment_docs/formatter.py diff --git a/qiskit_experiments/documentation/section_parsers.py b/docs/_ext/experiment_docs/section_parsers.py similarity index 99% rename from qiskit_experiments/documentation/section_parsers.py rename to docs/_ext/experiment_docs/section_parsers.py index 0501a20fcf..870d2011ae 100644 --- a/qiskit_experiments/documentation/section_parsers.py +++ b/docs/_ext/experiment_docs/section_parsers.py @@ -16,6 +16,7 @@ import re from typing import List + from .utils import _trim_empty_lines diff --git a/qiskit_experiments/documentation/styles.py b/docs/_ext/experiment_docs/styles.py similarity index 100% rename from qiskit_experiments/documentation/styles.py rename to docs/_ext/experiment_docs/styles.py diff --git a/qiskit_experiments/documentation/utils.py b/docs/_ext/experiment_docs/utils.py similarity index 100% rename from qiskit_experiments/documentation/utils.py rename to docs/_ext/experiment_docs/utils.py diff --git a/docs/conf.py b/docs/conf.py index 9fed00b22b..5f604ad4f8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,6 +27,7 @@ import sys sys.path.insert(0, os.path.abspath('.')) sys.path.append(os.path.abspath("./_ext")) +sys.path.append(os.path.abspath("./_ext/experiment_docs")) """ Sphinx documentation builder From bf9e3a4910aa4e53bd59c51470baf135dd2f5e9e Mon Sep 17 00:00:00 2001 From: knzwnao Date: Fri, 16 Jul 2021 01:37:59 +0900 Subject: [PATCH 39/46] fix import path --- docs/_ext/autodoc_analysis.py | 2 +- docs/_ext/autodoc_experiment.py | 2 +- .../example/__init__.py | 2 -- .../example/example_experiment.py | 0 .../formatter.py | 0 .../section_parsers.py | 0 .../{experiment_docs => custom_styles}/styles.py | 0 .../{experiment_docs => custom_styles}/utils.py | 0 docs/_ext/experiment_docs/__init__.py | 16 ---------------- docs/conf.py | 1 - 10 files changed, 2 insertions(+), 21 deletions(-) rename docs/_ext/{experiment_docs => custom_styles}/example/__init__.py (93%) rename docs/_ext/{experiment_docs => custom_styles}/example/example_experiment.py (100%) rename docs/_ext/{experiment_docs => custom_styles}/formatter.py (100%) rename docs/_ext/{experiment_docs => custom_styles}/section_parsers.py (100%) rename docs/_ext/{experiment_docs => custom_styles}/styles.py (100%) rename docs/_ext/{experiment_docs => custom_styles}/utils.py (100%) delete mode 100644 docs/_ext/experiment_docs/__init__.py diff --git a/docs/_ext/autodoc_analysis.py b/docs/_ext/autodoc_analysis.py index a9b53c5a09..2d947f76ce 100644 --- a/docs/_ext/autodoc_analysis.py +++ b/docs/_ext/autodoc_analysis.py @@ -20,7 +20,7 @@ from sphinx.ext.autodoc import ClassDocumenter from qiskit_experiments import BaseAnalysis -from .experiment_docs import AnalysisDocstring +from docs._ext.custom_styles.styles import AnalysisDocstring class AnalysisDocumenter(ClassDocumenter): diff --git a/docs/_ext/autodoc_experiment.py b/docs/_ext/autodoc_experiment.py index df155a09d9..1cc0a44cbf 100644 --- a/docs/_ext/autodoc_experiment.py +++ b/docs/_ext/autodoc_experiment.py @@ -21,7 +21,7 @@ from sphinx.ext.autodoc import ClassDocumenter from qiskit_experiments import BaseExperiment -from .experiment_docs import ExperimentDocstring +from docs._ext.custom_styles.styles import ExperimentDocstring class ExperimentDocumenter(ClassDocumenter): diff --git a/docs/_ext/experiment_docs/example/__init__.py b/docs/_ext/custom_styles/example/__init__.py similarity index 93% rename from docs/_ext/experiment_docs/example/__init__.py rename to docs/_ext/custom_styles/example/__init__.py index fc54e4b2c8..d2aeb844fd 100644 --- a/docs/_ext/experiment_docs/example/__init__.py +++ b/docs/_ext/custom_styles/example/__init__.py @@ -31,7 +31,6 @@ =========== .. autosummary:: :toctree: ../stubs/ - :template: autosummary/experiment.rst DocumentedExperiment @@ -39,7 +38,6 @@ ======== .. autosummary:: :toctree: ../stubs/ - :template: autosummary/analysis.rst DocumentedCurveAnalysis diff --git a/docs/_ext/experiment_docs/example/example_experiment.py b/docs/_ext/custom_styles/example/example_experiment.py similarity index 100% rename from docs/_ext/experiment_docs/example/example_experiment.py rename to docs/_ext/custom_styles/example/example_experiment.py diff --git a/docs/_ext/experiment_docs/formatter.py b/docs/_ext/custom_styles/formatter.py similarity index 100% rename from docs/_ext/experiment_docs/formatter.py rename to docs/_ext/custom_styles/formatter.py diff --git a/docs/_ext/experiment_docs/section_parsers.py b/docs/_ext/custom_styles/section_parsers.py similarity index 100% rename from docs/_ext/experiment_docs/section_parsers.py rename to docs/_ext/custom_styles/section_parsers.py diff --git a/docs/_ext/experiment_docs/styles.py b/docs/_ext/custom_styles/styles.py similarity index 100% rename from docs/_ext/experiment_docs/styles.py rename to docs/_ext/custom_styles/styles.py diff --git a/docs/_ext/experiment_docs/utils.py b/docs/_ext/custom_styles/utils.py similarity index 100% rename from docs/_ext/experiment_docs/utils.py rename to docs/_ext/custom_styles/utils.py diff --git a/docs/_ext/experiment_docs/__init__.py b/docs/_ext/experiment_docs/__init__.py deleted file mode 100644 index afe585ecaf..0000000000 --- a/docs/_ext/experiment_docs/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Automatic documentation package. -""" - -from .styles import ExperimentDocstring, AnalysisDocstring diff --git a/docs/conf.py b/docs/conf.py index 5f604ad4f8..9fed00b22b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,6 @@ import sys sys.path.insert(0, os.path.abspath('.')) sys.path.append(os.path.abspath("./_ext")) -sys.path.append(os.path.abspath("./_ext/experiment_docs")) """ Sphinx documentation builder From 1e4dd09be722e4331215cd7fbd4486b308392062 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Fri, 16 Jul 2021 02:10:17 +0900 Subject: [PATCH 40/46] fix missing docs --- docs/_ext/custom_styles/utils.py | 2 +- qiskit_experiments/analysis/curve_analysis.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/_ext/custom_styles/utils.py b/docs/_ext/custom_styles/utils.py index 5af9e9453b..380d788498 100644 --- a/docs/_ext/custom_styles/utils.py +++ b/docs/_ext/custom_styles/utils.py @@ -98,7 +98,7 @@ def _generate_options_documentation( # parse default options method parsed_lines, target_args = _parse_option_field( - docstring=default_opts.__doc__, + docstring=default_opts.__doc__ or "", config=config, target_args=target_args, indent=indent, diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 93821f48cb..1b08617530 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -306,7 +306,7 @@ def __init__(self): def _default_options(cls) -> Options: """Return default analysis options. - Options: + Analysis Options: curve_fitter (Callable): A callback function to perform fitting with formatted data. See :func:`~qiskit_experiments.analysis.multi_curve_fit` for example. data_processor (Callable): A callback function to format experiment data. @@ -322,6 +322,7 @@ def _default_options(cls) -> Options: axis (AxesSubplot): Optional. A matplotlib axis object to draw. xlabel (str): X label of fit result figure. ylabel (str): Y label of fit result figure. + ylim (Tuple[float, float]): Min and max height limit of fit plot. fit_reports (Dict[str, str]): Mapping of fit parameters and representation in the fit report. return_data_points (bool): Set ``True`` to return formatted XY data. From f6cda8f81fd44a21aaf89c7ab0b82f3515c5934f Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 20 Jul 2021 00:45:23 +0900 Subject: [PATCH 41/46] comment from Eli - add template for example - section indent and rf rule update --- docs/_ext/custom_styles/example/__init__.py | 2 + .../example/example_experiment.py | 153 +++++++++--------- docs/_ext/custom_styles/styles.py | 9 +- 3 files changed, 82 insertions(+), 82 deletions(-) diff --git a/docs/_ext/custom_styles/example/__init__.py b/docs/_ext/custom_styles/example/__init__.py index d2aeb844fd..fc54e4b2c8 100644 --- a/docs/_ext/custom_styles/example/__init__.py +++ b/docs/_ext/custom_styles/example/__init__.py @@ -31,6 +31,7 @@ =========== .. autosummary:: :toctree: ../stubs/ + :template: autosummary/experiment.rst DocumentedExperiment @@ -38,6 +39,7 @@ ======== .. autosummary:: :toctree: ../stubs/ + :template: autosummary/analysis.rst DocumentedCurveAnalysis diff --git a/docs/_ext/custom_styles/example/example_experiment.py b/docs/_ext/custom_styles/example/example_experiment.py index 99baffc18c..6c5debf33a 100644 --- a/docs/_ext/custom_styles/example/example_experiment.py +++ b/docs/_ext/custom_styles/example/example_experiment.py @@ -28,61 +28,58 @@ class DocumentedCurveAnalysis(CurveAnalysis): r"""One line summary of this class. This is shown in the top level contains list. # section: overview + Overview of this experiment. It is recommended to write this section. + Here you can explain technical aspect of fit algorithm or fit model. + Standard reStructuredText directives can be used. + No section level indent is necessary. - Overview of this experiment. It is recommended to write this section. - Here you can explain technical aspect of fit algorithm or fit model. - Standard reStructuredText directives can be used. - No section level indent is necessary. + You can use following sections - You can use following sections + - ``warning`` + - ``note`` + - ``example`` + - ``reference`` + - ``tutorial`` - - ``warning`` - - ``note`` - - ``example`` - - ``reference`` - - ``tutorial`` - - See :class:`DocumentedExperiment` for description of these sections. - In addition to above sections, analysis template provides following extra sections. + See :class:`DocumentedExperiment` for description of these sections. + In addition to above sections, analysis template provides following extra sections. # section: fit_model: + Here you can describe your fitting model. + Standard reStructuredText directives can be used. For example: - Here you can describe your fitting model. - Standard reStructuredText directives can be used. For example: - - .. math:: + .. math:: - F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b + F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b - enables you to use the Latex syntax to write your equation. + enables you to use the Latex syntax to write your equation. # section: fit_parameters - - Here you can explain fit parameter details. - This section provides a special syntax to describe details of each parameter. - Documentation except for this syntax will be just ignored. - - defpar a: - desc: Description of parameter :math:`a`. - init_guess: Here you can describe how this analysis estimate initial guess of - parameter :math:`a`. - bounds: Here you can describe how this analysis bounds parameter :math:`a` value - during the fit. - - defpar b: - desc: Description of parameter :math:`b`. - init_guess: Here you can describe how this analysis estimate initial guess of - parameter :math:`b`. - bounds: Here you can describe how this analysis bounds parameter :math:`b` value - during the fit. - - Note that you cannot write text block (i.e. bullet lines, math mode, parsed literal, ...) - in the ``defpar`` syntax items. These are a single line description of parameters. - You can write multiple ``defpar`` block for each fitting parameter. - - It would be nice if parameter names conform to the parameter key values appearing in the - analysis result. For example, if fit model defines the parameter :math:`\sigma` and - this appears as ``eta`` in the result, user cannot find correspondence of these parameters. + Here you can explain fit parameter details. + This section provides a special syntax to describe details of each parameter. + Documentation except for this syntax will be just ignored. + + defpar a: + desc: Description of parameter :math:`a`. + init_guess: Here you can describe how this analysis estimate initial guess of + parameter :math:`a`. + bounds: Here you can describe how this analysis bounds parameter :math:`a` value + during the fit. + + defpar b: + desc: Description of parameter :math:`b`. + init_guess: Here you can describe how this analysis estimate initial guess of + parameter :math:`b`. + bounds: Here you can describe how this analysis bounds parameter :math:`b` value + during the fit. + + Note that you cannot write text block (i.e. bullet lines, math mode, parsed literal, ...) + in the ``defpar`` syntax items. These are a single line description of parameters. + You can write multiple ``defpar`` block for each fitting parameter. + + It would be nice if parameter names conform to the parameter key values appearing in the + analysis result. For example, if fit model defines the parameter :math:`\sigma` and + this appears as ``eta`` in the result, user cannot find correspondence of these parameters. """ @@ -119,59 +116,53 @@ class DocumentedExperiment(BaseExperiment): """One line summary of this class. This is shown in the top level contains list. # section: overview - - Overview of this experiment. It is recommended to write this section. - Here you can explain technical aspect of experiment, protocol, etc... - Standard reStructuredText directives can be used. - No section level indent is necessary. + Overview of this experiment. It is recommended to write this section. + Here you can explain technical aspect of experiment, protocol, etc... + Standard reStructuredText directives can be used. + No section level indent is necessary. # section: warning - - Warning about this experiment if exist. - Some functionality is not available or under development, - you should write these details here. + Warning about this experiment if exist. + Some functionality is not available or under development, + you should write these details here. # section: note - - Notification about this experiment if exist. + Notification about this experiment if exist. # section: example + Example code of this experiment. + If this experiment requires user to manage complicated options, + it might be convenient for users to have some code example here. - Example code of this experiment. - If this experiment requires user to manage complicated options, - it might be convenient for users to have some code example here. + You can write code example, for example, as follows - You can write code example, for example, as follows + .. code-block:: python - .. code-block:: python - - import qiskit_experiments - my_experiment = qiskit_experiments.MyExperiment(**options) + import qiskit_experiments + my_experiment = qiskit_experiments.MyExperiment(**options) # section: reference + Currently this supports article reference in arXiv database. + You can use following helper directive. - Currently this supports article reference in arXiv database. - You can use following helper directive. - - .. ref_arxiv:: Auth2020a 21xx.01xxx + .. ref_arxiv:: Auth2020a 21xx.01xxx - This directive takes two arguments separated by a whitespace. - The first argument is arbitrary label for this article, which may be used to - refer to this paper from other sections. - Second argument is the arXiv ID of the paper referring to. - Once this directive is inserted, Sphinx searches the arXiv database and - automatically generates a formatted bibliography with the hyperlink to the online PDF. + This directive takes two arguments separated by a whitespace. + The first argument is arbitrary label for this article, which may be used to + refer to this paper from other sections. + Second argument is the arXiv ID of the paper referring to. + Once this directive is inserted, Sphinx searches the arXiv database and + automatically generates a formatted bibliography with the hyperlink to the online PDF. # section: tutorial + You can refer to the arbitrary web page here. + Following helper directive can be used. - You can refer to the arbitrary web page here. - Following helper directive can be used. - - .. ref_website:: Qiskit Experiment Github, https://github.com/Qiskit/qiskit-experiments + .. ref_website:: Qiskit Experiment Github, https://github.com/Qiskit/qiskit-experiments - This directive takes two arguments separated by a comma. - The first argument is arbitrary label shown before the link. Whitespace can be included. - The second argument is the URL of the website to hyperlink. + This directive takes two arguments separated by a comma. + The first argument is arbitrary label shown before the link. Whitespace can be included. + The second argument is the URL of the website to hyperlink. """ diff --git a/docs/_ext/custom_styles/styles.py b/docs/_ext/custom_styles/styles.py index 55e7e01321..91127a1034 100644 --- a/docs/_ext/custom_styles/styles.py +++ b/docs/_ext/custom_styles/styles.py @@ -91,7 +91,14 @@ def add_new_section(section: str, lines: List[str]): else: raise KeyError(f"Section name {section_name} is invalid.") continue - temp_lines.append(docstring_line) + + # remove section indent + if docstring_line.startswith(self._indent): + begin = len(self._indent) + else: + begin = 0 + temp_lines.append(docstring_line[begin:]) + # parse final section add_new_section(current_section, temp_lines) From 38327ead68d04cb64e26ec89e4bb285bf703308a Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 20 Jul 2021 01:53:18 +0900 Subject: [PATCH 42/46] fix indent correction logic --- .../example/example_experiment.py | 2 +- docs/_ext/custom_styles/styles.py | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/_ext/custom_styles/example/example_experiment.py b/docs/_ext/custom_styles/example/example_experiment.py index 6c5debf33a..4e86f096e4 100644 --- a/docs/_ext/custom_styles/example/example_experiment.py +++ b/docs/_ext/custom_styles/example/example_experiment.py @@ -44,7 +44,7 @@ class DocumentedCurveAnalysis(CurveAnalysis): See :class:`DocumentedExperiment` for description of these sections. In addition to above sections, analysis template provides following extra sections. - # section: fit_model: + # section: fit_model Here you can describe your fitting model. Standard reStructuredText directives can be used. For example: diff --git a/docs/_ext/custom_styles/styles.py b/docs/_ext/custom_styles/styles.py index 91127a1034..4514459e2a 100644 --- a/docs/_ext/custom_styles/styles.py +++ b/docs/_ext/custom_styles/styles.py @@ -15,6 +15,7 @@ """ import copy import re +import sys from abc import ABC from typing import Union, List, Dict @@ -78,28 +79,35 @@ def add_new_section(section: str, lines: List[str]): current_section = list(self.__sections__.keys())[0] temp_lines = list() + margin = sys.maxsize for docstring_line in docstrings: match = re.match(section_regex, docstring_line) if match: section_name = match["section_name"] if section_name in self.__sections__: # parse previous section + if margin < sys.maxsize: + temp_lines = [l[margin:] for l in temp_lines] add_new_section(current_section, temp_lines) # set new section current_section = section_name temp_lines.clear() + margin = sys.maxsize else: raise KeyError(f"Section name {section_name} is invalid.") continue - # remove section indent - if docstring_line.startswith(self._indent): - begin = len(self._indent) - else: - begin = 0 - temp_lines.append(docstring_line[begin:]) + # calculate section indent + if len(docstring_line) > 0: + # ignore empty line + indent = len(docstring_line) - len(docstring_line.lstrip()) + margin = min(indent, margin) + + temp_lines.append(docstring_line) # parse final section + if margin < sys.maxsize: + temp_lines = [l[margin:] for l in temp_lines] add_new_section(current_section, temp_lines) for section, lines in self._extra_sections().items(): From cf320e60059621ec69e93b5c5a7443b5a40ee771 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 20 Jul 2021 02:22:23 +0900 Subject: [PATCH 43/46] update example --- docs/_ext/custom_styles/example/example_experiment.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/_ext/custom_styles/example/example_experiment.py b/docs/_ext/custom_styles/example/example_experiment.py index 4e86f096e4..42b1b84ee7 100644 --- a/docs/_ext/custom_styles/example/example_experiment.py +++ b/docs/_ext/custom_styles/example/example_experiment.py @@ -31,7 +31,6 @@ class DocumentedCurveAnalysis(CurveAnalysis): Overview of this experiment. It is recommended to write this section. Here you can explain technical aspect of fit algorithm or fit model. Standard reStructuredText directives can be used. - No section level indent is necessary. You can use following sections @@ -119,7 +118,6 @@ class DocumentedExperiment(BaseExperiment): Overview of this experiment. It is recommended to write this section. Here you can explain technical aspect of experiment, protocol, etc... Standard reStructuredText directives can be used. - No section level indent is necessary. # section: warning Warning about this experiment if exist. From 7eed8ac5cc3069a4c28a16213f459eda446d8efc Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 22 Jul 2021 02:01:47 +0900 Subject: [PATCH 44/46] robust to indent error - add extra indent check - autofix section indent error --- docs/_ext/custom_styles/formatter.py | 13 +++++++++++++ docs/_ext/custom_styles/styles.py | 4 ++-- docs/_ext/custom_styles/utils.py | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/_ext/custom_styles/formatter.py b/docs/_ext/custom_styles/formatter.py index 5f1f4b2a04..40c61892ac 100644 --- a/docs/_ext/custom_styles/formatter.py +++ b/docs/_ext/custom_styles/formatter.py @@ -14,6 +14,7 @@ A class that formats documentation sections. """ from typing import List +from .utils import _check_no_indent class DocstringSectionFormatter: @@ -33,6 +34,7 @@ def format_header(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_overview(self, lines: List[str]) -> List[str]: """Format overview section.""" format_lines = [".. rubric:: Overview", ""] @@ -41,6 +43,7 @@ def format_overview(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_reference(self, lines: List[str]) -> List[str]: """Format reference section.""" format_lines = [".. rubric:: References", ""] @@ -58,6 +61,7 @@ def format_warning(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_example(self, lines: List[str]) -> List[str]: """Format example section.""" format_lines = [".. rubric:: Example", ""] @@ -75,6 +79,7 @@ def format_note(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_tutorial(self, lines: List[str]) -> List[str]: """Format tutorial section.""" format_lines = [".. rubric:: Tutorials", ""] @@ -87,6 +92,7 @@ def format_tutorial(self, lines: List[str]) -> List[str]: class ExperimentSectionFormatter(DocstringSectionFormatter): """Formatter for experiment class.""" + @_check_no_indent def format_analysis_ref(self, lines: List[str]) -> List[str]: """Format analysis class reference section.""" format_lines = [".. rubric:: Analysis Class Reference", ""] @@ -95,6 +101,7 @@ def format_analysis_ref(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_experiment_opts(self, lines: List[str]) -> List[str]: """Format experiment options section.""" format_lines = [ @@ -108,6 +115,7 @@ def format_experiment_opts(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_analysis_opts(self, lines: List[str]) -> List[str]: """Format analysis options section.""" format_lines = [ @@ -121,6 +129,7 @@ def format_analysis_opts(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_transpiler_opts(self, lines: List[str]) -> List[str]: """Format transpiler options section.""" format_lines = [ @@ -134,6 +143,7 @@ def format_transpiler_opts(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_run_opts(self, lines: List[str]) -> List[str]: """Format run options section.""" format_lines = [ @@ -151,6 +161,7 @@ def format_run_opts(self, lines: List[str]) -> List[str]: class AnalysisSectionFormatter(DocstringSectionFormatter): """Formatter for analysis class.""" + @_check_no_indent def format_analysis_opts(self, lines: List[str]) -> List[str]: """Format analysis options section.""" format_lines = [ @@ -164,6 +175,7 @@ def format_analysis_opts(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_fit_model(self, lines: List[str]) -> List[str]: """Format fit model section.""" format_lines = [ @@ -178,6 +190,7 @@ def format_fit_model(self, lines: List[str]) -> List[str]: return format_lines + @_check_no_indent def format_fit_parameters(self, lines: List[str]) -> List[str]: """Format fit parameter section.""" format_lines = [ diff --git a/docs/_ext/custom_styles/styles.py b/docs/_ext/custom_styles/styles.py index 4514459e2a..f51b29851c 100644 --- a/docs/_ext/custom_styles/styles.py +++ b/docs/_ext/custom_styles/styles.py @@ -81,7 +81,7 @@ def add_new_section(section: str, lines: List[str]): temp_lines = list() margin = sys.maxsize for docstring_line in docstrings: - match = re.match(section_regex, docstring_line) + match = re.match(section_regex, docstring_line.strip()) if match: section_name = match["section_name"] if section_name in self.__sections__: @@ -98,7 +98,7 @@ def add_new_section(section: str, lines: List[str]): continue # calculate section indent - if len(docstring_line) > 0: + if len(docstring_line) > 0 and not docstring_line.isspace(): # ignore empty line indent = len(docstring_line) - len(docstring_line.lstrip()) margin = min(indent, margin) diff --git a/docs/_ext/custom_styles/utils.py b/docs/_ext/custom_styles/utils.py index 380d788498..46e7e63381 100644 --- a/docs/_ext/custom_styles/utils.py +++ b/docs/_ext/custom_styles/utils.py @@ -15,7 +15,7 @@ import inspect import re -from typing import List, Tuple, Dict, Any +from typing import List, Tuple, Dict, Any, Callable from sphinx.config import Config as SphinxConfig from sphinx.ext.napoleon.docstring import GoogleDocstring @@ -146,3 +146,17 @@ def _format_default_options(defaults: Dict[str, Any], indent: str = "") -> List[ docstring_lines.append(indent * 2 + f"{par:<25} := {value_repr}") return docstring_lines + + +def _check_no_indent(method: Callable) -> Callable: + """Check indent of lines and return if this block is correctly indented.""" + def wraps(self, lines: List[str], *args, **kwargs): + if all(l.startswith(" ") for l in lines): + text_block = "\n".join(lines) + raise ValueError( + "Following documentation may have invalid indentation. " + f"Please carefully check all indent levels are aligned. \n\n{text_block}" + ) + return method(self, lines, *args, **kwargs) + + return wraps From b497f7cdb7306dd0d343436b7ebdf194869b9208 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 22 Jul 2021 03:38:31 +0900 Subject: [PATCH 45/46] fix import paths --- docs/_ext/autodoc_analysis.py | 5 ++--- docs/_ext/autodoc_experiment.py | 5 ++--- docs/_ext/custom_styles/example/example_experiment.py | 4 ++-- docs/_ext/custom_styles/styles.py | 4 ++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/_ext/autodoc_analysis.py b/docs/_ext/autodoc_analysis.py index 2d947f76ce..73c0cac5a9 100644 --- a/docs/_ext/autodoc_analysis.py +++ b/docs/_ext/autodoc_analysis.py @@ -16,12 +16,11 @@ from typing import Any +from docs._ext.custom_styles.styles import AnalysisDocstring +from qiskit_experiments.framework.base_analysis import BaseAnalysis from sphinx.application import Sphinx from sphinx.ext.autodoc import ClassDocumenter -from qiskit_experiments import BaseAnalysis -from docs._ext.custom_styles.styles import AnalysisDocstring - class AnalysisDocumenter(ClassDocumenter): """Sphinx extension for the custom documentation of the standard analysis class.""" diff --git a/docs/_ext/autodoc_experiment.py b/docs/_ext/autodoc_experiment.py index 1cc0a44cbf..389854d881 100644 --- a/docs/_ext/autodoc_experiment.py +++ b/docs/_ext/autodoc_experiment.py @@ -16,13 +16,12 @@ from typing import Any +from docs._ext.custom_styles.styles import ExperimentDocstring from qiskit.exceptions import QiskitError +from qiskit_experiments.framework.base_experiment import BaseExperiment from sphinx.application import Sphinx from sphinx.ext.autodoc import ClassDocumenter -from qiskit_experiments import BaseExperiment -from docs._ext.custom_styles.styles import ExperimentDocstring - class ExperimentDocumenter(ClassDocumenter): """Sphinx extension for the custom documentation of the standard experiment class.""" diff --git a/docs/_ext/custom_styles/example/example_experiment.py b/docs/_ext/custom_styles/example/example_experiment.py index 42b1b84ee7..4e1663460d 100644 --- a/docs/_ext/custom_styles/example/example_experiment.py +++ b/docs/_ext/custom_styles/example/example_experiment.py @@ -20,8 +20,8 @@ from qiskit.providers import Options -from qiskit_experiments.analysis import CurveAnalysis -from qiskit_experiments.base_experiment import BaseExperiment +from qiskit_experiments.curve_analysis.curve_analysis import CurveAnalysis +from qiskit_experiments.framework.base_experiment import BaseExperiment class DocumentedCurveAnalysis(CurveAnalysis): diff --git a/docs/_ext/custom_styles/styles.py b/docs/_ext/custom_styles/styles.py index f51b29851c..91d2dfaa3c 100644 --- a/docs/_ext/custom_styles/styles.py +++ b/docs/_ext/custom_styles/styles.py @@ -19,10 +19,10 @@ from abc import ABC from typing import Union, List, Dict +from qiskit_experiments.framework.base_analysis import BaseAnalysis +from qiskit_experiments.framework.base_experiment import BaseExperiment from sphinx.config import Config as SphinxConfig -from qiskit_experiments.base_analysis import BaseAnalysis -from qiskit_experiments.base_experiment import BaseExperiment from .formatter import ( ExperimentSectionFormatter, AnalysisSectionFormatter, From f7c907eb27120491ba8d68d367228c02b226728a Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 22 Jul 2021 04:22:04 +0900 Subject: [PATCH 46/46] fix example documentation --- .../example/example_experiment.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/_ext/custom_styles/example/example_experiment.py b/docs/_ext/custom_styles/example/example_experiment.py index 4e1663460d..1fa3a0ac4a 100644 --- a/docs/_ext/custom_styles/example/example_experiment.py +++ b/docs/_ext/custom_styles/example/example_experiment.py @@ -28,7 +28,7 @@ class DocumentedCurveAnalysis(CurveAnalysis): r"""One line summary of this class. This is shown in the top level contains list. # section: overview - Overview of this experiment. It is recommended to write this section. + Overview of this analysis. It is recommended to write this section. Here you can explain technical aspect of fit algorithm or fit model. Standard reStructuredText directives can be used. @@ -89,10 +89,15 @@ def _default_options(cls) -> Options: .. note:: This method documentation should conforms to the below documentation syntax. - Namely, the title should be "Analysis Options" and description should be - written in the Google docstring style. Numpy style is not accepted. - Documentation except for the experiment options will be just ignored, e.g. this note. - If experiment options contains some values from the parent class, + Namely, the title should be "Analysis Options" followed by a single colon + and description should be written in the Google docstring style. + Numpy style is not accepted. + + Google style docstring guideline: + https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html + + Documentation except for the analysis options will be just ignored, e.g. this note. + If analysis options contains some values from the parent class, the custom Sphinx parser searches for the parent class method documentation and automatically generate documentation for all available options. If there is any missing documentation the Sphinx build will fail. @@ -171,8 +176,13 @@ def _default_experiment_options(cls) -> Options: .. note:: This method documentation should conforms to the below documentation syntax. - Namely, the title should be "Experiment Options" and description should be - written in the Google docstring style. Numpy style is not accepted. + Namely, the title should be "Experiment Options" followed by a single colon + and description should be written in the Google docstring style. + Numpy style is not accepted. + + Google style docstring guideline: + https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html + Documentation except for the experiment options will be just ignored, e.g. this note. If experiment options contains some values from the parent class, the custom Sphinx parser searches for the parent class method documentation