From d3af4758394b59396946db3ba1e5f6788a1bac1a Mon Sep 17 00:00:00 2001 From: jessieyu Date: Wed, 16 Jun 2021 20:29:42 -0400 Subject: [PATCH 01/70] move from terra --- qiskit_experiments/store_data/__init__.py | 53 + .../store_data/analysis_result.py | 376 ++++++ .../store_data/device_component.py | 76 ++ qiskit_experiments/store_data/exceptions.py | 33 + .../store_data/experiment_service.py | 436 +++++++ qiskit_experiments/store_data/json.py | 47 + qiskit_experiments/store_data/stored_data.py | 1036 +++++++++++++++++ qiskit_experiments/store_data/utils.py | 219 ++++ test/stored_data/__init__.py | 13 + test/stored_data/test_analysisresult.py | 173 +++ test/stored_data/test_stored_data.py | 712 +++++++++++ 11 files changed, 3174 insertions(+) create mode 100644 qiskit_experiments/store_data/__init__.py create mode 100644 qiskit_experiments/store_data/analysis_result.py create mode 100644 qiskit_experiments/store_data/device_component.py create mode 100644 qiskit_experiments/store_data/exceptions.py create mode 100644 qiskit_experiments/store_data/experiment_service.py create mode 100644 qiskit_experiments/store_data/json.py create mode 100644 qiskit_experiments/store_data/stored_data.py create mode 100644 qiskit_experiments/store_data/utils.py create mode 100644 test/stored_data/__init__.py create mode 100644 test/stored_data/test_analysisresult.py create mode 100644 test/stored_data/test_stored_data.py diff --git a/qiskit_experiments/store_data/__init__.py b/qiskit_experiments/store_data/__init__.py new file mode 100644 index 0000000000..e3ad311c44 --- /dev/null +++ b/qiskit_experiments/store_data/__init__.py @@ -0,0 +1,53 @@ +# 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. + +""" +=================================================== +Stored Data (:mod:`qiskit_experiments.stored_data`) +=================================================== + +.. currentmodule:: qiskit_experiments.stored_data + +This module contains the classes used to define the data structure of +an experiment, including its data, metadata, analysis results, and figures. +The classes also provide an interface with a database service for storing +and retrieving experiment-related data. + +Classes +======= + +.. autosummary:: + :toctree: ../stubs/ + + StoredData + StoredDataV1 + AnalysisResult + AnalysisResultV1 + ExperimentService + ExperimentServiceV1 + + +Exceptions +========== + +.. autosummary:: + :toctree: ../stubs/ + + ExperimentError + ExperimentEntryNotFound + ExperimentEntryExists +""" + +from .stored_data import StoredData, StoredDataV1 +from .analysis_result import AnalysisResult, AnalysisResultV1 +from .experiment_service import ExperimentService, ExperimentServiceV1 +from .exceptions import ExperimentError, ExperimentEntryExists, ExperimentEntryNotFound diff --git a/qiskit_experiments/store_data/analysis_result.py b/qiskit_experiments/store_data/analysis_result.py new file mode 100644 index 0000000000..cf3e17530e --- /dev/null +++ b/qiskit_experiments/store_data/analysis_result.py @@ -0,0 +1,376 @@ +# 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. + +"""Analysis result abstract interface.""" + +import logging +from typing import Optional, List, Union, Dict, Callable, Any +import uuid +import json +import copy +from functools import wraps + +from .json import NumpyEncoder, NumpyDecoder +from .utils import save_data, qiskit_version +from .exceptions import ExperimentError +from .device_component import DeviceComponent, to_component + +LOG = logging.getLogger(__name__) + + +def auto_save(func: Callable): + """Decorate the input function to auto save data.""" + + @wraps(func) + def _wrapped(self, *args, **kwargs): + return_val = func(self, *args, **kwargs) + if self.auto_save: + self.save() + return return_val + + return _wrapped + + +class AnalysisResult: + """Base common type for all versioned AnalysisResult abstract classes. + + Note this class should not be inherited from directly, it is intended + to be used for type checking. When implementing a provider you should use + the versioned abstract classes as the parent class and not this class + directly. + """ + + version = 0 + + +class AnalysisResultV1(AnalysisResult): + """Class representing an analysis result for an experiment.""" + + version = 1 + _data_version = 1 + + _json_encoder = NumpyEncoder + _json_decoder = NumpyDecoder + + _extra_data = {} + + def __init__( + self, + result_data: Dict, + result_type: str, + device_components: List[Union[DeviceComponent, str]], + experiment_id: str, + result_id: Optional[str] = None, + quality: Optional[str] = None, + verified: bool = False, + tags: Optional[List[str]] = None, + service: Optional["ExperimentServiceV1"] = None, + **kwargs, + ): + """AnalysisResult constructor. + + Args: + result_data: Analysis result data. + result_type: Analysis result type. + device_components: Target device components this analysis is for. + experiment_id: ID of the experiment. + result_id: Result ID. If ``None``, one is generated. + quality: Quality of the analysis. Refer to the experiment service + provider for valid values. + verified: Whether the result quality has been verified. + tags: Tags for this analysis result. + service: Experiment service to be used to store result in database. + **kwargs: Additional analysis result attributes. + """ + result_data = result_data or {} + self._result_data = copy.deepcopy(result_data) + self._source = self._result_data.pop( + "_source", + { + "class": f"{self.__class__.__module__}.{self.__class__.__name__}", + "data_version": self._data_version, + "qiskit_version": qiskit_version(), + }, + ) + + # Data to be stored in DB. + self._experiment_id = experiment_id + self._id = result_id or str(uuid.uuid4()) + self._type = result_type + self._device_components = [] + for comp in device_components: + if isinstance(comp, str): + comp = to_component(comp) + self._device_components.append(comp) + + self._quality = quality + self._quality_verified = verified + self._tags = tags or [] + + # Other attributes. + self._service = service + self._created_in_db = False + self.auto_save = False + if self._service: + try: + self.auto_save = self._service.option("auto_save") + except AttributeError: + pass + self._extra_data = kwargs + + def serialize_data(self) -> str: + """Serialize result data into JSON string. + + Returns: + Serialized JSON string. + """ + return json.dumps(self._result_data, cls=self._json_encoder) + + @classmethod + def deserialize_data(cls, data: str) -> Dict: + """Deserialize experiment from JSON string. + + Args: + data: Data to be deserialized. + + Returns: + Deserialized data. + """ + return json.loads(data, cls=cls._json_decoder) + + @classmethod + def from_data( + cls, + result_data: Dict, + result_type: str, + device_components: List[Union[DeviceComponent, str]], + experiment_id: str, + **kwargs, + ) -> "AnalysisResultV1": + """Reconstruct the analysis result from input data. + + Args: + result_data: Analysis result data. + result_type: Analysis result type. + device_components: Target device components this analysis is for. + experiment_id: ID of the experiment. + **kwargs: Additional analysis result attributes. + + Returns: + Reconstructed analysis result. + """ + if result_data: + result_data = cls.deserialize_data(json.dumps(result_data)) + return cls( + result_data=result_data, + result_type=result_type, + device_components=device_components, + experiment_id=experiment_id, + **kwargs, + ) + + def save(self, service: Optional["ExperimentServiceV1"] = None) -> None: + """Save this analysis result in the database. + + Args: + service: Experiment service to be used to save the data. + If ``None``, the default, if any, is used. + + Raises: + ExperimentError: If the analysis result contains invalid data. + """ + service = service or self._service + if not service: + LOG.warning( + "Analysis result cannot be saved because no " "experiment service is available." + ) + return + + _result_data = json.loads(self.serialize_data()) + _result_data["_source"] = self._source + + new_data = { + "experiment_id": self._experiment_id, + "result_type": self.result_type, + "device_components": self.device_components, + } + update_data = { + "result_id": self.result_id, + "data": _result_data, + "tags": self.tags(), + "quality": self.quality, + "verified": self.verified, + } + + self._created_in_db, _ = save_data( + is_new=(not self._created_in_db), + new_func=service.create_analysis_result, + update_func=service.update_analysis_result, + new_data=new_data, + update_data=update_data, + ) + + def data(self) -> Dict: + """Return analysis result data. + + Returns: + Analysis result data. + """ + return self._result_data + + @auto_save + def update_data(self, new_data: Dict) -> None: + """Update result data. + + Args: + new_data: New analysis result data. + """ + self._result_data = new_data + + def tags(self): + """Return tags associated with this result.""" + return self._tags + + @auto_save + def update_tags(self, new_tags: List[str]) -> None: + """Set tags for this result. + + Args: + new_tags: New tags for the result. + """ + self._tags = new_tags + + @property + def result_id(self) -> str: + """Return analysis result ID. + + Returns: + ID for this analysis result. + """ + return self._id + + @property + def result_type(self) -> str: + """Return analysis result type. + + Returns: + Analysis result type. + """ + return self._type + + @property + def source(self) -> Dict: + """Return the class name and version.""" + return self._source + + @property + def quality(self) -> str: + """Return the quality of this analysis. + + Returns: + Quality of this analysis. + """ + return self._quality + + @quality.setter + def quality(self, new_quality: str) -> None: + """Set the quality of this analysis. + + Args: + new_quality: New analysis quality. + """ + self._quality = new_quality + if self.auto_save: + self.save() + + @property + def verified(self) -> bool: + """Return the verified flag. + + The ``verified`` flag is intended to indicate whether the quality + value has been verified by a human. + + Returns: + Whether the quality has been verified. + """ + return self._quality_verified + + @verified.setter + def verified(self, verified: bool) -> None: + """Set the verified flag. + + Args: + verified: Whether the quality is verified. + """ + self._quality_verified = verified + if self.auto_save: + self.save() + + @property + def experiment_id(self) -> str: + """Return the ID of the experiment associated with this analysis result. + + Returns: + ID of experiment associated with this analysis result. + """ + return self._experiment_id + + @property + def service(self) -> Optional["ExperimentServiceV1"]: + """Return the database service. + + Returns: + Service that can be used to store this analysis result in a database. + ``None`` if not available. + """ + return self._service + + @service.setter + def service(self, service: "ExperimentServiceV1") -> None: + """Set the service to be used for storing result data in a database. + + Args: + service: Service to be used. + + Raises: + ExperimentError: If an experiment service is already being used. + """ + if self._service: + raise ExperimentError("An experiment service is already being used.") + self._service = service + + @property + def device_components(self) -> List[DeviceComponent]: + """Return target device components for this analysis result. + + Returns: + Target device components. + """ + return self._device_components + + def __getattr__(self, name: str) -> Any: + try: + return self._extra_data[name] + except KeyError: + # pylint: disable=raise-missing-from + raise AttributeError("Attribute %s is not defined" % name) + + def __str__(self): + ret = f"\nAnalysis Result: {self.result_type}" + ret += f"\nAnalysis Result ID: {self.result_id}" + ret += f"\nExperiment ID: {self.experiment_id}" + ret += f"\nDevice Components: {self.device_components}" + ret += f"\nQuality: {self.quality}" + ret += "\nResult Data:" + for key, val in self.data().items(): + ret += f"\n - {key}: {val}" + return ret diff --git a/qiskit_experiments/store_data/device_component.py b/qiskit_experiments/store_data/device_component.py new file mode 100644 index 0000000000..8c602f94dd --- /dev/null +++ b/qiskit_experiments/store_data/device_component.py @@ -0,0 +1,76 @@ +# 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. + +"""Device component classes.""" + +from abc import ABC, abstractmethod + + +class DeviceComponent(ABC): + """Class representing a device component.""" + + @abstractmethod + def __str__(self): + pass + + def __repr__(self): + return f"<{self.__class__.__name__}({str(self)})>" + + +class Qubit(DeviceComponent): + """Class representing a qubit device component.""" + + def __init__(self, index: int): + self._index = index + + def __str__(self): + return f"Q{self._index}" + + +class Resonator(DeviceComponent): + """Class representing a resonator device component.""" + + def __init__(self, index: int): + self._index = index + + def __str__(self): + return f"R{self._index}" + + +class UnknownComponent(DeviceComponent): + """Class representing unknown device component.""" + + def __init__(self, component: str): + self._component = component + + def __str__(self): + return self._component + + +def to_component(string: str) -> DeviceComponent: + """Convert the input string to a ``DeviceComponent`` instance. + + Args: + string: String to be converted. + + Returns: + A ``DeviceComponent`` instance. + + Raises: + ValueError: If input string is not a valid device component. + """ + if string.startswith("Q"): + return Qubit(int(string[1:])) + elif string.startswith("R"): + return Resonator(int(string[1:])) + else: + return UnknownComponent(string) diff --git a/qiskit_experiments/store_data/exceptions.py b/qiskit_experiments/store_data/exceptions.py new file mode 100644 index 0000000000..1abfb80456 --- /dev/null +++ b/qiskit_experiments/store_data/exceptions.py @@ -0,0 +1,33 @@ +# 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. + +"""Exceptions for errors raised while handling experiments.""" + +from qiskit.exceptions import QiskitError + + +class ExperimentError(QiskitError): + """Base class for errors raised while handling experiments.""" + + pass + + +class ExperimentEntryNotFound(ExperimentError): + """Errors raised when an experiment entry cannot be found.""" + + pass + + +class ExperimentEntryExists(ExperimentError): + """Errors raised when an experiment entry already exists.""" + + pass diff --git a/qiskit_experiments/store_data/experiment_service.py b/qiskit_experiments/store_data/experiment_service.py new file mode 100644 index 0000000000..71fa8d0ab1 --- /dev/null +++ b/qiskit_experiments/store_data/experiment_service.py @@ -0,0 +1,436 @@ +# 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. + +"""Experiment service abstract interface.""" + +from abc import ABC, abstractmethod +from typing import Optional, Dict, List, Any, Union, Tuple, Type, TypeVar + +from .device_component import DeviceComponent + +T = TypeVar('T') + + +class ExperimentService: + """Base common type for all versioned ExperimentService abstract classes. + + Note this class should not be inherited from directly, it is intended + to be used for type checking. When implementing a subclass you should use + the versioned abstract classes as the parent class and not this class + directly. + """ + + version = 0 + + +class ExperimentServiceV1(ExperimentService, ABC): + """Class to provide experiment service. + + The experiment service allows you to store experiment data and metadata + in a database. An experiment can have one or more jobs, analysis results, + and figures. + + Each implementation of this service may use different data structure and + should issue a warning on unsupported keywords. + """ + + version = 1 + + def __init__(self): + """Initialize an ExperimentService instance.""" + self._options = self._default_options() + + @classmethod + @abstractmethod + def _default_options(cls) -> Dict: + """Return the default options + + Returns: + A dictionary of default options. + """ + pass + + @abstractmethod + def create_experiment( + self, + experiment_type: str, + backend_name: str, + metadata: Optional[Dict] = None, + experiment_id: Optional[str] = None, + job_ids: Optional[List[str]] = None, + tags: Optional[List[str]] = None, + notes: Optional[str] = None, + **kwargs: Any, + ) -> str: + """Create a new experiment in the database. + + Args: + experiment_type: Experiment type. + backend_name: Name of the backend the experiment ran on. + metadata: Experiment metadata. + experiment_id: Experiment ID. It must be in the ``uuid4`` format. + One will be generated if not supplied. + job_ids: IDs of experiment jobs. + tags: Tags to be associated with the experiment. + notes: Freeform notes about the experiment. + kwargs: Additional keywords supported by the service provider. + + Returns: + Experiment ID. + + Raises: + ExperimentEntryExists: If the experiment already exits. + """ + pass + + @abstractmethod + def update_experiment( + self, + experiment_id: str, + metadata: Optional[Dict] = None, + job_ids: Optional[List[str]] = None, + notes: Optional[str] = None, + tags: Optional[List[str]] = None, + **kwargs: Any, + ) -> None: + """Update an existing experiment. + + Args: + experiment_id: Experiment ID. + metadata: Experiment metadata. + job_ids: IDs of experiment jobs. + notes: Freeform notes about the experiment. + tags: Tags to be associated with the experiment. + kwargs: Additional keywords supported by the service provider. + + Raises: + ExperimentEntryNotFound: If the experiment does not exist. + """ + pass + + @abstractmethod + def experiment( + self, + experiment_id: str, + experiment_class: Optional[Type[T]] = None + ) -> Union[Dict, T]: + """Retrieve a previously stored experiment. + + Args: + experiment_id: Experiment ID. + experiment_class: Class used to instantiate the returned data object. + If a class is provided, its ``from_data()`` method is called + with the retrieved data, and its return value is returned. + + Returns: + A dictionary containing the retrieved experiment data if `experiment_class` + is ``None``. Otherwise an instance of the `experiment_class` class. + + Raises: + ExperimentEntryNotFound: If the experiment does not exist. + """ + pass + + @abstractmethod + def experiments( + self, + limit: Optional[int] = 10, + experiment_class: Optional[Type[T]] = None, + device_components: Optional[Union[str, DeviceComponent]] = None, + experiment_type: Optional[str] = None, + backend_name: Optional[str] = None, + tags: Optional[List[str]] = None, + tags_operator: Optional[str] = "OR", + **filters: Any, + ) -> List[Union[Dict, T]]: + """Retrieve all experiment data, with optional filtering. + + Args: + limit: Number of experiments to retrieve. ``None`` means no limit. + experiment_class: Class used to instantiate the returned data object. + If a class is provided, its ``from_data()`` method is called + with the retrieved data, and its return value is returned. + device_components: Filter by device components. An experiment must have analysis + results with device components matching the given list exactly to be included. + experiment_type: Experiment type used for filtering. + backend_name: Backend name used for filtering. + tags: Filter by tags assigned to experiments. This can be used + with `tags_operator` for granular filtering. + tags_operator: Logical operator to use when filtering by tags. Valid + values are "AND" and "OR": + + * If "AND" is specified, then an experiment must have all of the tags + specified in `tags` to be included. + * If "OR" is specified, then an experiment only needs to have any + of the tags specified in `tags` to be included. + + **filters: Additional filtering keywords supported by the service provider. + + Returns: + A list of experiments. Each experiment is either a dictionary containing the + retrieved experiment data, if `experiment_class` + is ``None``, or an instance of the `experiment_class` class. + """ + pass + + @abstractmethod + def delete_experiment(self, experiment_id: str) -> None: + """Delete an experiment. + + Args: + experiment_id: Experiment ID. + """ + pass + + @abstractmethod + def create_analysis_result( + self, + experiment_id: str, + data: Dict, + result_type: str, + device_components: Optional[Union[str, DeviceComponent]] = None, + tags: Optional[List[str]] = None, + quality: Optional[str] = None, + verified: bool = False, + result_id: Optional[str] = None, + **kwargs: Any, + ) -> str: + """Create a new analysis result in the database. + + Args: + experiment_id: ID of the experiment this result is for. + data: Result data to be stored. + result_type: Analysis result type. + device_components: Target device components, such as qubits. + tags: Tags to be associated with the analysis result. + quality: Quality of this analysis. + verified: Whether the result quality has been verified. + result_id: Analysis result ID. It must be in the ``uuid4`` format. + One will be generated if not supplied. + kwargs: Additional keywords supported by the service provider. + + Returns: + Analysis result ID. + + Raises: + ExperimentEntryExists: If the analysis result already exits. + """ + pass + + @abstractmethod + def update_analysis_result( + self, + result_id: str, + data: Optional[Dict] = None, + tags: Optional[List[str]] = None, + quality: Optional[str] = None, + verified: bool = None, + **kwargs: Any, + ) -> None: + """Update an existing analysis result. + + Args: + result_id: Analysis result ID. + data: Result data to be stored. + quality: Quality of this analysis. + verified: Whether the result quality has been verified. + tags: Tags to be associated with the analysis result. + kwargs: Additional keywords supported by the service provider. + + Raises: + ExperimentEntryNotFound: If the analysis result does not exist. + """ + pass + + @abstractmethod + def analysis_result( + self, + result_id: str, + result_class: Optional[Type[T]] = None + ) -> Union[Dict, T]: + """Retrieve a previously stored experiment. + + Args: + result_id: Analysis result ID. + result_class: Class used to instantiate the returned data object. + If a class is provided, its ``from_data()`` method is called + with the retrieved data, and its return value is returned. + + Returns: + Retrieved analysis result. + + Raises: + ExperimentEntryNotFound: If the analysis result does not exist. + """ + pass + + @abstractmethod + def analysis_results( + self, + limit: Optional[int] = 10, + result_class: Optional[Type[T]] = None, + device_components: Optional[Union[str, DeviceComponent]] = None, + experiment_id: Optional[str] = None, + result_type: Optional[str] = None, + backend_name: Optional[str] = None, + quality: Optional[str] = None, + verified: Optional[bool] = None, + tags: Optional[List[str]] = None, + tags_operator: Optional[str] = "OR", + **filters: Any, + ) -> List[Union[Dict, T]]: + """Retrieve all analysis results, with optional filtering. + + Args: + limit: Number of analysis results to retrieve. ``None`` means no limit. + result_class: Class used to instantiate the returned data object. + If a class is provided, its ``from_data()`` method is called + with the retrieved data, and its return value is returned. + device_components: Target device components, such as qubits. + experiment_id: Experiment ID used for filtering. + result_type: Analysis result type used for filtering. + backend_name: Backend name used for filtering. If specified, analysis + results associated with experiments on that backend are returned. + quality: Quality value used for filtering. + verified: Whether the result quality has been verified. + tags: Filter by tags assigned to analysis results. This can be used + with `tags_operator` for granular filtering. + tags_operator: Logical operator to use when filtering by tags. Valid + values are "AND" and "OR": + + * If "AND" is specified, then an analysis result must have all of the tags + specified in `tags` to be included. + * If "OR" is specified, then an analysis result only needs to have any + of the tags specified in `tags` to be included. + + **filters: Additional filtering keywords supported by the service provider. + + Returns: + A list of analysis results. Each analysis result is either a dictionary + containing the retrieved analysis result, if `result_class` + is ``None``, or an instance of the `result_class` class. + """ + pass + + @abstractmethod + def delete_analysis_result(self, result_id: str) -> None: + """Delete an analysis result. + + Args: + result_id: Analysis result ID. + """ + pass + + @abstractmethod + def create_figure( + self, experiment_id: str, figure: Union[str, bytes], figure_name: Optional[str] + ) -> Tuple[str, int]: + """Store a new figure in the database. + + Args: + experiment_id: ID of the experiment this figure is for. + figure: Name of the figure file or figure data to store. + figure_name: Name of the figure. If ``None``, the figure file name, if + given, or a generated name is used. + + Returns: + A tuple of the name and size of the saved figure. + + Raises: + ExperimentEntryExists: If the figure already exits. + """ + pass + + @abstractmethod + def update_figure( + self, experiment_id: str, figure: Union[str, bytes], figure_name: str + ) -> Tuple[str, int]: + """Update an existing figure. + + Args: + experiment_id: Experiment ID. + figure: Name of the figure file or figure data to store. + figure_name: Name of the figure. + + Returns: + A tuple of the name and size of the saved figure. + + Raises: + ExperimentEntryNotFound: If the figure does not exist. + """ + pass + + @abstractmethod + def figure( + self, experiment_id: str, figure_name: str, file_name: Optional[str] = None + ) -> Union[int, bytes]: + """Retrieve an existing figure. + + Args: + experiment_id: Experiment ID. + figure_name: Name of the figure. + file_name: Name of the local file to save the figure to. If ``None``, + the content of the figure is returned instead. + + Returns: + The size of the figure if `file_name` is specified. Otherwise the + content of the figure in bytes. + + Raises: + ExperimentEntryNotFound: If the figure does not exist. + """ + pass + + @abstractmethod + def delete_figure( + self, + experiment_id: str, + figure_name: str, + ) -> None: + """Delete an existing figure. + + Args: + experiment_id: Experiment ID. + figure_name: Name of the figure. + """ + pass + + def set_options(self, **fields): + """Set the options fields for the service. + + Args: + fields: The fields to update the options + + Raises: + AttributeError: If the field passed in is not part of the + options + """ + for field in fields: + if field not in self._options: + raise AttributeError("Options field %s is not valid for this " "service." % field) + self._options.update(**fields) + + def option(self, field: str) -> Any: + """Get the value of the specified option. + + Args: + field: Option field to retrieve. + + Returns: + Option value. + + Raises: + AttributeError: If the input field is not valid for the service. + """ + if field not in self._options: + raise AttributeError(f"Options field {field} is not valid for this service.") + return self._options[field] diff --git a/qiskit_experiments/store_data/json.py b/qiskit_experiments/store_data/json.py new file mode 100644 index 0000000000..8dbb37f2cc --- /dev/null +++ b/qiskit_experiments/store_data/json.py @@ -0,0 +1,47 @@ +# 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. +# pylint: disable=method-hidden + +"""Experiment serialization methods.""" + +import json +from typing import Any + +import numpy as np + + +class NumpyEncoder(json.JSONEncoder): + """JSON Encoder for Numpy arrays and complex numbers.""" + + def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ + if hasattr(obj, "tolist"): + return {"type": "array", "value": obj.tolist()} + if isinstance(obj, complex): + return {"type": "complex", "value": [obj.real, obj.imag]} + return super().default(obj) + + +class NumpyDecoder(json.JSONDecoder): + """JSON Decoder for Numpy arrays and complex numbers.""" + + def __init__(self, *args, **kwargs): + super().__init__(object_hook=self.object_hook, *args, **kwargs) + + def object_hook(self, obj): + """Object hook.""" + if "type" in obj: + if obj["type"] == "complex": + val = obj["value"] + return val[0] + 1j * val[1] + if obj["type"] == "array": + return np.array(obj["value"]) + return obj diff --git a/qiskit_experiments/store_data/stored_data.py b/qiskit_experiments/store_data/stored_data.py new file mode 100644 index 0000000000..2c40cce9bc --- /dev/null +++ b/qiskit_experiments/store_data/stored_data.py @@ -0,0 +1,1036 @@ +# 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. + +"""Stored data class.""" + +import logging +import uuid +import json +from typing import Optional, List, Any, Union, Callable, Dict +import copy +from concurrent import futures +from functools import wraps +import traceback +import contextlib +from collections import deque +from datetime import datetime + +from qiskit.providers import Job, BaseJob, Backend, BaseBackend, Provider +from qiskit.result import Result +from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES +from qiskit.providers.exceptions import JobError +from qiskit.visualization import HAS_MATPLOTLIB + +from .exceptions import ExperimentError, ExperimentEntryNotFound, ExperimentEntryExists +from .analysis_result import AnalysisResultV1 as AnalysisResult +from .json import NumpyEncoder, NumpyDecoder +from .utils import ( + save_data, + qiskit_version, + plot_to_svg_bytes, + ThreadSafeOrderedDict, + ThreadSafeList, +) + +LOG = logging.getLogger(__name__) + + +def auto_save(func: Callable): + """Decorate the input function to auto save data.""" + + @wraps(func) + def _wrapped(self, *args, **kwargs): + return_val = func(self, *args, **kwargs) + if self.auto_save: + self.save() + return return_val + + return _wrapped + + +class StoredData: + """Base common type for all versioned StoredData classes. + + Note this class should not be inherited from directly, it is intended + to be used for type checking. When implementing a provider you should use + the versioned abstract classes as the parent class and not this class + directly. + """ + + version = 0 + + +class StoredDataV1(StoredData): + """Class to handle stored data. + + This class serves as a container for data to be stored in a database, which + may include experiment metadata, analysis results, and figures. It also + provides methods used to interact with the database, such as storing into + and retrieving from the database. + """ + + version = 1 + _metadata_version = 1 + _executor = futures.ThreadPoolExecutor() + """Threads used for asynchronous processing.""" + + _json_encoder = NumpyEncoder + _json_decoder = NumpyDecoder + + def __init__( + self, + experiment_type: str, + backend: Optional[Union[Backend, BaseBackend]] = None, + experiment_id: Optional[str] = None, + tags: Optional[List[str]] = None, + job_ids: Optional[List[str]] = None, + share_level: Optional[str] = None, + metadata: Optional[Dict] = None, + figure_names: Optional[List[str]] = None, + notes: Optional[str] = None, + **kwargs, + ): + """Initializes the StoredDataV1 instance. + + Args: + experiment_type: Experiment type. + backend: Backend the experiment runs on. + experiment_id: Experiment ID. One will be generated if not supplied. + tags: Tags to be associated with the experiment. + job_ids: IDs of jobs submitted for the experiment. + share_level: Whether this experiment can be shared with others. This + is applicable only if the experiment service supports sharing. See + the specific service provider's documentation on valid values. + metadata: Additional experiment metadata. + figure_names: Name of figures associated with this experiment. + notes: Freeform notes about the experiment. + **kwargs: Additional experiment attributes. + + Raises: + ExperimentError: If an input argument is invalid. + """ + metadata = metadata or {} + self._metadata = copy.deepcopy(metadata) + self._source = self._metadata.pop( + "_source", + { + "class": f"{self.__class__.__module__}.{self.__class__.__name__}", + "metadata_version": self._metadata_version, + "qiskit_version": qiskit_version(), + }, + ) + + self._service = None + self._backend = backend + self.auto_save = False + self._set_service_from_backend(backend) + + self._id = experiment_id or str(uuid.uuid4()) + self._type = experiment_type + self._tags = tags or [] + self._share_level = share_level + self._notes = notes or "" + + self._jobs = ThreadSafeOrderedDict(job_ids or []) + self._job_futures = ThreadSafeList() + self._errors = [] + + self._data = ThreadSafeList() + self._figures = ThreadSafeOrderedDict(figure_names or []) + self._analysis_results = ThreadSafeOrderedDict() + + self._deleted_figures = deque() + self._deleted_analysis_results = deque() + + self._created_in_db = False + self._extra_data = kwargs + + def _set_service_from_backend(self, backend: Union[Backend, BaseBackend]) -> None: + """Set the service to be used from the input backend. + + Args: + backend: Backend whose provider may offer experiment service. + """ + with contextlib.suppress(Exception): + self._service = backend.provider().service("experiment") + with contextlib.suppress(Exception): + self.auto_save = self._service.option("auto_save") + + def add_data( + self, + data: Union[Result, List[Result], Job, List[Job], Dict, List[Dict]], + post_processing_callback: Optional[Callable] = None, + **kwargs: Any, + ) -> None: + """Add experiment data. + + Note: + This method is not thread safe and should not be called by the + `post_processing_callback` function. + + Note: + If `data` is a ``Job``, this method waits for the job to finish + and calls the `post_processing_callback` function asynchronously. + + Args: + data: Experiment data to add. + Several types are accepted for convenience: + + * Result: Add data from this ``Result`` object. + * List[Result]: Add data from the ``Result`` objects. + * Job: Add data from the job result. + * List[Job]: Add data from the job results. + * Dict: Add this data. + * List[Dict]: Add this list of data. + + post_processing_callback: Callback function invoked when data is + added. If `data` is a ``Job``, the callback is only invoked when + the job finishes successfully. + The following positional arguments are provided to the callback function: + + * This ``StoredData`` object. + * Index of the data added. + * Additional keyword arguments passed to this method. + + **kwargs: Keyword arguments to be passed to the callback function. + + Raises: + TypeError: If the input data type is invalid. + """ + if isinstance(data, (Job, BaseJob)): + if self.backend and self.backend.name() != data.backend().name(): + LOG.warning( + "Adding a job from a backend (%s) that is different " + "than the current backend (%s). " + "The new backend will be used, but " + "service is not changed if one already exists.", + data.backend(), + self.backend, + ) + self._backend = data.backend() + if not self._service: + self._set_service_from_backend(self._backend) + + self._jobs[data.job_id()] = data + self._job_futures.append( + ( + data, + self._executor.submit( + self._wait_for_job, data, post_processing_callback, **kwargs + ), + ) + ) + if self.auto_save: + self.save() + return + + if isinstance(data, dict): + self._add_single_data(data) + elif isinstance(data, Result): + self._add_result_data(data) + elif isinstance(data, list): + # TODO use loop instead of recursion for fewer save() + for dat in data: + self.add_data(dat) + else: + raise TypeError(f"Invalid data type {type(data)}.") + + if post_processing_callback is not None: + post_processing_callback(self, len(self._data)-1, **kwargs) + + def _wait_for_job( + self, + job: Union[Job, BaseJob], + job_done_callback: Optional[Callable] = None, + **kwargs: Any, + ) -> None: + """Wait for a job to finish. + + Args: + job: Job to wait for. + job_done_callback: Callback function to invoke when job finishes. + **kwargs: Keyword arguments to be passed to the callback function. + """ + LOG.debug("Waiting for job %s to finish.", job.job_id()) + try: + job_result = job.result() + with self._data.lock: + # Hold the lock so we add the block of results together. + self._add_result_data(job_result) + data_index = len(self._data)-1 + except JobError as err: + LOG.warning("Job %s failed: %s", job.job_id(), str(err)) + return + if job_done_callback: + job_done_callback(self, data_index, **kwargs) + + def _add_result_data(self, result: Result) -> None: + """Add data from a Result object + + Args: + result: Result object containing data to be added. + """ + if result.job_id not in self._jobs: + self._jobs[result.job_id] = None + for i in range(len(result.results)): + data = result.data(i) + data["job_id"] = result.job_id + if "counts" in data: + # Format to Counts object rather than hex dict + data["counts"] = result.get_counts(i) + expr_result = result.results[i] + if hasattr(expr_result, "header") and hasattr(expr_result.header, "metadata"): + data["metadata"] = expr_result.header.metadata + self._add_single_data(data) + + def _add_single_data(self, data: Dict[str, any]) -> None: + """Add a single data dictionary to the experiment. + + Args: + data: Data to be added. + """ + self._data.append(data) + + def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, List[Dict]]: + """Return the experiment data at the specified index. + + Args: + index: Index of the data to be returned. + Several types are accepted for convenience: + + * None: Return all experiment data. + * int: Specific index of the data. + * slice: A list slice of data indexes. + * str: ID of the job that produced the data. + + Returns: + Experiment data. + + Raises: + TypeError: If the input `index` has an invalid type. + """ + # Get job results if missing experiment data. + if (not self._data) and self._provider: + with self._jobs.lock: + for jid in self._jobs: + if self._jobs[jid] is None: + try: + self._jobs[jid] = self._provider.retrieve_job(jid) + except Exception: # pylint: disable=broad-except + pass + if self._jobs[jid] is not None: + self._add_result_data(self._jobs[jid].result()) + + if index is None: + return self._data.copy() + if isinstance(index, (int, slice)): + return self._data[index] + if isinstance(index, str): + return [data for data in self._data if data.get("job_id") == index] + raise TypeError(f"Invalid index type {type(index)}.") + + @auto_save + def add_figures( + self, + figures: Union[List[Union[str, bytes, "pyplot.Figure"]], str, bytes, "pyplot.Figure"], + figure_names: Optional[Union[List[str], str]] = None, + overwrite: bool = False, + save_figure: Optional[bool] = None, + service: Optional["ExperimentServiceV1"] = None, + ) -> Union[str, List[str]]: + """Add the experiment figure. + + Args: + figures: Names of the figure files or figure data. + figure_names: Names of the figures. If ``None``, use the figure file + names, if given, or a generated name. If `figures` is a list, then + `figure_names` must also be a list of the same length or ``None``. + overwrite: Whether to overwrite the figure if one already exists with + the same name. + save_figure: Whether to save the figure in the database. If ``None``, + the ``auto-save`` attribute is used. + service: Experiment service to be used to update the database, if + the figure is to be uploaded. If ``None``, the default service is used. + + Returns: + Figure names. + + Raises: + ExperimentEntryExists: If the figure with the same name already exists, + and `overwrite=True` is not specified. + ValueError: If an input parameter has an invalid value. + """ + if ( + isinstance(figures, list) + and figure_names is not None + and (not isinstance(figure_names, list) or len(figures) != len(figure_names)) + ): + raise ValueError( + "The parameter figure_names must be None or a list of " + "the same size as the parameter figures." + ) + if not isinstance(figures, list): + figures = [figures] + if figure_names is not None and not isinstance(figure_names, list): + figure_names = [figure_names] + + added_figs = [] + for idx, figure in enumerate(figures): + if figure_names is None: + if isinstance(figure, str): + fig_name = figure + else: + fig_name = ( + f"figure_{self.experiment_id[:8]}_" + f"{datetime.now().isoformat()}_{len(self._figures)}" + ) + else: + fig_name = figure_names[idx] + + existing_figure = fig_name in self._figures + if existing_figure and not overwrite: + raise ExperimentEntryExists( + f"A figure with the name {fig_name} for this experiment " + f"already exists. Specify overwrite=True if you " + f"want to overwrite it." + ) + # figure_data = None + if isinstance(figure, str): + with open(figure, "rb") as file: + figure = file.read() + + self._figures[fig_name] = figure + + service = service or self._service + save = save_figure if save_figure is not None else self.auto_save + if save and service: + if HAS_MATPLOTLIB: + from matplotlib import pyplot + + if isinstance(figure, pyplot.Figure): + figure = plot_to_svg_bytes(figure) + data = { + "experiment_id": self.experiment_id, + "figure": figure, + "figure_name": fig_name, + } + save_data( + is_new=(not existing_figure), + new_func=service.create_figure, + update_func=service.update_figure, + new_data={}, + update_data=data, + ) + added_figs.append(fig_name) + + return added_figs if len(added_figs) > 1 else added_figs[0] + + @auto_save + def delete_figure( + self, + figure_key: Union[str, int], + service: Optional["ExperimentServiceV1"] = None, + ) -> str: + """Add the experiment figure. + + Args: + figure_key: Name or index of the figure. + service: Experiment service to be used to update the database. If ``None``, + the default service is used. + + Returns: + Figure name. + + Raises: + ExperimentEntryNotFound: If the figure is not found. + """ + if isinstance(figure_key, int): + figure_key = self._figures.keys()[figure_key] + elif figure_key not in self._figures: + raise ExperimentEntryNotFound(f"Figure {figure_key} not found.") + + del self._figures[figure_key] + self._deleted_figures.append(figure_key) + + service = service or self._service + if service and self.auto_save: + with contextlib.suppress(ExperimentEntryNotFound): + self.service.delete_figure(experiment_id=self.experiment_id, figure_name=figure_key) + self._deleted_figures.remove(figure_key) + + return figure_key + + def figure( + self, figure_key: Union[str, int], file_name: Optional[str] = None + ) -> Union[int, bytes]: + """Retrieve the specified experiment figure. + + Args: + figure_key: Name or index of the figure. + file_name: Name of the local file to save the figure to. If ``None``, + the content of the figure is returned instead. + + Returns: + The size of the figure if `file_name` is specified. Otherwise the + content of the figure in bytes. + + Raises: + ExperimentEntryNotFound: If the figure cannot be found. + """ + if isinstance(figure_key, int): + figure_key = self._figures.keys()[figure_key] + + figure_data = self._figures.get(figure_key, None) + if figure_data is None and self.service: + figure_data = self.service.figure( + experiment_id=self.experiment_id, figure_name=figure_key + ) + self._figures[figure_key] = figure_data + + if figure_data is None: + raise ExperimentEntryNotFound(f"Figure {figure_key} not found.") + + if file_name: + with open(file_name, "wb") as output: + num_bytes = output.write(figure_data) + return num_bytes + return figure_data + + @auto_save + def add_analysis_results( + self, + results: Union[AnalysisResult, List[AnalysisResult]], + service: "ExperimentServiceV1" = None, + ) -> None: + """Save the analysis result. + + Args: + results: Analysis results to be saved. + service: Experiment service to be used to update the database. If ``None``, + the default service is used. + """ + if not isinstance(results, list): + results = [results] + + for result in results: + self._analysis_results[result.result_id] = result + + with contextlib.suppress(ExperimentError): + result.service = self.service + result.auto_save = self.auto_save + + use_service = service or self.service + if self.auto_save and use_service: + result.save(service=service) + + @auto_save + def delete_analysis_result( + self, result_key: Union[int, str], service: "ExperimentServiceV1" = None + ) -> str: + """Delete the analysis result. + + Args: + result_key: ID or index of the analysis result to be delete. + service: Experiment service to be used to update the database. If ``None``, + the default service is used. + + Returns: + Analysis result ID. + + Raises: + ExperimentEntryNotFound: If analysis result not found. + """ + if isinstance(result_key, int): + result_key = self._analysis_results.keys()[result_key] + elif result_key not in self._analysis_results: + raise ExperimentEntryNotFound(f"Analysis result {result_key} not found.") + + del self._analysis_results[result_key] + self._deleted_analysis_results.append(result_key) + + service = service or self._service + if service and self.auto_save: + with contextlib.suppress(ExperimentEntryNotFound): + self.service.delete_analysis_result(result_id=result_key) + self._deleted_analysis_results.remove(result_key) + + return result_key + + def analysis_result( + self, index: Optional[Union[int, slice, str]] = None, refresh: bool = False + ) -> Union[AnalysisResult, List[AnalysisResult]]: + """Return analysis results associated with this experiment. + + Args: + index: Index of the analysis result to be returned. + Several types are accepted for convenience: + + * None: Return all analysis results. + * int: Specific index of the analysis results. + * slice: A list slice of indexes. + * str: ID of the analysis result. + refresh: Retrieve the latest analysis results from the server, if + an experiment service is available. + + Returns: + Analysis results for this experiment. + + Raises: + TypeError: If the input `index` has an invalid type. + ExperimentEntryNotFound: If the entry cannot be found. + """ + if self.service and (not self._analysis_results or refresh): + retrieved_results = self.service.analysis_results( + experiment_id=self.experiment_id, limit=None + ) + for result in retrieved_results: + self._analysis_results[result.result_id] = result + + if index is None: + return self._analysis_results.values() + if isinstance(index, (int, slice)): + return self._analysis_results.values()[index] + if isinstance(index, str): + if index not in self._analysis_results: + raise ExperimentEntryNotFound(f"Analysis result {index} not found.") + return self._analysis_results[index] + + raise TypeError(f"Invalid index type {type(index)}.") + + def save(self, service: Optional["ExperimentServiceV1"] = None) -> None: + """Save this experiment in the database. + + Note: + Not all experiment properties are saved. + See :meth:`qiskit.providers.experiment.ExperimentServiceV1.create_experiment` + for fields that are saved. + + Note: + Note that this method does not save analysis results nor figures. Use + ``save_all()`` if you want to save those. + + Args: + service: Experiment service to be used to save the data. + If ``None``, the provider used to submit jobs will be used. + + Raises: + ExperimentError: If the experiment contains invalid data. + """ + service = service or self._service + if not service: + LOG.warning("Experiment cannot be saved because no experiment service is available.") + return + + if not self._backend: + LOG.warning("Experiment cannot be saved because backend is missing.") + return + + metadata = json.loads(self.serialize_metadata()) + metadata["_source"] = self._source + + update_data = { + "experiment_id": self._id, + "metadata": metadata, + "job_ids": self.job_ids, + "tags": self.tags(), + "notes": self.notes, + } + new_data = {"experiment_type": self._type, "backend_name": self._backend.name()} + if self.share_level: + update_data["share_level"] = self.share_level + + self._created_in_db, _ = save_data( + is_new=(not self._created_in_db), + new_func=service.create_experiment, + update_func=service.update_experiment, + new_data=new_data, + update_data=update_data, + ) + + def save_all(self, service: Optional["ExperimentServiceV1"] = None) -> None: + """Save this experiment and its analysis results and figures in the database. + + Note: + Depending on the amount of data, this operation could take a while. + + Args: + service: Experiment service to be used to save the data. + If ``None``, the provider used to submit jobs will be used. + + Raises: + ExperimentError: If the experiment contains invalid data. + """ + # TODO - track changes + use_service = service or self._service + if not use_service: + LOG.warning("Experiment cannot be saved because no experiment service is available.") + return + + self.save(service=use_service) + for result in self._analysis_results.values(): + result.save(service) + + for result in self._deleted_analysis_results.copy(): + try: + use_service.delete_analysis_result(result_id=result) + except ExperimentEntryNotFound: + pass + self._deleted_analysis_results.remove(result) + + with self._figures.lock: + for name, figure in self._figures.items(): + if HAS_MATPLOTLIB: + from matplotlib import pyplot + + if isinstance(figure, pyplot.Figure): + figure = plot_to_svg_bytes(figure) + data = {"experiment_id": self.experiment_id, "figure": figure, "figure_name": name} + save_data( + is_new=True, + new_func=use_service.create_figure, + update_func=use_service.update_figure, + new_data={}, + update_data=data, + ) + + for name in self._deleted_figures.copy(): + try: + use_service.delete_figure(experiment_id=self.experiment_id, figure_name=name) + except ExperimentEntryNotFound: + pass + self._deleted_figures.remove(name) + + def serialize_metadata(self) -> str: + """Serialize experiment metadata into a JSON string. + + Returns: + Serialized JSON string. + """ + return json.dumps(self._metadata, cls=self._json_encoder) + + @classmethod + def deserialize_metadata(cls, data: str) -> Any: + """Deserialize experiment metadata from a JSON string. + + Args: + data: Data to be deserialized. + + Returns: + Deserialized data. + """ + return json.loads(data, cls=cls._json_decoder) + + @classmethod + def from_data( + cls, + experiment_type: str, + experiment_id: str, + metadata: Optional[Dict] = None, + **kwargs, + ) -> "StoredDataV1": + """Reconstruct a StoredData using the input data. + + Args: + experiment_type: Experiment type. + experiment_id: Experiment ID. One will be generated if not supplied. + metadata: Additional experiment metadata. + **kwargs: Additional experiment attributes. + + Returns: + An StoredData instance. + """ + if metadata: + metadata = cls.deserialize_metadata(json.dumps(metadata)) + return cls( + experiment_type=experiment_type, + experiment_id=experiment_id, + metadata=metadata, + **kwargs, + ) + + def cancel_jobs(self) -> None: + """Cancel any running jobs.""" + for job, fut in self._job_futures.copy(): + if not fut.done() and job.status() not in JOB_FINAL_STATES: + try: + job.cancel() + except Exception as err: # pylint: disable=broad-except + LOG.info("Unable to cancel job %s: %s", job.job_id(), err) + + def block_for_results(self) -> None: + """Block until all pending jobs and their post processing finish.""" + for job, fut in self._job_futures.copy(): + LOG.info("Waiting for job %s and its post processing to finish.", job.job_id()) + with contextlib.suppress(Exception): + fut.result() + + def status(self) -> str: + """Return the data processing status. + + If the experiment consists of multiple jobs, the returned status is mapped + in the following order: + + * INITIALIZING - if any job is being initialized. + * VALIDATING - if any job is being validated. + * QUEUED - if any job is queued. + * RUNNING - if any job is still running. + * ERROR - if any job incurred an error. + * CANCELLED - if any job is cancelled. + * POST_PROCESSING - if any of the post-processing functions is still running. + * DONE - if all jobs and their post-processing functions finished. + + Returns: + Data processing status. + """ + if all( + len(container) == 0 + for container in [self._data, self._jobs, self._figures, self._analysis_results] + ): + return "INITIALIZING" + + statuses = set() + with self._job_futures.lock: + for idx, item in enumerate(self._job_futures): + job, fut = item + job_status = job.status() + statuses.add(job_status) + if job_status == JobStatus.ERROR: + job_err = "." + if hasattr(job, "error_message"): + job_err = ": " + job.error_message() + self._errors.append(f"Job {job.job_id()} failed{job_err}") + + if fut.done(): + self._job_futures[idx] = None + ex = fut.exception() + if ex: + self._errors.append( + f"Post processing for job {job.job_id()} failed: \n" + + "".join(traceback.format_exception(type(ex), ex, ex.__traceback__)) + ) + statuses.add(JobStatus.ERROR) + + self._job_futures = ThreadSafeList(list(filter(None, self._job_futures))) + + for stat in [ + JobStatus.INITIALIZING, + JobStatus.VALIDATING, + JobStatus.QUEUED, + JobStatus.RUNNING, + JobStatus.ERROR, + JobStatus.CANCELLED, + ]: + if stat in statuses: + return stat.name + + if self._job_futures: + return "POST_PROCESSING" + + return "DONE" + + def errors(self) -> str: + """Return errors encountered. + + Returns: + Experiment errors. + """ + self.status() # Collect new errors. + return "\n".join(self._errors) + + def tags(self) -> List[str]: + """Return tags assigned to this experiment data. + + Returns: + A list of tags assigned to this experiment data. + + """ + return self._tags + + @auto_save + def update_tags(self, new_tags: List[str]) -> None: + """Set tags for this experiment. + + Args: + new_tags: New tags for the experiment. + """ + self._tags = new_tags + + def metadata(self) -> Dict: + """Return experiment metadata. + + Returns: + Experiment metadata. + """ + return self._metadata + + @auto_save + def update_metadata(self, metadata: Dict) -> None: + """Update metadata for this experiment. + + Args: + metadata: New metadata for the experiment. + """ + self._metadata = copy.deepcopy(metadata) + + @property + def _provider(self) -> Optional[Provider]: + """Return the provider. + + Returns: + Provider used for the experiment, or ``None`` if unknown. + """ + if self._backend is None: + return None + return self._backend.provider() + + @property + def experiment_id(self) -> str: + """Return experiment ID + + Returns: + Experiment ID. + """ + return self._id + + @property + def job_ids(self) -> List[str]: + """Return experiment job IDs. + + Returns: IDs of jobs submitted for this experiment. + """ + return self._jobs.keys() + + @property + def backend(self) -> Optional[Union[BaseBackend, Backend]]: + """Return backend. + + Returns: + Backend this experiment is for, or ``None`` if backend is unknown. + """ + return self._backend + + @property + def experiment_type(self) -> str: + """Return experiment type. + + Returns: + Experiment type. + """ + return self._type + + @property + def figure_names(self) -> List[str]: + """Return names of the figures associated with this experiment. + + Returns: + Names of figures associated with this experiment. + """ + return self._figures.keys() + + @property + def share_level(self) -> str: + """Return the share level fo this experiment. + + Returns: + Experiment share level. + """ + return self._share_level + + @share_level.setter + def share_level(self, new_level: str) -> None: + """Set the experiment share level. + + Args: + new_level: New experiment share level. Valid share levels are provider- + specified. For example, IBMQ allows "global", "hub", "group", + "project", and "private". + """ + self._share_level = new_level + if self.auto_save: + self.save() + + @property + def notes(self) -> str: + """Return experiment notes. + + Returns: + Experiment notes. + """ + return self._notes + + @notes.setter + def notes(self, new_notes: str) -> None: + """Update experiment notes. + + Args: + new_notes: New experiment notes. + """ + self._notes = new_notes + if self.auto_save: + self.save() + + @property + def service(self) -> Optional["ExperimentServiceV1"]: + """Return the database service. + + Returns: + Service that can be used to access this experiment in a database. + """ + return self._service + + @service.setter + def service(self, service: "ExperimentServiceV1") -> None: + """Set the service to be used for storing experiment data remotely. + + Args: + service: Service to be used. + + Raises: + ExperimentError: If a remote experiment service is already being used. + """ + if self._service: + raise ExperimentError("An experiment service is already being used.") + self._service = service + with contextlib.suppress(Exception): + self.auto_save = self._service.option("auto_save") + + @property + def source(self) -> Dict: + """Return the class name and version.""" + return self._source + + def __str__(self): + line = 51 * "-" + n_res = len(self._analysis_results) + status = self.status() + ret = line + ret += f"\nExperiment: {self.experiment_type}" + ret += f"\nExperiment ID: {self.experiment_id}" + ret += f"\nStatus: {status}" + if status == "ERROR": + ret += "\n " + ret += "\n ".join(self._errors) + ret += f"\nData: {len(self._data)}" + ret += f"\nAnalysis Results: {n_res}" + ret += f"\nFigures: {len(self._figures)}" + ret += "\n" + line + if n_res: + ret += "\nLast Analysis Result:" + ret += f"\n{str(self._analysis_results.values()[-1])}" + return ret + + def __getattr__(self, name: str) -> Any: + try: + return self._extra_data[name] + except KeyError: + # pylint: disable=raise-missing-from + raise AttributeError("Attribute %s is not defined" % name) diff --git a/qiskit_experiments/store_data/utils.py b/qiskit_experiments/store_data/utils.py new file mode 100644 index 0000000000..66a0b6f6e1 --- /dev/null +++ b/qiskit_experiments/store_data/utils.py @@ -0,0 +1,219 @@ +# 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. + +"""Experiment utility functions.""" + +import logging +from typing import Callable, Tuple, Dict, Any, Union +import io +from datetime import datetime, timezone +import threading +from collections import OrderedDict +from abc import ABC, abstractmethod +import traceback +import pkg_resources + +import dateutil.parser +from dateutil import tz + +from qiskit.version import __version__ as terra_version + +from .exceptions import ExperimentEntryNotFound, ExperimentEntryExists, ExperimentError + +LOG = logging.getLogger(__name__) + + +def qiskit_version(): + """Return the Qiskit version.""" + try: + return pkg_resources.get_distribution("qiskit").version + except Exception: # pylint: disable=broad-except + return {"qiskit-terra": terra_version} + + +def parse_timestamp(utc_dt: Union[datetime, str]) -> datetime: + """Parse a UTC ``datetime`` object or string. + + Args: + utc_dt: Input UTC `datetime` or string. + + Returns: + A ``datetime`` with the UTC timezone. + + Raises: + TypeError: If the input parameter value is not valid. + """ + if isinstance(utc_dt, str): + utc_dt = dateutil.parser.parse(utc_dt) + if not isinstance(utc_dt, datetime): + raise TypeError("Input `utc_dt` is not string or datetime.") + utc_dt = utc_dt.replace(tzinfo=timezone.utc) + return utc_dt + + +def utc_to_local(utc_dt: datetime) -> datetime: + """Convert input UTC timestamp to local timezone. + + Args: + utc_dt: Input UTC timestamp. + + Returns: + A ``datetime`` with the local timezone. + """ + local_dt = utc_dt.astimezone(tz.tzlocal()) + return local_dt + + +def plot_to_svg_bytes(figure: "pyplot.Figure") -> bytes: + """Convert a pyplot Figure to SVG in bytes. + + Args: + figure: Figure to be converted + + Returns: + Figure in bytes. + """ + buf = io.BytesIO() + opaque_color = list(figure.get_facecolor()) + opaque_color[3] = 1.0 # set alpha to opaque + figure.savefig(buf, format="svg", facecolor=tuple(opaque_color), edgecolor="none") + buf.seek(0) + figure_data = buf.read() + buf.close() + return figure_data + + +def save_data( + is_new: bool, new_func: Callable, update_func: Callable, new_data: Dict, update_data: Dict +) -> Tuple[bool, Any]: + """Save data in the database. + + Args: + is_new: ``True`` if `new_func` should be called. Otherwise `update_func` is called. + new_func: Function to create new entry in the database. + update_func: Function to update an existing entry in the database. + new_data: In addition to `update_data`, this data will be stored if creating + a new entry. + update_data: Data to be stored if updating an existing entry. + + Returns: + A tuple of whether the data was saved and the function return value. + + Raises: + ExperimentError: If unable to determine whether the entry exists. + """ + attempts = 0 + try: + # Attempt 3x for the unlikely scenario wherein is_new=False but the + # entry doesn't actually exists. The second try might also fail if an entry + # with the same ID somehow got created in the meantime. + while attempts < 3: + attempts += 1 + if is_new: + try: + return True, new_func(**{**new_data, **update_data}) + except ExperimentEntryExists: + is_new = False + else: + try: + return True, update_func(**update_data) + except ExperimentEntryNotFound: + is_new = True + raise ExperimentError("Unable to determine the existence of the entry.") + except Exception: # pylint: disable=broad-except + # Don't fail the experiment just because its data cannot be saved. + LOG.error("Unable to save the experiment data: %s", traceback.format_exc()) + return False, None + + +class ThreadSafeContainer(ABC): + """Base class for thread safe container.""" + + def __init__(self, init_values=None): + """ThreadSafeContainer constructor.""" + self._lock = threading.RLock() + self._container = self._init_container(init_values) + + @abstractmethod + def _init_container(self, init_values): + """Initialize the container.""" + pass + + def __getitem__(self, key): + with self._lock: + return self._container[key] + + def __setitem__(self, key, value): + with self._lock: + self._container[key] = value + + def __delitem__(self, key): + with self._lock: + del self._container[key] + + def __contains__(self, item): + with self._lock: + return item in self._container + + def __len__(self): + with self._lock: + return len(self._container) + + @property + def lock(self): + """Return lock used for this container.""" + return self._lock + + +class ThreadSafeOrderedDict(ThreadSafeContainer): + """Thread safe OrderedDict.""" + + def _init_container(self, init_values): + """Initialize the container.""" + return OrderedDict.fromkeys(init_values or []) + + def get(self, key, default): + """Return the value of the given key.""" + with self._lock: + return self._container.get(key, default) + + def keys(self): + """Return all key values.""" + with self._lock: + return list(self._container.keys()) + + def values(self): + """Return all values.""" + with self._lock: + return list(self._container.values()) + + def items(self): + """Return the key value pairs.""" + return self._container.items() + + +class ThreadSafeList(ThreadSafeContainer): + """Thread safe list.""" + + def _init_container(self, init_values): + """Initialize the container.""" + return init_values or [] + + def append(self, value): + """Append to the list.""" + with self._lock: + self._container.append(value) + + def copy(self): + """Returns a copy of the list.""" + with self.lock: + return self._container.copy() diff --git a/test/stored_data/__init__.py b/test/stored_data/__init__.py new file mode 100644 index 0000000000..62b7e61d2b --- /dev/null +++ b/test/stored_data/__init__.py @@ -0,0 +1,13 @@ +# 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 cases for stored data module.""" diff --git a/test/stored_data/test_analysisresult.py b/test/stored_data/test_analysisresult.py new file mode 100644 index 0000000000..6a41506997 --- /dev/null +++ b/test/stored_data/test_analysisresult.py @@ -0,0 +1,173 @@ +# 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. + +# pylint: disable=missing-docstring + +"""Test AnalysisResult.""" + +from unittest import mock +import json + +import numpy as np + +from qiskit.test import QiskitTestCase +from qiskit_experiments.store_data import AnalysisResultV1 as AnalysisResult +from qiskit_experiments.store_data.device_component import Qubit, Resonator, to_component +from qiskit_experiments.store_data.experiment_service import ExperimentServiceV1 +from qiskit_experiments.store_data.exceptions import ExperimentError + + +class TestAnalysisResult(QiskitTestCase): + """Test the AnalysisResult class.""" + + def test_analysis_result_attributes(self): + """Test analysis result attributes.""" + attrs = { + "result_type": "my_type", + "device_components": [Qubit(1), Qubit(2)], + "experiment_id": "1234", + "result_id": "5678", + "quality": "Good", + "verified": False, + } + result = AnalysisResult(result_data={"foo": "bar"}, tags=["tag1", "tag2"], **attrs) + self.assertEqual({"foo": "bar"}, result.data()) + self.assertEqual(["tag1", "tag2"], result.tags()) + for key, val in attrs.items(): + self.assertEqual(val, getattr(result, key)) + + def test_save(self): + """Test saving analysis result.""" + mock_service = mock.create_autospec(ExperimentServiceV1) + result = self._new_analysis_result() + result.save(service=mock_service) + mock_service.create_analysis_result.assert_called_once() + + def test_auto_save(self): + """Test auto saving.""" + mock_service = mock.create_autospec(ExperimentServiceV1) + result = self._new_analysis_result(service=mock_service) + result.auto_save = True + result.save() + + subtests = [ + # update function, update parameters, service called + (result.update_tags, (["foo"],)), + (result.update_data, ({"foo": "bar"},)), + (setattr, (result, "quality", "GOOD")), + (setattr, (result, "verified", True)), + ] + + for func, params in subtests: + with self.subTest(func=func): + func(*params) + mock_service.update_analysis_result.assert_called_once() + mock_service.reset_mock() + + def test_set_service_init(self): + """Test setting service in init.""" + mock_service = mock.create_autospec(ExperimentServiceV1) + result = self._new_analysis_result(service=mock_service) + self.assertEqual(mock_service, result.service) + + def test_set_service_direct(self): + """Test setting service directly.""" + mock_service = mock.create_autospec(ExperimentServiceV1) + result = self._new_analysis_result() + result.service = mock_service + self.assertEqual(mock_service, result.service) + + with self.assertRaises(ExperimentError): + result.service = mock_service + + def test_set_service_save(self): + """Test setting service when saving.""" + orig_service = mock.create_autospec(ExperimentServiceV1) + result = self._new_analysis_result(service=orig_service) + new_service = mock.create_autospec(ExperimentServiceV1) + result.save(service=new_service) + new_service.create_analysis_result.assert_called() + orig_service.create_analysis_result.assert_not_called() + + def test_update_data(self): + """Test updating data.""" + result = self._new_analysis_result() + result.update_data({"foo": "new data"}) + self.assertEqual({"foo": "new data"}, result.data()) + + def test_update_tags(self): + """Test updating tags.""" + result = self._new_analysis_result() + result.update_tags(["new_tag"]) + self.assertEqual(["new_tag"], result.tags()) + + def test_update_quality(self): + """Test updating quality.""" + result = self._new_analysis_result(quality="BAD") + result.quality = "GOOD" + self.assertEqual("GOOD", result.quality) + + def test_update_verified(self): + """Test updating verified.""" + result = self._new_analysis_result(verified=False) + result.verified = True + self.assertTrue(result.verified) + + def test_additional_attr(self): + """Test additional attributes.""" + result = self._new_analysis_result(foo="bar") + self.assertEqual("bar", result.foo) + + def test_data_serialization(self): + """Test result data serialization.""" + result = self._new_analysis_result(result_data={"complex": 2 + 3j, "numpy": np.zeros(2)}) + serialized = result.serialize_data() + self.assertIsInstance(serialized, str) + self.assertTrue(json.loads(serialized)) + + def test_source(self): + """Test getting analysis result source.""" + result = self._new_analysis_result() + source_vals = "\n".join([str(val) for val in result.source.values()]) + self.assertIn("AnalysisResultV1", source_vals) + self.assertIn("qiskit-terra", source_vals) + + def _new_analysis_result(self, **kwargs): + """Return a new analysis result.""" + values = { + "result_data": {"foo": "bar"}, + "result_type": "some_type", + "device_components": ["Q1", "Q1"], + "experiment_id": "1234", + } + values.update(kwargs) + return AnalysisResult(**values) + + +class TestDeviceComponent(QiskitTestCase): + """Test the DeviceComponent class.""" + + def test_str(self): + """Test string representation.""" + q1 = Qubit(1) + r1 = Resonator(1) + self.assertEqual("Q1", str(q1)) + self.assertEqual("R1", str(r1)) + + def test_to_component(self): + """Test converting string to component object.""" + q1 = to_component("Q1") + self.assertIsInstance(q1, Qubit) + self.assertEqual("Q1", str(q1)) + r1 = to_component("R1") + self.assertIsInstance(r1, Resonator) + self.assertEqual("R1", str(r1)) diff --git a/test/stored_data/test_stored_data.py b/test/stored_data/test_stored_data.py new file mode 100644 index 0000000000..6dce6f555c --- /dev/null +++ b/test/stored_data/test_stored_data.py @@ -0,0 +1,712 @@ +# 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. + +# pylint: disable=missing-docstring + +"""Test ExperimentData.""" + +import os +from unittest import mock, skipIf +import copy +from random import randrange +import time +import threading +import json +import re + +import numpy as np + +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeMelbourne +from qiskit.result import Result +from qiskit.providers import JobV1 as Job +from qiskit.providers import JobStatus +from qiskit.tools.visualization import HAS_MATPLOTLIB +from qiskit_experiments.store_data import StoredDataV1 as StoredData +from qiskit_experiments.store_data import ExperimentServiceV1 +from qiskit_experiments.store_data.exceptions import (ExperimentError, ExperimentEntryNotFound, + ExperimentEntryExists) + + +class TestExperimentData(QiskitTestCase): + """Test the ExperimentData class.""" + + def setUp(self): + super().setUp() + self.backend = FakeMelbourne() + + def test_stored_data_attributes(self): + """Test stored data attributes.""" + attrs = { + "job_ids": ["job1"], + "share_level": "global", + "figure_names": ["figure1"], + "notes": "some notes", + } + exp_data = StoredData( + backend=self.backend, + experiment_type="qiskit_test", + experiment_id="1234", + tags=["tag1", "tag2"], + metadata={"foo": "bar"}, + **attrs, + ) + self.assertEqual(exp_data.backend.name(), self.backend.name()) + self.assertEqual(exp_data.experiment_type, "qiskit_test") + self.assertEqual(exp_data.experiment_id, "1234") + self.assertEqual(exp_data.tags(), ["tag1", "tag2"]) + self.assertEqual(exp_data.metadata(), {"foo": "bar"}) + for key, val in attrs.items(): + self.assertEqual(getattr(exp_data, key), val) + + def test_add_data_dict(self): + """Test add data in dictionary.""" + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + a_dict = {"counts": {"01": 518}} + dicts = [{"counts": {"00": 284}}, {"counts": {"00": 14}}] + + exp_data.add_data(a_dict) + exp_data.add_data(dicts) + self.assertEqual([a_dict] + dicts, exp_data.data()) + + def test_add_data_result(self): + """Test add result data.""" + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + a_result = self._get_job_result(1) + results = [self._get_job_result(2), self._get_job_result(3)] + + expected = [a_result.get_counts()] + for res in results: + expected.extend(res.get_counts()) + + exp_data.add_data(a_result) + exp_data.add_data(results) + self.assertEqual(expected, [sdata["counts"] for sdata in exp_data.data()]) + self.assertIn(a_result.job_id, exp_data.job_ids) + + def test_add_data_result_metadata(self): + """Test add result metadata.""" + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + result1 = self._get_job_result(1, has_metadata=False) + result2 = self._get_job_result(1, has_metadata=True) + + exp_data.add_data(result1) + exp_data.add_data(result2) + self.assertNotIn("metadata", exp_data.data(0)) + self.assertIn("metadata", exp_data.data(1)) + + def test_add_data_job(self): + """Test add job data.""" + a_job = mock.create_autospec(Job, instance=True) + a_job.result.return_value = self._get_job_result(3) + jobs = [] + for _ in range(2): + job = mock.create_autospec(Job, instance=True) + job.result.return_value = self._get_job_result(2) + jobs.append(job) + + expected = a_job.result().get_counts() + for job in jobs: + expected.extend(job.result().get_counts()) + + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data.add_data(a_job) + exp_data.add_data(jobs) + exp_data.block_for_results() + self.assertEqual(expected, [sdata["counts"] for sdata in exp_data.data()]) + self.assertIn(a_job.job_id(), exp_data.job_ids) + + def test_add_data_job_callback(self): + """Test add job data with callback.""" + + def _callback(_exp_data, data_index): + self.assertIsInstance(_exp_data, StoredData) + self.assertEqual( + [dat["counts"] for dat in _exp_data.data()], a_job.result().get_counts() + ) + self.assertEqual(len(a_job.result().results)-1, data_index) + exp_data.add_figures(str.encode("hello world")) + exp_data.add_analysis_results(mock.MagicMock()) + nonlocal called_back + called_back = True + + a_job = mock.create_autospec(Job, instance=True) + a_job.result.return_value = self._get_job_result(2) + + called_back = False + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data.add_data(a_job, post_processing_callback=_callback) + exp_data.block_for_results() + self.assertTrue(called_back) + + def test_add_data_callback(self): + """Test add data with callback.""" + + def _callback(_exp_data, data_index): + self.assertIsInstance(_exp_data, StoredData) + nonlocal called_back_count, expected_data, subtests + expected_data.extend(subtests[called_back_count][1]) + self.assertEqual([dat["counts"] for dat in _exp_data.data()], expected_data) + self.assertEqual(len(_exp_data.data())-1, data_index) + called_back_count += 1 + + a_result = self._get_job_result(1) + results = [self._get_job_result(1), self._get_job_result(1)] + a_dict = {"counts": {"01": 518}} + dicts = [{"counts": {"00": 284}}, {"counts": {"00": 14}}] + + subtests = [ + (a_result, [a_result.get_counts()]), + (results, [res.get_counts() for res in results]), + (a_dict, [a_dict["counts"]]), + (dicts, [dat["counts"] for dat in dicts]), + ] + + called_back_count = 0 + expected_data = [] + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + + for data, _ in subtests: + with self.subTest(data=data): + exp_data.add_data(data, post_processing_callback=_callback) + + self.assertEqual(len(subtests), called_back_count) + + def test_add_data_job_callback_kwargs(self): + """Test add job data with callback and additional arguments.""" + + def _callback(_exp_data, data_index, **kwargs): + self.assertIsInstance(_exp_data, StoredData) + self.assertEqual(len(_exp_data.data())-1, data_index) + self.assertEqual({"foo": callback_kwargs}, kwargs) + nonlocal called_back + called_back = True + + a_job = mock.create_autospec(Job, instance=True) + a_job.result.return_value = self._get_job_result(2) + + called_back = False + callback_kwargs = "foo" + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data.add_data(a_job, _callback, foo=callback_kwargs) + exp_data.block_for_results() + self.assertTrue(called_back) + + def test_get_data(self): + """Test getting data.""" + data1 = [] + for _ in range(5): + data1.append({"counts": {"00": randrange(1024)}}) + results = self._get_job_result(3) + + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data(data1) + exp_data.add_data(results) + self.assertEqual(data1[1], exp_data.data(1)) + self.assertEqual(data1[2:4], exp_data.data(slice(2, 4))) + self.assertEqual( + results.get_counts(), [sdata["counts"] for sdata in exp_data.data(results.job_id)] + ) + + def test_add_figure(self): + """Test adding a new figure.""" + hello_bytes = str.encode("hello world") + file_name = "hello_world.svg" + self.addCleanup(os.remove, file_name) + with open(file_name, "wb") as file: + file.write(hello_bytes) + + sub_tests = [ + ("file name", file_name, None), + ("file bytes", hello_bytes, None), + ("new name", hello_bytes, "hello_again.svg"), + ] + + for name, figure, figure_name in sub_tests: + with self.subTest(name=name): + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + fn = exp_data.add_figures(figure, figure_name) + self.assertEqual(hello_bytes, exp_data.figure(fn)) + + @skipIf(not HAS_MATPLOTLIB, "matplotlib not available.") + def test_add_figure_plot(self): + """Test adding a matplotlib figure.""" + import matplotlib.pyplot as plt + + figure, ax = plt.subplots() + ax.plot([1, 2, 3]) + + service = self._set_mock_service() + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data.add_figures(figure, save_figure=True) + self.assertEqual(figure, exp_data.figure(0)) + service.create_figure.assert_called_once() + _, kwargs = service.create_figure.call_args + self.assertIsInstance(kwargs["figure"], bytes) + + def test_add_figures(self): + """Test adding multiple new figures.""" + hello_bytes = [str.encode("hello world"), str.encode("hello friend")] + file_names = ["hello_world.svg", "hello_friend.svg"] + for idx, fn in enumerate(file_names): + self.addCleanup(os.remove, fn) + with open(fn, "wb") as file: + file.write(hello_bytes[idx]) + + sub_tests = [ + ("file names", file_names, None), + ("file bytes", hello_bytes, None), + ("new names", hello_bytes, ["hello1.svg", "hello2.svg"]), + ] + + for name, figures, figure_names in sub_tests: + with self.subTest(name=name): + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + added_names = exp_data.add_figures(figures, figure_names) + for idx, added_fn in enumerate(added_names): + self.assertEqual(hello_bytes[idx], exp_data.figure(added_fn)) + + def test_add_figure_overwrite(self): + """Test updating an existing figure.""" + hello_bytes = str.encode("hello world") + friend_bytes = str.encode("hello friend!") + + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + fn = exp_data.add_figures(hello_bytes) + with self.assertRaises(ExperimentEntryExists): + exp_data.add_figures(friend_bytes, fn) + + exp_data.add_figures(friend_bytes, fn, overwrite=True) + self.assertEqual(friend_bytes, exp_data.figure(fn)) + + def test_add_figure_save(self): + """Test saving a figure in the database.""" + hello_bytes = str.encode("hello world") + service = self._set_mock_service() + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data.add_figures(hello_bytes, save_figure=True) + service.create_figure.assert_called_once() + _, kwargs = service.create_figure.call_args + self.assertEqual(kwargs["figure"], hello_bytes) + self.assertEqual(kwargs["experiment_id"], exp_data.experiment_id) + + def test_add_figure_bad_input(self): + """Test adding figures with bad input.""" + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + self.assertRaises(ValueError, exp_data.add_figures, ["foo", "bar"], ["name"]) + + def test_get_figure(self): + """Test getting figure.""" + exp_data = StoredData(experiment_type="qiskit_test") + figure_template = "hello world {}" + name_template = "figure_{}" + for idx in range(3): + exp_data.add_figures( + str.encode(figure_template.format(idx)), figure_names=name_template.format(idx) + ) + idx = randrange(3) + expected_figure = str.encode(figure_template.format(idx)) + self.assertEqual(expected_figure, exp_data.figure(name_template.format(idx))) + self.assertEqual(expected_figure, exp_data.figure(idx)) + + file_name = "hello_world.svg" + self.addCleanup(os.remove, file_name) + exp_data.figure(idx, file_name) + with open(file_name, "rb") as file: + self.assertEqual(expected_figure, file.read()) + + def test_delete_figure(self): + """Test deleting a figure.""" + exp_data = StoredData(experiment_type="qiskit_test") + id_template = "figure_{}" + for idx in range(3): + exp_data.add_figures(str.encode("hello world"), id_template.format(idx)) + + sub_tests = [(1, id_template.format(1)), (id_template.format(2), id_template.format(2))] + + for del_key, figure_name in sub_tests: + with self.subTest(del_key=del_key): + exp_data.delete_figure(del_key) + self.assertRaises(ExperimentEntryNotFound, exp_data.figure, figure_name) + + def test_delayed_backend(self): + """Test initializing experiment data without a backend.""" + exp_data = StoredData(experiment_type="qiskit_test") + self.assertIsNone(exp_data.backend) + self.assertIsNone(exp_data.service) + exp_data.save() + a_job = mock.create_autospec(Job, instance=True) + exp_data.add_data(a_job) + self.assertIsNotNone(exp_data.backend) + self.assertIsNotNone(exp_data.service) + + def test_different_backend(self): + """Test setting a different backend.""" + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + a_job = mock.create_autospec(Job, instance=True) + self.assertNotEqual(exp_data.backend, a_job.backend()) + with self.assertLogs("qiskit_experiments", "WARNING"): + exp_data.add_data(a_job) + + def test_add_get_analysis_result(self): + """Test adding and getting analysis results.""" + exp_data = StoredData(experiment_type="qiskit_test") + results = [] + for idx in range(5): + res = mock.MagicMock() + res.result_id = idx + results.append(res) + exp_data.add_analysis_results(res) + + self.assertEqual(results, exp_data.analysis_result()) + self.assertEqual(results[1], exp_data.analysis_result(1)) + self.assertEqual(results[2:4], exp_data.analysis_result(slice(2, 4))) + self.assertEqual(results[4], exp_data.analysis_result(results[4].result_id)) + + def test_add_get_analysis_results(self): + """Test adding and getting a list of analysis results.""" + exp_data = StoredData(experiment_type="qiskit_test") + results = [] + for idx in range(5): + res = mock.MagicMock() + res.result_id = idx + results.append(res) + exp_data.add_analysis_results(results) + + self.assertEqual(results, exp_data.analysis_result()) + + def test_delete_analysis_result(self): + """Test deleting analysis result.""" + exp_data = StoredData(experiment_type="qiskit_test") + id_template = "result_{}" + for idx in range(3): + res = mock.MagicMock() + res.result_id = id_template.format(idx) + exp_data.add_analysis_results(res) + + subtests = [(0, id_template.format(0)), (id_template.format(2), id_template.format(2))] + for del_key, res_id in subtests: + with self.subTest(del_key=del_key): + exp_data.delete_analysis_result(del_key) + self.assertRaises(ExperimentEntryNotFound, exp_data.analysis_result, res_id) + + def test_save(self): + """Test saving experiment data.""" + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + service = mock.create_autospec(ExperimentServiceV1, instance=True) + exp_data.save(service=service) + service.create_experiment.assert_called_once() + _, kwargs = service.create_experiment.call_args + self.assertEqual(exp_data.experiment_id, kwargs["experiment_id"]) + exp_data.save(service=service) + service.update_experiment.assert_called_once() + _, kwargs = service.update_experiment.call_args + self.assertEqual(exp_data.experiment_id, kwargs["experiment_id"]) + + def test_save_all(self): + """Test saving all experiment related data.""" + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + service = mock.create_autospec(ExperimentServiceV1, instance=True) + exp_data.add_figures(str.encode("hello world")) + analysis_result = mock.MagicMock() + exp_data.add_analysis_results(analysis_result) + exp_data.save_all(service=service) + service.create_experiment.assert_called_once() + service.create_figure.assert_called_once() + analysis_result.save.assert_called_once() + + def test_save_all_delete(self): + """Test saving all deletion.""" + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + service = mock.create_autospec(ExperimentServiceV1, instance=True) + exp_data.add_figures(str.encode("hello world")) + exp_data.add_analysis_results(mock.MagicMock()) + exp_data.delete_analysis_result(0) + exp_data.delete_figure(0) + + exp_data.save_all(service=service) + service.create_experiment.assert_called_once() + service.delete_figure.assert_called_once() + service.delete_analysis_result.assert_called_once() + + def test_set_service_backend(self): + """Test setting service via backend.""" + mock_service = self._set_mock_service() + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + self.assertEqual(mock_service, exp_data.service) + + def test_set_service_job(self): + """Test setting service via adding a job.""" + mock_service = self._set_mock_service() + job = mock.create_autospec(Job, instance=True) + job.backend.return_value = self.backend + exp_data = StoredData(experiment_type="qiskit_test") + self.assertIsNone(exp_data.service) + exp_data.add_data(job) + self.assertEqual(mock_service, exp_data.service) + + def test_set_service_direct(self): + """Test setting service directly.""" + exp_data = StoredData(experiment_type="qiskit_test") + self.assertIsNone(exp_data.service) + mock_service = mock.MagicMock() + exp_data.service = mock_service + self.assertEqual(mock_service, exp_data.service) + + with self.assertRaises(ExperimentError): + exp_data.service = mock_service + + def test_set_service_save(self): + """Test setting service when saving.""" + orig_service = self._set_mock_service() + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + new_service = mock.create_autospec(ExperimentServiceV1, instance=True) + exp_data.save(service=new_service) + new_service.create_experiment.assert_called() + orig_service.create_experiment.assert_not_called() + + def test_new_backend_has_service(self): + """Test changing backend doesn't change existing service.""" + orig_service = self._set_mock_service() + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + self.assertEqual(orig_service, exp_data.service) + + job = mock.create_autospec(Job, instance=True) + new_service = self._set_mock_service() + self.assertNotEqual(orig_service, new_service) + job.backend.return_value = self.backend + exp_data.add_data(job) + self.assertEqual(orig_service, exp_data.service) + + def test_auto_save(self): + """Test auto save.""" + service = self._set_mock_service() + exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data.auto_save = True + + mock_result = mock.MagicMock() + exp_data.save() + + subtests = [ + # update function, update parameters, service called + (exp_data.add_analysis_results, (mock_result,), mock_result.save), + (exp_data.add_figures, (str.encode("hello world"),), service.create_figure), + (exp_data.delete_figure, (0,), service.delete_figure), + (exp_data.delete_analysis_result, (0,), service.delete_analysis_result), + (exp_data.update_tags, (["foo"],), None), + (exp_data.update_metadata, ({"foo": "bar"},), None), + (setattr, (exp_data, "notes", "foo"), None), + (setattr, (exp_data, "share_level", "hub"), None), + ] + + for func, params, called in subtests: + with self.subTest(func=func): + func(*params) + service.update_experiment.assert_called_once() + if called: + called.assert_called_once() + service.reset_mock() + + def test_status_job_pending(self): + """Test experiment status when job is pending.""" + job1 = mock.create_autospec(Job, instance=True) + job1.result.return_value = self._get_job_result(3) + job1.status.return_value = JobStatus.DONE + + event = threading.Event() + job2 = mock.create_autospec(Job, instance=True) + job2.result = lambda *args, **kwargs: event.wait() + job2.status.return_value = JobStatus.RUNNING + self.addCleanup(event.set) + + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data(job1) + exp_data.add_data(job2, lambda *args, **kwargs: event.wait()) + self.assertEqual("RUNNING", exp_data.status()) + + def test_status_job_error(self): + """Test experiment status when job failed.""" + job1 = mock.create_autospec(Job, instance=True) + job1.result.return_value = self._get_job_result(3) + job1.status.return_value = JobStatus.DONE + + job2 = mock.create_autospec(Job, instance=True) + job2.status.return_value = JobStatus.ERROR + + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data([job1, job2]) + self.assertEqual("ERROR", exp_data.status()) + + def test_status_post_processing(self): + """Test experiment status during post processing.""" + job = mock.create_autospec(Job, instance=True) + job.result.return_value = self._get_job_result(3) + + event = threading.Event() + self.addCleanup(event.set) + + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data(job) + exp_data.add_data(job, lambda *args, **kwargs: event.wait()) + self.assertEqual("POST_PROCESSING", exp_data.status()) + + def test_status_post_processing_error(self): + """Test experiment status when post processing failed.""" + + def _post_processing(*args, **kwargs): + raise ValueError("Kaboom!") + + job = mock.create_autospec(Job, instance=True) + job.result.return_value = self._get_job_result(3) + + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data(job) + exp_data.add_data(job, _post_processing) + exp_data.block_for_results() + self.assertEqual("ERROR", exp_data.status()) + + def test_status_done(self): + """Test experiment status when all jobs are done.""" + job = mock.create_autospec(Job, instance=True) + job.result.return_value = self._get_job_result(3) + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data(job) + exp_data.add_data(job, lambda *args, **kwargs: time.sleep(1)) + exp_data.block_for_results() + self.assertEqual("DONE", exp_data.status()) + + def test_update_tags(self): + """Test updating experiment tags.""" + exp_data = StoredData(experiment_type="qiskit_test", tags=["foo"]) + self.assertEqual(["foo"], exp_data.tags()) + exp_data.update_tags(["bar"]) + self.assertEqual(["bar"], exp_data.tags()) + + def test_update_metadata(self): + """Test updating experiment metadata.""" + exp_data = StoredData(experiment_type="qiskit_test", metadata={"foo": "bar"}) + self.assertEqual({"foo": "bar"}, exp_data.metadata()) + exp_data.update_metadata({"bar": "foo"}) + self.assertEqual({"bar": "foo"}, exp_data.metadata()) + + def test_cancel_jobs(self): + """Test canceling experiment jobs.""" + exp_data = StoredData(experiment_type="qiskit_test") + event = threading.Event() + self.addCleanup(event.set) + job = mock.create_autospec(Job, instance=True) + job.result = lambda *args, **kwargs: event.wait() + exp_data.add_data(job) + exp_data.cancel_jobs() + job.cancel.assert_called_once() + + def test_metadata_serialization(self): + """Test experiment metadata serialization.""" + metadata = {"complex": 2 + 3j, "numpy": np.zeros(2)} + exp_data = StoredData(experiment_type="qiskit_test", metadata=metadata) + serialized = exp_data.serialize_metadata() + self.assertIsInstance(serialized, str) + self.assertTrue(json.loads(serialized)) + + deserialized = StoredData.deserialize_metadata(serialized) + self.assertEqual(metadata["complex"], deserialized["complex"]) + self.assertEqual(metadata["numpy"].all(), deserialized["numpy"].all()) + + def test_errors(self): + """Test getting experiment error message.""" + + def _post_processing(*args, **kwargs): # pylint: disable=unused-argument + raise ValueError("Kaboom!") + + job1 = mock.create_autospec(Job, instance=True) + job1.job_id.return_value = "1234" + + job2 = mock.create_autospec(Job, instance=True) + job2.status.return_value = JobStatus.ERROR + job2.job_id.return_value = "5678" + + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data(job1, _post_processing) + exp_data.add_data(job2) + exp_data.block_for_results() + self.assertEqual("ERROR", exp_data.status()) + self.assertTrue(re.match(r".*1234.*Kaboom!.*5678", exp_data.errors(), re.DOTALL)) + + def test_source(self): + """Test getting experiment source.""" + exp_data = StoredData(experiment_type="qiskit_test") + source_vals = "\n".join([str(val) for val in exp_data.source.values()]) + self.assertIn("StoredDataV1", source_vals) + self.assertIn("qiskit-terra", source_vals) + + def test_block_for_jobs(self): + """Test blocking for jobs.""" + + def _sleeper(*args, **kwargs): # pylint: disable=unused-argument + time.sleep(2) + nonlocal sleep_count + sleep_count += 1 + return self._get_job_result(1) + + sleep_count = 0 + job = mock.create_autospec(Job, instance=True) + job.result = _sleeper + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data(job, _sleeper) + exp_data.block_for_results() + self.assertEqual(2, sleep_count) + + def test_additional_attr(self): + """Test additional experiment attributes.""" + exp_data = StoredData(experiment_type="qiskit_test", foo="foo") + self.assertEqual("foo", exp_data.foo) + + def test_str(self): + """Test the string representation.""" + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data(self._get_job_result(1)) + result = mock.MagicMock() + exp_data.add_analysis_results(result) + exp_data_str = str(exp_data) + self.assertIn(exp_data.experiment_type, exp_data_str) + self.assertIn(exp_data.experiment_id, exp_data_str) + self.assertIn(str(result), exp_data_str) + + def _get_job_result(self, circ_count, has_metadata=False): + """Return a job result with random counts.""" + job_result = { + "backend_name": self.backend.name(), + "backend_version": "1.1.1", + "qobj_id": "1234", + "job_id": "some_job_id", + "success": True, + "results": [], + } + circ_result_template = {"shots": 1024, "success": True, "data": {}} + + for _ in range(circ_count): + counts = randrange(1024) + circ_result = copy.copy(circ_result_template) + circ_result["data"] = {"counts": {"0x0": counts, "0x3": 1024 - counts}} + if has_metadata: + circ_result["header"] = {"metadata": {"meas_basis": "pauli"}} + job_result["results"].append(circ_result) + + return Result.from_dict(job_result) + + def _set_mock_service(self): + """Add a mock service to the backend.""" + mock_provider = mock.MagicMock() + self.backend._provider = mock_provider + mock_service = mock.create_autospec(ExperimentServiceV1, instance=True) + mock_provider.service.return_value = mock_service + return mock_service From 75067595d11109ddbb6114a31ffcd168d5dd014d Mon Sep 17 00:00:00 2001 From: jessieyu Date: Wed, 16 Jun 2021 20:50:24 -0400 Subject: [PATCH 02/70] run black --- .../store_data/analysis_result.py | 34 ++--- .../store_data/experiment_service.py | 130 +++++++++--------- qiskit_experiments/store_data/stored_data.py | 92 ++++++------- qiskit_experiments/store_data/utils.py | 2 +- test/stored_data/test_stored_data.py | 20 +-- 5 files changed, 139 insertions(+), 139 deletions(-) diff --git a/qiskit_experiments/store_data/analysis_result.py b/qiskit_experiments/store_data/analysis_result.py index cf3e17530e..c27aafe5c6 100644 --- a/qiskit_experiments/store_data/analysis_result.py +++ b/qiskit_experiments/store_data/analysis_result.py @@ -64,17 +64,17 @@ class AnalysisResultV1(AnalysisResult): _extra_data = {} def __init__( - self, - result_data: Dict, - result_type: str, - device_components: List[Union[DeviceComponent, str]], - experiment_id: str, - result_id: Optional[str] = None, - quality: Optional[str] = None, - verified: bool = False, - tags: Optional[List[str]] = None, - service: Optional["ExperimentServiceV1"] = None, - **kwargs, + self, + result_data: Dict, + result_type: str, + device_components: List[Union[DeviceComponent, str]], + experiment_id: str, + result_id: Optional[str] = None, + quality: Optional[str] = None, + verified: bool = False, + tags: Optional[List[str]] = None, + service: Optional["ExperimentServiceV1"] = None, + **kwargs, ): """AnalysisResult constructor. @@ -149,12 +149,12 @@ def deserialize_data(cls, data: str) -> Dict: @classmethod def from_data( - cls, - result_data: Dict, - result_type: str, - device_components: List[Union[DeviceComponent, str]], - experiment_id: str, - **kwargs, + cls, + result_data: Dict, + result_type: str, + device_components: List[Union[DeviceComponent, str]], + experiment_id: str, + **kwargs, ) -> "AnalysisResultV1": """Reconstruct the analysis result from input data. diff --git a/qiskit_experiments/store_data/experiment_service.py b/qiskit_experiments/store_data/experiment_service.py index 71fa8d0ab1..0cf172ffca 100644 --- a/qiskit_experiments/store_data/experiment_service.py +++ b/qiskit_experiments/store_data/experiment_service.py @@ -17,7 +17,7 @@ from .device_component import DeviceComponent -T = TypeVar('T') +T = TypeVar("T") class ExperimentService: @@ -61,15 +61,15 @@ def _default_options(cls) -> Dict: @abstractmethod def create_experiment( - self, - experiment_type: str, - backend_name: str, - metadata: Optional[Dict] = None, - experiment_id: Optional[str] = None, - job_ids: Optional[List[str]] = None, - tags: Optional[List[str]] = None, - notes: Optional[str] = None, - **kwargs: Any, + self, + experiment_type: str, + backend_name: str, + metadata: Optional[Dict] = None, + experiment_id: Optional[str] = None, + job_ids: Optional[List[str]] = None, + tags: Optional[List[str]] = None, + notes: Optional[str] = None, + **kwargs: Any, ) -> str: """Create a new experiment in the database. @@ -94,13 +94,13 @@ def create_experiment( @abstractmethod def update_experiment( - self, - experiment_id: str, - metadata: Optional[Dict] = None, - job_ids: Optional[List[str]] = None, - notes: Optional[str] = None, - tags: Optional[List[str]] = None, - **kwargs: Any, + self, + experiment_id: str, + metadata: Optional[Dict] = None, + job_ids: Optional[List[str]] = None, + notes: Optional[str] = None, + tags: Optional[List[str]] = None, + **kwargs: Any, ) -> None: """Update an existing experiment. @@ -119,9 +119,7 @@ def update_experiment( @abstractmethod def experiment( - self, - experiment_id: str, - experiment_class: Optional[Type[T]] = None + self, experiment_id: str, experiment_class: Optional[Type[T]] = None ) -> Union[Dict, T]: """Retrieve a previously stored experiment. @@ -142,15 +140,15 @@ def experiment( @abstractmethod def experiments( - self, - limit: Optional[int] = 10, - experiment_class: Optional[Type[T]] = None, - device_components: Optional[Union[str, DeviceComponent]] = None, - experiment_type: Optional[str] = None, - backend_name: Optional[str] = None, - tags: Optional[List[str]] = None, - tags_operator: Optional[str] = "OR", - **filters: Any, + self, + limit: Optional[int] = 10, + experiment_class: Optional[Type[T]] = None, + device_components: Optional[Union[str, DeviceComponent]] = None, + experiment_type: Optional[str] = None, + backend_name: Optional[str] = None, + tags: Optional[List[str]] = None, + tags_operator: Optional[str] = "OR", + **filters: Any, ) -> List[Union[Dict, T]]: """Retrieve all experiment data, with optional filtering. @@ -193,16 +191,16 @@ def delete_experiment(self, experiment_id: str) -> None: @abstractmethod def create_analysis_result( - self, - experiment_id: str, - data: Dict, - result_type: str, - device_components: Optional[Union[str, DeviceComponent]] = None, - tags: Optional[List[str]] = None, - quality: Optional[str] = None, - verified: bool = False, - result_id: Optional[str] = None, - **kwargs: Any, + self, + experiment_id: str, + data: Dict, + result_type: str, + device_components: Optional[Union[str, DeviceComponent]] = None, + tags: Optional[List[str]] = None, + quality: Optional[str] = None, + verified: bool = False, + result_id: Optional[str] = None, + **kwargs: Any, ) -> str: """Create a new analysis result in the database. @@ -228,13 +226,13 @@ def create_analysis_result( @abstractmethod def update_analysis_result( - self, - result_id: str, - data: Optional[Dict] = None, - tags: Optional[List[str]] = None, - quality: Optional[str] = None, - verified: bool = None, - **kwargs: Any, + self, + result_id: str, + data: Optional[Dict] = None, + tags: Optional[List[str]] = None, + quality: Optional[str] = None, + verified: bool = None, + **kwargs: Any, ) -> None: """Update an existing analysis result. @@ -253,9 +251,7 @@ def update_analysis_result( @abstractmethod def analysis_result( - self, - result_id: str, - result_class: Optional[Type[T]] = None + self, result_id: str, result_class: Optional[Type[T]] = None ) -> Union[Dict, T]: """Retrieve a previously stored experiment. @@ -275,18 +271,18 @@ def analysis_result( @abstractmethod def analysis_results( - self, - limit: Optional[int] = 10, - result_class: Optional[Type[T]] = None, - device_components: Optional[Union[str, DeviceComponent]] = None, - experiment_id: Optional[str] = None, - result_type: Optional[str] = None, - backend_name: Optional[str] = None, - quality: Optional[str] = None, - verified: Optional[bool] = None, - tags: Optional[List[str]] = None, - tags_operator: Optional[str] = "OR", - **filters: Any, + self, + limit: Optional[int] = 10, + result_class: Optional[Type[T]] = None, + device_components: Optional[Union[str, DeviceComponent]] = None, + experiment_id: Optional[str] = None, + result_type: Optional[str] = None, + backend_name: Optional[str] = None, + quality: Optional[str] = None, + verified: Optional[bool] = None, + tags: Optional[List[str]] = None, + tags_operator: Optional[str] = "OR", + **filters: Any, ) -> List[Union[Dict, T]]: """Retrieve all analysis results, with optional filtering. @@ -332,7 +328,7 @@ def delete_analysis_result(self, result_id: str) -> None: @abstractmethod def create_figure( - self, experiment_id: str, figure: Union[str, bytes], figure_name: Optional[str] + self, experiment_id: str, figure: Union[str, bytes], figure_name: Optional[str] ) -> Tuple[str, int]: """Store a new figure in the database. @@ -352,7 +348,7 @@ def create_figure( @abstractmethod def update_figure( - self, experiment_id: str, figure: Union[str, bytes], figure_name: str + self, experiment_id: str, figure: Union[str, bytes], figure_name: str ) -> Tuple[str, int]: """Update an existing figure. @@ -371,7 +367,7 @@ def update_figure( @abstractmethod def figure( - self, experiment_id: str, figure_name: str, file_name: Optional[str] = None + self, experiment_id: str, figure_name: str, file_name: Optional[str] = None ) -> Union[int, bytes]: """Retrieve an existing figure. @@ -392,9 +388,9 @@ def figure( @abstractmethod def delete_figure( - self, - experiment_id: str, - figure_name: str, + self, + experiment_id: str, + figure_name: str, ) -> None: """Delete an existing figure. diff --git a/qiskit_experiments/store_data/stored_data.py b/qiskit_experiments/store_data/stored_data.py index 2c40cce9bc..0903d5fc51 100644 --- a/qiskit_experiments/store_data/stored_data.py +++ b/qiskit_experiments/store_data/stored_data.py @@ -87,17 +87,17 @@ class StoredDataV1(StoredData): _json_decoder = NumpyDecoder def __init__( - self, - experiment_type: str, - backend: Optional[Union[Backend, BaseBackend]] = None, - experiment_id: Optional[str] = None, - tags: Optional[List[str]] = None, - job_ids: Optional[List[str]] = None, - share_level: Optional[str] = None, - metadata: Optional[Dict] = None, - figure_names: Optional[List[str]] = None, - notes: Optional[str] = None, - **kwargs, + self, + experiment_type: str, + backend: Optional[Union[Backend, BaseBackend]] = None, + experiment_id: Optional[str] = None, + tags: Optional[List[str]] = None, + job_ids: Optional[List[str]] = None, + share_level: Optional[str] = None, + metadata: Optional[Dict] = None, + figure_names: Optional[List[str]] = None, + notes: Optional[str] = None, + **kwargs, ): """Initializes the StoredDataV1 instance. @@ -166,10 +166,10 @@ def _set_service_from_backend(self, backend: Union[Backend, BaseBackend]) -> Non self.auto_save = self._service.option("auto_save") def add_data( - self, - data: Union[Result, List[Result], Job, List[Job], Dict, List[Dict]], - post_processing_callback: Optional[Callable] = None, - **kwargs: Any, + self, + data: Union[Result, List[Result], Job, List[Job], Dict, List[Dict]], + post_processing_callback: Optional[Callable] = None, + **kwargs: Any, ) -> None: """Add experiment data. @@ -245,13 +245,13 @@ def add_data( raise TypeError(f"Invalid data type {type(data)}.") if post_processing_callback is not None: - post_processing_callback(self, len(self._data)-1, **kwargs) + post_processing_callback(self, len(self._data) - 1, **kwargs) def _wait_for_job( - self, - job: Union[Job, BaseJob], - job_done_callback: Optional[Callable] = None, - **kwargs: Any, + self, + job: Union[Job, BaseJob], + job_done_callback: Optional[Callable] = None, + **kwargs: Any, ) -> None: """Wait for a job to finish. @@ -266,7 +266,7 @@ def _wait_for_job( with self._data.lock: # Hold the lock so we add the block of results together. self._add_result_data(job_result) - data_index = len(self._data)-1 + data_index = len(self._data) - 1 except JobError as err: LOG.warning("Job %s failed: %s", job.job_id(), str(err)) return @@ -340,12 +340,12 @@ def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, Li @auto_save def add_figures( - self, - figures: Union[List[Union[str, bytes, "pyplot.Figure"]], str, bytes, "pyplot.Figure"], - figure_names: Optional[Union[List[str], str]] = None, - overwrite: bool = False, - save_figure: Optional[bool] = None, - service: Optional["ExperimentServiceV1"] = None, + self, + figures: Union[List[Union[str, bytes, "pyplot.Figure"]], str, bytes, "pyplot.Figure"], + figure_names: Optional[Union[List[str], str]] = None, + overwrite: bool = False, + save_figure: Optional[bool] = None, + service: Optional["ExperimentServiceV1"] = None, ) -> Union[str, List[str]]: """Add the experiment figure. @@ -370,9 +370,9 @@ def add_figures( ValueError: If an input parameter has an invalid value. """ if ( - isinstance(figures, list) - and figure_names is not None - and (not isinstance(figure_names, list) or len(figures) != len(figure_names)) + isinstance(figures, list) + and figure_names is not None + and (not isinstance(figure_names, list) or len(figures) != len(figure_names)) ): raise ValueError( "The parameter figure_names must be None or a list of " @@ -436,9 +436,9 @@ def add_figures( @auto_save def delete_figure( - self, - figure_key: Union[str, int], - service: Optional["ExperimentServiceV1"] = None, + self, + figure_key: Union[str, int], + service: Optional["ExperimentServiceV1"] = None, ) -> str: """Add the experiment figure. @@ -470,7 +470,7 @@ def delete_figure( return figure_key def figure( - self, figure_key: Union[str, int], file_name: Optional[str] = None + self, figure_key: Union[str, int], file_name: Optional[str] = None ) -> Union[int, bytes]: """Retrieve the specified experiment figure. @@ -507,9 +507,9 @@ def figure( @auto_save def add_analysis_results( - self, - results: Union[AnalysisResult, List[AnalysisResult]], - service: "ExperimentServiceV1" = None, + self, + results: Union[AnalysisResult, List[AnalysisResult]], + service: "ExperimentServiceV1" = None, ) -> None: """Save the analysis result. @@ -534,7 +534,7 @@ def add_analysis_results( @auto_save def delete_analysis_result( - self, result_key: Union[int, str], service: "ExperimentServiceV1" = None + self, result_key: Union[int, str], service: "ExperimentServiceV1" = None ) -> str: """Delete the analysis result. @@ -566,7 +566,7 @@ def delete_analysis_result( return result_key def analysis_result( - self, index: Optional[Union[int, slice, str]] = None, refresh: bool = False + self, index: Optional[Union[int, slice, str]] = None, refresh: bool = False ) -> Union[AnalysisResult, List[AnalysisResult]]: """Return analysis results associated with this experiment. @@ -731,11 +731,11 @@ def deserialize_metadata(cls, data: str) -> Any: @classmethod def from_data( - cls, - experiment_type: str, - experiment_id: str, - metadata: Optional[Dict] = None, - **kwargs, + cls, + experiment_type: str, + experiment_id: str, + metadata: Optional[Dict] = None, + **kwargs, ) -> "StoredDataV1": """Reconstruct a StoredData using the input data. @@ -792,8 +792,8 @@ def status(self) -> str: Data processing status. """ if all( - len(container) == 0 - for container in [self._data, self._jobs, self._figures, self._analysis_results] + len(container) == 0 + for container in [self._data, self._jobs, self._figures, self._analysis_results] ): return "INITIALIZING" diff --git a/qiskit_experiments/store_data/utils.py b/qiskit_experiments/store_data/utils.py index 66a0b6f6e1..201a4edd8e 100644 --- a/qiskit_experiments/store_data/utils.py +++ b/qiskit_experiments/store_data/utils.py @@ -93,7 +93,7 @@ def plot_to_svg_bytes(figure: "pyplot.Figure") -> bytes: def save_data( - is_new: bool, new_func: Callable, update_func: Callable, new_data: Dict, update_data: Dict + is_new: bool, new_func: Callable, update_func: Callable, new_data: Dict, update_data: Dict ) -> Tuple[bool, Any]: """Save data in the database. diff --git a/test/stored_data/test_stored_data.py b/test/stored_data/test_stored_data.py index 6dce6f555c..f7c18ebcd4 100644 --- a/test/stored_data/test_stored_data.py +++ b/test/stored_data/test_stored_data.py @@ -22,6 +22,7 @@ import threading import json import re +import uuid import numpy as np @@ -33,8 +34,11 @@ from qiskit.tools.visualization import HAS_MATPLOTLIB from qiskit_experiments.store_data import StoredDataV1 as StoredData from qiskit_experiments.store_data import ExperimentServiceV1 -from qiskit_experiments.store_data.exceptions import (ExperimentError, ExperimentEntryNotFound, - ExperimentEntryExists) +from qiskit_experiments.store_data.exceptions import ( + ExperimentError, + ExperimentEntryNotFound, + ExperimentEntryExists, +) class TestExperimentData(QiskitTestCase): @@ -133,7 +137,7 @@ def _callback(_exp_data, data_index): self.assertEqual( [dat["counts"] for dat in _exp_data.data()], a_job.result().get_counts() ) - self.assertEqual(len(a_job.result().results)-1, data_index) + self.assertEqual(len(a_job.result().results) - 1, data_index) exp_data.add_figures(str.encode("hello world")) exp_data.add_analysis_results(mock.MagicMock()) nonlocal called_back @@ -156,7 +160,7 @@ def _callback(_exp_data, data_index): nonlocal called_back_count, expected_data, subtests expected_data.extend(subtests[called_back_count][1]) self.assertEqual([dat["counts"] for dat in _exp_data.data()], expected_data) - self.assertEqual(len(_exp_data.data())-1, data_index) + self.assertEqual(len(_exp_data.data()) - 1, data_index) called_back_count += 1 a_result = self._get_job_result(1) @@ -186,7 +190,7 @@ def test_add_data_job_callback_kwargs(self): def _callback(_exp_data, data_index, **kwargs): self.assertIsInstance(_exp_data, StoredData) - self.assertEqual(len(_exp_data.data())-1, data_index) + self.assertEqual(len(_exp_data.data()) - 1, data_index) self.assertEqual({"foo": callback_kwargs}, kwargs) nonlocal called_back called_back = True @@ -220,7 +224,7 @@ def test_get_data(self): def test_add_figure(self): """Test adding a new figure.""" hello_bytes = str.encode("hello world") - file_name = "hello_world.svg" + file_name = uuid.uuid4().hex self.addCleanup(os.remove, file_name) with open(file_name, "wb") as file: file.write(hello_bytes) @@ -256,7 +260,7 @@ def test_add_figure_plot(self): def test_add_figures(self): """Test adding multiple new figures.""" hello_bytes = [str.encode("hello world"), str.encode("hello friend")] - file_names = ["hello_world.svg", "hello_friend.svg"] + file_names = [uuid.uuid4().hex, uuid.uuid4().hex] for idx, fn in enumerate(file_names): self.addCleanup(os.remove, fn) with open(fn, "wb") as file: @@ -318,7 +322,7 @@ def test_get_figure(self): self.assertEqual(expected_figure, exp_data.figure(name_template.format(idx))) self.assertEqual(expected_figure, exp_data.figure(idx)) - file_name = "hello_world.svg" + file_name = uuid.uuid4().hex self.addCleanup(os.remove, file_name) exp_data.figure(idx, file_name) with open(file_name, "rb") as file: From 4503267e856394791046cebd1342f56de0b71ffb Mon Sep 17 00:00:00 2001 From: jessieyu Date: Wed, 16 Jun 2021 21:17:28 -0400 Subject: [PATCH 03/70] convert service exception to log --- qiskit_experiments/store_data/stored_data.py | 34 +++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/qiskit_experiments/store_data/stored_data.py b/qiskit_experiments/store_data/stored_data.py index 0903d5fc51..0a33c8fc51 100644 --- a/qiskit_experiments/store_data/stored_data.py +++ b/qiskit_experiments/store_data/stored_data.py @@ -57,6 +57,15 @@ def _wrapped(self, *args, **kwargs): return _wrapped +@contextlib.contextmanager +def service_exception_to_warning(): + """Convert an exception raised by experiment service to a warning.""" + try: + yield + except Exception: # pylint: disable=broad-except + LOG.warning("Experiment service operation failed: %s", traceback.format_exc()) + + class StoredData: """Base common type for all versioned StoredData classes. @@ -114,9 +123,6 @@ def __init__( figure_names: Name of figures associated with this experiment. notes: Freeform notes about the experiment. **kwargs: Additional experiment attributes. - - Raises: - ExperimentError: If an input argument is invalid. """ metadata = metadata or {} self._metadata = copy.deepcopy(metadata) @@ -463,7 +469,7 @@ def delete_figure( service = service or self._service if service and self.auto_save: - with contextlib.suppress(ExperimentEntryNotFound): + with service_exception_to_warning(): self.service.delete_figure(experiment_id=self.experiment_id, figure_name=figure_key) self._deleted_figures.remove(figure_key) @@ -559,7 +565,7 @@ def delete_analysis_result( service = service or self._service if service and self.auto_save: - with contextlib.suppress(ExperimentEntryNotFound): + with service_exception_to_warning(): self.service.delete_analysis_result(result_id=result_key) self._deleted_analysis_results.remove(result_key) @@ -621,9 +627,6 @@ def save(self, service: Optional["ExperimentServiceV1"] = None) -> None: Args: service: Experiment service to be used to save the data. If ``None``, the provider used to submit jobs will be used. - - Raises: - ExperimentError: If the experiment contains invalid data. """ service = service or self._service if not service: @@ -665,9 +668,6 @@ def save_all(self, service: Optional["ExperimentServiceV1"] = None) -> None: Args: service: Experiment service to be used to save the data. If ``None``, the provider used to submit jobs will be used. - - Raises: - ExperimentError: If the experiment contains invalid data. """ # TODO - track changes use_service = service or self._service @@ -680,10 +680,8 @@ def save_all(self, service: Optional["ExperimentServiceV1"] = None) -> None: result.save(service) for result in self._deleted_analysis_results.copy(): - try: + with service_exception_to_warning(): use_service.delete_analysis_result(result_id=result) - except ExperimentEntryNotFound: - pass self._deleted_analysis_results.remove(result) with self._figures.lock: @@ -703,10 +701,8 @@ def save_all(self, service: Optional["ExperimentServiceV1"] = None) -> None: ) for name in self._deleted_figures.copy(): - try: + with service_exception_to_warning(): use_service.delete_figure(experiment_id=self.experiment_id, figure_name=name) - except ExperimentEntryNotFound: - pass self._deleted_figures.remove(name) def serialize_metadata(self) -> str: @@ -989,13 +985,13 @@ def service(self) -> Optional["ExperimentServiceV1"]: @service.setter def service(self, service: "ExperimentServiceV1") -> None: - """Set the service to be used for storing experiment data remotely. + """Set the service to be used for storing experiment data. Args: service: Service to be used. Raises: - ExperimentError: If a remote experiment service is already being used. + ExperimentError: If an experiment service is already being used. """ if self._service: raise ExperimentError("An experiment service is already being used.") From 5e971467289dd2e5d5d7d61c3b43dfd74d4f9fc2 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Wed, 16 Jun 2021 21:38:45 -0400 Subject: [PATCH 04/70] fix lint --- .../store_data/experiment_service.py | 18 +++++++++--------- qiskit_experiments/store_data/stored_data.py | 2 ++ test/stored_data/test_stored_data.py | 1 + 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/store_data/experiment_service.py b/qiskit_experiments/store_data/experiment_service.py index 0cf172ffca..1c7267be41 100644 --- a/qiskit_experiments/store_data/experiment_service.py +++ b/qiskit_experiments/store_data/experiment_service.py @@ -17,7 +17,7 @@ from .device_component import DeviceComponent -T = TypeVar("T") +ExperimentClass = TypeVar("ExperimentClass") class ExperimentService: @@ -119,8 +119,8 @@ def update_experiment( @abstractmethod def experiment( - self, experiment_id: str, experiment_class: Optional[Type[T]] = None - ) -> Union[Dict, T]: + self, experiment_id: str, experiment_class: Optional[Type[ExperimentClass]] = None + ) -> Union[Dict, ExperimentClass]: """Retrieve a previously stored experiment. Args: @@ -142,14 +142,14 @@ def experiment( def experiments( self, limit: Optional[int] = 10, - experiment_class: Optional[Type[T]] = None, + experiment_class: Optional[Type[ExperimentClass]] = None, device_components: Optional[Union[str, DeviceComponent]] = None, experiment_type: Optional[str] = None, backend_name: Optional[str] = None, tags: Optional[List[str]] = None, tags_operator: Optional[str] = "OR", **filters: Any, - ) -> List[Union[Dict, T]]: + ) -> List[Union[Dict, ExperimentClass]]: """Retrieve all experiment data, with optional filtering. Args: @@ -251,8 +251,8 @@ def update_analysis_result( @abstractmethod def analysis_result( - self, result_id: str, result_class: Optional[Type[T]] = None - ) -> Union[Dict, T]: + self, result_id: str, result_class: Optional[Type[ExperimentClass]] = None + ) -> Union[Dict, ExperimentClass]: """Retrieve a previously stored experiment. Args: @@ -273,7 +273,7 @@ def analysis_result( def analysis_results( self, limit: Optional[int] = 10, - result_class: Optional[Type[T]] = None, + result_class: Optional[Type[ExperimentClass]] = None, device_components: Optional[Union[str, DeviceComponent]] = None, experiment_id: Optional[str] = None, result_type: Optional[str] = None, @@ -283,7 +283,7 @@ def analysis_results( tags: Optional[List[str]] = None, tags_operator: Optional[str] = "OR", **filters: Any, - ) -> List[Union[Dict, T]]: + ) -> List[Union[Dict, ExperimentClass]]: """Retrieve all analysis results, with optional filtering. Args: diff --git a/qiskit_experiments/store_data/stored_data.py b/qiskit_experiments/store_data/stored_data.py index 0a33c8fc51..e20b27b155 100644 --- a/qiskit_experiments/store_data/stored_data.py +++ b/qiskit_experiments/store_data/stored_data.py @@ -420,6 +420,7 @@ def add_figures( save = save_figure if save_figure is not None else self.auto_save if save and service: if HAS_MATPLOTLIB: + # pylint: disable=import-error from matplotlib import pyplot if isinstance(figure, pyplot.Figure): @@ -687,6 +688,7 @@ def save_all(self, service: Optional["ExperimentServiceV1"] = None) -> None: with self._figures.lock: for name, figure in self._figures.items(): if HAS_MATPLOTLIB: + # pylint: disable=import-error from matplotlib import pyplot if isinstance(figure, pyplot.Figure): diff --git a/test/stored_data/test_stored_data.py b/test/stored_data/test_stored_data.py index f7c18ebcd4..b99618525a 100644 --- a/test/stored_data/test_stored_data.py +++ b/test/stored_data/test_stored_data.py @@ -244,6 +244,7 @@ def test_add_figure(self): @skipIf(not HAS_MATPLOTLIB, "matplotlib not available.") def test_add_figure_plot(self): """Test adding a matplotlib figure.""" + # pylint: disable=import-error import matplotlib.pyplot as plt figure, ax = plt.subplots() From b5abffaf104b340fe3d96b65e8111fa9d5655ea4 Mon Sep 17 00:00:00 2001 From: yaelbh Date: Thu, 17 Jun 2021 14:05:46 +0300 Subject: [PATCH 05/70] Integration with result database Co-authored-by: Jessie Yu --- qiskit_experiments/__init__.py | 4 +- qiskit_experiments/analysis/curve_analysis.py | 59 ++-- qiskit_experiments/analysis/curve_fitting.py | 4 +- qiskit_experiments/analysis/plotting.py | 6 +- qiskit_experiments/base_analysis.py | 32 +- qiskit_experiments/base_experiment.py | 16 +- .../characterization/t1_experiment.py | 27 +- .../characterization/t2star_experiment.py | 26 +- .../composite/composite_analysis.py | 17 +- .../composite/composite_experiment.py | 11 +- .../composite/composite_experiment_data.py | 15 +- qiskit_experiments/experiment_data.py | 321 +----------------- qiskit_experiments/matplotlib.py | 16 +- .../interleaved_rb_analysis.py | 18 +- test/data_processing/test_data_processing.py | 2 + test/test_t1.py | 33 +- test/test_t2star.py | 24 +- test/test_utils.py | 50 +++ 18 files changed, 248 insertions(+), 433 deletions(-) create mode 100644 test/test_utils.py diff --git a/qiskit_experiments/__init__.py b/qiskit_experiments/__init__.py index 9f1c6fa3c2..dfe5f2d237 100644 --- a/qiskit_experiments/__init__.py +++ b/qiskit_experiments/__init__.py @@ -30,7 +30,7 @@ :toctree: ../stubs/ ExperimentData - AnalysisResult + ResultDict Experiment Base Classes @@ -49,7 +49,7 @@ from .version import __version__ # Base Classes -from .experiment_data import ExperimentData, AnalysisResult +from .experiment_data import ExperimentData, ResultDict from .base_analysis import BaseAnalysis from .base_experiment import BaseExperiment diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 7e10e9751c..65a4bad8be 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -30,7 +30,9 @@ 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.experiment_data import ResultDict, ExperimentData +from qiskit_experiments.store_data import AnalysisResultV1 +from qiskit_experiments.store_data.device_component import Qubit @dataclasses.dataclass(frozen=True) @@ -301,18 +303,18 @@ def data_processor(data: Dict[str, Any]) -> Tuple[float, float] return_data_points=False, ) - def _create_figures(self, analysis_results: CurveAnalysisResult) -> List["Figure"]: + def _create_figures(self, result_data: CurveAnalysisResult) -> List["Figure"]: """Create new figures with the fit result and raw data. Subclass can override this method to create different type of figures. Args: - analysis_results: Analysis result containing fit parameters. + result_data: Result containing fit parameters. Returns: List of figures. """ - fit_available = all(key in analysis_results for key in ("popt", "popt_err", "xrange")) + fit_available = all(key in result_data for key in ("popt", "popt_err", "xrange")) if plotting.HAS_MATPLOTLIB: @@ -360,7 +362,7 @@ def _create_figures(self, analysis_results: CurveAnalysisResult) -> List["Figure if fit_available: plotting.plot_curve_fit( func=series_def.fit_func, - result=analysis_results, + result=result_data, ax=axis, color=series_def.plot_color, zorder=2, @@ -391,14 +393,14 @@ def _create_figures(self, analysis_results: CurveAnalysisResult) -> List["Figure for par_name, label in fit_reports.items(): try: # fit value - pval = get_opt_value(analysis_results, par_name) - perr = get_opt_error(analysis_results, par_name) + pval = get_opt_value(result_data, par_name) + perr = get_opt_error(result_data, par_name) except ValueError: # maybe post processed value - pval = analysis_results[par_name] - perr = analysis_results[f"{par_name}_err"] + pval = result_data[par_name] + perr = result_data[f"{par_name}_err"] analysis_description += f"{label} = {pval: .3e}\u00B1{perr: .3e}\n" - chisq = analysis_results["reduced_chisq"] + chisq = result_data["reduced_chisq"] analysis_description += f"Fit \u03C7-squared = {chisq: .4f}" report_handler = axis.text( @@ -505,18 +507,18 @@ def _format_data(self, data: CurveData) -> CurveData: metadata=data.metadata, ) - def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: """Calculate new quantity from the fit result. Subclasses can override this method to do post analysis. Args: - analysis_result: Analysis result containing fit result. + result_data: Result containing fit result. Returns: New CurveAnalysisResult instance containing the result of post analysis. """ - return analysis_result + return result_data def _extract_curves( self, experiment_data: ExperimentData, data_processor: Union[Callable, DataProcessor] @@ -761,7 +763,7 @@ def _get_option(self, arg_name: str) -> Any: def _run_analysis( self, experiment_data: ExperimentData, **options - ) -> Tuple[List[AnalysisResult], List["pyplot.Figure"]]: + ) -> Tuple[List[AnalysisResultV1], List["pyplot.Figure"]]: """Run analysis on circuit data. Args: @@ -771,14 +773,14 @@ def _run_analysis( 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 + AnalysisResultV1 objects, and ``figures`` is a list of any figures for the experiment. Raises: AnalysisError: if the analysis fails. """ - analysis_result = CurveAnalysisResult() - analysis_result["analysis_type"] = self.__class__.__name__ + result_data = CurveAnalysisResult() + result_data["analysis_type"] = self.__class__.__name__ figures = list() # pop arguments that are not given to fitter @@ -835,7 +837,7 @@ def _run_analysis( sigma=formatted_data.y_err, **fit_options, ) - analysis_result.update(**fit_result) + result_data.update(**fit_result) else: # Multiple initial guesses fit_options_candidates = [ @@ -862,24 +864,24 @@ def _run_analysis( ) # Sort by chi squared value fit_results = sorted(fit_results, key=lambda r: r["reduced_chisq"]) - analysis_result.update(**fit_results[0]) + result_data.update(**fit_results[0]) except AnalysisError as ex: - analysis_result["error_message"] = str(ex) - analysis_result["success"] = False + result_data["error_message"] = str(ex) + result_data["success"] = False else: # # 4. Post-process analysis data # - analysis_result = self._post_analysis(analysis_result=analysis_result) + result_data = self._post_analysis(result_data=result_data) finally: # # 5. Create figures # if self._get_option("plot"): - figures.extend(self._create_figures(analysis_results=analysis_result)) + figures.extend(self._create_figures(result_data=result_data)) # # 6. Optionally store raw data points @@ -893,6 +895,15 @@ def _run_analysis( "ydata": series_data.y, "sigma": series_data.y_err, } - analysis_result["raw_data"] = raw_data_dict + result_data["raw_data"] = raw_data_dict + + analysis_result = AnalysisResultV1( + result_data=result_data, + result_type=result_data["analysis_type"], + device_components=[ + Qubit(qubit) for qubit in experiment_data.data(0)["metadata"]["qubits"] + ], + experiment_id=experiment_data.experiment_id, + ) return [analysis_result], figures diff --git a/qiskit_experiments/analysis/curve_fitting.py b/qiskit_experiments/analysis/curve_fitting.py index c42ee17219..baf8b05497 100644 --- a/qiskit_experiments/analysis/curve_fitting.py +++ b/qiskit_experiments/analysis/curve_fitting.py @@ -19,11 +19,11 @@ import numpy as np import scipy.optimize as opt from qiskit_experiments.exceptions import AnalysisError -from qiskit_experiments.base_analysis import AnalysisResult +from qiskit_experiments.experiment_data import ResultDict from qiskit_experiments.analysis.data_processing import filter_data -class CurveAnalysisResult(AnalysisResult): +class CurveAnalysisResult(ResultDict): """Analysis data container for curve fit analysis. Class Attributes: diff --git a/qiskit_experiments/analysis/plotting.py b/qiskit_experiments/analysis/plotting.py index 5f21f9494b..e8769202b7 100644 --- a/qiskit_experiments/analysis/plotting.py +++ b/qiskit_experiments/analysis/plotting.py @@ -15,7 +15,7 @@ from typing import Callable, Optional import numpy as np -from qiskit_experiments.base_analysis import AnalysisResult +from qiskit_experiments.experiment_data import ResultDict from qiskit_experiments.matplotlib import pyplot, requires_matplotlib # pylint: disable = unused-import @@ -25,7 +25,7 @@ @requires_matplotlib def plot_curve_fit( func: Callable, - result: AnalysisResult, + result: ResultDict, confidence_interval: bool = True, ax=None, num_fit_points: int = 100, @@ -39,7 +39,7 @@ def plot_curve_fit( Args: func: the fit function for curve_fit. - result: an AnalysisResult from curve_fit. + result: a result dictiionary from curve_fit. confidence_interval: if True plot the confidence interval from popt_err. ax (matplotlib.axes.Axes): Optional, a matplotlib axes to add the plot to. num_fit_points: the number of points to plot for xrange. diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index f5abc3244c..e77d1b9086 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -20,7 +20,8 @@ from qiskit.providers.options import Options from qiskit_experiments.exceptions import AnalysisError -from qiskit_experiments.experiment_data import ExperimentData, AnalysisResult +from qiskit_experiments.experiment_data import ExperimentData, ResultDict +from qiskit_experiments.store_data import AnalysisResultV1 class BaseAnalysis(ABC): @@ -66,11 +67,11 @@ def run( supported options. Returns: - List[AnalysisResult]: the output for analysis that produces - multiple results. + List[AnalysisResultV1]: the output for analysis that produces + multiple results. 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 + may be a single or list of :class:`AnalysisResultV1` objects, and ``figures`` may be None, a single figure, or a list of figures. Raises: @@ -87,25 +88,14 @@ def run( analysis_options = analysis_options.__dict__ # Run analysis - 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)] - figures = None + analysis_results, figures = self._run_analysis(experiment_data, **analysis_options) # Save to experiment data if save: - if isinstance(analysis_results, AnalysisResult): - experiment_data.add_analysis_result(analysis_results) - else: - for res in analysis_results: - experiment_data.add_analysis_result(res) + experiment_data.add_analysis_results(analysis_results) if figures: - for fig in figures: - experiment_data.add_figure(fig) + experiment_data.add_figures(figures) + if return_figures: return analysis_results, figures return analysis_results @@ -113,7 +103,7 @@ def run( @abstractmethod def _run_analysis( self, experiment_data: ExperimentData, **options - ) -> Tuple[List[AnalysisResult], List["matplotlib.figure.Figure"]]: + ) -> Tuple[List[AnalysisResultV1], List["matplotlib.figure.Figure"]]: """Run analysis on circuit data. Args: @@ -124,7 +114,7 @@ def _run_analysis( Returns: A pair ``(analysis_results, figures)`` where ``analysis_results`` - may be a single or list of AnalysisResult objects, and ``figures`` + may be a single or list of AnalysisResultV1 objects, and ``figures`` is a list of any figures for the experiment. Raises: diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 016b48fde4..d2265b23d8 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -117,12 +117,13 @@ def run( else: job = backend.run(circuits, **run_opts) - # Add Job to ExperimentData - experiment_data.add_data(job) - - # Queue analysis of data for when job is finished + # Add Job to ExperimentData and add analysis for post processing. + run_analysis = None + if analysis and self.__analysis_class__ is not None: - self.run_analysis(experiment_data) + run_analysis = self.run_analysis + + experiment_data.add_data(job, post_processing_callback=run_analysis) # Return the ExperimentData future return experiment_data @@ -162,6 +163,11 @@ def physical_qubits(self) -> Tuple[int]: """Return the physical qubits for this experiment.""" return self._physical_qubits + @property + def experiment_type(self) -> str: + """Return experiment type.""" + return self._type + @classmethod def analysis(cls): """Return the default Analysis class for the experiment.""" diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index e79c09aa2b..fe486ffb46 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -26,7 +26,9 @@ from qiskit_experiments.analysis.curve_fitting import process_curve_data, curve_fit from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting -from qiskit_experiments.experiment_data import AnalysisResult +from qiskit_experiments.experiment_data import ResultDict +from qiskit_experiments.store_data import AnalysisResultV1 +from qiskit_experiment.store_data.device_component import Qubit class T1Analysis(BaseAnalysis): @@ -69,7 +71,7 @@ def _run_analysis( offset_bounds=None, plot=True, ax=None, - ) -> Tuple[List[AnalysisResult], List["matplotlib.figure.Figure"]]: + ) -> Tuple[List[AnalysisResultV1], List["matplotlib.figure.Figure"]]: """ Calculate T1 @@ -127,7 +129,7 @@ def fit_fun(x, a, tau, c): bounds = {"a": amplitude_bounds, "tau": t1_bounds, "c": offset_bounds} fit_result = curve_fit(fit_fun, xdata, ydata, init, sigma=sigma, bounds=bounds) - analysis_result = AnalysisResult( + result_data = ResultDict( { "value": fit_result["popt"][1], "stderr": fit_result["popt_err"][1], @@ -140,9 +142,9 @@ def fit_fun(x, a, tau, c): } ) - analysis_result["fit"]["circuit_unit"] = unit + result_data["fit"]["circuit_unit"] = unit if unit == "dt": - analysis_result["fit"]["dt"] = conversion_factor + result_data["fit"]["dt"] = conversion_factor # Generate fit plot if plot and plotting.HAS_MATPLOTLIB: @@ -153,7 +155,16 @@ def fit_fun(x, a, tau, c): else: figures = None - return [analysis_result], figures + res_v1 = AnalysisResultV1( + result_data=result_data, + result_type="T1", + device_components=[Qubit(data[0]["metadata"]["qubit"])], + experiment_id=experiment_data.experiment_id, + quality=result_data["quality"], + verified=True, + ) + + return [res_v1], figures @staticmethod def _fit_quality(fit_out, fit_err, reduced_chisq): @@ -166,9 +177,9 @@ def _fit_quality(fit_out, fit_err, reduced_chisq): and (fit_err[1] is None or fit_err[1] < fit_out[1]) and (fit_err[2] is None or fit_err[2] < 0.1) ): - return "computer_good" + return "good" else: - return "computer_bad" + return "bad" @classmethod def _format_plot(cls, ax, analysis_result, qubit=None, add_label=True): diff --git a/qiskit_experiments/characterization/t2star_experiment.py b/qiskit_experiments/characterization/t2star_experiment.py index e7f8082847..b26bec6479 100644 --- a/qiskit_experiments/characterization/t2star_experiment.py +++ b/qiskit_experiments/characterization/t2star_experiment.py @@ -26,7 +26,10 @@ from qiskit_experiments.analysis.curve_fitting import curve_fit, process_curve_data from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting -from ..experiment_data import ExperimentData +from qiskit_experiments.experiment_data import ExperimentData, ResultDict +from qiskit_experiments.store_data import AnalysisResultV1 +from qiskit_experiment.store_data.device_component import Qubit + # pylint: disable = invalid-name class T2StarAnalysis(BaseAnalysis): @@ -45,7 +48,7 @@ def _run_analysis( plot: bool = True, ax: Optional["AxesSubplot"] = None, **kwargs, - ) -> Tuple[List[AnalysisResult], List["matplotlib.figure.Figure"]]: + ) -> Tuple[List[AnalysisResultV1], List["matplotlib.figure.Figure"]]: r"""Calculate T2Star experiment. The probability of measuring `+` is assumed to be of the form @@ -110,7 +113,7 @@ def _format_plot(ax, unit): figures = None # Output unit is 'sec', regardless of the unit used in the input - analysis_result = AnalysisResult( + result_data = ResultDict( { "t2star_value": fit_result["popt"][1], "frequency_value": fit_result["popt"][2], @@ -124,9 +127,18 @@ def _format_plot(ax, unit): } ) - analysis_result["fit"]["circuit_unit"] = unit + result_data["fit"]["circuit_unit"] = unit if unit == "dt": - analysis_result["fit"]["dt"] = conversion_factor + result_data["fit"]["dt"] = conversion_factor + + analysis_result = AnalysisResultV1( + result_data=result_data, + result_type="T2Star", + device_components=[Qubit(data[0]["metadata"]["qubit"])], + experiment_id=experiment_data.experiment_id, + quality=result_data["quality"], + ) + return [analysis_result], figures def _t2star_default_params( @@ -179,9 +191,9 @@ def _fit_quality(fit_out, fit_err, reduced_chisq): and (fit_err[1] is None or fit_err[1] < 0.1 * fit_out[1]) and (fit_err[2] is None or fit_err[2] < 0.1 * fit_out[2]) ): - return "computer_good" + return "good" else: - return "computer_bad" + return "bad" class T2StarExperiment(BaseExperiment): diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 1c80d2ac5e..82a93a6a38 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -14,8 +14,10 @@ """ from qiskit.exceptions import QiskitError -from qiskit_experiments.base_analysis import BaseAnalysis, AnalysisResult +from qiskit_experiments.base_analysis import BaseAnalysis from .composite_experiment_data import CompositeExperimentData +from qiskit_experiments.store_data import AnalysisResultV1 +from qiskit_experiment.store_data.device_component import Qubit class CompositeAnalysis(BaseAnalysis): @@ -69,11 +71,16 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): sub_ids.append(expdata.experiment_id) sub_qubits.append(expdata.experiment.physical_qubits) - analysis_result = AnalysisResult( + analysis_result = AnalysisResultV1( { - "experiment_types": sub_types, - "experiment_ids": sub_ids, - "experiment_qubits": sub_qubits, + result_data={ + "experiment_types": sub_types, + "experiment_ids": sub_ids, + "experiment_qubits": sub_qubits, + }, + result_type="composite", + device_components=[Qubit(qidx) for qidx in sub_qubits], + experiment_id=experiment_data.experiment_id, } ) return [analysis_result], None diff --git a/qiskit_experiments/composite/composite_experiment.py b/qiskit_experiments/composite/composite_experiment.py index 829390a8fe..91d8e3c7c4 100644 --- a/qiskit_experiments/composite/composite_experiment.py +++ b/qiskit_experiments/composite/composite_experiment.py @@ -48,8 +48,15 @@ def num_experiments(self): """Return the number of sub experiments""" return self._num_experiments - def component_experiment(self, index): - """Return the component Experiment object""" + def component_experiment(self, index=None): + """Return the component Experiment object. + Args: + index: Experiment index, or ``None`` if all experiments are to be returned. + Returns: + The component experiment(s). + """ + if index is None: + return self._experiments return self._experiments[index] def component_analysis(self, index): diff --git a/qiskit_experiments/composite/composite_experiment_data.py b/qiskit_experiments/composite/composite_experiment_data.py index 2e3f493097..0e3fdc5040 100644 --- a/qiskit_experiments/composite/composite_experiment_data.py +++ b/qiskit_experiments/composite/composite_experiment_data.py @@ -26,7 +26,6 @@ def __init__( self, experiment, backend=None, - job_ids=None, ): """Initialize experiment data. @@ -34,7 +33,6 @@ def __init__( experiment (CompositeExperiment): experiment object that generated the data. backend (Backend): Optional, Backend the experiment runs on. It can either be a :class:`~qiskit.providers.Backend` instance or just backend name. - job_ids (list[str]): Optional, IDs of jobs submitted for the experiment. Raises: ExperimentError: If an input argument is invalid. @@ -43,11 +41,12 @@ def __init__( super().__init__( experiment, backend=backend, - job_ids=job_ids, ) # Initialize sub experiments - self._components = [expr.__experiment_data__(expr) for expr in experiment._experiments] + self._components = [ + expr.__experiment_data__(expr) for expr in experiment.component_experiment() + ] def __str__(self): line = 51 * "-" @@ -57,14 +56,16 @@ def __str__(self): ret += f"\nExperiment: {self.experiment_type}" ret += f"\nExperiment ID: {self.experiment_id}" ret += f"\nStatus: {status}" + if status == "ERROR": + ret += "\n " + ret += "\n ".join(self._errors) ret += f"\nComponent Experiments: {len(self._components)}" ret += f"\nCircuits: {len(self._data)}" ret += f"\nAnalysis Results: {n_res}" ret += "\n" + line if n_res: - ret += "\nLast Analysis Result" - for key, value in self._analysis_results[-1].items(): - ret += f"\n- {key}: {value}" + ret += "\nLast Analysis Result:" + ret += f"\n{str(self._analysis_results.values()[-1])}" return ret def component_experiment_data( diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index c6569894b0..a9f617e587 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -13,24 +13,15 @@ Experiment Data class """ import logging -from typing import Optional, Union, List, Dict, Tuple -import os -import uuid -from collections import OrderedDict -from qiskit.result import Result from qiskit.providers import Backend -from qiskit.exceptions import QiskitError -from qiskit.providers import Job, BaseJob -from qiskit.providers.exceptions import JobError - -from qiskit_experiments.matplotlib import pyplot, HAS_MATPLOTLIB +from qiskit_experiments.store_data import StoreDataV1 LOG = logging.getLogger(__name__) -class AnalysisResult(dict): +class ResiltDict(dict): """Placeholder class""" __keys_not_shown__ = tuple() @@ -44,14 +35,13 @@ def __str__(self): return out -class ExperimentData: +class ExperimentData(StoreDataV1): """Qiskit Experiments Data container class""" def __init__( self, experiment=None, backend=None, - job_ids=None, ): """Initialize experiment data. @@ -63,22 +53,10 @@ def __init__( Raises: ExperimentError: If an input argument is invalid. """ - # Experiment class object self._experiment = experiment - - # Terra ExperimentDataV1 attributes - self._backend = backend - self._id = str(uuid.uuid4()) - if experiment is not None: - self._type = experiment._type - else: - self._type = None - job_ids = job_ids or [] - self._jobs = OrderedDict((k, None) for k in job_ids) - self._data = [] - self._figures = OrderedDict() - self._figure_names = [] - self._analysis_results = [] + super().__init__( + experiment_type=experiment.experiment_type if experiment else "unknown", backend=backend + ) @property def experiment(self): @@ -88,290 +66,3 @@ def experiment(self): BaseExperiment: the experiment object. """ return self._experiment - - @property - def experiment_type(self) -> str: - """Return the experiment type.""" - return self._type - - @property - def experiment_id(self) -> str: - """Return the experiment id.""" - return self._id - - @property - def job_ids(self) -> List[str]: - """Return experiment job IDs. - - Returns: - IDs of jobs submitted for this experiment. - """ - return list(self._jobs.keys()) - - @property - def backend(self) -> Backend: - """Return backend. - - Returns: - Backend this experiment is for. - """ - return self._backend - - def add_data( - self, - data: Union[Result, List[Result], Job, List[Job], Dict, List[Dict]], - ): - """Add experiment data. - Args: - data: Experiment data to add. Several types are accepted for convenience: - - * Result: Add data from this ``Result`` object. - * List[Result]: Add data from the ``Result`` objects. - * Job: Add data from the job result. - * List[Job]: Add data from the job results. - * Dict: Add this data. - * List[Dict]: Add this list of data. - - Raises: - QiskitError: if data format is invalid. - KeyboardInterrupt: when job is cancelled by users. - """ - # Set backend from the job, this could be added to base class - if isinstance(data, (Job, BaseJob)): - backend = data.backend() - if self.backend is not None and str(self.backend) != str(backend): - LOG.warning( - "Adding a job from a backend (%s) that is different than" - " the current ExperimentData backend (%s).", - backend, - self.backend, - ) - self._backend = backend - self._jobs[data.job_id()] = data - self._add_result_data(data.result()) - elif isinstance(data, dict): - self._add_single_data(data) - elif isinstance(data, (Job, BaseJob)): - try: - result = data.result() - except JobError as ex: - if hasattr(data, "error_message"): - msg = data.error_message - else: - msg = "Please contact to administrator of your provider." - raise QiskitError(f"Execution of experiment failed. {msg}") from ex - except KeyboardInterrupt as ex: - # remove job from queue list and return the empty result - data.cancel() - raise KeyboardInterrupt(ex) from ex - self._add_result_data(result) - elif isinstance(data, Result): - self._add_result_data(data) - elif isinstance(data, list): - for dat in data: - self.add_data(dat) - else: - raise QiskitError(f"Invalid data type {type(data)}.") - - def _add_result_data(self, result: Result) -> None: - """Add data from a Result object - - Args: - result: Result object containing data to be added. - """ - num_data = len(result.results) - for i in range(num_data): - metadata = result.results[i].header.metadata - if metadata.get("experiment_type") == self._type: - data = result.data(i) - data["metadata"] = metadata - if "counts" in data: - # Format to Counts object rather than hex dict - data["counts"] = result.get_counts(i) - self._add_single_data(data) - - def _add_single_data(self, data: Dict[str, any]) -> None: - """Add a single data dictionary to the experiment. - Args: - data: Data to be added. - """ - self._data.append(data) - - def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, List[Dict]]: - """Return the experiment data at the specified index. - - Args: - index: Index of the data to be returned. - Several types are accepted for convenience: - - * None: Return all experiment data. - * int: Specific index of the data. - * slice: A list slice of data indexes. - * str: ID of the job that produced the data. - - Returns: - Experiment data. - - Raises: - QiskitError: if index is invalid. - """ - if index is None: - return self._data - if isinstance(index, (int, slice)): - return self._data[index] - if isinstance(index, str): - return [data for data in self._data if data.get("job_id") == index] - raise QiskitError(f"Invalid index type {type(index)}.") - - def add_figure( - self, - figure, - figure_name: Optional[str] = None, - overwrite: bool = False, - ) -> Tuple[str, int]: - """Save the experiment figure. - - Args: - figure (Union[str, bytes, :class:`~matplotlib.figure.Figure`]): Name of the figure file - or figure data to store. This can either be a ``str`` (for a filename to load), - ``bytes`` (for the raw image data), or a :class:`~matplotlib.figure.Figure` object. - figure_name: Name of the figure. If ``None``, use the figure file name, if - given, or a generated name. - overwrite: Whether to overwrite the figure if one already exists with - the same name. - - Returns: - A tuple of the name and size of the saved figure. Returned size - is 0 if there is no experiment service to use. - - Raises: - QiskitError: If the figure with the same name already exists, - and `overwrite=True` is not specified. - """ - if not figure_name: - if isinstance(figure, str): - figure_name = figure - else: - figure_name = f"figure_{self.experiment_id}_{len(self.figure_names)}" - - existing_figure = figure_name in self._figure_names - if existing_figure and not overwrite: - raise QiskitError( - f"A figure with the name {figure_name} for this experiment " - f"already exists. Specify overwrite=True if you " - f"want to overwrite it." - ) - out = [figure_name, 0] - self._figures[figure_name] = figure - self._figure_names.append(figure_name) - return out - - def figure(self, figure_name: Union[str, int], file_name: Optional[str] = None): - """Retrieve the specified experiment figure. - - Args: - figure_name: Name of the figure or figure position. - file_name: Name of the local file to save the figure to. If ``None``, - the content of the figure is returned instead. - - Returns: - Union[int, bytes, :class:`~matplotlib.figure.Figure`]: - - The size of the figure as an ``int`` if ``file_name`` is specified. Otherwise - the content of the figure as ``bytes`` object or a - :class:`~matplotlib.figure.Figure` depending on how the image was loaded. - - Raises: - QiskitError: If the figure cannot be found. - """ - if isinstance(figure_name, int): - figure_name = self._figure_names[figure_name] - - figure_data = self._figures.get(figure_name, None) - if figure_data is not None: - if isinstance(figure_data, str): - with open(figure_data, "rb") as file: - figure_data = file.read() - if file_name: - with open(file_name, "wb") as output: - if HAS_MATPLOTLIB and isinstance(figure_data, pyplot.Figure): - figure_data.savefig(output, format="svg") - num_bytes = os.path.getsize(file_name) - else: - num_bytes = output.write(figure_data) - return num_bytes - return figure_data - raise QiskitError(f"Figure {figure_name} not found.") - - @property - def figure_names(self) -> List[str]: - """Return names of the figures associated with this experiment. - Returns: - Names of figures associated with this experiment. - """ - return self._figure_names - - def add_analysis_result(self, result: AnalysisResult) -> None: - """Save the analysis result. - Args: - result: Analysis result to be saved. - """ - self._analysis_results.append(result) - - def analysis_result( - self, index: Optional[Union[int, slice, str]] - ) -> Union[AnalysisResult, List[AnalysisResult]]: - """Return analysis results associated with this experiment. - - Args: - index: Index of the analysis result to be returned. - Several types are accepted for convenience: - - * None: Return all analysis results. - * int: Specific index of the analysis results. - * slice: A list slice of indexes. - * str: ID of the analysis result. - - Returns: - Analysis results for this experiment. - - Raises: - QiskitError: if index is invalid. - """ - if index is None: - return self._analysis_results - if isinstance(index, (int, slice)): - return self._analysis_results[index] - if isinstance(index, str): - for res in self._analysis_results: - if res.id == index: - return res - raise QiskitError(f"Analysis result {index} not found.") - raise QiskitError(f"Invalid index type {type(index)}.") - - def status(self) -> str: - """Return the data processing status. - - Returns: - Data processing status. - """ - # TODO: Figure out what statuses should be returned including - # execution and analysis status - if not self._jobs and not self._data: - return "EMPTY" - return "DONE" - - def __str__(self): - line = 51 * "-" - n_res = len(self._analysis_results) - ret = line - ret += f"\nExperiment: {self.experiment_type}" - ret += f"\nExperiment ID: {self.experiment_id}" - ret += f"\nStatus: {self.status()}" - ret += f"\nCircuits: {len(self._data)}" - ret += f"\nAnalysis Results: {n_res}" - ret += "\n" + line - if n_res: - ret += "\nLast Analysis Result" - ret += str(self._analysis_results[-1]) - return ret diff --git a/qiskit_experiments/matplotlib.py b/qiskit_experiments/matplotlib.py index b3c8dccac9..cc15210f9c 100644 --- a/qiskit_experiments/matplotlib.py +++ b/qiskit_experiments/matplotlib.py @@ -34,6 +34,20 @@ def wrapped(*args, **kwargs): f"{func} requires matplotlib to generate curve fit plot." ' Run "pip install matplotlib" before.' ) - return func(*args, **kwargs) + + # Analysis/plotting is done in a separate thread (so it doesn't block the + # main thread), but matplotlib doesn't support GUI mode in a child thread. + # The code below switches to a non-GUI backend "Agg" when creating the + # plot. An alternative is to run this in a separate process, but then + # we'd need to deal with pickling issues. + from matplotlib import pyplot + + saved_backend = pyplot.get_backend() + pyplot.switch_backend("Agg") + try: + ret_val = func(*args, **kwargs) + finally: + pyplot.switch_backend(saved_backend) + return ret_val return wrapped diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index babdb11bef..122c1570f0 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -165,20 +165,20 @@ def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any] return fit_option - def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: """Calculate EPC.""" # Add EPC data nrb = 2 ** self._num_qubits scale = (nrb - 1) / nrb - alpha = get_opt_value(analysis_result, "alpha") - alpha_c = get_opt_value(analysis_result, "alpha_c") - alpha_c_err = get_opt_error(analysis_result, "alpha_c") + alpha = get_opt_value(result_data, "alpha") + alpha_c = get_opt_value(result_data, "alpha_c") + alpha_c_err = get_opt_error(result_data, "alpha_c") # Calculate epc_est (=r_c^est) - Eq. (4): epc_est = scale * (1 - alpha_c) epc_est_err = scale * alpha_c_err - analysis_result["EPC"] = epc_est - analysis_result["EPC_err"] = epc_est_err + result_data["EPC"] = epc_est + result_data["EPC_err"] = epc_est_err # Calculate the systematic error bounds - Eq. (5): systematic_err_1 = scale * (abs(alpha - alpha_c) + (1 - alpha)) @@ -189,7 +189,7 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR systematic_err = min(systematic_err_1, systematic_err_2) systematic_err_l = epc_est - systematic_err systematic_err_r = epc_est + systematic_err - analysis_result["EPC_systematic_err"] = systematic_err - analysis_result["EPC_systematic_bounds"] = [max(systematic_err_l, 0), systematic_err_r] + result_data["EPC_systematic_err"] = systematic_err + result_data["EPC_systematic_bounds"] = [max(systematic_err_l, 0), systematic_err_r] - return analysis_result + return result_data diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 70b4126d77..3639d57b2d 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -98,6 +98,7 @@ def test_to_real(self): [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], ], "metadata": {"experiment_type": "fake_test_experiment"}, + "job_id": "job-123", } expected_new = np.array([[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]]) @@ -144,6 +145,7 @@ def test_to_imag(self): [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], ], "metadata": {"experiment_type": "fake_test_experiment"}, + "job_id": "job-123", } expected_new = np.array( diff --git a/test/test_t1.py b/test/test_t1.py index 39edee147a..6885bceaed 100644 --- a/test/test_t1.py +++ b/test/test_t1.py @@ -22,10 +22,12 @@ from qiskit.providers.options import Options from qiskit.providers.models import QasmBackendConfiguration from qiskit.result import Result -from qiskit_experiments import ExperimentData +from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.composite import ParallelExperiment from qiskit_experiments.characterization import T1Experiment, T1Analysis +from test.utils import FakeJob + class T1Backend(BackendV1): """ @@ -141,7 +143,7 @@ def run(self, run_input, **options): } ) - return Result.from_dict(result) + return FakeJob(self, result=Result.from_dict(result)) class TestT1(QiskitTestCase): @@ -175,10 +177,12 @@ def test_t1_end2end(self): exp = T1Experiment(0, delays, unit="dt") exp.set_analysis_options(amplitude_guess=1, t1_guess=t1 / dt_factor, offset_guess=0) - res = exp.run(backend, shots=10000).analysis_result(0) + exp_data = exp.run(backend, shots=10000) + exp_data.block_for_results() # Wait for analysis to finish. + res = exp_data.analysis_result(0) - self.assertEqual(res["quality"], "computer_good") - self.assertAlmostEqual(res["value"], t1, delta=3) + self.assertEqual(res.quality, "good") + self.assertAlmostEqual(res.data()["value"], t1, delta=3) def test_t1_parallel(self): """ @@ -192,22 +196,23 @@ def test_t1_parallel(self): exp2 = T1Experiment(2, delays) par_exp = ParallelExperiment([exp0, exp2]) res = par_exp.run(T1Backend([t1[0], None, t1[1]])) + res.block_for_results() for i in range(2): sub_res = res.component_experiment_data(i).analysis_result(0) - self.assertTrue(sub_res["quality"], "computer_good") - self.assertAlmostEqual(sub_res["value"], t1[i], delta=3) + self.assertTrue(sub_res.quality, "good") + self.assertAlmostEqual(sub_res.data()["value"], t1[i], delta=3) def test_t1_analysis(self): """ Test T1Analysis """ - data = ExperimentData(None) + data = ExperimentData() numbers = [750, 1800, 2750, 3550, 4250, 4850, 5450, 5900, 6400, 6800, 7000, 7350, 7700] for i, count0 in enumerate(numbers): - data._data.append( + data.add_data( { "counts": {"0": count0, "1": 10000 - count0}, "metadata": { @@ -221,8 +226,8 @@ def test_t1_analysis(self): ) res = T1Analysis()._run_analysis(data)[0] - self.assertEqual(res[0]["quality"], "computer_good") - self.assertAlmostEqual(res[0]["value"], 25e-9, delta=3) + self.assertEqual(res.quality, "good") + self.assertAlmostEqual(res.data()["value"], 25e-9, delta=3) def test_t1_metadata(self): """ @@ -251,10 +256,10 @@ def test_t1_low_quality(self): A test where the fit's quality will be low """ - data = ExperimentData(None) + data = ExperimentData() for i in range(10): - data._data.append( + data.add_data( { "counts": {"0": 10, "1": 10}, "metadata": { @@ -268,7 +273,7 @@ def test_t1_low_quality(self): ) res = T1Analysis()._run_analysis(data)[0] - self.assertEqual(res[0]["quality"], "computer_bad") + self.assertEqual(res.quality, "bad") if __name__ == "__main__": diff --git a/test/test_t2star.py b/test/test_t2star.py index b2eda68289..42f2d9629a 100644 --- a/test/test_t2star.py +++ b/test/test_t2star.py @@ -22,6 +22,8 @@ from qiskit_experiments.composite import ParallelExperiment from qiskit_experiments.characterization import T2StarExperiment +from test.utils import FakeJob + class T2starBackend(BackendV1): """ @@ -150,7 +152,8 @@ def run(self, run_input, **options): "data": {"counts": counts}, } ) - return Result.from_dict(result) + + return FakeJob(self, result=Result.from_dict(result)) class TestT2Star(QiskitTestCase): @@ -212,16 +215,18 @@ def test_t2star_run_end2end(self): backend=backend, shots=2000, ) + expdata.block_for_results() # Wait for job/analysis to finish. result = expdata.analysis_result(0) + result_data = result.data() self.assertAlmostEqual( - result["t2star_value"], + result_data["t2star_value"], estimated_t2star * dt_factor, - delta=0.08 * result["t2star_value"], + delta=0.08 * result_data["t2star_value"], ) self.assertAlmostEqual( - result["frequency_value"], + result_data["frequency_value"], estimated_freq / dt_factor, - delta=0.08 * result["frequency_value"], + delta=0.08 * result_data["frequency_value"], ) self.assertEqual( result["quality"], "computer_good", "Result quality bad for unit " + str(unit) @@ -249,16 +254,19 @@ def test_t2star_parallel(self): } backend = T2starBackend(p0) res = par_exp.run(backend=backend, shots=1000) + res.block_for_results() for i in range(2): sub_res = res.component_experiment_data(i).analysis_result(0) + sub_res = expdata.component_experiment_data(i).analysis_result(0) + sub_res_data = sub_res.data() self.assertAlmostEqual( - sub_res["t2star_value"], t2star[i], delta=0.08 * sub_res["t2star_value"] + sub_res_data["t2star_value"], t2star[i], delta=0.08 * sub_res_data["t2star_value"] ) self.assertAlmostEqual( - sub_res["frequency_value"], + sub_res_data["frequency_value"], estimated_freq[i], - delta=0.08 * sub_res["frequency_value"], + delta=0.08 * sub_res_data["frequency_value"], ) self.assertEqual( sub_res["quality"], diff --git a/test/test_utils.py b/test/test_utils.py new file mode 100644 index 0000000000..e640b74312 --- /dev/null +++ b/test/test_utils.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# 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 utility functions.""" + +import uuid +from typing import Optional, Union +import time + +from qiskit.providers.job import JobV1 as Job +from qiskit.providers.jobstatus import JobStatus +from qiskit.providers.backend import BackendV1 as Backend +from qiskit.providers import BaseBackend +from qiskit.result import Result + + +class FakeJob(Job): + """Fake job.""" + + def __init__(self, backend: Union[Backend, BaseBackend], result: Optional[Result] = None): + """Initialize FakeJob.""" + job_id = uuid.uuid4().hex + super().__init__(backend, job_id) + self._result = result + + def result(self): + """Return job result.""" + time.sleep(3) + return self._result + + def submit(self): + """Submit the job to the backend for execution.""" + pass + + def status(self) -> JobStatus: + """Return the status of the job, among the values of ``JobStatus``.""" + if self._result: + return JobStatus.DONE + return JobStatus.RUNNING From d94668f19ac30ca16dd33eed72b90e881085c37e Mon Sep 17 00:00:00 2001 From: yaelbh Date: Thu, 17 Jun 2021 14:29:30 +0300 Subject: [PATCH 06/70] some brief fixes --- qiskit_experiments/analysis/utils.py | 28 +++++++++---------- .../characterization/t1_experiment.py | 2 +- .../characterization/t2star_experiment.py | 4 +-- .../composite/composite_analysis.py | 7 ++--- qiskit_experiments/experiment_data.py | 6 ++-- test/{test_utils.py => utils.py} | 0 6 files changed, 23 insertions(+), 24 deletions(-) rename test/{test_utils.py => utils.py} (100%) diff --git a/qiskit_experiments/analysis/utils.py b/qiskit_experiments/analysis/utils.py index 09837306af..dbc9adfaf5 100644 --- a/qiskit_experiments/analysis/utils.py +++ b/qiskit_experiments/analysis/utils.py @@ -13,14 +13,14 @@ """Analysis utility functions.""" -from qiskit_experiments.experiment_data import AnalysisResult +from qiskit_experiments.experiment_data import ResultDict -def get_opt_value(analysis_result: AnalysisResult, param_name: str) -> float: - """A helper function to get parameter value from analysis result. +def get_opt_value(result_data: ResultDict, param_name: str) -> float: + """A helper function to get parameter value from a result dictionary. Args: - analysis_result: Analysis result object. + result_data: Result object. param_name: Name of parameter to extract. Returns: @@ -28,27 +28,27 @@ def get_opt_value(analysis_result: AnalysisResult, param_name: str) -> float: Raises: KeyError: - - When analysis result does not contain parameter information. + - When the result does not contain parameter information. ValueError: - When specified parameter is not defined. """ try: - index = analysis_result["popt_keys"].index(param_name) - return analysis_result["popt"][index] + index = result_data["popt_keys"].index(param_name) + return result_data["popt"][index] except KeyError as ex: raise KeyError( - "Input analysis result has not fit parameter information. " + "Input result has not fit parameter information. " "Please confirm if the fit is successfully completed." ) from ex except ValueError as ex: raise ValueError(f"Parameter {param_name} is not defined.") from ex -def get_opt_error(analysis_result: AnalysisResult, param_name: str) -> float: +def get_opt_error(result_data: ResultDict, param_name: str) -> float: """A helper function to get error value from analysis result. Args: - analysis_result: Analysis result object. + result_data: Result object. param_name: Name of parameter to extract. Returns: @@ -56,16 +56,16 @@ def get_opt_error(analysis_result: AnalysisResult, param_name: str) -> float: Raises: KeyError: - - When analysis result does not contain parameter information. + - When the result does not contain parameter information. ValueError: - When specified parameter is not defined. """ try: - index = analysis_result["popt_keys"].index(param_name) - return analysis_result["popt_err"][index] + index = result_data["popt_keys"].index(param_name) + return result_data["popt_err"][index] except KeyError as ex: raise KeyError( - "Input analysis result has not fit parameter information. " + "Input result has not fit parameter information. " "Please confirm if the fit is successfully completed." ) from ex except ValueError as ex: diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index fe486ffb46..c045035e41 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -28,7 +28,7 @@ from qiskit_experiments.analysis import plotting from qiskit_experiments.experiment_data import ResultDict from qiskit_experiments.store_data import AnalysisResultV1 -from qiskit_experiment.store_data.device_component import Qubit +from qiskit_experiments.store_data.device_component import Qubit class T1Analysis(BaseAnalysis): diff --git a/qiskit_experiments/characterization/t2star_experiment.py b/qiskit_experiments/characterization/t2star_experiment.py index b26bec6479..d79a1e5802 100644 --- a/qiskit_experiments/characterization/t2star_experiment.py +++ b/qiskit_experiments/characterization/t2star_experiment.py @@ -22,13 +22,13 @@ from qiskit.utils import apply_prefix from qiskit.providers.options import Options from qiskit_experiments.base_experiment import BaseExperiment -from qiskit_experiments.base_analysis import BaseAnalysis, AnalysisResult +from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.analysis.curve_fitting import curve_fit, process_curve_data from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting from qiskit_experiments.experiment_data import ExperimentData, ResultDict from qiskit_experiments.store_data import AnalysisResultV1 -from qiskit_experiment.store_data.device_component import Qubit +from qiskit_experiments.store_data.device_component import Qubit # pylint: disable = invalid-name diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 82a93a6a38..390a625907 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -17,7 +17,7 @@ from qiskit_experiments.base_analysis import BaseAnalysis from .composite_experiment_data import CompositeExperimentData from qiskit_experiments.store_data import AnalysisResultV1 -from qiskit_experiment.store_data.device_component import Qubit +from qiskit_experiments.store_data.device_component import Qubit class CompositeAnalysis(BaseAnalysis): @@ -72,7 +72,6 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): sub_qubits.append(expdata.experiment.physical_qubits) analysis_result = AnalysisResultV1( - { result_data={ "experiment_types": sub_types, "experiment_ids": sub_ids, @@ -81,6 +80,6 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): result_type="composite", device_components=[Qubit(qidx) for qidx in sub_qubits], experiment_id=experiment_data.experiment_id, - } - ) + ) + return [analysis_result], None diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index a9f617e587..d8388d0886 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -15,13 +15,13 @@ import logging from qiskit.providers import Backend -from qiskit_experiments.store_data import StoreDataV1 +from qiskit_experiments.store_data import StoredDataV1 LOG = logging.getLogger(__name__) -class ResiltDict(dict): +class ResultDict(dict): """Placeholder class""" __keys_not_shown__ = tuple() @@ -35,7 +35,7 @@ def __str__(self): return out -class ExperimentData(StoreDataV1): +class ExperimentData(StoredDataV1): """Qiskit Experiments Data container class""" def __init__( diff --git a/test/test_utils.py b/test/utils.py similarity index 100% rename from test/test_utils.py rename to test/utils.py From 6bd5c6455d9333c8d03cf6473286478ee23eecb2 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 17 Jun 2021 14:41:44 -0400 Subject: [PATCH 07/70] log post processing failure --- qiskit_experiments/store_data/stored_data.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/store_data/stored_data.py b/qiskit_experiments/store_data/stored_data.py index e20b27b155..b9d6a31ee2 100644 --- a/qiskit_experiments/store_data/stored_data.py +++ b/qiskit_experiments/store_data/stored_data.py @@ -204,7 +204,7 @@ def add_data( The following positional arguments are provided to the callback function: * This ``StoredData`` object. - * Index of the data added. + * Index of the last data added. * Additional keyword arguments passed to this method. **kwargs: Keyword arguments to be passed to the callback function. @@ -276,8 +276,13 @@ def _wait_for_job( except JobError as err: LOG.warning("Job %s failed: %s", job.job_id(), str(err)) return - if job_done_callback: - job_done_callback(self, data_index, **kwargs) + + try: + if job_done_callback: + job_done_callback(self, data_index, **kwargs) + except Exception: # pylint: disable=broad-except + LOG.warning("Post processing function failed:\n%s", traceback.format_exc()) + raise def _add_result_data(self, result: Result) -> None: """Add data from a Result object From 1780a37d3131920f2511c307e8c5f87c3869c1ce Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 17 Jun 2021 15:30:02 -0400 Subject: [PATCH 08/70] remove data index --- qiskit_experiments/store_data/stored_data.py | 16 +++++++--- test/stored_data/test_stored_data.py | 31 +++++++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/qiskit_experiments/store_data/stored_data.py b/qiskit_experiments/store_data/stored_data.py index b9d6a31ee2..c1728806d7 100644 --- a/qiskit_experiments/store_data/stored_data.py +++ b/qiskit_experiments/store_data/stored_data.py @@ -204,7 +204,6 @@ def add_data( The following positional arguments are provided to the callback function: * This ``StoredData`` object. - * Index of the last data added. * Additional keyword arguments passed to this method. **kwargs: Keyword arguments to be passed to the callback function. @@ -212,6 +211,13 @@ def add_data( Raises: TypeError: If the input data type is invalid. """ + with self._job_futures.lock: + if any(not fut.done() for _, fut in self._job_futures): + LOG.warning( + "Not all post-processing has finished. Adding new data " + "may create unexpected analysis results." + ) + if isinstance(data, (Job, BaseJob)): if self.backend and self.backend.name() != data.backend().name(): LOG.warning( @@ -251,7 +257,7 @@ def add_data( raise TypeError(f"Invalid data type {type(data)}.") if post_processing_callback is not None: - post_processing_callback(self, len(self._data) - 1, **kwargs) + post_processing_callback(self, **kwargs) def _wait_for_job( self, @@ -265,6 +271,9 @@ def _wait_for_job( job: Job to wait for. job_done_callback: Callback function to invoke when job finishes. **kwargs: Keyword arguments to be passed to the callback function. + + Raises: + Exception: If post processing failed. """ LOG.debug("Waiting for job %s to finish.", job.job_id()) try: @@ -272,14 +281,13 @@ def _wait_for_job( with self._data.lock: # Hold the lock so we add the block of results together. self._add_result_data(job_result) - data_index = len(self._data) - 1 except JobError as err: LOG.warning("Job %s failed: %s", job.job_id(), str(err)) return try: if job_done_callback: - job_done_callback(self, data_index, **kwargs) + job_done_callback(self, **kwargs) except Exception: # pylint: disable=broad-except LOG.warning("Post processing function failed:\n%s", traceback.format_exc()) raise diff --git a/test/stored_data/test_stored_data.py b/test/stored_data/test_stored_data.py index b99618525a..ef53153976 100644 --- a/test/stored_data/test_stored_data.py +++ b/test/stored_data/test_stored_data.py @@ -41,7 +41,10 @@ ) -class TestExperimentData(QiskitTestCase): +import unittest + + +class TestExperimentData(unittest.TestCase): """Test the ExperimentData class.""" def setUp(self): @@ -132,12 +135,11 @@ def test_add_data_job(self): def test_add_data_job_callback(self): """Test add job data with callback.""" - def _callback(_exp_data, data_index): + def _callback(_exp_data): self.assertIsInstance(_exp_data, StoredData) self.assertEqual( [dat["counts"] for dat in _exp_data.data()], a_job.result().get_counts() ) - self.assertEqual(len(a_job.result().results) - 1, data_index) exp_data.add_figures(str.encode("hello world")) exp_data.add_analysis_results(mock.MagicMock()) nonlocal called_back @@ -155,12 +157,11 @@ def _callback(_exp_data, data_index): def test_add_data_callback(self): """Test add data with callback.""" - def _callback(_exp_data, data_index): + def _callback(_exp_data): self.assertIsInstance(_exp_data, StoredData) nonlocal called_back_count, expected_data, subtests expected_data.extend(subtests[called_back_count][1]) self.assertEqual([dat["counts"] for dat in _exp_data.data()], expected_data) - self.assertEqual(len(_exp_data.data()) - 1, data_index) called_back_count += 1 a_result = self._get_job_result(1) @@ -188,9 +189,8 @@ def _callback(_exp_data, data_index): def test_add_data_job_callback_kwargs(self): """Test add job data with callback and additional arguments.""" - def _callback(_exp_data, data_index, **kwargs): + def _callback(_exp_data, **kwargs): self.assertIsInstance(_exp_data, StoredData) - self.assertEqual(len(_exp_data.data()) - 1, data_index) self.assertEqual({"foo": callback_kwargs}, kwargs) nonlocal called_back called_back = True @@ -205,6 +205,23 @@ def _callback(_exp_data, data_index, **kwargs): exp_data.block_for_results() self.assertTrue(called_back) + def test_add_data_pending_post_processing(self): + """Test add job data while post processing is still running.""" + + def _callback(_exp_data, **kwargs): + kwargs["event"].wait(timeout=3) + + a_job = mock.create_autospec(Job, instance=True) + a_job.result.return_value = self._get_job_result(2) + + event = threading.Event() + self.addCleanup(event.set) + + exp_data = StoredData(experiment_type="qiskit_test") + exp_data.add_data(a_job, _callback, event=event) + with self.assertLogs("qiskit_experiments", "WARNING"): + exp_data.add_data({"foo": "bar"}) + def test_get_data(self): """Test getting data.""" data1 = [] From 26b76dfa902de07f6e5e4e5d724b9826778c3bf2 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 17 Jun 2021 16:01:51 -0400 Subject: [PATCH 09/70] fix t1 t2 tests --- qiskit_experiments/base_analysis.py | 8 +++++--- qiskit_experiments/characterization/t1_experiment.py | 4 ++-- .../characterization/t2star_experiment.py | 7 ++++--- test/test_t2star.py | 11 +++++------ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index e77d1b9086..00c9bec794 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -14,7 +14,7 @@ """ from abc import ABC, abstractmethod -from typing import List, Tuple +from typing import List, Tuple, Union from qiskit.exceptions import QiskitError from qiskit.providers.options import Options @@ -102,8 +102,10 @@ def run( @abstractmethod def _run_analysis( - self, experiment_data: ExperimentData, **options - ) -> Tuple[List[AnalysisResultV1], List["matplotlib.figure.Figure"]]: + self, + experiment_data: ExperimentData, + **options + ) -> Tuple[Union[AnalysisResultV1, List[AnalysisResultV1]], List["matplotlib.figure.Figure"]]: """Run analysis on circuit data. Args: diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index c045035e41..8e7fbcd0de 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -71,7 +71,7 @@ def _run_analysis( offset_bounds=None, plot=True, ax=None, - ) -> Tuple[List[AnalysisResultV1], List["matplotlib.figure.Figure"]]: + ) -> Tuple[AnalysisResultV1, Optional[List["matplotlib.figure.Figure"]]]: """ Calculate T1 @@ -164,7 +164,7 @@ def fit_fun(x, a, tau, c): verified=True, ) - return [res_v1], figures + return res_v1, figures @staticmethod def _fit_quality(fit_out, fit_err, reduced_chisq): diff --git a/qiskit_experiments/characterization/t2star_experiment.py b/qiskit_experiments/characterization/t2star_experiment.py index d79a1e5802..64f92a6b75 100644 --- a/qiskit_experiments/characterization/t2star_experiment.py +++ b/qiskit_experiments/characterization/t2star_experiment.py @@ -85,8 +85,9 @@ def _format_plot(ax, unit): ax.set_ylabel("Probability to measure |0>", fontsize=12) # implementation of _run_analysis - unit = experiment_data._data[0]["metadata"]["unit"] - conversion_factor = experiment_data._data[0]["metadata"].get("dt_factor", None) + circ_metadata = experiment_data.data(0)["metadata"] + unit = circ_metadata["unit"] + conversion_factor = circ_metadata.get("dt_factor", None) if conversion_factor is None: conversion_factor = 1 if unit == "s" else apply_prefix(1, unit) xdata, ydata, sigma = process_curve_data( @@ -134,7 +135,7 @@ def _format_plot(ax, unit): analysis_result = AnalysisResultV1( result_data=result_data, result_type="T2Star", - device_components=[Qubit(data[0]["metadata"]["qubit"])], + device_components=[Qubit(circ_metadata["qubit"])], experiment_id=experiment_data.experiment_id, quality=result_data["quality"], ) diff --git a/test/test_t2star.py b/test/test_t2star.py index 42f2d9629a..c59c0576d4 100644 --- a/test/test_t2star.py +++ b/test/test_t2star.py @@ -229,7 +229,7 @@ def test_t2star_run_end2end(self): delta=0.08 * result_data["frequency_value"], ) self.assertEqual( - result["quality"], "computer_good", "Result quality bad for unit " + str(unit) + result.quality, "good", "Result quality bad for unit " + str(unit) ) def test_t2star_parallel(self): @@ -253,11 +253,10 @@ def test_t2star_parallel(self): "b_guess": [0.5, None, 0.5], } backend = T2starBackend(p0) - res = par_exp.run(backend=backend, shots=1000) - res.block_for_results() + expdata = par_exp.run(backend=backend, shots=1000) + expdata.block_for_results() for i in range(2): - sub_res = res.component_experiment_data(i).analysis_result(0) sub_res = expdata.component_experiment_data(i).analysis_result(0) sub_res_data = sub_res.data() self.assertAlmostEqual( @@ -269,8 +268,8 @@ def test_t2star_parallel(self): delta=0.08 * sub_res_data["frequency_value"], ) self.assertEqual( - sub_res["quality"], - "computer_good", + sub_res.quality, + "good", "Result quality bad for experiment on qubit " + str(i), ) From e3a2d318326039f1db16a41cf8785d7e5477a606 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 17 Jun 2021 16:27:46 -0400 Subject: [PATCH 10/70] fix test can package typo --- .../{store_data => stored_data}/__init__.py | 0 .../analysis_result.py | 0 .../device_component.py | 0 .../{store_data => stored_data}/exceptions.py | 0 .../experiment_service.py | 0 .../{store_data => stored_data}/json.py | 0 .../{store_data => stored_data}/stored_data.py | 0 .../{store_data => stored_data}/utils.py | 0 test/stored_data/test_analysisresult.py | 8 ++++---- test/stored_data/test_stored_data.py | 16 ++++++---------- 10 files changed, 10 insertions(+), 14 deletions(-) rename qiskit_experiments/{store_data => stored_data}/__init__.py (100%) rename qiskit_experiments/{store_data => stored_data}/analysis_result.py (100%) rename qiskit_experiments/{store_data => stored_data}/device_component.py (100%) rename qiskit_experiments/{store_data => stored_data}/exceptions.py (100%) rename qiskit_experiments/{store_data => stored_data}/experiment_service.py (100%) rename qiskit_experiments/{store_data => stored_data}/json.py (100%) rename qiskit_experiments/{store_data => stored_data}/stored_data.py (100%) rename qiskit_experiments/{store_data => stored_data}/utils.py (100%) diff --git a/qiskit_experiments/store_data/__init__.py b/qiskit_experiments/stored_data/__init__.py similarity index 100% rename from qiskit_experiments/store_data/__init__.py rename to qiskit_experiments/stored_data/__init__.py diff --git a/qiskit_experiments/store_data/analysis_result.py b/qiskit_experiments/stored_data/analysis_result.py similarity index 100% rename from qiskit_experiments/store_data/analysis_result.py rename to qiskit_experiments/stored_data/analysis_result.py diff --git a/qiskit_experiments/store_data/device_component.py b/qiskit_experiments/stored_data/device_component.py similarity index 100% rename from qiskit_experiments/store_data/device_component.py rename to qiskit_experiments/stored_data/device_component.py diff --git a/qiskit_experiments/store_data/exceptions.py b/qiskit_experiments/stored_data/exceptions.py similarity index 100% rename from qiskit_experiments/store_data/exceptions.py rename to qiskit_experiments/stored_data/exceptions.py diff --git a/qiskit_experiments/store_data/experiment_service.py b/qiskit_experiments/stored_data/experiment_service.py similarity index 100% rename from qiskit_experiments/store_data/experiment_service.py rename to qiskit_experiments/stored_data/experiment_service.py diff --git a/qiskit_experiments/store_data/json.py b/qiskit_experiments/stored_data/json.py similarity index 100% rename from qiskit_experiments/store_data/json.py rename to qiskit_experiments/stored_data/json.py diff --git a/qiskit_experiments/store_data/stored_data.py b/qiskit_experiments/stored_data/stored_data.py similarity index 100% rename from qiskit_experiments/store_data/stored_data.py rename to qiskit_experiments/stored_data/stored_data.py diff --git a/qiskit_experiments/store_data/utils.py b/qiskit_experiments/stored_data/utils.py similarity index 100% rename from qiskit_experiments/store_data/utils.py rename to qiskit_experiments/stored_data/utils.py diff --git a/test/stored_data/test_analysisresult.py b/test/stored_data/test_analysisresult.py index 6a41506997..30cc064d20 100644 --- a/test/stored_data/test_analysisresult.py +++ b/test/stored_data/test_analysisresult.py @@ -20,10 +20,10 @@ import numpy as np from qiskit.test import QiskitTestCase -from qiskit_experiments.store_data import AnalysisResultV1 as AnalysisResult -from qiskit_experiments.store_data.device_component import Qubit, Resonator, to_component -from qiskit_experiments.store_data.experiment_service import ExperimentServiceV1 -from qiskit_experiments.store_data.exceptions import ExperimentError +from qiskit_experiments.stored_data import AnalysisResultV1 as AnalysisResult +from qiskit_experiments.stored_data.device_component import Qubit, Resonator, to_component +from qiskit_experiments.stored_data.experiment_service import ExperimentServiceV1 +from qiskit_experiments.stored_data.exceptions import ExperimentError class TestAnalysisResult(QiskitTestCase): diff --git a/test/stored_data/test_stored_data.py b/test/stored_data/test_stored_data.py index ef53153976..b76978be8a 100644 --- a/test/stored_data/test_stored_data.py +++ b/test/stored_data/test_stored_data.py @@ -32,19 +32,16 @@ from qiskit.providers import JobV1 as Job from qiskit.providers import JobStatus from qiskit.tools.visualization import HAS_MATPLOTLIB -from qiskit_experiments.store_data import StoredDataV1 as StoredData -from qiskit_experiments.store_data import ExperimentServiceV1 -from qiskit_experiments.store_data.exceptions import ( +from qiskit_experiments.stored_data import StoredDataV1 as StoredData +from qiskit_experiments.stored_data import ExperimentServiceV1 +from qiskit_experiments.stored_data.exceptions import ( ExperimentError, ExperimentEntryNotFound, ExperimentEntryExists, ) -import unittest - - -class TestExperimentData(unittest.TestCase): +class TestStoredData(QiskitTestCase): """Test the ExperimentData class.""" def setUp(self): @@ -666,9 +663,8 @@ def _post_processing(*args, **kwargs): # pylint: disable=unused-argument def test_source(self): """Test getting experiment source.""" exp_data = StoredData(experiment_type="qiskit_test") - source_vals = "\n".join([str(val) for val in exp_data.source.values()]) - self.assertIn("StoredDataV1", source_vals) - self.assertIn("qiskit-terra", source_vals) + self.assertIn("StoredDataV1", exp_data.source["class"]) + self.assertTrue(exp_data.source["qiskit_version"]) def test_block_for_jobs(self): """Test blocking for jobs.""" From ce1c917d6e44fb5425b285dcec48e3e7b7fe7d17 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 17 Jun 2021 16:43:05 -0400 Subject: [PATCH 11/70] fix package typo --- qiskit_experiments/analysis/curve_analysis.py | 4 ++-- qiskit_experiments/base_analysis.py | 2 +- qiskit_experiments/characterization/t1_experiment.py | 4 ++-- qiskit_experiments/characterization/t2star_experiment.py | 4 ++-- qiskit_experiments/composite/composite_analysis.py | 4 ++-- qiskit_experiments/experiment_data.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 65a4bad8be..b7c0cc4d2d 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -31,8 +31,8 @@ from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ResultDict, ExperimentData -from qiskit_experiments.store_data import AnalysisResultV1 -from qiskit_experiments.store_data.device_component import Qubit +from qiskit_experiments.stored_data import AnalysisResultV1 +from qiskit_experiments.stored_data.device_component import Qubit @dataclasses.dataclass(frozen=True) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 00c9bec794..a686a1ab33 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -21,7 +21,7 @@ from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ExperimentData, ResultDict -from qiskit_experiments.store_data import AnalysisResultV1 +from qiskit_experiments.stored_data import AnalysisResultV1 class BaseAnalysis(ABC): diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index 8e7fbcd0de..26b2077624 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -27,8 +27,8 @@ from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting from qiskit_experiments.experiment_data import ResultDict -from qiskit_experiments.store_data import AnalysisResultV1 -from qiskit_experiments.store_data.device_component import Qubit +from qiskit_experiments.stored_data import AnalysisResultV1 +from qiskit_experiments.stored_data.device_component import Qubit class T1Analysis(BaseAnalysis): diff --git a/qiskit_experiments/characterization/t2star_experiment.py b/qiskit_experiments/characterization/t2star_experiment.py index 64f92a6b75..4181970ea5 100644 --- a/qiskit_experiments/characterization/t2star_experiment.py +++ b/qiskit_experiments/characterization/t2star_experiment.py @@ -27,8 +27,8 @@ from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting from qiskit_experiments.experiment_data import ExperimentData, ResultDict -from qiskit_experiments.store_data import AnalysisResultV1 -from qiskit_experiments.store_data.device_component import Qubit +from qiskit_experiments.stored_data import AnalysisResultV1 +from qiskit_experiments.stored_data.device_component import Qubit # pylint: disable = invalid-name diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 390a625907..9363fad213 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -16,8 +16,8 @@ from qiskit.exceptions import QiskitError from qiskit_experiments.base_analysis import BaseAnalysis from .composite_experiment_data import CompositeExperimentData -from qiskit_experiments.store_data import AnalysisResultV1 -from qiskit_experiments.store_data.device_component import Qubit +from qiskit_experiments.stored_data import AnalysisResultV1 +from qiskit_experiments.stored_data.device_component import Qubit class CompositeAnalysis(BaseAnalysis): diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index d8388d0886..79aeabc745 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -15,7 +15,7 @@ import logging from qiskit.providers import Backend -from qiskit_experiments.store_data import StoredDataV1 +from qiskit_experiments.stored_data import StoredDataV1 LOG = logging.getLogger(__name__) From c06627432e2bd5fc95293d53eea43e3047c213a6 Mon Sep 17 00:00:00 2001 From: yaelbh Date: Sun, 20 Jun 2021 11:30:43 +0300 Subject: [PATCH 12/70] fixed errors in conlicts resolve --- qiskit_experiments/characterization/t2star_experiment.py | 3 ++- test/test_t2star.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/characterization/t2star_experiment.py b/qiskit_experiments/characterization/t2star_experiment.py index 60f2092f0c..0efa247186 100644 --- a/qiskit_experiments/characterization/t2star_experiment.py +++ b/qiskit_experiments/characterization/t2star_experiment.py @@ -86,7 +86,8 @@ def _format_plot(ax, unit): # implementation of _run_analysis - circ_metadata = experiment_data.data(0)["metadata"] + data = experiment_data.data() + circ_metadata = data[0]["metadata"] unit = circ_metadata["unit"] conversion_factor = circ_metadata.get("dt_factor", None) if conversion_factor is None: diff --git a/test/test_t2star.py b/test/test_t2star.py index b8e482ef5a..16f07fdb16 100644 --- a/test/test_t2star.py +++ b/test/test_t2star.py @@ -260,7 +260,6 @@ def test_t2star_parallel(self): estimated_freq[i], delta=3, ) - sub_res = res.component_experiment_data(i).analysis_result(0) self.assertEqual( sub_res.quality, "good", From e4bdbe699f6c0daef6dbb0c91ac41aab25d561d7 Mon Sep 17 00:00:00 2001 From: yaelbh Date: Sun, 20 Jun 2021 11:44:10 +0300 Subject: [PATCH 13/70] adjusted a test to the new classes --- test/analysis/test_curve_fit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/analysis/test_curve_fit.py b/test/analysis/test_curve_fit.py index 1b6b2ea0d1..0695410a89 100644 --- a/test/analysis/test_curve_fit.py +++ b/test/analysis/test_curve_fit.py @@ -349,7 +349,7 @@ def test_run_single_curve_fail(self): fit_reports=None, return_data_points=True, ) - result = results[0] + result = results[0].data() self.assertFalse(result["success"]) From 7100638a09ea2bd06fcfbfbe13caff0b572ede37 Mon Sep 17 00:00:00 2001 From: yaelbh Date: Sun, 20 Jun 2021 13:19:25 +0300 Subject: [PATCH 14/70] made test_spectroscopy_end2end_classified pass --- qiskit_experiments/analysis/curve_analysis.py | 10 ++++++++-- .../characterization/qubit_spectroscopy.py | 20 +++++++++---------- test/test_qubit_spectroscopy.py | 20 ++++++++++++------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index b7c0cc4d2d..1c526d0d5b 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -790,7 +790,10 @@ def _run_analysis( try: self.__qubits = experiment_data.data(0)["metadata"]["qubits"] except KeyError: - pass + try: + self.__qubits = [experiment_data.data(0)["metadata"]["qubit"]] + except KeyError: + pass # # 1. Setup data processor @@ -866,6 +869,8 @@ def _run_analysis( fit_results = sorted(fit_results, key=lambda r: r["reduced_chisq"]) result_data.update(**fit_results[0]) + result_data["success"] = True + except AnalysisError as ex: result_data["error_message"] = str(ex) result_data["success"] = False @@ -901,9 +906,10 @@ def _run_analysis( result_data=result_data, result_type=result_data["analysis_type"], device_components=[ - Qubit(qubit) for qubit in experiment_data.data(0)["metadata"]["qubits"] + Qubit(qubit) for qubit in self.__qubits ], experiment_id=experiment_data.experiment_id, + quality=result_data["quality"], ) return [analysis_result], figures diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 34acf7814d..641e7290da 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -131,7 +131,7 @@ def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any] return fit_option - def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: """Algorithmic criteria for whether the fit is good or bad. A good fit has: @@ -150,11 +150,11 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR min_freq = np.min(curve_data.x) freq_increment = np.mean(np.diff(curve_data.x)) - fit_a = get_opt_value(analysis_result, "a") - fit_b = get_opt_value(analysis_result, "b") - fit_freq = get_opt_value(analysis_result, "freq") - fit_sigma = get_opt_value(analysis_result, "sigma") - fit_sigma_err = get_opt_error(analysis_result, "sigma") + fit_a = get_opt_value(result_data, "a") + fit_b = get_opt_value(result_data, "b") + fit_freq = get_opt_value(result_data, "freq") + fit_sigma = get_opt_value(result_data, "sigma") + fit_sigma_err = get_opt_error(result_data, "sigma") snr = abs(fit_a) / np.sqrt(abs(np.median(curve_data.y) - fit_b)) fit_width_ratio = fit_sigma / (max_freq - min_freq) @@ -163,17 +163,17 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR min_freq <= fit_freq <= max_freq, 1.5 * freq_increment < fit_sigma, fit_width_ratio < 0.25, - analysis_result["reduced_chisq"] < 3, + result_data["reduced_chisq"] < 3, (fit_sigma_err is None or fit_sigma_err < fit_sigma), snr > 2, ] if all(criteria): - analysis_result["quality"] = "computer_good" + result_data["quality"] = "computer_good" else: - analysis_result["quality"] = "computer_bad" + result_data["quality"] = "computer_bad" - return analysis_result + return result_data class QubitSpectroscopy(BaseExperiment): diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 47deefdca9..283d15dda5 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -60,26 +60,32 @@ def test_spectroscopy_end2end_classified(self): spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) - result = spec.run(backend).analysis_result(0) + expdata = spec.run(backend) + expdata.block_for_results() + result = expdata.analysis_result(0) + result_data = result.data() - value = get_opt_value(result, "freq") + value = get_opt_value(result_data, "freq") self.assertTrue(abs(value) < 1e6) - self.assertTrue(result["success"]) - self.assertEqual(result["quality"], "computer_good") + self.assertTrue(result_data["success"]) + self.assertEqual(result.quality, "computer_good") # Test if we find still find the peak when it is shifted by 5 MHz. backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) - result = spec.run(backend).analysis_result(0) + expdata = spec.run(backend) + expdata.block_for_results() + result = expdata.analysis_result(0) + result_data = result.data() - value = get_opt_value(result, "freq") + value = get_opt_value(result_data, "freq") self.assertTrue(value < 5.1e6) self.assertTrue(value > 4.9e6) - self.assertEqual(result["quality"], "computer_good") + self.assertEqual(result.quality, "computer_good") def test_spectroscopy_end2end_kerneled(self): """End to end test of the spectroscopy experiment on IQ data.""" From f0319475cdaa874b7af6a306573b1929606c176b Mon Sep 17 00:00:00 2001 From: yaelbh Date: Sun, 20 Jun 2021 13:42:34 +0300 Subject: [PATCH 15/70] adjusted all spectro tests --- test/test_qubit_spectroscopy.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 283d15dda5..7891c86ceb 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -93,31 +93,40 @@ def test_spectroscopy_end2end_kerneled(self): backend = SpectroscopyBackend(line_width=2e6) spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") - result = spec.run(backend).analysis_result(0) + expdata = spec.run(backend) + expdata.block_for_results() + result = expdata.analysis_result(0) + result_data = result.data() - value = get_opt_value(result, "freq") + value = get_opt_value(result_data, "freq") self.assertTrue(abs(value) < 1e6) - self.assertTrue(result["success"]) - self.assertEqual(result["quality"], "computer_good") + self.assertTrue(result_data["success"]) + self.assertEqual(result.quality, "computer_good") # Test if we find still find the peak when it is shifted by 5 MHz. backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") - result = spec.run(backend).analysis_result(0) + expdata = spec.run(backend) + expdata.block_for_results() + result = expdata.analysis_result(0) + result_data = result.data() - value = get_opt_value(result, "freq") + value = get_opt_value(result_data, "freq") self.assertTrue(value < 5.1e6) self.assertTrue(value > 4.9e6) - self.assertEqual(result["quality"], "computer_good") + self.assertEqual(result.quality, "computer_good") spec.set_run_options(meas_return="avg") - result = spec.run(backend).analysis_result(0) + expdata = spec.run(backend) + expdata.block_for_results() + result = expdata.analysis_result(0) + result_data = result.data() - value = get_opt_value(result, "freq") + value = get_opt_value(result_data, "freq") self.assertTrue(value < 5.1e6) self.assertTrue(value > 4.9e6) - self.assertEqual(result["quality"], "computer_good") + self.assertEqual(result.quality, "computer_good") From e83add3176e952f41b0773e146309e5f03a5a6f0 Mon Sep 17 00:00:00 2001 From: yaelbh Date: Sun, 20 Jun 2021 14:06:10 +0300 Subject: [PATCH 16/70] test_curve_fit passes --- qiskit_experiments/analysis/curve_analysis.py | 2 +- test/analysis/test_curve_fit.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 1c526d0d5b..18d84d4b7d 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -909,7 +909,7 @@ def _run_analysis( Qubit(qubit) for qubit in self.__qubits ], experiment_id=experiment_data.experiment_id, - quality=result_data["quality"], + quality=result_data.get("quality", None), ) return [analysis_result], figures diff --git a/test/analysis/test_curve_fit.py b/test/analysis/test_curve_fit.py index 0695410a89..51bb4516c8 100644 --- a/test/analysis/test_curve_fit.py +++ b/test/analysis/test_curve_fit.py @@ -301,7 +301,7 @@ def test_run_single_curve_analysis(self): fit_reports=None, return_data_points=False, ) - result = results[0] + result = results[0].data() ref_popt = np.asarray([ref_p0, ref_p1, ref_p2, ref_p3]) @@ -413,7 +413,7 @@ def test_run_two_curves_with_same_fitfunc(self): fit_reports=None, return_data_points=False, ) - result = results[0] + result = results[0].data() ref_popt = np.asarray([ref_p0, ref_p1, ref_p2, ref_p3, ref_p4]) @@ -476,7 +476,7 @@ def test_run_two_curves_with_two_fitfuncs(self): fit_reports=None, return_data_points=False, ) - result = results[0] + result = results[0].data() ref_popt = np.asarray([ref_p0, ref_p1, ref_p2, ref_p3]) From cf57d8170bcb8ee010eb505b4887802985f3fc53 Mon Sep 17 00:00:00 2001 From: yaelbh Date: Sun, 20 Jun 2021 14:29:48 +0300 Subject: [PATCH 17/70] adjusted rabi --- qiskit_experiments/analysis/curve_analysis.py | 2 +- .../calibration/experiments/rabi.py | 14 +++---- test/calibration/experiments/test_rabi.py | 40 +++++++++++-------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 18d84d4b7d..e8a813f4a1 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -907,7 +907,7 @@ def _run_analysis( result_type=result_data["analysis_type"], device_components=[ Qubit(qubit) for qubit in self.__qubits - ], + ] if self.__qubits else [], experiment_id=experiment_data.experiment_id, quality=result_data.get("quality", None), ) diff --git a/qiskit_experiments/calibration/experiments/rabi.py b/qiskit_experiments/calibration/experiments/rabi.py index 43c1abdf78..3a88a42eca 100644 --- a/qiskit_experiments/calibration/experiments/rabi.py +++ b/qiskit_experiments/calibration/experiments/rabi.py @@ -128,7 +128,7 @@ def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any] return fit_options - def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: """Algorithmic criteria for whether the fit is good or bad. A good fit has: @@ -137,21 +137,21 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR - less than 10 full periods, and - an error on the fit frequency lower than the fit frequency. """ - fit_freq = get_opt_value(analysis_result, "freq") - fit_freq_err = get_opt_error(analysis_result, "freq") + fit_freq = get_opt_value(result_data, "freq") + fit_freq_err = get_opt_error(result_data, "freq") criteria = [ - analysis_result["reduced_chisq"] < 3, + result_data["reduced_chisq"] < 3, 1.0 / 4.0 < fit_freq < 10.0, (fit_freq_err is None or (fit_freq_err < fit_freq)), ] if all(criteria): - analysis_result["quality"] = "computer_good" + result_data["quality"] = "computer_good" else: - analysis_result["quality"] = "computer_bad" + result_data["quality"] = "computer_bad" - return analysis_result + return result_data class Rabi(BaseExperiment): diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 89305a1ba5..096f8d0d42 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -65,27 +65,35 @@ def test_rabi_end_to_end(self): rabi = Rabi(3) rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) - result = rabi.run(backend).analysis_result(0) - - self.assertEqual(result["quality"], "computer_good") - self.assertTrue(abs(result["popt"][1] - backend.rabi_rate) < test_tol) - + expdata = rabi.run(backend) + expdata.block_for_results() + result = expdata.analysis_result(0) + result_data = result.data() + + self.assertEqual(result.quality, "computer_good") + self.assertTrue(abs(result_data["popt"][1] - backend.rabi_rate) < test_tol) + backend = RabiBackend(amplitude_to_angle=np.pi / 2) - + rabi = Rabi(3) rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) - result = rabi.run(backend).analysis_result(0) - self.assertEqual(result["quality"], "computer_good") - self.assertTrue(abs(result["popt"][1] - backend.rabi_rate) < test_tol) + expdata = rabi.run(backend) + expdata.block_for_results() + result = expdata.analysis_result(0) + result_data = result.data() + self.assertEqual(result.quality, "computer_good") + self.assertTrue(abs(result_data["popt"][1] - backend.rabi_rate) < test_tol) backend = RabiBackend(amplitude_to_angle=2.5 * np.pi) rabi = Rabi(3) rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 101)) - result = rabi.run(backend).analysis_result(0) - - self.assertEqual(result["quality"], "computer_good") - self.assertTrue(abs(result["popt"][1] - backend.rabi_rate) < test_tol) + expdata = rabi.run(backend) + expdata.block_for_results() + result = expdata.analysis_result(0) + result_data = result.data() + self.assertEqual(result.quality, "computer_good") + self.assertTrue(abs(result_data["popt"][1] - backend.rabi_rate) < test_tol) class TestRabiCircuits(QiskitTestCase): @@ -170,8 +178,8 @@ def test_good_analysis(self): result = RabiAnalysis().run(experiment_data, data_processor=data_processor, plot=False) - self.assertEqual(result[0]["quality"], "computer_good") - self.assertTrue(abs(result[0]["popt"][1] - expected_rate) < test_tol) + self.assertEqual(result[0].quality, "computer_good") + self.assertTrue(abs(result[0].data()["popt"][1] - expected_rate) < test_tol) def test_bad_analysis(self): """Test the Rabi analysis.""" @@ -186,4 +194,4 @@ def test_bad_analysis(self): result = RabiAnalysis().run(experiment_data, data_processor=data_processor, plot=False) - self.assertEqual(result[0]["quality"], "computer_bad") + self.assertEqual(result[0].quality, "computer_bad") From a14144ca87b4c8a1680863f876f23eba0dde0740 Mon Sep 17 00:00:00 2001 From: yaelbh Date: Sun, 20 Jun 2021 14:30:13 +0300 Subject: [PATCH 18/70] black --- qiskit_experiments/analysis/curve_analysis.py | 4 +--- qiskit_experiments/base_analysis.py | 4 +--- qiskit_experiments/base_experiment.py | 2 +- .../characterization/t2star_experiment.py | 2 +- .../composite/composite_analysis.py | 20 +++++++++---------- test/calibration/experiments/test_rabi.py | 4 ++-- test/test_t2star.py | 8 ++------ 7 files changed, 18 insertions(+), 26 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index e8a813f4a1..27f5f9bad4 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -905,9 +905,7 @@ def _run_analysis( analysis_result = AnalysisResultV1( result_data=result_data, result_type=result_data["analysis_type"], - device_components=[ - Qubit(qubit) for qubit in self.__qubits - ] if self.__qubits else [], + device_components=[Qubit(qubit) for qubit in self.__qubits] if self.__qubits else [], experiment_id=experiment_data.experiment_id, quality=result_data.get("quality", None), ) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index a686a1ab33..f9e1a2dd33 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -102,9 +102,7 @@ def run( @abstractmethod def _run_analysis( - self, - experiment_data: ExperimentData, - **options + self, experiment_data: ExperimentData, **options ) -> Tuple[Union[AnalysisResultV1, List[AnalysisResultV1]], List["matplotlib.figure.Figure"]]: """Run analysis on circuit data. diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index d2265b23d8..5a514cd5b2 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -119,7 +119,7 @@ def run( # Add Job to ExperimentData and add analysis for post processing. run_analysis = None - + if analysis and self.__analysis_class__ is not None: run_analysis = self.run_analysis diff --git a/qiskit_experiments/characterization/t2star_experiment.py b/qiskit_experiments/characterization/t2star_experiment.py index 0efa247186..3938fba19d 100644 --- a/qiskit_experiments/characterization/t2star_experiment.py +++ b/qiskit_experiments/characterization/t2star_experiment.py @@ -139,7 +139,7 @@ def _format_plot(ax, unit): experiment_id=experiment_data.experiment_id, quality=result_data["quality"], ) - + return [analysis_result], figures def _t2star_default_params( diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 9363fad213..950be71a7e 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -72,14 +72,14 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): sub_qubits.append(expdata.experiment.physical_qubits) analysis_result = AnalysisResultV1( - result_data={ - "experiment_types": sub_types, - "experiment_ids": sub_ids, - "experiment_qubits": sub_qubits, - }, - result_type="composite", - device_components=[Qubit(qidx) for qidx in sub_qubits], - experiment_id=experiment_data.experiment_id, - ) - + result_data={ + "experiment_types": sub_types, + "experiment_ids": sub_ids, + "experiment_qubits": sub_qubits, + }, + result_type="composite", + device_components=[Qubit(qidx) for qidx in sub_qubits], + experiment_id=experiment_data.experiment_id, + ) + return [analysis_result], None diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 096f8d0d42..9576c3f36d 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -72,9 +72,9 @@ def test_rabi_end_to_end(self): self.assertEqual(result.quality, "computer_good") self.assertTrue(abs(result_data["popt"][1] - backend.rabi_rate) < test_tol) - + backend = RabiBackend(amplitude_to_angle=np.pi / 2) - + rabi = Rabi(3) rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) expdata = rabi.run(backend) diff --git a/test/test_t2star.py b/test/test_t2star.py index 16f07fdb16..e1441ae950 100644 --- a/test/test_t2star.py +++ b/test/test_t2star.py @@ -221,9 +221,7 @@ def test_t2star_run_end2end(self): estimated_freq / dt_factor, delta=3 / dt_factor, ) - self.assertEqual( - result.quality, "good", "Result quality bad for unit " + str(unit) - ) + self.assertEqual(result.quality, "good", "Result quality bad for unit " + str(unit)) def test_t2star_parallel(self): """ @@ -252,9 +250,7 @@ def test_t2star_parallel(self): for i in range(2): sub_res = expdata.component_experiment_data(i).analysis_result(0) sub_res_data = sub_res.data() - self.assertAlmostEqual( - sub_res_data["t2star_value"], t2star[i], delta=3 - ) + self.assertAlmostEqual(sub_res_data["t2star_value"], t2star[i], delta=3) self.assertAlmostEqual( sub_res_data["frequency_value"], estimated_freq[i], From efe028e9cf23d2273e95cfa2b74209e8584e83e0 Mon Sep 17 00:00:00 2001 From: yaelbh Date: Sun, 20 Jun 2021 14:53:51 +0300 Subject: [PATCH 19/70] lint --- qiskit_experiments/analysis/curve_analysis.py | 2 +- qiskit_experiments/base_analysis.py | 3 +-- qiskit_experiments/composite/composite_analysis.py | 2 +- qiskit_experiments/composite/composite_experiment.py | 4 ++-- qiskit_experiments/experiment_data.py | 1 - qiskit_experiments/matplotlib.py | 1 - .../randomized_benchmarking/rb_analysis.py | 12 ++++++------ test/test_t1.py | 3 +-- test/test_t2star.py | 4 ++-- 9 files changed, 14 insertions(+), 18 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 27f5f9bad4..8e068195c9 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -30,7 +30,7 @@ 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 ResultDict, ExperimentData +from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.stored_data import AnalysisResultV1 from qiskit_experiments.stored_data.device_component import Qubit diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index f9e1a2dd33..268561e7b2 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -19,8 +19,7 @@ from qiskit.exceptions import QiskitError from qiskit.providers.options import Options -from qiskit_experiments.exceptions import AnalysisError -from qiskit_experiments.experiment_data import ExperimentData, ResultDict +from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.stored_data import AnalysisResultV1 diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 950be71a7e..3d2d5529c1 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -15,7 +15,7 @@ from qiskit.exceptions import QiskitError from qiskit_experiments.base_analysis import BaseAnalysis -from .composite_experiment_data import CompositeExperimentData +from qiskit_experiments.composite.composite_experiment_data import CompositeExperimentData from qiskit_experiments.stored_data import AnalysisResultV1 from qiskit_experiments.stored_data.device_component import Qubit diff --git a/qiskit_experiments/composite/composite_experiment.py b/qiskit_experiments/composite/composite_experiment.py index 91d8e3c7c4..f7bedab50b 100644 --- a/qiskit_experiments/composite/composite_experiment.py +++ b/qiskit_experiments/composite/composite_experiment.py @@ -51,9 +51,9 @@ def num_experiments(self): def component_experiment(self, index=None): """Return the component Experiment object. Args: - index: Experiment index, or ``None`` if all experiments are to be returned. + index (int): Experiment index, or ``None`` if all experiments are to be returned. Returns: - The component experiment(s). + BaseExperiment: The component experiment(s). """ if index is None: return self._experiments diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index 79aeabc745..a4fceb41e1 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -14,7 +14,6 @@ """ import logging -from qiskit.providers import Backend from qiskit_experiments.stored_data import StoredDataV1 diff --git a/qiskit_experiments/matplotlib.py b/qiskit_experiments/matplotlib.py index cc15210f9c..fb3f7825e3 100644 --- a/qiskit_experiments/matplotlib.py +++ b/qiskit_experiments/matplotlib.py @@ -40,7 +40,6 @@ def wrapped(*args, **kwargs): # The code below switches to a non-GUI backend "Agg" when creating the # plot. An alternative is to run this in a separate process, but then # we'd need to deal with pickling issues. - from matplotlib import pyplot saved_backend = pyplot.get_backend() pyplot.switch_backend("Agg") diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index f42f7359da..53ae415058 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -148,13 +148,13 @@ def _format_data(self, data: CurveData) -> CurveData: data_index=mean_data_index, ) - def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: """Calculate EPC.""" - alpha = get_opt_value(analysis_result, "alpha") - alpha_err = get_opt_error(analysis_result, "alpha") + alpha = get_opt_value(result_data, "alpha") + alpha_err = get_opt_error(result_data, "alpha") scale = (2 ** self._num_qubits - 1) / (2 ** self._num_qubits) - analysis_result["EPC"] = scale * (1 - alpha) - analysis_result["EPC_err"] = scale * alpha_err / alpha + result_data["EPC"] = scale * (1 - alpha) + result_data["EPC_err"] = scale * alpha_err / alpha - return analysis_result + return result_data diff --git a/test/test_t1.py b/test/test_t1.py index 6885bceaed..0c32253410 100644 --- a/test/test_t1.py +++ b/test/test_t1.py @@ -16,6 +16,7 @@ """ import unittest +from test.utils import FakeJob import numpy as np from qiskit.test import QiskitTestCase from qiskit.providers import BackendV1 @@ -26,8 +27,6 @@ from qiskit_experiments.composite import ParallelExperiment from qiskit_experiments.characterization import T1Experiment, T1Analysis -from test.utils import FakeJob - class T1Backend(BackendV1): """ diff --git a/test/test_t2star.py b/test/test_t2star.py index e1441ae950..ca7ff0e3fa 100644 --- a/test/test_t2star.py +++ b/test/test_t2star.py @@ -11,6 +11,8 @@ Test T2Star experiment """ import unittest + +from test.utils import FakeJob import numpy as np from qiskit.utils import apply_prefix @@ -22,8 +24,6 @@ from qiskit_experiments.composite import ParallelExperiment from qiskit_experiments.characterization import T2StarExperiment -from test.utils import FakeJob - class T2starBackend(BackendV1): """ From ad33800d1a17e9f3288fa670b4e16c42a70406fa Mon Sep 17 00:00:00 2001 From: yaelbh Date: Sun, 20 Jun 2021 15:03:49 +0300 Subject: [PATCH 20/70] fixed test_source --- test/stored_data/test_analysisresult.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/stored_data/test_analysisresult.py b/test/stored_data/test_analysisresult.py index 30cc064d20..b79b2f9627 100644 --- a/test/stored_data/test_analysisresult.py +++ b/test/stored_data/test_analysisresult.py @@ -139,7 +139,7 @@ def test_source(self): result = self._new_analysis_result() source_vals = "\n".join([str(val) for val in result.source.values()]) self.assertIn("AnalysisResultV1", source_vals) - self.assertIn("qiskit-terra", source_vals) + self.assertTrue(result.source["qiskit_version"]) def _new_analysis_result(self, **kwargs): """Return a new analysis result.""" From 557556e896ac9fd08fdc8bc3abae77df0a0c7a0e Mon Sep 17 00:00:00 2001 From: jessieyu Date: Mon, 21 Jun 2021 16:46:06 -0400 Subject: [PATCH 21/70] fix doc build --- qiskit_experiments/stored_data/stored_data.py | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/qiskit_experiments/stored_data/stored_data.py b/qiskit_experiments/stored_data/stored_data.py index c1728806d7..f92745e31e 100644 --- a/qiskit_experiments/stored_data/stored_data.py +++ b/qiskit_experiments/stored_data/stored_data.py @@ -40,6 +40,7 @@ ThreadSafeOrderedDict, ThreadSafeList, ) +from .experiment_service import ExperimentServiceV1 LOG = logging.getLogger(__name__) @@ -360,28 +361,29 @@ def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, Li @auto_save def add_figures( self, - figures: Union[List[Union[str, bytes, "pyplot.Figure"]], str, bytes, "pyplot.Figure"], - figure_names: Optional[Union[List[str], str]] = None, - overwrite: bool = False, - save_figure: Optional[bool] = None, - service: Optional["ExperimentServiceV1"] = None, - ) -> Union[str, List[str]]: + figures, + figure_names=None, + overwrite=False, + save_figure=None, + service=None, + ): """Add the experiment figure. Args: - figures: Names of the figure files or figure data. - figure_names: Names of the figures. If ``None``, use the figure file + figures (str or bytes or matplotlib.pyplot.Figure): Names of the + figure files or figure data. + figure_names (list or str): Names of the figures. If ``None``, use the figure file names, if given, or a generated name. If `figures` is a list, then `figure_names` must also be a list of the same length or ``None``. - overwrite: Whether to overwrite the figure if one already exists with + overwrite (bool): Whether to overwrite the figure if one already exists with the same name. - save_figure: Whether to save the figure in the database. If ``None``, + save_figure (bool): Whether to save the figure in the database. If ``None``, the ``auto-save`` attribute is used. - service: Experiment service to be used to update the database, if - the figure is to be uploaded. If ``None``, the default service is used. + service (ExperimentServiceV1): Experiment service to be used to update the database, + if the figure is to be uploaded. If ``None``, the default service is used. Returns: - Figure names. + list or str: Figure names. Raises: ExperimentEntryExists: If the figure with the same name already exists, @@ -458,7 +460,7 @@ def add_figures( def delete_figure( self, figure_key: Union[str, int], - service: Optional["ExperimentServiceV1"] = None, + service: Optional[ExperimentServiceV1] = None, ) -> str: """Add the experiment figure. @@ -529,7 +531,7 @@ def figure( def add_analysis_results( self, results: Union[AnalysisResult, List[AnalysisResult]], - service: "ExperimentServiceV1" = None, + service: ExperimentServiceV1 = None, ) -> None: """Save the analysis result. @@ -554,7 +556,7 @@ def add_analysis_results( @auto_save def delete_analysis_result( - self, result_key: Union[int, str], service: "ExperimentServiceV1" = None + self, result_key: Union[int, str], service: ExperimentServiceV1 = None ) -> str: """Delete the analysis result. @@ -626,12 +628,12 @@ def analysis_result( raise TypeError(f"Invalid index type {type(index)}.") - def save(self, service: Optional["ExperimentServiceV1"] = None) -> None: + def save(self, service: Optional[ExperimentServiceV1] = None) -> None: """Save this experiment in the database. Note: Not all experiment properties are saved. - See :meth:`qiskit.providers.experiment.ExperimentServiceV1.create_experiment` + See :meth:`qiskit_experiments.stored_data.ExperimentServiceV1.create_experiment` for fields that are saved. Note: @@ -673,7 +675,7 @@ def save(self, service: Optional["ExperimentServiceV1"] = None) -> None: update_data=update_data, ) - def save_all(self, service: Optional["ExperimentServiceV1"] = None) -> None: + def save_all(self, service: Optional[ExperimentServiceV1] = None) -> None: """Save this experiment and its analysis results and figures in the database. Note: @@ -990,7 +992,7 @@ def notes(self, new_notes: str) -> None: self.save() @property - def service(self) -> Optional["ExperimentServiceV1"]: + def service(self) -> Optional[ExperimentServiceV1]: """Return the database service. Returns: @@ -999,7 +1001,7 @@ def service(self) -> Optional["ExperimentServiceV1"]: return self._service @service.setter - def service(self, service: "ExperimentServiceV1") -> None: + def service(self, service: ExperimentServiceV1) -> None: """Set the service to be used for storing experiment data. Args: From b040f2456980a759d707cf909ded63835856bf41 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Tue, 22 Jun 2021 16:28:02 -0400 Subject: [PATCH 22/70] fix figure thread error --- qiskit_experiments/analysis/curve_analysis.py | 188 +++++++++--------- 1 file changed, 95 insertions(+), 93 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 8e068195c9..c157670d20 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -33,6 +33,7 @@ from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.stored_data import AnalysisResultV1 from qiskit_experiments.stored_data.device_component import Qubit +from qiskit_experiments.matplotlib import requires_matplotlib @dataclasses.dataclass(frozen=True) @@ -303,11 +304,16 @@ def data_processor(data: Dict[str, Any]) -> Tuple[float, float] return_data_points=False, ) + @requires_matplotlib def _create_figures(self, result_data: CurveAnalysisResult) -> List["Figure"]: """Create new figures with the fit result and raw data. Subclass can override this method to create different type of figures. + Note: + The ``requires_matplotlib`` decorator is needed to ensure this method + works with ``StoredData``. + Args: result_data: Result containing fit parameters. @@ -316,111 +322,107 @@ def _create_figures(self, result_data: CurveAnalysisResult) -> List["Figure"]: """ fit_available = all(key in result_data for key in ("popt", "popt_err", "xrange")) - if plotting.HAS_MATPLOTLIB: + axis = self._get_option("axis") + if axis is None: + figure = plotting.pyplot.figure(figsize=(8, 5)) + axis = figure.subplots(nrows=1, ncols=1) + else: + figure = axis.get_figure() - axis = self._get_option("axis") - if axis is None: - figure = plotting.pyplot.figure(figsize=(8, 5)) - axis = figure.subplots(nrows=1, ncols=1) - else: - figure = axis.get_figure() + ymin, ymax = np.inf, -np.inf + for series_def in self.__series__: - ymin, ymax = np.inf, -np.inf - for series_def in self.__series__: + # plot raw data - # plot raw data + curve_data_raw = self._data(series_name=series_def.name, label="raw_data") + ymin = min(ymin, *curve_data_raw.y) + ymax = max(ymax, *curve_data_raw.y) + plotting.plot_scatter( + xdata=curve_data_raw.x, ydata=curve_data_raw.y, ax=axis, zorder=0 + ) - curve_data_raw = self._data(series_name=series_def.name, label="raw_data") - ymin = min(ymin, *curve_data_raw.y) - ymax = max(ymax, *curve_data_raw.y) - plotting.plot_scatter( - xdata=curve_data_raw.x, ydata=curve_data_raw.y, ax=axis, zorder=0 - ) + # plot formatted data - # plot formatted data + curve_data_fit = self._data(series_name=series_def.name, label="fit_ready") + if np.all(np.isnan(curve_data_fit.y_err)): + sigma = None + else: + sigma = np.nan_to_num(curve_data_fit.y_err) + + plotting.plot_errorbar( + xdata=curve_data_fit.x, + ydata=curve_data_fit.y, + sigma=sigma, + ax=axis, + label=series_def.name, + marker=series_def.plot_symbol, + color=series_def.plot_color, + zorder=1, + linestyle="", + ) - curve_data_fit = self._data(series_name=series_def.name, label="fit_ready") - if np.all(np.isnan(curve_data_fit.y_err)): - sigma = None - else: - sigma = np.nan_to_num(curve_data_fit.y_err) + # plot fit curve - plotting.plot_errorbar( - xdata=curve_data_fit.x, - ydata=curve_data_fit.y, - sigma=sigma, + if fit_available: + plotting.plot_curve_fit( + func=series_def.fit_func, + result=result_data, ax=axis, - label=series_def.name, - marker=series_def.plot_symbol, color=series_def.plot_color, - zorder=1, - linestyle="", + zorder=2, ) - # plot fit curve - - if fit_available: - plotting.plot_curve_fit( - func=series_def.fit_func, - result=result_data, - ax=axis, - color=series_def.plot_color, - zorder=2, - ) - - # format axis - - if len(self.__series__) > 1: - axis.legend(loc="center right") - axis.set_xlabel(self._get_option("xlabel"), fontsize=16) - axis.set_ylabel(self._get_option("ylabel"), fontsize=16) - axis.tick_params(labelsize=14) - axis.grid(True) - - # automatic scaling y axis by actual data point. - # note that y axis will be scaled by confidence interval by default. - # sometimes we cannot see any data point if variance of parameters is too large. - - height = ymax - ymin - axis.set_ylim(ymin - 0.1 * height, ymax + 0.1 * height) - - # write analysis report - - fit_reports = self._get_option("fit_reports") - if fit_reports and fit_available: - # write fit status in the plot - analysis_description = "" - for par_name, label in fit_reports.items(): - try: - # fit value - pval = get_opt_value(result_data, par_name) - perr = get_opt_error(result_data, par_name) - except ValueError: - # maybe post processed value - pval = result_data[par_name] - perr = result_data[f"{par_name}_err"] - analysis_description += f"{label} = {pval: .3e}\u00B1{perr: .3e}\n" - chisq = result_data["reduced_chisq"] - analysis_description += f"Fit \u03C7-squared = {chisq: .4f}" - - report_handler = axis.text( - 0.60, - 0.95, - analysis_description, - ha="center", - va="top", - size=14, - transform=axis.transAxes, - ) + # format axis + + if len(self.__series__) > 1: + axis.legend(loc="center right") + axis.set_xlabel(self._get_option("xlabel"), fontsize=16) + axis.set_ylabel(self._get_option("ylabel"), fontsize=16) + axis.tick_params(labelsize=14) + axis.grid(True) + + # automatic scaling y axis by actual data point. + # note that y axis will be scaled by confidence interval by default. + # sometimes we cannot see any data point if variance of parameters is too large. + + height = ymax - ymin + axis.set_ylim(ymin - 0.1 * height, ymax + 0.1 * height) + + # write analysis report + + fit_reports = self._get_option("fit_reports") + if fit_reports and fit_available: + # write fit status in the plot + analysis_description = "" + for par_name, label in fit_reports.items(): + try: + # fit value + pval = get_opt_value(result_data, par_name) + perr = get_opt_error(result_data, par_name) + except ValueError: + # maybe post processed value + pval = result_data[par_name] + perr = result_data[f"{par_name}_err"] + analysis_description += f"{label} = {pval: .3e}\u00B1{perr: .3e}\n" + chisq = result_data["reduced_chisq"] + analysis_description += f"Fit \u03C7-squared = {chisq: .4f}" + + report_handler = axis.text( + 0.60, + 0.95, + analysis_description, + ha="center", + va="top", + size=14, + transform=axis.transAxes, + ) - bbox_props = dict( - boxstyle="square, pad=0.3", fc="white", ec="black", lw=1, alpha=0.8 - ) - report_handler.set_bbox(bbox_props) + bbox_props = dict( + boxstyle="square, pad=0.3", fc="white", ec="black", lw=1, alpha=0.8 + ) + report_handler.set_bbox(bbox_props) - return [figure] - else: - return list() + return [figure] def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any]]]: """An analysis subroutine that is called to set fitter options. @@ -885,7 +887,7 @@ def _run_analysis( # # 5. Create figures # - if self._get_option("plot"): + if self._get_option("plot") and plotting.HAS_MATPLOTLIB: figures.extend(self._create_figures(result_data=result_data)) # From 12aa89d5c6c5668a5da8de9340dff47681538556 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Tue, 22 Jun 2021 21:01:06 -0400 Subject: [PATCH 23/70] run black --- qiskit_experiments/analysis/curve_analysis.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index c157670d20..812e0c213b 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -337,9 +337,7 @@ def _create_figures(self, result_data: CurveAnalysisResult) -> List["Figure"]: curve_data_raw = self._data(series_name=series_def.name, label="raw_data") ymin = min(ymin, *curve_data_raw.y) ymax = max(ymax, *curve_data_raw.y) - plotting.plot_scatter( - xdata=curve_data_raw.x, ydata=curve_data_raw.y, ax=axis, zorder=0 - ) + plotting.plot_scatter(xdata=curve_data_raw.x, ydata=curve_data_raw.y, ax=axis, zorder=0) # plot formatted data @@ -417,9 +415,7 @@ def _create_figures(self, result_data: CurveAnalysisResult) -> List["Figure"]: transform=axis.transAxes, ) - bbox_props = dict( - boxstyle="square, pad=0.3", fc="white", ec="black", lw=1, alpha=0.8 - ) + bbox_props = dict(boxstyle="square, pad=0.3", fc="white", ec="black", lw=1, alpha=0.8) report_handler.set_bbox(bbox_props) return [figure] From bac561d18460cb4ae76596bde49636c76d5b5868 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Tue, 22 Jun 2021 21:56:31 -0400 Subject: [PATCH 24/70] remove ResultDict --- qiskit_experiments/__init__.py | 3 +- qiskit_experiments/analysis/curve_analysis.py | 9 +++-- qiskit_experiments/analysis/curve_fitting.py | 23 +++++++------ qiskit_experiments/analysis/plotting.py | 7 ++-- qiskit_experiments/analysis/utils.py | 11 +++--- .../characterization/qubit_spectroscopy.py | 3 +- .../characterization/t1_experiment.py | 31 +++++++++-------- .../characterization/t2star_experiment.py | 34 ++++++++++--------- qiskit_experiments/experiment_data.py | 15 -------- .../interleaved_rb_analysis.py | 3 +- .../randomized_benchmarking/rb_analysis.py | 3 +- 11 files changed, 62 insertions(+), 80 deletions(-) diff --git a/qiskit_experiments/__init__.py b/qiskit_experiments/__init__.py index dfe5f2d237..25b2b57b7d 100644 --- a/qiskit_experiments/__init__.py +++ b/qiskit_experiments/__init__.py @@ -30,7 +30,6 @@ :toctree: ../stubs/ ExperimentData - ResultDict Experiment Base Classes @@ -49,7 +48,7 @@ from .version import __version__ # Base Classes -from .experiment_data import ExperimentData, ResultDict +from .experiment_data import ExperimentData from .base_analysis import BaseAnalysis from .base_experiment import BaseExperiment diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 812e0c213b..43c93ea656 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -305,7 +305,7 @@ def data_processor(data: Dict[str, Any]) -> Tuple[float, float] ) @requires_matplotlib - def _create_figures(self, result_data: CurveAnalysisResult) -> List["Figure"]: + def _create_figures(self, result_data: Dict) -> List["Figure"]: """Create new figures with the fit result and raw data. Subclass can override this method to create different type of figures. @@ -505,7 +505,7 @@ def _format_data(self, data: CurveData) -> CurveData: metadata=data.metadata, ) - def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: Dict) -> Dict: """Calculate new quantity from the fit result. Subclasses can override this method to do post analysis. @@ -777,8 +777,7 @@ def _run_analysis( Raises: AnalysisError: if the analysis fails. """ - result_data = CurveAnalysisResult() - result_data["analysis_type"] = self.__class__.__name__ + result_data = {"analysis_type": self.__class__.__name__} figures = list() # pop arguments that are not given to fitter @@ -900,7 +899,7 @@ def _run_analysis( } result_data["raw_data"] = raw_data_dict - analysis_result = AnalysisResultV1( + analysis_result = CurveAnalysisResult( result_data=result_data, result_type=result_data["analysis_type"], device_components=[Qubit(qubit) for qubit in self.__qubits] if self.__qubits else [], diff --git a/qiskit_experiments/analysis/curve_fitting.py b/qiskit_experiments/analysis/curve_fitting.py index baf8b05497..fcde1a33f5 100644 --- a/qiskit_experiments/analysis/curve_fitting.py +++ b/qiskit_experiments/analysis/curve_fitting.py @@ -19,11 +19,11 @@ import numpy as np import scipy.optimize as opt from qiskit_experiments.exceptions import AnalysisError -from qiskit_experiments.experiment_data import ResultDict from qiskit_experiments.analysis.data_processing import filter_data +from qiskit_experiments.stored_data import AnalysisResultV1 -class CurveAnalysisResult(ResultDict): +class CurveAnalysisResult(AnalysisResultV1): """Analysis data container for curve fit analysis. Class Attributes: @@ -59,14 +59,15 @@ class CurveAnalysisResult(ResultDict): def __str__(self): out = "" - if self.get("success"): - popt_keys = self.get("popt_keys") - popt = self.get("popt") - popt_err = self.get("popt_err") + result_data = self.data() + if result_data.get("success"): + popt_keys = result_data.get("popt_keys") + popt = result_data.get("popt") + popt_err = result_data.get("popt_err") for key, value, error in zip(popt_keys, popt, popt_err): - out += f"\n- {key}: {value} \u00B1 {error}" - out += super().__str__() + out += f"\n - {key}: {value} \u00B1 {error}" + out = super().__str__() + out return out @@ -79,7 +80,7 @@ def curve_fit( sigma: Optional[np.ndarray] = None, bounds: Optional[Union[Dict[str, Tuple[float, float]], Tuple[np.ndarray, np.ndarray]]] = None, **kwargs, -) -> CurveAnalysisResult: +) -> Dict: r"""Perform a non-linear least squares to fit This solves the optimization problem @@ -200,7 +201,7 @@ def fit_func(x, *params): "xrange": xdata_range, } - return CurveAnalysisResult(result) + return result def multi_curve_fit( @@ -213,7 +214,7 @@ def multi_curve_fit( weights: Optional[np.ndarray] = None, bounds: Optional[Union[Dict[str, Tuple[float, float]], Tuple[np.ndarray, np.ndarray]]] = None, **kwargs, -) -> CurveAnalysisResult: +) -> Dict: r"""Perform a linearized multi-objective non-linear least squares fit. This solves the optimization problem diff --git a/qiskit_experiments/analysis/plotting.py b/qiskit_experiments/analysis/plotting.py index e8769202b7..f72876b2a3 100644 --- a/qiskit_experiments/analysis/plotting.py +++ b/qiskit_experiments/analysis/plotting.py @@ -12,10 +12,9 @@ """ Plotting functions for experiment analysis """ -from typing import Callable, Optional +from typing import Callable, Optional, Dict import numpy as np -from qiskit_experiments.experiment_data import ResultDict from qiskit_experiments.matplotlib import pyplot, requires_matplotlib # pylint: disable = unused-import @@ -25,7 +24,7 @@ @requires_matplotlib def plot_curve_fit( func: Callable, - result: ResultDict, + result: Dict, confidence_interval: bool = True, ax=None, num_fit_points: int = 100, @@ -39,7 +38,7 @@ def plot_curve_fit( Args: func: the fit function for curve_fit. - result: a result dictiionary from curve_fit. + result: a result dictionary from curve_fit. confidence_interval: if True plot the confidence interval from popt_err. ax (matplotlib.axes.Axes): Optional, a matplotlib axes to add the plot to. num_fit_points: the number of points to plot for xrange. diff --git a/qiskit_experiments/analysis/utils.py b/qiskit_experiments/analysis/utils.py index dbc9adfaf5..16d2d8d304 100644 --- a/qiskit_experiments/analysis/utils.py +++ b/qiskit_experiments/analysis/utils.py @@ -12,15 +12,14 @@ """Analysis utility functions.""" +from typing import Dict -from qiskit_experiments.experiment_data import ResultDict - -def get_opt_value(result_data: ResultDict, param_name: str) -> float: +def get_opt_value(result_data: Dict, param_name: str) -> float: """A helper function to get parameter value from a result dictionary. Args: - result_data: Result object. + result_data: Result data. param_name: Name of parameter to extract. Returns: @@ -44,11 +43,11 @@ def get_opt_value(result_data: ResultDict, param_name: str) -> float: raise ValueError(f"Parameter {param_name} is not defined.") from ex -def get_opt_error(result_data: ResultDict, param_name: str) -> float: +def get_opt_error(result_data: Dict, param_name: str) -> float: """A helper function to get error value from analysis result. Args: - result_data: Result object. + result_data: Result data. param_name: Name of parameter to extract. Returns: diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 641e7290da..7db5095c80 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -25,7 +25,6 @@ from qiskit_experiments.analysis import ( CurveAnalysis, - CurveAnalysisResult, SeriesDef, fit_function, get_opt_value, @@ -131,7 +130,7 @@ def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any] return fit_option - def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: Dict) -> Dict: """Algorithmic criteria for whether the fit is good or bad. A good fit has: diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index 26b2077624..fa9e757b77 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -23,10 +23,13 @@ from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.base_analysis import BaseAnalysis -from qiskit_experiments.analysis.curve_fitting import process_curve_data, curve_fit +from qiskit_experiments.analysis.curve_fitting import ( + process_curve_data, + curve_fit, + CurveAnalysisResult, +) from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting -from qiskit_experiments.experiment_data import ResultDict from qiskit_experiments.stored_data import AnalysisResultV1 from qiskit_experiments.stored_data.device_component import Qubit @@ -129,18 +132,16 @@ def fit_fun(x, a, tau, c): bounds = {"a": amplitude_bounds, "tau": t1_bounds, "c": offset_bounds} fit_result = curve_fit(fit_fun, xdata, ydata, init, sigma=sigma, bounds=bounds) - result_data = ResultDict( - { - "value": fit_result["popt"][1], - "stderr": fit_result["popt_err"][1], - "unit": "s", - "label": "T1", - "fit": fit_result, - "quality": self._fit_quality( - fit_result["popt"], fit_result["popt_err"], fit_result["reduced_chisq"] - ), - } - ) + result_data = { + "value": fit_result["popt"][1], + "stderr": fit_result["popt_err"][1], + "unit": "s", + "label": "T1", + "fit": fit_result, + "quality": self._fit_quality( + fit_result["popt"], fit_result["popt_err"], fit_result["reduced_chisq"] + ), + } result_data["fit"]["circuit_unit"] = unit if unit == "dt": @@ -155,7 +156,7 @@ def fit_fun(x, a, tau, c): else: figures = None - res_v1 = AnalysisResultV1( + res_v1 = CurveAnalysisResult( result_data=result_data, result_type="T1", device_components=[Qubit(data[0]["metadata"]["qubit"])], diff --git a/qiskit_experiments/characterization/t2star_experiment.py b/qiskit_experiments/characterization/t2star_experiment.py index 3938fba19d..d0376f6ac8 100644 --- a/qiskit_experiments/characterization/t2star_experiment.py +++ b/qiskit_experiments/characterization/t2star_experiment.py @@ -23,10 +23,14 @@ from qiskit.providers.options import Options from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.base_analysis import BaseAnalysis -from qiskit_experiments.analysis.curve_fitting import curve_fit, process_curve_data +from qiskit_experiments.analysis.curve_fitting import ( + curve_fit, + process_curve_data, + CurveAnalysisResult, +) from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting -from qiskit_experiments.experiment_data import ExperimentData, ResultDict +from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.stored_data import AnalysisResultV1 from qiskit_experiments.stored_data.device_component import Qubit @@ -114,25 +118,23 @@ def _format_plot(ax, unit): figures = None # Output unit is 'sec', regardless of the unit used in the input - result_data = ResultDict( - { - "t2star_value": fit_result["popt"][1], - "frequency_value": fit_result["popt"][2], - "stderr": fit_result["popt_err"][1], - "unit": "s", - "label": "T2*", - "fit": fit_result, - "quality": self._fit_quality( - fit_result["popt"], fit_result["popt_err"], fit_result["reduced_chisq"] - ), - } - ) + result_data = { + "t2star_value": fit_result["popt"][1], + "frequency_value": fit_result["popt"][2], + "stderr": fit_result["popt_err"][1], + "unit": "s", + "label": "T2*", + "fit": fit_result, + "quality": self._fit_quality( + fit_result["popt"], fit_result["popt_err"], fit_result["reduced_chisq"] + ), + } result_data["fit"]["circuit_unit"] = unit if unit == "dt": result_data["fit"]["dt"] = conversion_factor - analysis_result = AnalysisResultV1( + analysis_result = CurveAnalysisResult( result_data=result_data, result_type="T2Star", device_components=[Qubit(circ_metadata["qubit"])], diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index 03e41f3cae..5ff5cf7d72 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -20,20 +20,6 @@ LOG = logging.getLogger(__name__) -class ResultDict(dict): - """Placeholder class""" - - __keys_not_shown__ = tuple() - - def __str__(self): - out = "" - for key, value in self.items(): - if key in self.__keys_not_shown__: - continue - out += f"\n- {key}: {value}" - return out - - class ExperimentData(StoredDataV1): """Qiskit Experiments Data container class""" @@ -47,7 +33,6 @@ def __init__( Args: experiment (BaseExperiment): Optional, experiment object that generated the data. backend (Backend): Optional, Backend the experiment runs on. - job_ids (list[str]): Optional, IDs of jobs submitted for the experiment. Raises: ExperimentError: If an input argument is invalid. diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index 122c1570f0..0fe7a590aa 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -17,7 +17,6 @@ import numpy as np from qiskit_experiments.analysis import ( - CurveAnalysisResult, SeriesDef, fit_function, get_opt_value, @@ -165,7 +164,7 @@ def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any] return fit_option - def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: Dict) -> Dict: """Calculate EPC.""" # Add EPC data nrb = 2 ** self._num_qubits diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index 53ae415058..0c6843685c 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -19,7 +19,6 @@ from qiskit_experiments.analysis import ( CurveAnalysis, - CurveAnalysisResult, SeriesDef, CurveData, fit_function, @@ -148,7 +147,7 @@ def _format_data(self, data: CurveData) -> CurveData: data_index=mean_data_index, ) - def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: Dict) -> Dict: """Calculate EPC.""" alpha = get_opt_value(result_data, "alpha") alpha_err = get_opt_error(result_data, "alpha") From 1fd9dd55373fadb8220e10d63df2abd8fce8d699 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Wed, 23 Jun 2021 11:19:45 -0400 Subject: [PATCH 25/70] remove experiment_class and result_class --- .../stored_data/experiment_service.py | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/qiskit_experiments/stored_data/experiment_service.py b/qiskit_experiments/stored_data/experiment_service.py index 1c7267be41..ab2c0935f8 100644 --- a/qiskit_experiments/stored_data/experiment_service.py +++ b/qiskit_experiments/stored_data/experiment_service.py @@ -13,12 +13,11 @@ """Experiment service abstract interface.""" from abc import ABC, abstractmethod -from typing import Optional, Dict, List, Any, Union, Tuple, Type, TypeVar +from typing import Optional, Dict, List, Any, Union, Tuple +from types import SimpleNamespace from .device_component import DeviceComponent -ExperimentClass = TypeVar("ExperimentClass") - class ExperimentService: """Base common type for all versioned ExperimentService abstract classes. @@ -33,9 +32,12 @@ class ExperimentService: class ExperimentServiceV1(ExperimentService, ABC): - """Class to provide experiment service. + """Interface for providing experiment service. + + This class defines the interface ``qiskit_experiments`` expects from an + experiment service. - The experiment service allows you to store experiment data and metadata + An experiment service allows you to store experiment data and metadata in a database. An experiment can have one or more jobs, analysis results, and figures. @@ -119,15 +121,12 @@ def update_experiment( @abstractmethod def experiment( - self, experiment_id: str, experiment_class: Optional[Type[ExperimentClass]] = None - ) -> Union[Dict, ExperimentClass]: + self, experiment_id: str + ) -> SimpleNamespace: """Retrieve a previously stored experiment. Args: experiment_id: Experiment ID. - experiment_class: Class used to instantiate the returned data object. - If a class is provided, its ``from_data()`` method is called - with the retrieved data, and its return value is returned. Returns: A dictionary containing the retrieved experiment data if `experiment_class` @@ -142,21 +141,17 @@ def experiment( def experiments( self, limit: Optional[int] = 10, - experiment_class: Optional[Type[ExperimentClass]] = None, device_components: Optional[Union[str, DeviceComponent]] = None, experiment_type: Optional[str] = None, backend_name: Optional[str] = None, tags: Optional[List[str]] = None, tags_operator: Optional[str] = "OR", **filters: Any, - ) -> List[Union[Dict, ExperimentClass]]: + ) -> List[SimpleNamespace]: """Retrieve all experiment data, with optional filtering. Args: limit: Number of experiments to retrieve. ``None`` means no limit. - experiment_class: Class used to instantiate the returned data object. - If a class is provided, its ``from_data()`` method is called - with the retrieved data, and its return value is returned. device_components: Filter by device components. An experiment must have analysis results with device components matching the given list exactly to be included. experiment_type: Experiment type used for filtering. @@ -251,15 +246,12 @@ def update_analysis_result( @abstractmethod def analysis_result( - self, result_id: str, result_class: Optional[Type[ExperimentClass]] = None - ) -> Union[Dict, ExperimentClass]: + self, result_id: str + ) -> SimpleNamespace: """Retrieve a previously stored experiment. Args: result_id: Analysis result ID. - result_class: Class used to instantiate the returned data object. - If a class is provided, its ``from_data()`` method is called - with the retrieved data, and its return value is returned. Returns: Retrieved analysis result. @@ -273,7 +265,6 @@ def analysis_result( def analysis_results( self, limit: Optional[int] = 10, - result_class: Optional[Type[ExperimentClass]] = None, device_components: Optional[Union[str, DeviceComponent]] = None, experiment_id: Optional[str] = None, result_type: Optional[str] = None, @@ -283,14 +274,11 @@ def analysis_results( tags: Optional[List[str]] = None, tags_operator: Optional[str] = "OR", **filters: Any, - ) -> List[Union[Dict, ExperimentClass]]: + ) -> List[SimpleNamespace]: """Retrieve all analysis results, with optional filtering. Args: limit: Number of analysis results to retrieve. ``None`` means no limit. - result_class: Class used to instantiate the returned data object. - If a class is provided, its ``from_data()`` method is called - with the retrieved data, and its return value is returned. device_components: Target device components, such as qubits. experiment_id: Experiment ID used for filtering. result_type: Analysis result type used for filtering. From 404d7be03c774096438b7278a332b8c82aa6e32c Mon Sep 17 00:00:00 2001 From: jessieyu Date: Wed, 23 Jun 2021 16:52:17 -0400 Subject: [PATCH 26/70] fix lint --- qiskit_experiments/stored_data/experiment_service.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/stored_data/experiment_service.py b/qiskit_experiments/stored_data/experiment_service.py index ab2c0935f8..42357c2f79 100644 --- a/qiskit_experiments/stored_data/experiment_service.py +++ b/qiskit_experiments/stored_data/experiment_service.py @@ -120,9 +120,7 @@ def update_experiment( pass @abstractmethod - def experiment( - self, experiment_id: str - ) -> SimpleNamespace: + def experiment(self, experiment_id: str) -> SimpleNamespace: """Retrieve a previously stored experiment. Args: @@ -245,9 +243,7 @@ def update_analysis_result( pass @abstractmethod - def analysis_result( - self, result_id: str - ) -> SimpleNamespace: + def analysis_result(self, result_id: str) -> SimpleNamespace: """Retrieve a previously stored experiment. Args: From 3c0fa2662080776696c5c244d75d2ae95bee2622 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 24 Jun 2021 11:43:42 +0300 Subject: [PATCH 27/70] fixes --- qiskit_experiments/experiment_data.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index 01547cb017..19daf5c9fd 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -57,3 +57,11 @@ def experiment(self): BaseExperiment: the experiment object. """ return self._experiment + + @property + def completion_times(self) -> Dict[str, datetime]: + """Returns the completion times of the jobs.""" + job_times = {} + for job_id, job in self._jobs.items(): + if job is not None and "COMPLETED" in job.time_per_step(): + job_times[job_id] = job.time_per_step().get("COMPLETED") From 7bc4b2a2f2a169940683ffa4278c2d5d0e2aebe2 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 24 Jun 2021 13:16:26 +0300 Subject: [PATCH 28/70] adjusted tes_update_library --- qiskit_experiments/base_analysis.py | 1 + qiskit_experiments/calibration/update_library.py | 4 ++-- qiskit_experiments/experiment_data.py | 2 ++ test/calibration/test_update_library.py | 13 ++++++++----- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 268561e7b2..01ffbaa17a 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -97,6 +97,7 @@ def run( if return_figures: return analysis_results, figures + return analysis_results @abstractmethod diff --git a/qiskit_experiments/calibration/update_library.py b/qiskit_experiments/calibration/update_library.py index aeb45df7c5..cc9bc3d447 100644 --- a/qiskit_experiments/calibration/update_library.py +++ b/qiskit_experiments/calibration/update_library.py @@ -121,7 +121,7 @@ def update( from qiskit_experiments.characterization.qubit_spectroscopy import SpectroscopyAnalysis - result = exp_data.analysis_result(result_index) + result = exp_data.analysis_result(result_index).data() if "freq" not in result["popt_keys"]: raise CalibrationError( @@ -170,7 +170,7 @@ def update( angles_schedules = [(np.pi, "amp", "xp")] if isinstance(exp_data.experiment, Rabi): - result = exp_data.analysis_result(result_index) + result = exp_data.analysis_result(result_index).data() freq = result["popt"][result["popt_keys"].index("freq")] rate = 2 * np.pi * freq diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index 19daf5c9fd..4e2ce91e0f 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -65,3 +65,5 @@ def completion_times(self) -> Dict[str, datetime]: for job_id, job in self._jobs.items(): if job is not None and "COMPLETED" in job.time_per_step(): job_times[job_id] = job.time_per_step().get("COMPLETED") + + return job_times diff --git a/test/calibration/test_update_library.py b/test/calibration/test_update_library.py index 40c3efde07..af4d3d070f 100644 --- a/test/calibration/test_update_library.py +++ b/test/calibration/test_update_library.py @@ -54,6 +54,7 @@ def test_amplitude(self): rabi = Rabi(3) rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) exp_data = rabi.run(RabiBackend()) + exp_data.block_for_results() for qubit in [0, 3]: with self.assertRaises(CalibrationError): @@ -71,7 +72,7 @@ def test_amplitude(self): self.assertEqual(len(cals.parameters_table()), 2) # Now check the corresponding schedules - result = exp_data.analysis_result(-1) + result = exp_data.analysis_result(-1).data() rate = 2 * np.pi * result["popt"][1] amp = np.round(np.pi / rate, decimals=8) with pulse.build(name="xp") as expected: @@ -93,16 +94,18 @@ def test_frequency(self): spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) exp_data = spec.run(backend) + exp_data.block_for_results() result = exp_data.analysis_result(0) + result_data = result.data() - value = get_opt_value(result, "freq") + value = get_opt_value(result_data, "freq") self.assertTrue(value < 5.1e6) self.assertTrue(value > 4.9e6) - self.assertEqual(result["quality"], "computer_good") + self.assertEqual(result.quality, "computer_good") # Test the integration with the BackendCalibrations cals = BackendCalibrations(FakeAthens()) - self.assertNotEqual(cals.get_qubit_frequencies()[3], result["popt"][2]) + self.assertNotEqual(cals.get_qubit_frequencies()[3], result_data["popt"][2]) Frequency.update(cals, exp_data) - self.assertEqual(cals.get_qubit_frequencies()[3], result["popt"][2]) + self.assertEqual(cals.get_qubit_frequencies()[3], result_data["popt"][2]) From 34def38ad080a809f7904cc7b7a3a321f419bd85 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 24 Jun 2021 13:25:31 +0300 Subject: [PATCH 29/70] lint --- qiskit_experiments/experiment_data.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index 4e2ce91e0f..93f20f9501 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -13,10 +13,7 @@ Experiment Data class """ import logging -from typing import Optional, Union, List, Dict, Tuple -import os -import uuid -from collections import OrderedDict +from typing import Dict from datetime import datetime from qiskit_experiments.stored_data import StoredDataV1 From 84d339f79ba01f10a3b302d5ef351ddb32ba47f8 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Sun, 27 Jun 2021 12:18:42 +0300 Subject: [PATCH 30/70] black --- .../characterization/t1_analysis.py | 24 +++++++++++-------- test/calibration/test_update_library.py | 1 - test/test_t1.py | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/qiskit_experiments/characterization/t1_analysis.py b/qiskit_experiments/characterization/t1_analysis.py index 6b10c7e201..1b5814169f 100644 --- a/qiskit_experiments/characterization/t1_analysis.py +++ b/qiskit_experiments/characterization/t1_analysis.py @@ -22,7 +22,11 @@ from qiskit_experiments.stored_data import AnalysisResultV1 from qiskit_experiments.stored_data.device_component import Qubit from qiskit_experiments.base_analysis import BaseAnalysis -from qiskit_experiments.analysis.curve_fitting import process_curve_data, curve_fit, CurveAnalysisResult +from qiskit_experiments.analysis.curve_fitting import ( + process_curve_data, + curve_fit, + CurveAnalysisResult, +) from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting @@ -126,15 +130,15 @@ def fit_fun(x, a, tau, c): fit_result = curve_fit(fit_fun, xdata, ydata, init, sigma=sigma, bounds=bounds) result_data = { - "value": fit_result["popt"][1], - "stderr": fit_result["popt_err"][1], - "unit": "s", - "label": "T1", - "fit": fit_result, - "quality": self._fit_quality( - fit_result["popt"], fit_result["popt_err"], fit_result["reduced_chisq"] - ), - } + "value": fit_result["popt"][1], + "stderr": fit_result["popt_err"][1], + "unit": "s", + "label": "T1", + "fit": fit_result, + "quality": self._fit_quality( + fit_result["popt"], fit_result["popt_err"], fit_result["reduced_chisq"] + ), + } result_data["fit"]["circuit_unit"] = unit if unit == "dt": diff --git a/test/calibration/test_update_library.py b/test/calibration/test_update_library.py index af086e4233..fc88f018fb 100644 --- a/test/calibration/test_update_library.py +++ b/test/calibration/test_update_library.py @@ -113,4 +113,3 @@ def test_frequency(self): self.assertNotEqual(cals.get_qubit_frequencies()[qubit], result_data["popt"][2]) Frequency.update(cals, exp_data) self.assertEqual(cals.get_qubit_frequencies()[qubit], result_data["popt"][2]) - diff --git a/test/test_t1.py b/test/test_t1.py index f497bb0355..a92c0d98b6 100644 --- a/test/test_t1.py +++ b/test/test_t1.py @@ -223,7 +223,7 @@ def test_t1_analysis(self): } ) - res = T1Analysis()._run_analysis(data)[0] + res = T1Analysis()._run_analysis(data)[0][0] self.assertEqual(res.quality, "good") self.assertAlmostEqual(res.data()["value"], 25e-9, delta=3) @@ -270,5 +270,5 @@ def test_t1_low_quality(self): } ) - res = T1Analysis()._run_analysis(data)[0] + res = T1Analysis()._run_analysis(data)[0][0] self.assertEqual(res.quality, "bad") From f09667cf23de9c19699fb86201fc2130aa752c65 Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Mon, 28 Jun 2021 16:37:44 -0400 Subject: [PATCH 31/70] doc update Co-authored-by: Naoki Kanazawa --- qiskit_experiments/stored_data/analysis_result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/stored_data/analysis_result.py b/qiskit_experiments/stored_data/analysis_result.py index c27aafe5c6..ca0ad12427 100644 --- a/qiskit_experiments/stored_data/analysis_result.py +++ b/qiskit_experiments/stored_data/analysis_result.py @@ -191,7 +191,7 @@ def save(self, service: Optional["ExperimentServiceV1"] = None) -> None: service = service or self._service if not service: LOG.warning( - "Analysis result cannot be saved because no " "experiment service is available." + "Analysis result cannot be saved because no experiment service is available." ) return From c98e2c678793eca641c2a08ca74d354a5dc3ef1d Mon Sep 17 00:00:00 2001 From: jessieyu Date: Mon, 28 Jun 2021 17:02:56 -0400 Subject: [PATCH 32/70] rename classes --- .../database_service/__init__.py | 53 +++++++ .../database_service.py} | 44 +++--- .../db_analysis_result.py} | 23 +-- .../db_experiment_data.py} | 70 +++++---- .../device_component.py | 0 .../exceptions.py | 10 +- .../{stored_data => database_service}/json.py | 0 .../utils.py | 11 +- qiskit_experiments/stored_data/__init__.py | 53 ------- .../__init__.py | 2 +- .../test_db_analysis_result.py} | 35 +++-- .../test_db_experiment_data.py} | 136 +++++++++--------- 12 files changed, 217 insertions(+), 220 deletions(-) create mode 100644 qiskit_experiments/database_service/__init__.py rename qiskit_experiments/{stored_data/experiment_service.py => database_service/database_service.py} (90%) rename qiskit_experiments/{stored_data/analysis_result.py => database_service/db_analysis_result.py} (94%) rename qiskit_experiments/{stored_data/stored_data.py => database_service/db_experiment_data.py} (93%) rename qiskit_experiments/{stored_data => database_service}/device_component.py (100%) rename qiskit_experiments/{stored_data => database_service}/exceptions.py (71%) rename qiskit_experiments/{stored_data => database_service}/json.py (100%) rename qiskit_experiments/{stored_data => database_service}/utils.py (93%) delete mode 100644 qiskit_experiments/stored_data/__init__.py rename test/{stored_data => database_service}/__init__.py (91%) rename test/{stored_data/test_analysisresult.py => database_service/test_db_analysis_result.py} (82%) rename test/{stored_data/test_stored_data.py => database_service/test_db_experiment_data.py} (83%) diff --git a/qiskit_experiments/database_service/__init__.py b/qiskit_experiments/database_service/__init__.py new file mode 100644 index 0000000000..eb5d298962 --- /dev/null +++ b/qiskit_experiments/database_service/__init__.py @@ -0,0 +1,53 @@ +# 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. + +""" +============================================================= +Database Service (:mod:`qiskit_experiments.database_service`) +============================================================= + +.. currentmodule:: qiskit_experiments.database_service + +This subpackage contains classes used to define the data structure of +an experiment, including its data, metadata, analysis results, and figures, as +well as the interface to an experiment database service. An experiment database +service allows one to store, retrieve, and query experiment related data. + +Classes +======= + +.. autosummary:: + :toctree: ../stubs/ + + DbExperimentData + DbExperimentDataV1 + DbAnalysisResult + DbAnalysisResultV1 + DatabaseService + DatabaseServiceV1 + + +Exceptions +========== + +.. autosummary:: + :toctree: ../stubs/ + + DbExperimentDataError + DbExperimentEntryExists + DbExperimentEntryNotFound +""" + +from .db_experiment_data import DbExperimentData, DbExperimentDataV1 +from .db_analysis_result import DbAnalysisResult, DbAnalysisResultV1 +from .database_service import DatabaseService, DatabaseServiceV1 +from .exceptions import DbExperimentDataError, DbExperimentEntryExists, DbExperimentEntryNotFound diff --git a/qiskit_experiments/stored_data/experiment_service.py b/qiskit_experiments/database_service/database_service.py similarity index 90% rename from qiskit_experiments/stored_data/experiment_service.py rename to qiskit_experiments/database_service/database_service.py index 42357c2f79..f9680d725b 100644 --- a/qiskit_experiments/stored_data/experiment_service.py +++ b/qiskit_experiments/database_service/database_service.py @@ -10,17 +10,18 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Experiment service abstract interface.""" +"""Experiment database service abstract interface.""" from abc import ABC, abstractmethod from typing import Optional, Dict, List, Any, Union, Tuple -from types import SimpleNamespace + +from qiskit.providers import Options from .device_component import DeviceComponent -class ExperimentService: - """Base common type for all versioned ExperimentService abstract classes. +class DatabaseService: + """Base common type for all versioned DatabaseService abstract classes. Note this class should not be inherited from directly, it is intended to be used for type checking. When implementing a subclass you should use @@ -31,13 +32,13 @@ class ExperimentService: version = 0 -class ExperimentServiceV1(ExperimentService, ABC): - """Interface for providing experiment service. +class DatabaseServiceV1(DatabaseService, ABC): + """Interface for providing experiment database service. This class defines the interface ``qiskit_experiments`` expects from an - experiment service. + experiment database service. - An experiment service allows you to store experiment data and metadata + An experiment database service allows you to store experiment data and metadata in a database. An experiment can have one or more jobs, analysis results, and figures. @@ -48,16 +49,16 @@ class ExperimentServiceV1(ExperimentService, ABC): version = 1 def __init__(self): - """Initialize an ExperimentService instance.""" + """Initialize an DatabaseService instance.""" self._options = self._default_options() @classmethod @abstractmethod - def _default_options(cls) -> Dict: + def _default_options(cls) -> Options: """Return the default options Returns: - A dictionary of default options. + Default options. """ pass @@ -120,15 +121,14 @@ def update_experiment( pass @abstractmethod - def experiment(self, experiment_id: str) -> SimpleNamespace: + def experiment(self, experiment_id: str) -> Dict: """Retrieve a previously stored experiment. Args: experiment_id: Experiment ID. Returns: - A dictionary containing the retrieved experiment data if `experiment_class` - is ``None``. Otherwise an instance of the `experiment_class` class. + A dictionary containing the retrieved experiment data. Raises: ExperimentEntryNotFound: If the experiment does not exist. @@ -145,11 +145,11 @@ def experiments( tags: Optional[List[str]] = None, tags_operator: Optional[str] = "OR", **filters: Any, - ) -> List[SimpleNamespace]: + ) -> List[Dict]: """Retrieve all experiment data, with optional filtering. Args: - limit: Number of experiments to retrieve. ``None`` means no limit. + limit: Number of experiment data entries to retrieve. ``None`` means no limit. device_components: Filter by device components. An experiment must have analysis results with device components matching the given list exactly to be included. experiment_type: Experiment type used for filtering. @@ -167,9 +167,7 @@ def experiments( **filters: Additional filtering keywords supported by the service provider. Returns: - A list of experiments. Each experiment is either a dictionary containing the - retrieved experiment data, if `experiment_class` - is ``None``, or an instance of the `experiment_class` class. + A list of experiments. """ pass @@ -243,7 +241,7 @@ def update_analysis_result( pass @abstractmethod - def analysis_result(self, result_id: str) -> SimpleNamespace: + def analysis_result(self, result_id: str) -> Dict: """Retrieve a previously stored experiment. Args: @@ -270,7 +268,7 @@ def analysis_results( tags: Optional[List[str]] = None, tags_operator: Optional[str] = "OR", **filters: Any, - ) -> List[SimpleNamespace]: + ) -> List[Dict]: """Retrieve all analysis results, with optional filtering. Args: @@ -295,9 +293,7 @@ def analysis_results( **filters: Additional filtering keywords supported by the service provider. Returns: - A list of analysis results. Each analysis result is either a dictionary - containing the retrieved analysis result, if `result_class` - is ``None``, or an instance of the `result_class` class. + A list of analysis results. """ pass diff --git a/qiskit_experiments/stored_data/analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py similarity index 94% rename from qiskit_experiments/stored_data/analysis_result.py rename to qiskit_experiments/database_service/db_analysis_result.py index c27aafe5c6..40fbefa088 100644 --- a/qiskit_experiments/stored_data/analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -21,7 +21,7 @@ from .json import NumpyEncoder, NumpyDecoder from .utils import save_data, qiskit_version -from .exceptions import ExperimentError +from .exceptions import DbExperimentDataError from .device_component import DeviceComponent, to_component LOG = logging.getLogger(__name__) @@ -40,7 +40,7 @@ def _wrapped(self, *args, **kwargs): return _wrapped -class AnalysisResult: +class DbAnalysisResult: """Base common type for all versioned AnalysisResult abstract classes. Note this class should not be inherited from directly, it is intended @@ -52,8 +52,11 @@ class AnalysisResult: version = 0 -class AnalysisResultV1(AnalysisResult): - """Class representing an analysis result for an experiment.""" +class DbAnalysisResultV1(DbAnalysisResult): + """Class representing an analysis result for an experiment. + + Analysis results can also be stored in a database. + """ version = 1 _data_version = 1 @@ -73,7 +76,7 @@ def __init__( quality: Optional[str] = None, verified: bool = False, tags: Optional[List[str]] = None, - service: Optional["ExperimentServiceV1"] = None, + service: Optional["DatabaseServiceV1"] = None, **kwargs, ): """AnalysisResult constructor. @@ -155,7 +158,7 @@ def from_data( device_components: List[Union[DeviceComponent, str]], experiment_id: str, **kwargs, - ) -> "AnalysisResultV1": + ) -> "DbAnalysisResultV1": """Reconstruct the analysis result from input data. Args: @@ -178,7 +181,7 @@ def from_data( **kwargs, ) - def save(self, service: Optional["ExperimentServiceV1"] = None) -> None: + def save(self, service: Optional["DatabaseServiceV1"] = None) -> None: """Save this analysis result in the database. Args: @@ -325,7 +328,7 @@ def experiment_id(self) -> str: return self._experiment_id @property - def service(self) -> Optional["ExperimentServiceV1"]: + def service(self) -> Optional["DatabaseServiceV1"]: """Return the database service. Returns: @@ -335,7 +338,7 @@ def service(self) -> Optional["ExperimentServiceV1"]: return self._service @service.setter - def service(self, service: "ExperimentServiceV1") -> None: + def service(self, service: "DatabaseServiceV1") -> None: """Set the service to be used for storing result data in a database. Args: @@ -345,7 +348,7 @@ def service(self, service: "ExperimentServiceV1") -> None: ExperimentError: If an experiment service is already being used. """ if self._service: - raise ExperimentError("An experiment service is already being used.") + raise DbExperimentDataError("An experiment service is already being used.") self._service = service @property diff --git a/qiskit_experiments/stored_data/stored_data.py b/qiskit_experiments/database_service/db_experiment_data.py similarity index 93% rename from qiskit_experiments/stored_data/stored_data.py rename to qiskit_experiments/database_service/db_experiment_data.py index c1728806d7..845be84893 100644 --- a/qiskit_experiments/stored_data/stored_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -30,8 +30,8 @@ from qiskit.providers.exceptions import JobError from qiskit.visualization import HAS_MATPLOTLIB -from .exceptions import ExperimentError, ExperimentEntryNotFound, ExperimentEntryExists -from .analysis_result import AnalysisResultV1 as AnalysisResult +from .exceptions import DbExperimentDataError, DbExperimentEntryNotFound, DbExperimentEntryExists +from .db_analysis_result import DbAnalysisResultV1 as DbAnalysisResult from .json import NumpyEncoder, NumpyDecoder from .utils import ( save_data, @@ -66,8 +66,8 @@ def service_exception_to_warning(): LOG.warning("Experiment service operation failed: %s", traceback.format_exc()) -class StoredData: - """Base common type for all versioned StoredData classes. +class DbExperimentData: + """Base common type for all versioned DbExperimentData classes. Note this class should not be inherited from directly, it is intended to be used for type checking. When implementing a provider you should use @@ -78,13 +78,13 @@ class StoredData: version = 0 -class StoredDataV1(StoredData): - """Class to handle stored data. +class DbExperimentDataV1(DbExperimentData): + """Class to define and handle experiment data stored in a database. - This class serves as a container for data to be stored in a database, which - may include experiment metadata, analysis results, and figures. It also - provides methods used to interact with the database, such as storing into - and retrieving from the database. + This class serves as a container for experiment related data to be stored + in a database, which may include experiment metadata, analysis results, + and figures. It also provides methods used to interact with the database, + such as storing into and retrieving from the database. """ version = 1 @@ -108,7 +108,7 @@ def __init__( notes: Optional[str] = None, **kwargs, ): - """Initializes the StoredDataV1 instance. + """Initializes the DbExperimentData instance. Args: experiment_type: Experiment type. @@ -117,7 +117,7 @@ def __init__( tags: Tags to be associated with the experiment. job_ids: IDs of jobs submitted for the experiment. share_level: Whether this experiment can be shared with others. This - is applicable only if the experiment service supports sharing. See + is applicable only if the database service supports sharing. See the specific service provider's documentation on valid values. metadata: Additional experiment metadata. figure_names: Name of figures associated with this experiment. @@ -168,7 +168,6 @@ def _set_service_from_backend(self, backend: Union[Backend, BaseBackend]) -> Non """ with contextlib.suppress(Exception): self._service = backend.provider().service("experiment") - with contextlib.suppress(Exception): self.auto_save = self._service.option("auto_save") def add_data( @@ -203,7 +202,7 @@ def add_data( the job finishes successfully. The following positional arguments are provided to the callback function: - * This ``StoredData`` object. + * This ``DbExperimentData`` object. * Additional keyword arguments passed to this method. **kwargs: Keyword arguments to be passed to the callback function. @@ -250,7 +249,6 @@ def add_data( elif isinstance(data, Result): self._add_result_data(data) elif isinstance(data, list): - # TODO use loop instead of recursion for fewer save() for dat in data: self.add_data(dat) else: @@ -364,7 +362,7 @@ def add_figures( figure_names: Optional[Union[List[str], str]] = None, overwrite: bool = False, save_figure: Optional[bool] = None, - service: Optional["ExperimentServiceV1"] = None, + service: Optional["DatabaseServiceV1"] = None, ) -> Union[str, List[str]]: """Add the experiment figure. @@ -417,7 +415,7 @@ def add_figures( existing_figure = fig_name in self._figures if existing_figure and not overwrite: - raise ExperimentEntryExists( + raise DbExperimentEntryExists( f"A figure with the name {fig_name} for this experiment " f"already exists. Specify overwrite=True if you " f"want to overwrite it." @@ -458,7 +456,7 @@ def add_figures( def delete_figure( self, figure_key: Union[str, int], - service: Optional["ExperimentServiceV1"] = None, + service: Optional["DatabaseServiceV1"] = None, ) -> str: """Add the experiment figure. @@ -476,7 +474,7 @@ def delete_figure( if isinstance(figure_key, int): figure_key = self._figures.keys()[figure_key] elif figure_key not in self._figures: - raise ExperimentEntryNotFound(f"Figure {figure_key} not found.") + raise DbExperimentEntryNotFound(f"Figure {figure_key} not found.") del self._figures[figure_key] self._deleted_figures.append(figure_key) @@ -517,7 +515,7 @@ def figure( self._figures[figure_key] = figure_data if figure_data is None: - raise ExperimentEntryNotFound(f"Figure {figure_key} not found.") + raise DbExperimentEntryNotFound(f"Figure {figure_key} not found.") if file_name: with open(file_name, "wb") as output: @@ -528,8 +526,8 @@ def figure( @auto_save def add_analysis_results( self, - results: Union[AnalysisResult, List[AnalysisResult]], - service: "ExperimentServiceV1" = None, + results: Union[DbAnalysisResult, List[DbAnalysisResult]], + service: "DatabaseServiceV1" = None, ) -> None: """Save the analysis result. @@ -544,7 +542,7 @@ def add_analysis_results( for result in results: self._analysis_results[result.result_id] = result - with contextlib.suppress(ExperimentError): + with contextlib.suppress(DbExperimentDataError): result.service = self.service result.auto_save = self.auto_save @@ -554,7 +552,7 @@ def add_analysis_results( @auto_save def delete_analysis_result( - self, result_key: Union[int, str], service: "ExperimentServiceV1" = None + self, result_key: Union[int, str], service: "DatabaseServiceV1" = None ) -> str: """Delete the analysis result. @@ -572,7 +570,7 @@ def delete_analysis_result( if isinstance(result_key, int): result_key = self._analysis_results.keys()[result_key] elif result_key not in self._analysis_results: - raise ExperimentEntryNotFound(f"Analysis result {result_key} not found.") + raise DbExperimentEntryNotFound(f"Analysis result {result_key} not found.") del self._analysis_results[result_key] self._deleted_analysis_results.append(result_key) @@ -587,7 +585,7 @@ def delete_analysis_result( def analysis_result( self, index: Optional[Union[int, slice, str]] = None, refresh: bool = False - ) -> Union[AnalysisResult, List[AnalysisResult]]: + ) -> Union[DbAnalysisResult, List[DbAnalysisResult]]: """Return analysis results associated with this experiment. Args: @@ -621,17 +619,17 @@ def analysis_result( return self._analysis_results.values()[index] if isinstance(index, str): if index not in self._analysis_results: - raise ExperimentEntryNotFound(f"Analysis result {index} not found.") + raise DbExperimentEntryNotFound(f"Analysis result {index} not found.") return self._analysis_results[index] raise TypeError(f"Invalid index type {type(index)}.") - def save(self, service: Optional["ExperimentServiceV1"] = None) -> None: + def save(self, service: Optional["DatabaseServiceV1"] = None) -> None: """Save this experiment in the database. Note: Not all experiment properties are saved. - See :meth:`qiskit.providers.experiment.ExperimentServiceV1.create_experiment` + See :meth:`qiskit.providers.experiment.DatabaseServiceV1.create_experiment` for fields that are saved. Note: @@ -673,7 +671,7 @@ def save(self, service: Optional["ExperimentServiceV1"] = None) -> None: update_data=update_data, ) - def save_all(self, service: Optional["ExperimentServiceV1"] = None) -> None: + def save_all(self, service: Optional["DatabaseServiceV1"] = None) -> None: """Save this experiment and its analysis results and figures in the database. Note: @@ -747,8 +745,8 @@ def from_data( experiment_id: str, metadata: Optional[Dict] = None, **kwargs, - ) -> "StoredDataV1": - """Reconstruct a StoredData using the input data. + ) -> "DbExperimentDataV1": + """Reconstruct a DbExperimentDataV1 using the input data. Args: experiment_type: Experiment type. @@ -757,7 +755,7 @@ def from_data( **kwargs: Additional experiment attributes. Returns: - An StoredData instance. + An DbExperimentDataV1 instance. """ if metadata: metadata = cls.deserialize_metadata(json.dumps(metadata)) @@ -990,7 +988,7 @@ def notes(self, new_notes: str) -> None: self.save() @property - def service(self) -> Optional["ExperimentServiceV1"]: + def service(self) -> Optional["DatabaseServiceV1"]: """Return the database service. Returns: @@ -999,7 +997,7 @@ def service(self) -> Optional["ExperimentServiceV1"]: return self._service @service.setter - def service(self, service: "ExperimentServiceV1") -> None: + def service(self, service: "DatabaseServiceV1") -> None: """Set the service to be used for storing experiment data. Args: @@ -1009,7 +1007,7 @@ def service(self, service: "ExperimentServiceV1") -> None: ExperimentError: If an experiment service is already being used. """ if self._service: - raise ExperimentError("An experiment service is already being used.") + raise DbExperimentDataError("An experiment service is already being used.") self._service = service with contextlib.suppress(Exception): self.auto_save = self._service.option("auto_save") diff --git a/qiskit_experiments/stored_data/device_component.py b/qiskit_experiments/database_service/device_component.py similarity index 100% rename from qiskit_experiments/stored_data/device_component.py rename to qiskit_experiments/database_service/device_component.py diff --git a/qiskit_experiments/stored_data/exceptions.py b/qiskit_experiments/database_service/exceptions.py similarity index 71% rename from qiskit_experiments/stored_data/exceptions.py rename to qiskit_experiments/database_service/exceptions.py index 1abfb80456..3e9af85fac 100644 --- a/qiskit_experiments/stored_data/exceptions.py +++ b/qiskit_experiments/database_service/exceptions.py @@ -10,24 +10,24 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Exceptions for errors raised while handling experiments.""" +"""Exceptions for errors raised by experiment service data.""" from qiskit.exceptions import QiskitError -class ExperimentError(QiskitError): - """Base class for errors raised while handling experiments.""" +class DbExperimentDataError(QiskitError): + """Base class for errors raised by experiment service data.""" pass -class ExperimentEntryNotFound(ExperimentError): +class DbExperimentEntryNotFound(DbExperimentDataError): """Errors raised when an experiment entry cannot be found.""" pass -class ExperimentEntryExists(ExperimentError): +class DbExperimentEntryExists(DbExperimentDataError): """Errors raised when an experiment entry already exists.""" pass diff --git a/qiskit_experiments/stored_data/json.py b/qiskit_experiments/database_service/json.py similarity index 100% rename from qiskit_experiments/stored_data/json.py rename to qiskit_experiments/database_service/json.py diff --git a/qiskit_experiments/stored_data/utils.py b/qiskit_experiments/database_service/utils.py similarity index 93% rename from qiskit_experiments/stored_data/utils.py rename to qiskit_experiments/database_service/utils.py index 201a4edd8e..919c0fd753 100644 --- a/qiskit_experiments/stored_data/utils.py +++ b/qiskit_experiments/database_service/utils.py @@ -26,8 +26,9 @@ from dateutil import tz from qiskit.version import __version__ as terra_version +from ..version import __version__ as experiments_version -from .exceptions import ExperimentEntryNotFound, ExperimentEntryExists, ExperimentError +from .exceptions import DbExperimentEntryNotFound, DbExperimentEntryExists, DbExperimentDataError LOG = logging.getLogger(__name__) @@ -37,7 +38,7 @@ def qiskit_version(): try: return pkg_resources.get_distribution("qiskit").version except Exception: # pylint: disable=broad-except - return {"qiskit-terra": terra_version} + return {"qiskit-terra": terra_version, "qiskit-experiments": experiments_version} def parse_timestamp(utc_dt: Union[datetime, str]) -> datetime: @@ -121,14 +122,14 @@ def save_data( if is_new: try: return True, new_func(**{**new_data, **update_data}) - except ExperimentEntryExists: + except DbExperimentEntryExists: is_new = False else: try: return True, update_func(**update_data) - except ExperimentEntryNotFound: + except DbExperimentEntryNotFound: is_new = True - raise ExperimentError("Unable to determine the existence of the entry.") + raise DbExperimentDataError("Unable to determine the existence of the entry.") except Exception: # pylint: disable=broad-except # Don't fail the experiment just because its data cannot be saved. LOG.error("Unable to save the experiment data: %s", traceback.format_exc()) diff --git a/qiskit_experiments/stored_data/__init__.py b/qiskit_experiments/stored_data/__init__.py deleted file mode 100644 index e3ad311c44..0000000000 --- a/qiskit_experiments/stored_data/__init__.py +++ /dev/null @@ -1,53 +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. - -""" -=================================================== -Stored Data (:mod:`qiskit_experiments.stored_data`) -=================================================== - -.. currentmodule:: qiskit_experiments.stored_data - -This module contains the classes used to define the data structure of -an experiment, including its data, metadata, analysis results, and figures. -The classes also provide an interface with a database service for storing -and retrieving experiment-related data. - -Classes -======= - -.. autosummary:: - :toctree: ../stubs/ - - StoredData - StoredDataV1 - AnalysisResult - AnalysisResultV1 - ExperimentService - ExperimentServiceV1 - - -Exceptions -========== - -.. autosummary:: - :toctree: ../stubs/ - - ExperimentError - ExperimentEntryNotFound - ExperimentEntryExists -""" - -from .stored_data import StoredData, StoredDataV1 -from .analysis_result import AnalysisResult, AnalysisResultV1 -from .experiment_service import ExperimentService, ExperimentServiceV1 -from .exceptions import ExperimentError, ExperimentEntryExists, ExperimentEntryNotFound diff --git a/test/stored_data/__init__.py b/test/database_service/__init__.py similarity index 91% rename from test/stored_data/__init__.py rename to test/database_service/__init__.py index 62b7e61d2b..e0bf9c8b4e 100644 --- a/test/stored_data/__init__.py +++ b/test/database_service/__init__.py @@ -10,4 +10,4 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Test cases for stored data module.""" +"""Test cases for database service modules.""" diff --git a/test/stored_data/test_analysisresult.py b/test/database_service/test_db_analysis_result.py similarity index 82% rename from test/stored_data/test_analysisresult.py rename to test/database_service/test_db_analysis_result.py index 30cc064d20..f4fe877757 100644 --- a/test/stored_data/test_analysisresult.py +++ b/test/database_service/test_db_analysis_result.py @@ -20,14 +20,14 @@ import numpy as np from qiskit.test import QiskitTestCase -from qiskit_experiments.stored_data import AnalysisResultV1 as AnalysisResult -from qiskit_experiments.stored_data.device_component import Qubit, Resonator, to_component -from qiskit_experiments.stored_data.experiment_service import ExperimentServiceV1 -from qiskit_experiments.stored_data.exceptions import ExperimentError +from qiskit_experiments.database_service import DbAnalysisResultV1 as DbAnalysisResult +from qiskit_experiments.database_service.device_component import Qubit, Resonator, to_component +from qiskit_experiments.database_service.database_service import DatabaseServiceV1 +from qiskit_experiments.database_service.exceptions import DbExperimentDataError -class TestAnalysisResult(QiskitTestCase): - """Test the AnalysisResult class.""" +class TestDbAnalysisResult(QiskitTestCase): + """Test the DbAnalysisResult class.""" def test_analysis_result_attributes(self): """Test analysis result attributes.""" @@ -39,7 +39,7 @@ def test_analysis_result_attributes(self): "quality": "Good", "verified": False, } - result = AnalysisResult(result_data={"foo": "bar"}, tags=["tag1", "tag2"], **attrs) + result = DbAnalysisResult(result_data={"foo": "bar"}, tags=["tag1", "tag2"], **attrs) self.assertEqual({"foo": "bar"}, result.data()) self.assertEqual(["tag1", "tag2"], result.tags()) for key, val in attrs.items(): @@ -47,14 +47,14 @@ def test_analysis_result_attributes(self): def test_save(self): """Test saving analysis result.""" - mock_service = mock.create_autospec(ExperimentServiceV1) + mock_service = mock.create_autospec(DatabaseServiceV1) result = self._new_analysis_result() result.save(service=mock_service) mock_service.create_analysis_result.assert_called_once() def test_auto_save(self): """Test auto saving.""" - mock_service = mock.create_autospec(ExperimentServiceV1) + mock_service = mock.create_autospec(DatabaseServiceV1) result = self._new_analysis_result(service=mock_service) result.auto_save = True result.save() @@ -75,25 +75,25 @@ def test_auto_save(self): def test_set_service_init(self): """Test setting service in init.""" - mock_service = mock.create_autospec(ExperimentServiceV1) + mock_service = mock.create_autospec(DatabaseServiceV1) result = self._new_analysis_result(service=mock_service) self.assertEqual(mock_service, result.service) def test_set_service_direct(self): """Test setting service directly.""" - mock_service = mock.create_autospec(ExperimentServiceV1) + mock_service = mock.create_autospec(DatabaseServiceV1) result = self._new_analysis_result() result.service = mock_service self.assertEqual(mock_service, result.service) - with self.assertRaises(ExperimentError): + with self.assertRaises(DbExperimentDataError): result.service = mock_service def test_set_service_save(self): """Test setting service when saving.""" - orig_service = mock.create_autospec(ExperimentServiceV1) + orig_service = mock.create_autospec(DatabaseServiceV1) result = self._new_analysis_result(service=orig_service) - new_service = mock.create_autospec(ExperimentServiceV1) + new_service = mock.create_autospec(DatabaseServiceV1) result.save(service=new_service) new_service.create_analysis_result.assert_called() orig_service.create_analysis_result.assert_not_called() @@ -137,9 +137,8 @@ def test_data_serialization(self): def test_source(self): """Test getting analysis result source.""" result = self._new_analysis_result() - source_vals = "\n".join([str(val) for val in result.source.values()]) - self.assertIn("AnalysisResultV1", source_vals) - self.assertIn("qiskit-terra", source_vals) + self.assertIn("DbAnalysisResultV1", result.source["class"]) + self.assertTrue(result.source["qiskit_version"]) def _new_analysis_result(self, **kwargs): """Return a new analysis result.""" @@ -150,7 +149,7 @@ def _new_analysis_result(self, **kwargs): "experiment_id": "1234", } values.update(kwargs) - return AnalysisResult(**values) + return DbAnalysisResult(**values) class TestDeviceComponent(QiskitTestCase): diff --git a/test/stored_data/test_stored_data.py b/test/database_service/test_db_experiment_data.py similarity index 83% rename from test/stored_data/test_stored_data.py rename to test/database_service/test_db_experiment_data.py index b76978be8a..ec618b0d9d 100644 --- a/test/stored_data/test_stored_data.py +++ b/test/database_service/test_db_experiment_data.py @@ -32,17 +32,17 @@ from qiskit.providers import JobV1 as Job from qiskit.providers import JobStatus from qiskit.tools.visualization import HAS_MATPLOTLIB -from qiskit_experiments.stored_data import StoredDataV1 as StoredData -from qiskit_experiments.stored_data import ExperimentServiceV1 -from qiskit_experiments.stored_data.exceptions import ( - ExperimentError, - ExperimentEntryNotFound, - ExperimentEntryExists, +from qiskit_experiments.database_service import DbExperimentDataV1 as DbExperimentData +from qiskit_experiments.database_service import DatabaseServiceV1 +from qiskit_experiments.database_service.exceptions import ( + DbExperimentDataError, + DbExperimentEntryNotFound, + DbExperimentEntryExists, ) -class TestStoredData(QiskitTestCase): - """Test the ExperimentData class.""" +class TestDbExperimentData(QiskitTestCase): + """Test the DbExperimentData class.""" def setUp(self): super().setUp() @@ -56,7 +56,7 @@ def test_stored_data_attributes(self): "figure_names": ["figure1"], "notes": "some notes", } - exp_data = StoredData( + exp_data = DbExperimentData( backend=self.backend, experiment_type="qiskit_test", experiment_id="1234", @@ -74,7 +74,7 @@ def test_stored_data_attributes(self): def test_add_data_dict(self): """Test add data in dictionary.""" - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") a_dict = {"counts": {"01": 518}} dicts = [{"counts": {"00": 284}}, {"counts": {"00": 14}}] @@ -84,7 +84,7 @@ def test_add_data_dict(self): def test_add_data_result(self): """Test add result data.""" - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") a_result = self._get_job_result(1) results = [self._get_job_result(2), self._get_job_result(3)] @@ -99,7 +99,7 @@ def test_add_data_result(self): def test_add_data_result_metadata(self): """Test add result metadata.""" - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") result1 = self._get_job_result(1, has_metadata=False) result2 = self._get_job_result(1, has_metadata=True) @@ -122,7 +122,7 @@ def test_add_data_job(self): for job in jobs: expected.extend(job.result().get_counts()) - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") exp_data.add_data(a_job) exp_data.add_data(jobs) exp_data.block_for_results() @@ -133,7 +133,7 @@ def test_add_data_job_callback(self): """Test add job data with callback.""" def _callback(_exp_data): - self.assertIsInstance(_exp_data, StoredData) + self.assertIsInstance(_exp_data, DbExperimentData) self.assertEqual( [dat["counts"] for dat in _exp_data.data()], a_job.result().get_counts() ) @@ -146,7 +146,7 @@ def _callback(_exp_data): a_job.result.return_value = self._get_job_result(2) called_back = False - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") exp_data.add_data(a_job, post_processing_callback=_callback) exp_data.block_for_results() self.assertTrue(called_back) @@ -155,7 +155,7 @@ def test_add_data_callback(self): """Test add data with callback.""" def _callback(_exp_data): - self.assertIsInstance(_exp_data, StoredData) + self.assertIsInstance(_exp_data, DbExperimentData) nonlocal called_back_count, expected_data, subtests expected_data.extend(subtests[called_back_count][1]) self.assertEqual([dat["counts"] for dat in _exp_data.data()], expected_data) @@ -175,7 +175,7 @@ def _callback(_exp_data): called_back_count = 0 expected_data = [] - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") for data, _ in subtests: with self.subTest(data=data): @@ -187,7 +187,7 @@ def test_add_data_job_callback_kwargs(self): """Test add job data with callback and additional arguments.""" def _callback(_exp_data, **kwargs): - self.assertIsInstance(_exp_data, StoredData) + self.assertIsInstance(_exp_data, DbExperimentData) self.assertEqual({"foo": callback_kwargs}, kwargs) nonlocal called_back called_back = True @@ -197,7 +197,7 @@ def _callback(_exp_data, **kwargs): called_back = False callback_kwargs = "foo" - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") exp_data.add_data(a_job, _callback, foo=callback_kwargs) exp_data.block_for_results() self.assertTrue(called_back) @@ -214,7 +214,7 @@ def _callback(_exp_data, **kwargs): event = threading.Event() self.addCleanup(event.set) - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data(a_job, _callback, event=event) with self.assertLogs("qiskit_experiments", "WARNING"): exp_data.add_data({"foo": "bar"}) @@ -226,7 +226,7 @@ def test_get_data(self): data1.append({"counts": {"00": randrange(1024)}}) results = self._get_job_result(3) - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data(data1) exp_data.add_data(results) self.assertEqual(data1[1], exp_data.data(1)) @@ -251,7 +251,7 @@ def test_add_figure(self): for name, figure, figure_name in sub_tests: with self.subTest(name=name): - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") fn = exp_data.add_figures(figure, figure_name) self.assertEqual(hello_bytes, exp_data.figure(fn)) @@ -265,7 +265,7 @@ def test_add_figure_plot(self): ax.plot([1, 2, 3]) service = self._set_mock_service() - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") exp_data.add_figures(figure, save_figure=True) self.assertEqual(figure, exp_data.figure(0)) service.create_figure.assert_called_once() @@ -289,7 +289,7 @@ def test_add_figures(self): for name, figures, figure_names in sub_tests: with self.subTest(name=name): - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") added_names = exp_data.add_figures(figures, figure_names) for idx, added_fn in enumerate(added_names): self.assertEqual(hello_bytes[idx], exp_data.figure(added_fn)) @@ -299,9 +299,9 @@ def test_add_figure_overwrite(self): hello_bytes = str.encode("hello world") friend_bytes = str.encode("hello friend!") - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") fn = exp_data.add_figures(hello_bytes) - with self.assertRaises(ExperimentEntryExists): + with self.assertRaises(DbExperimentEntryExists): exp_data.add_figures(friend_bytes, fn) exp_data.add_figures(friend_bytes, fn, overwrite=True) @@ -311,7 +311,7 @@ def test_add_figure_save(self): """Test saving a figure in the database.""" hello_bytes = str.encode("hello world") service = self._set_mock_service() - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") exp_data.add_figures(hello_bytes, save_figure=True) service.create_figure.assert_called_once() _, kwargs = service.create_figure.call_args @@ -320,12 +320,12 @@ def test_add_figure_save(self): def test_add_figure_bad_input(self): """Test adding figures with bad input.""" - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") self.assertRaises(ValueError, exp_data.add_figures, ["foo", "bar"], ["name"]) def test_get_figure(self): """Test getting figure.""" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") figure_template = "hello world {}" name_template = "figure_{}" for idx in range(3): @@ -345,7 +345,7 @@ def test_get_figure(self): def test_delete_figure(self): """Test deleting a figure.""" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") id_template = "figure_{}" for idx in range(3): exp_data.add_figures(str.encode("hello world"), id_template.format(idx)) @@ -355,11 +355,11 @@ def test_delete_figure(self): for del_key, figure_name in sub_tests: with self.subTest(del_key=del_key): exp_data.delete_figure(del_key) - self.assertRaises(ExperimentEntryNotFound, exp_data.figure, figure_name) + self.assertRaises(DbExperimentEntryNotFound, exp_data.figure, figure_name) def test_delayed_backend(self): """Test initializing experiment data without a backend.""" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") self.assertIsNone(exp_data.backend) self.assertIsNone(exp_data.service) exp_data.save() @@ -370,7 +370,7 @@ def test_delayed_backend(self): def test_different_backend(self): """Test setting a different backend.""" - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") a_job = mock.create_autospec(Job, instance=True) self.assertNotEqual(exp_data.backend, a_job.backend()) with self.assertLogs("qiskit_experiments", "WARNING"): @@ -378,7 +378,7 @@ def test_different_backend(self): def test_add_get_analysis_result(self): """Test adding and getting analysis results.""" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") results = [] for idx in range(5): res = mock.MagicMock() @@ -393,7 +393,7 @@ def test_add_get_analysis_result(self): def test_add_get_analysis_results(self): """Test adding and getting a list of analysis results.""" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") results = [] for idx in range(5): res = mock.MagicMock() @@ -405,7 +405,7 @@ def test_add_get_analysis_results(self): def test_delete_analysis_result(self): """Test deleting analysis result.""" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") id_template = "result_{}" for idx in range(3): res = mock.MagicMock() @@ -416,12 +416,12 @@ def test_delete_analysis_result(self): for del_key, res_id in subtests: with self.subTest(del_key=del_key): exp_data.delete_analysis_result(del_key) - self.assertRaises(ExperimentEntryNotFound, exp_data.analysis_result, res_id) + self.assertRaises(DbExperimentEntryNotFound, exp_data.analysis_result, res_id) def test_save(self): """Test saving experiment data.""" - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") - service = mock.create_autospec(ExperimentServiceV1, instance=True) + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") + service = mock.create_autospec(DatabaseServiceV1, instance=True) exp_data.save(service=service) service.create_experiment.assert_called_once() _, kwargs = service.create_experiment.call_args @@ -433,8 +433,8 @@ def test_save(self): def test_save_all(self): """Test saving all experiment related data.""" - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") - service = mock.create_autospec(ExperimentServiceV1, instance=True) + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") + service = mock.create_autospec(DatabaseServiceV1, instance=True) exp_data.add_figures(str.encode("hello world")) analysis_result = mock.MagicMock() exp_data.add_analysis_results(analysis_result) @@ -445,8 +445,8 @@ def test_save_all(self): def test_save_all_delete(self): """Test saving all deletion.""" - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") - service = mock.create_autospec(ExperimentServiceV1, instance=True) + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") + service = mock.create_autospec(DatabaseServiceV1, instance=True) exp_data.add_figures(str.encode("hello world")) exp_data.add_analysis_results(mock.MagicMock()) exp_data.delete_analysis_result(0) @@ -460,7 +460,7 @@ def test_save_all_delete(self): def test_set_service_backend(self): """Test setting service via backend.""" mock_service = self._set_mock_service() - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") self.assertEqual(mock_service, exp_data.service) def test_set_service_job(self): @@ -468,27 +468,27 @@ def test_set_service_job(self): mock_service = self._set_mock_service() job = mock.create_autospec(Job, instance=True) job.backend.return_value = self.backend - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") self.assertIsNone(exp_data.service) exp_data.add_data(job) self.assertEqual(mock_service, exp_data.service) def test_set_service_direct(self): """Test setting service directly.""" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") self.assertIsNone(exp_data.service) mock_service = mock.MagicMock() exp_data.service = mock_service self.assertEqual(mock_service, exp_data.service) - with self.assertRaises(ExperimentError): + with self.assertRaises(DbExperimentDataError): exp_data.service = mock_service def test_set_service_save(self): """Test setting service when saving.""" orig_service = self._set_mock_service() - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") - new_service = mock.create_autospec(ExperimentServiceV1, instance=True) + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") + new_service = mock.create_autospec(DatabaseServiceV1, instance=True) exp_data.save(service=new_service) new_service.create_experiment.assert_called() orig_service.create_experiment.assert_not_called() @@ -496,7 +496,7 @@ def test_set_service_save(self): def test_new_backend_has_service(self): """Test changing backend doesn't change existing service.""" orig_service = self._set_mock_service() - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") self.assertEqual(orig_service, exp_data.service) job = mock.create_autospec(Job, instance=True) @@ -509,7 +509,7 @@ def test_new_backend_has_service(self): def test_auto_save(self): """Test auto save.""" service = self._set_mock_service() - exp_data = StoredData(backend=self.backend, experiment_type="qiskit_test") + exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") exp_data.auto_save = True mock_result = mock.MagicMock() @@ -547,7 +547,7 @@ def test_status_job_pending(self): job2.status.return_value = JobStatus.RUNNING self.addCleanup(event.set) - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data(job1) exp_data.add_data(job2, lambda *args, **kwargs: event.wait()) self.assertEqual("RUNNING", exp_data.status()) @@ -561,7 +561,7 @@ def test_status_job_error(self): job2 = mock.create_autospec(Job, instance=True) job2.status.return_value = JobStatus.ERROR - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data([job1, job2]) self.assertEqual("ERROR", exp_data.status()) @@ -573,7 +573,7 @@ def test_status_post_processing(self): event = threading.Event() self.addCleanup(event.set) - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data(job) exp_data.add_data(job, lambda *args, **kwargs: event.wait()) self.assertEqual("POST_PROCESSING", exp_data.status()) @@ -587,7 +587,7 @@ def _post_processing(*args, **kwargs): job = mock.create_autospec(Job, instance=True) job.result.return_value = self._get_job_result(3) - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data(job) exp_data.add_data(job, _post_processing) exp_data.block_for_results() @@ -597,7 +597,7 @@ def test_status_done(self): """Test experiment status when all jobs are done.""" job = mock.create_autospec(Job, instance=True) job.result.return_value = self._get_job_result(3) - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data(job) exp_data.add_data(job, lambda *args, **kwargs: time.sleep(1)) exp_data.block_for_results() @@ -605,21 +605,21 @@ def test_status_done(self): def test_update_tags(self): """Test updating experiment tags.""" - exp_data = StoredData(experiment_type="qiskit_test", tags=["foo"]) + exp_data = DbExperimentData(experiment_type="qiskit_test", tags=["foo"]) self.assertEqual(["foo"], exp_data.tags()) exp_data.update_tags(["bar"]) self.assertEqual(["bar"], exp_data.tags()) def test_update_metadata(self): """Test updating experiment metadata.""" - exp_data = StoredData(experiment_type="qiskit_test", metadata={"foo": "bar"}) + exp_data = DbExperimentData(experiment_type="qiskit_test", metadata={"foo": "bar"}) self.assertEqual({"foo": "bar"}, exp_data.metadata()) exp_data.update_metadata({"bar": "foo"}) self.assertEqual({"bar": "foo"}, exp_data.metadata()) def test_cancel_jobs(self): """Test canceling experiment jobs.""" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") event = threading.Event() self.addCleanup(event.set) job = mock.create_autospec(Job, instance=True) @@ -631,12 +631,12 @@ def test_cancel_jobs(self): def test_metadata_serialization(self): """Test experiment metadata serialization.""" metadata = {"complex": 2 + 3j, "numpy": np.zeros(2)} - exp_data = StoredData(experiment_type="qiskit_test", metadata=metadata) + exp_data = DbExperimentData(experiment_type="qiskit_test", metadata=metadata) serialized = exp_data.serialize_metadata() self.assertIsInstance(serialized, str) self.assertTrue(json.loads(serialized)) - deserialized = StoredData.deserialize_metadata(serialized) + deserialized = DbExperimentData.deserialize_metadata(serialized) self.assertEqual(metadata["complex"], deserialized["complex"]) self.assertEqual(metadata["numpy"].all(), deserialized["numpy"].all()) @@ -653,7 +653,7 @@ def _post_processing(*args, **kwargs): # pylint: disable=unused-argument job2.status.return_value = JobStatus.ERROR job2.job_id.return_value = "5678" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data(job1, _post_processing) exp_data.add_data(job2) exp_data.block_for_results() @@ -662,8 +662,8 @@ def _post_processing(*args, **kwargs): # pylint: disable=unused-argument def test_source(self): """Test getting experiment source.""" - exp_data = StoredData(experiment_type="qiskit_test") - self.assertIn("StoredDataV1", exp_data.source["class"]) + exp_data = DbExperimentData(experiment_type="qiskit_test") + self.assertIn("DbExperimentDataV1", exp_data.source["class"]) self.assertTrue(exp_data.source["qiskit_version"]) def test_block_for_jobs(self): @@ -678,19 +678,19 @@ def _sleeper(*args, **kwargs): # pylint: disable=unused-argument sleep_count = 0 job = mock.create_autospec(Job, instance=True) job.result = _sleeper - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data(job, _sleeper) exp_data.block_for_results() self.assertEqual(2, sleep_count) def test_additional_attr(self): """Test additional experiment attributes.""" - exp_data = StoredData(experiment_type="qiskit_test", foo="foo") + exp_data = DbExperimentData(experiment_type="qiskit_test", foo="foo") self.assertEqual("foo", exp_data.foo) def test_str(self): """Test the string representation.""" - exp_data = StoredData(experiment_type="qiskit_test") + exp_data = DbExperimentData(experiment_type="qiskit_test") exp_data.add_data(self._get_job_result(1)) result = mock.MagicMock() exp_data.add_analysis_results(result) @@ -725,6 +725,6 @@ def _set_mock_service(self): """Add a mock service to the backend.""" mock_provider = mock.MagicMock() self.backend._provider = mock_provider - mock_service = mock.create_autospec(ExperimentServiceV1, instance=True) + mock_service = mock.create_autospec(DatabaseServiceV1, instance=True) mock_provider.service.return_value = mock_service return mock_service From 9070a8958bcbd5f4b90d3b36f3dce60550e62fe7 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Mon, 28 Jun 2021 17:35:17 -0400 Subject: [PATCH 33/70] review comments --- .../database_service/database_service.py | 7 +++- .../database_service/db_analysis_result.py | 6 ++-- .../database_service/db_experiment_data.py | 34 ++++++++++++++----- .../test_db_analysis_result.py | 16 ++++----- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/qiskit_experiments/database_service/database_service.py b/qiskit_experiments/database_service/database_service.py index f9680d725b..5d12e1a8cf 100644 --- a/qiskit_experiments/database_service/database_service.py +++ b/qiskit_experiments/database_service/database_service.py @@ -393,7 +393,7 @@ def set_options(self, **fields): for field in fields: if field not in self._options: raise AttributeError("Options field %s is not valid for this " "service." % field) - self._options.update(**fields) + self._options.update_options(**fields) def option(self, field: str) -> Any: """Get the value of the specified option. @@ -410,3 +410,8 @@ def option(self, field: str) -> Any: if field not in self._options: raise AttributeError(f"Options field {field} is not valid for this service.") return self._options[field] + + @property + def options(self) -> Options: + """Return the options for the service.""" + return self._options diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index 40fbefa088..d451626e4e 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -231,8 +231,8 @@ def data(self) -> Dict: return self._result_data @auto_save - def update_data(self, new_data: Dict) -> None: - """Update result data. + def set_data(self, new_data: Dict) -> None: + """Set result data. Args: new_data: New analysis result data. @@ -244,7 +244,7 @@ def tags(self): return self._tags @auto_save - def update_tags(self, new_tags: List[str]) -> None: + def set_tags(self, new_tags: List[str]) -> None: """Set tags for this result. Args: diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 845be84893..c46182573e 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -168,12 +168,13 @@ def _set_service_from_backend(self, backend: Union[Backend, BaseBackend]) -> Non """ with contextlib.suppress(Exception): self._service = backend.provider().service("experiment") - self.auto_save = self._service.option("auto_save") + self.auto_save = self._service.options.get("auto_save", False) def add_data( self, data: Union[Result, List[Result], Job, List[Job], Dict, List[Dict]], post_processing_callback: Optional[Callable] = None, + timeout: Optional[float] = None, **kwargs: Any, ) -> None: """Add experiment data. @@ -205,6 +206,8 @@ def add_data( * This ``DbExperimentData`` object. * Additional keyword arguments passed to this method. + timeout: Timeout waiting for job to finish, if `data` is a ``Job``. + **kwargs: Keyword arguments to be passed to the callback function. Raises: @@ -236,7 +239,7 @@ def add_data( ( data, self._executor.submit( - self._wait_for_job, data, post_processing_callback, **kwargs + self._wait_for_job, data, post_processing_callback, timeout, **kwargs ), ) ) @@ -261,6 +264,7 @@ def _wait_for_job( self, job: Union[Job, BaseJob], job_done_callback: Optional[Callable] = None, + timeout: Optional[float] = None, **kwargs: Any, ) -> None: """Wait for a job to finish. @@ -268,6 +272,7 @@ def _wait_for_job( Args: job: Job to wait for. job_done_callback: Callback function to invoke when job finishes. + timeout: Timeout waiting for job to finish. **kwargs: Keyword arguments to be passed to the callback function. Raises: @@ -275,7 +280,10 @@ def _wait_for_job( """ LOG.debug("Waiting for job %s to finish.", job.job_id()) try: - job_result = job.result() + try: + job_result = job.result(timeout=timeout) + except TypeError: # Not all jobs take timeout. + job_result = job.result() with self._data.lock: # Hold the lock so we add the block of results together. self._add_result_data(job_result) @@ -307,6 +315,10 @@ def _add_result_data(self, result: Result) -> None: expr_result = result.results[i] if hasattr(expr_result, "header") and hasattr(expr_result.header, "metadata"): data["metadata"] = expr_result.header.metadata + data["shots"] = expr_result.shots + data["meas_level"] = expr_result.meas_level + if hasattr(expr_result, "meas_return"): + data["meas_return"] = expr_result.meas_return self._add_single_data(data) def _add_single_data(self, data: Dict[str, any]) -> None: @@ -775,12 +787,16 @@ def cancel_jobs(self) -> None: except Exception as err: # pylint: disable=broad-except LOG.info("Unable to cancel job %s: %s", job.job_id(), err) - def block_for_results(self) -> None: - """Block until all pending jobs and their post processing finish.""" + def block_for_results(self, timeout: Optional[float] = None) -> None: + """Block until all pending jobs and their post processing finish. + + Args: + timeout: Timeout waiting for results. + """ for job, fut in self._job_futures.copy(): LOG.info("Waiting for job %s and its post processing to finish.", job.job_id()) with contextlib.suppress(Exception): - fut.result() + fut.result(timeout) def status(self) -> str: """Return the data processing status. @@ -960,8 +976,8 @@ def share_level(self, new_level: str) -> None: Args: new_level: New experiment share level. Valid share levels are provider- - specified. For example, IBMQ allows "global", "hub", "group", - "project", and "private". + specified. For example, IBM Quantum experiment service allows + "global", "hub", "group", "project", and "private". """ self._share_level = new_level if self.auto_save: @@ -1010,7 +1026,7 @@ def service(self, service: "DatabaseServiceV1") -> None: raise DbExperimentDataError("An experiment service is already being used.") self._service = service with contextlib.suppress(Exception): - self.auto_save = self._service.option("auto_save") + self.auto_save = self._service.options.get("auto_save", False) @property def source(self) -> Dict: diff --git a/test/database_service/test_db_analysis_result.py b/test/database_service/test_db_analysis_result.py index f4fe877757..e688f57aab 100644 --- a/test/database_service/test_db_analysis_result.py +++ b/test/database_service/test_db_analysis_result.py @@ -61,8 +61,8 @@ def test_auto_save(self): subtests = [ # update function, update parameters, service called - (result.update_tags, (["foo"],)), - (result.update_data, ({"foo": "bar"},)), + (result.set_tags, (["foo"],)), + (result.set_data, ({"foo": "bar"},)), (setattr, (result, "quality", "GOOD")), (setattr, (result, "verified", True)), ] @@ -98,16 +98,16 @@ def test_set_service_save(self): new_service.create_analysis_result.assert_called() orig_service.create_analysis_result.assert_not_called() - def test_update_data(self): - """Test updating data.""" + def test_set_data(self): + """Test setting data.""" result = self._new_analysis_result() - result.update_data({"foo": "new data"}) + result.set_data({"foo": "new data"}) self.assertEqual({"foo": "new data"}, result.data()) - def test_update_tags(self): - """Test updating tags.""" + def test_set_tags(self): + """Test setting tags.""" result = self._new_analysis_result() - result.update_tags(["new_tag"]) + result.set_tags(["new_tag"]) self.assertEqual(["new_tag"], result.tags()) def test_update_quality(self): From 5205ee6f81b85a5fb0ceaca8bd51a75ec22a9849 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Mon, 28 Jun 2021 18:15:40 -0400 Subject: [PATCH 34/70] fix lint --- .../database_service/db_analysis_result.py | 4 ++-- .../database_service/db_experiment_data.py | 12 ++++++------ qiskit_experiments/database_service/utils.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index 63d4b86e8a..460914de93 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -189,7 +189,7 @@ def save(self, service: Optional["DatabaseServiceV1"] = None) -> None: If ``None``, the default, if any, is used. Raises: - ExperimentError: If the analysis result contains invalid data. + DbExperimentDataError: If the analysis result contains invalid data. """ service = service or self._service if not service: @@ -345,7 +345,7 @@ def service(self, service: "DatabaseServiceV1") -> None: service: Service to be used. Raises: - ExperimentError: If an experiment service is already being used. + DbExperimentDataError: If an experiment service is already being used. """ if self._service: raise DbExperimentDataError("An experiment service is already being used.") diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index c46182573e..08c4ad11c1 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -394,7 +394,7 @@ def add_figures( Figure names. Raises: - ExperimentEntryExists: If the figure with the same name already exists, + DbExperimentEntryNotFound: If the figure with the same name already exists, and `overwrite=True` is not specified. ValueError: If an input parameter has an invalid value. """ @@ -481,7 +481,7 @@ def delete_figure( Figure name. Raises: - ExperimentEntryNotFound: If the figure is not found. + DbExperimentEntryNotFound: If the figure is not found. """ if isinstance(figure_key, int): figure_key = self._figures.keys()[figure_key] @@ -514,7 +514,7 @@ def figure( content of the figure in bytes. Raises: - ExperimentEntryNotFound: If the figure cannot be found. + DbExperimentEntryNotFound: If the figure cannot be found. """ if isinstance(figure_key, int): figure_key = self._figures.keys()[figure_key] @@ -577,7 +577,7 @@ def delete_analysis_result( Analysis result ID. Raises: - ExperimentEntryNotFound: If analysis result not found. + DbExperimentEntryNotFound: If analysis result not found. """ if isinstance(result_key, int): result_key = self._analysis_results.keys()[result_key] @@ -616,7 +616,7 @@ def analysis_result( Raises: TypeError: If the input `index` has an invalid type. - ExperimentEntryNotFound: If the entry cannot be found. + DbExperimentEntryNotFound: If the entry cannot be found. """ if self.service and (not self._analysis_results or refresh): retrieved_results = self.service.analysis_results( @@ -1020,7 +1020,7 @@ def service(self, service: "DatabaseServiceV1") -> None: service: Service to be used. Raises: - ExperimentError: If an experiment service is already being used. + DbExperimentDataError: If an experiment service is already being used. """ if self._service: raise DbExperimentDataError("An experiment service is already being used.") diff --git a/qiskit_experiments/database_service/utils.py b/qiskit_experiments/database_service/utils.py index 919c0fd753..8c0322f382 100644 --- a/qiskit_experiments/database_service/utils.py +++ b/qiskit_experiments/database_service/utils.py @@ -110,7 +110,7 @@ def save_data( A tuple of whether the data was saved and the function return value. Raises: - ExperimentError: If unable to determine whether the entry exists. + DbExperimentDataError: If unable to determine whether the entry exists. """ attempts = 0 try: From d188bebde4444ea0209c17fcbaeed574e8f5370d Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Tue, 29 Jun 2021 12:11:39 +0300 Subject: [PATCH 35/70] black --- qiskit_experiments/base_analysis.py | 4 +++- test/data_processing/test_data_processing.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 576909327b..4bb51f9f64 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -103,7 +103,9 @@ def run( @abstractmethod def _run_analysis( self, experiment_data: ExperimentData, **options - ) -> Tuple[Union[DbAnalysisResultV1, List[DbAnalysisResultV1]], List["matplotlib.figure.Figure"]]: + ) -> Tuple[ + Union[DbAnalysisResultV1, List[DbAnalysisResultV1]], List["matplotlib.figure.Figure"] + ]: """Run analysis on circuit data. Args: diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 8c9f524451..b542436d37 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -100,7 +100,7 @@ def test_to_real(self): "metadata": {"experiment_type": "fake_test_experiment"}, "job_id": "job-123", "meas_level": 1, - "shots": 3 + "shots": 3, } expected_new = np.array([[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]]) @@ -149,7 +149,7 @@ def test_to_imag(self): "metadata": {"experiment_type": "fake_test_experiment"}, "job_id": "job-123", "meas_level": 1, - "shots": 3 + "shots": 3, } expected_new = np.array( From d5bb8d0509da743033f5975a6e3376a43386cd50 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Tue, 29 Jun 2021 14:02:23 +0300 Subject: [PATCH 36/70] black --- qiskit_experiments/characterization/t2ramsey_analysis.py | 1 - qiskit_experiments/tomography/tomography_analysis.py | 4 ++-- test/test_t2ramsey.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/characterization/t2ramsey_analysis.py b/qiskit_experiments/characterization/t2ramsey_analysis.py index 2ce3e1e1a9..bc88d0bca6 100644 --- a/qiskit_experiments/characterization/t2ramsey_analysis.py +++ b/qiskit_experiments/characterization/t2ramsey_analysis.py @@ -192,4 +192,3 @@ def _fit_quality(fit_out, fit_err, reduced_chisq): return "good" else: return "bad" - diff --git a/qiskit_experiments/tomography/tomography_analysis.py b/qiskit_experiments/tomography/tomography_analysis.py index 114a700828..ec2a245745 100644 --- a/qiskit_experiments/tomography/tomography_analysis.py +++ b/qiskit_experiments/tomography/tomography_analysis.py @@ -110,7 +110,7 @@ def _run_analysis(self, experiment_data, **options): result = { "state": Choi(state) if preparation_basis else DensityMatrix(state), "fitter_metadata": fitter_metadata, - "success": True + "success": True, } self._postprocess_fit( @@ -130,7 +130,7 @@ def _run_analysis(self, experiment_data, **options): device_components=[], experiment_id=experiment_data.experiment_id, quality=None, - ) + ) return [analysis_result], [None] diff --git a/test/test_t2ramsey.py b/test/test_t2ramsey.py index 3532bd7119..bbe9bb4b11 100644 --- a/test/test_t2ramsey.py +++ b/test/test_t2ramsey.py @@ -241,7 +241,7 @@ def test_t2ramsey_parallel(self): "phi_guess": [0, None, 0], "b_guess": [0.5, None, 0.5], } - + backend = T2RamseyBackend(p0) expdata = par_exp.run(backend=backend, shots=1000) expdata.block_for_results() From e2b486ccc953e4bad47796dd3dde3371bc1d76db Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 7 Jul 2021 11:35:40 +0300 Subject: [PATCH 37/70] catch failed analysis + black --- qiskit_experiments/base_analysis.py | 23 +++++++++++++++---- .../randomized_benchmarking/rb_analysis.py | 1 - test/test_t1.py | 2 +- test/test_t2ramsey.py | 4 +--- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 16d729d8eb..7d0ac7405e 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -19,8 +19,10 @@ from qiskit.exceptions import QiskitError from qiskit.providers.options import Options +from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.database_service import DbAnalysisResultV1 +from qiskit_experiments.database_service.device_component import Qubit class BaseAnalysis(ABC): @@ -87,10 +89,23 @@ def run( analysis_options = analysis_options.__dict__ # Run analysis - analysis_results, figures = self._run_analysis(experiment_data, **analysis_options) - for res in analysis_results: - if "success" not in res.data(): - res._result_data["success"] = True + try: + analysis_results, figures = self._run_analysis(experiment_data, **analysis_options) + for res in analysis_results: + if "success" not in res.data(): + res._result_data["success"] = True + except AnalysisError as ex: + analysis_results = [ + DbAnalysisResultV1( + result_data={"success": False, "error_message": ex}, + result_type=experiment_data.metadata()["experiment_type"], + device_components=[ + Qubit(qubit) for qubit in experiment_data.metadata()["physical_qubits"] + ], + experiment_id=experiment_data.experiment_id, + ) + ] + figures = None # Save to experiment data if save: diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index cf0827df2a..de74421218 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -201,4 +201,3 @@ def _post_analysis(self, result_data: Dict) -> Dict: ) result_data["EPG"] = epg return result_data - diff --git a/test/test_t1.py b/test/test_t1.py index 3509064d45..db3de8ff25 100644 --- a/test/test_t1.py +++ b/test/test_t1.py @@ -99,7 +99,7 @@ def test_t1_parallel_different_analysis_options(self): for i in range(2): sub_res.append(res.component_experiment_data(i).analysis_results(0)) - self.assertEqual(sub_res[0].quality, "computer_good") + self.assertEqual(sub_res[0].quality, "good") self.assertAlmostEqual(sub_res[0].data()["value"], t1, delta=3) self.assertFalse(sub_res[1].data()["success"]) diff --git a/test/test_t2ramsey.py b/test/test_t2ramsey.py index 54c5c8153b..05e6646036 100644 --- a/test/test_t2ramsey.py +++ b/test/test_t2ramsey.py @@ -317,8 +317,6 @@ def test_t2ramsey_concat_2_experiments(self): self.assertAlmostEqual( result1_data["frequency_value"], estimated_freq / dt_factor, delta=3 / dt_factor ) - self.assertEqual( - result1.quality, "good", "Result quality bad for unit " + str(unit) - ) + self.assertEqual(result1.quality, "good", "Result quality bad for unit " + str(unit)) self.assertLessEqual(result1_data["stderr"], result0_data["stderr"]) self.assertEqual(len(expdata1.data()), len(delays0) + len(delays1)) From 7208399b88f830e3eddfa017febff770e91b17ad Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 7 Jul 2021 11:45:56 +0300 Subject: [PATCH 38/70] omit prefix computer_ from quality --- .../calibration/experiments/drag_analysis.py | 4 ++-- qiskit_experiments/calibration/experiments/rabi.py | 4 ++-- .../characterization/qubit_spectroscopy.py | 4 ++-- test/calibration/experiments/test_drag.py | 6 +++--- test/calibration/experiments/test_rabi.py | 10 +++++----- test/calibration/test_update_library.py | 4 ++-- test/test_qubit_spectroscopy.py | 12 ++++++------ 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/qiskit_experiments/calibration/experiments/drag_analysis.py b/qiskit_experiments/calibration/experiments/drag_analysis.py index 9f1efbea2a..3f8cf079a3 100644 --- a/qiskit_experiments/calibration/experiments/drag_analysis.py +++ b/qiskit_experiments/calibration/experiments/drag_analysis.py @@ -202,8 +202,8 @@ def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResul ] if all(criteria): - result_data["quality"] = "computer_good" + result_data["quality"] = "good" else: - result_data["quality"] = "computer_bad" + result_data["quality"] = "bad" return result_data diff --git a/qiskit_experiments/calibration/experiments/rabi.py b/qiskit_experiments/calibration/experiments/rabi.py index 12086076ff..84d0a95bf4 100644 --- a/qiskit_experiments/calibration/experiments/rabi.py +++ b/qiskit_experiments/calibration/experiments/rabi.py @@ -147,9 +147,9 @@ def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResul ] if all(criteria): - result_data["quality"] = "computer_good" + result_data["quality"] = "good" else: - result_data["quality"] = "computer_bad" + result_data["quality"] = "bad" return result_data diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 1763b212bc..320d246e10 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -169,9 +169,9 @@ def _post_analysis(self, result_data: Dict) -> Dict: ] if all(criteria): - result_data["quality"] = "computer_good" + result_data["quality"] = "good" else: - result_data["quality"] = "computer_bad" + result_data["quality"] = "bad" return result_data diff --git a/test/calibration/experiments/test_drag.py b/test/calibration/experiments/test_drag.py index 920ebc1814..25fbc2fd77 100644 --- a/test/calibration/experiments/test_drag.py +++ b/test/calibration/experiments/test_drag.py @@ -59,7 +59,7 @@ def test_end_to_end(self): result_data = result.data() self.assertTrue(abs(result_data["popt"][4] - backend.ideal_beta) < test_tol) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") # Small leakage will make the curves very flat. backend = DragBackend(leakage=0.005) @@ -75,7 +75,7 @@ def test_end_to_end(self): result_data = result.data() self.assertTrue(abs(result_data["popt"][4] - backend.ideal_beta) < test_tol) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") # Large leakage will make the curves oscillate quickly. backend = DragBackend(leakage=0.05) @@ -91,7 +91,7 @@ def test_end_to_end(self): result_data = result.data() self.assertTrue(abs(result_data["popt"][4] - backend.ideal_beta) < test_tol) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") class TestDragCircuits(QiskitTestCase): diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 12208d7644..4ebc857b8c 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -72,7 +72,7 @@ def test_rabi_end_to_end(self): result = expdata.analysis_results(0) result_data = result.data() - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") self.assertTrue(abs(result_data["popt"][1] - backend.rabi_rate) < test_tol) backend = RabiBackend(amplitude_to_angle=np.pi / 2) @@ -83,7 +83,7 @@ def test_rabi_end_to_end(self): expdata.block_for_results() result = expdata.analysis_results(0) result_data = result.data() - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") self.assertTrue(abs(result_data["popt"][1] - backend.rabi_rate) < test_tol) backend = RabiBackend(amplitude_to_angle=2.5 * np.pi) @@ -94,7 +94,7 @@ def test_rabi_end_to_end(self): expdata.block_for_results() result = expdata.analysis_results(0) result_data = result.data() - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") self.assertTrue(abs(result_data["popt"][1] - backend.rabi_rate) < test_tol) @@ -180,7 +180,7 @@ def test_good_analysis(self): result = RabiAnalysis().run(experiment_data, data_processor=data_processor, plot=False) - self.assertEqual(result[0].quality, "computer_good") + self.assertEqual(result[0].quality, "good") self.assertTrue(abs(result[0].data()["popt"][1] - expected_rate) < test_tol) def test_bad_analysis(self): @@ -196,7 +196,7 @@ def test_bad_analysis(self): result = RabiAnalysis().run(experiment_data, data_processor=data_processor, plot=False) - self.assertEqual(result[0].quality, "computer_bad") + self.assertEqual(result[0].quality, "bad") class TestCompositeExperiment(QiskitTestCase): diff --git a/test/calibration/test_update_library.py b/test/calibration/test_update_library.py index ba4db57ea3..e5c04bfdad 100644 --- a/test/calibration/test_update_library.py +++ b/test/calibration/test_update_library.py @@ -108,7 +108,7 @@ def test_frequency(self): value = get_opt_value(result_data, "freq") self.assertTrue(freq01 + peak_offset - 2e6 < value < freq01 + peak_offset + 2e6) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") # Test the integration with the BackendCalibrations cals = BackendCalibrations(FakeAthens()) @@ -159,7 +159,7 @@ def test_drag(self): # Test the fit for good measure. self.assertTrue(abs(result_data["popt"][4] - backend.ideal_beta) < test_tol) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") # Check schedules pre-update expected = x_plus.assign_parameters({beta: 0.2, chan: 1}, inplace=False) diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 1558ce0a00..03cb0d6236 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -75,7 +75,7 @@ def test_spectroscopy_end2end_classified(self): self.assertTrue(4.999e9 < value < 5.001e9) self.assertTrue(result_data["success"]) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") # Test if we find still find the peak when it is shifted by 5 MHz. backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) @@ -90,7 +90,7 @@ def test_spectroscopy_end2end_classified(self): value = get_opt_value(result_data, "freq") self.assertTrue(5.0049e9 < value < 5.0051e9) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") def test_spectroscopy_end2end_kerneled(self): """End to end test of the spectroscopy experiment on IQ data.""" @@ -110,7 +110,7 @@ def test_spectroscopy_end2end_kerneled(self): self.assertTrue(freq01 - 2e6 < value < freq01 + 2e6) self.assertTrue(result_data["success"]) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") # Test if we find still find the peak when it is shifted by 5 MHz. backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) @@ -124,7 +124,7 @@ def test_spectroscopy_end2end_kerneled(self): value = get_opt_value(result_data, "freq") self.assertTrue(freq01 + 3e6 < value < freq01 + 8e6) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") spec.set_run_options(meas_return="avg") expdata = spec.run(backend) @@ -135,7 +135,7 @@ def test_spectroscopy_end2end_kerneled(self): value = get_opt_value(result_data, "freq") self.assertTrue(freq01 + 3e6 < value < freq01 + 8e6) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") def test_spectroscopy12_end2end_classified(self): """End to end test of the spectroscopy experiment with an x pulse.""" @@ -158,7 +158,7 @@ def test_spectroscopy12_end2end_classified(self): self.assertTrue(freq01 - 2e6 < value < freq01 + 2e6) self.assertTrue(result_data["success"]) - self.assertEqual(result.quality, "computer_good") + self.assertEqual(result.quality, "good") # Test the circuits circ = spec.circuits(backend)[0] From 8ff3f11bef1d8ff0e9440fce47ef5e963a0c1554 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 7 Jul 2021 11:53:02 +0300 Subject: [PATCH 39/70] removed debug print --- test/calibration/experiments/test_drag.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/calibration/experiments/test_drag.py b/test/calibration/experiments/test_drag.py index 25fbc2fd77..eb48964f71 100644 --- a/test/calibration/experiments/test_drag.py +++ b/test/calibration/experiments/test_drag.py @@ -70,7 +70,6 @@ def test_end_to_end(self): drag.set_run_options(meas_level=MeasLevel.KERNELED) expdata = drag.run(backend) expdata.block_for_results() - print(expdata) result = expdata.analysis_results(0) result_data = result.data() From c9ad44860939bc75c7510140eafe2268777a5605 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 7 Jul 2021 12:37:36 +0300 Subject: [PATCH 40/70] fix docs build --- .../database_service/db_experiment_data.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 9dd0f88857..ef5d1b26c2 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -373,25 +373,27 @@ def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, Li @auto_save def add_figures( self, - figures: Union[List[Union[str, bytes, "pyplot.Figure"]], str, bytes, "pyplot.Figure"], - figure_names: Optional[Union[List[str], str]] = None, - overwrite: bool = False, - save_figure: Optional[bool] = None, + figures, + figure_names=None, + overwrite=False, + save_figure=None, ) -> Union[str, List[str]]: """Add the experiment figure. Args: - figures: Paths of the figure files or figure data. - figure_names: Names of the figures. If ``None``, use the figure file + figures (str or bytes or pyplot.Figure or list): Paths of the figure + files or figure data. + figure_names (str or list): Names of the figures. If ``None``, use the figure file names, if given, or a generated name. If `figures` is a list, then `figure_names` must also be a list of the same length or ``None``. - overwrite: Whether to overwrite the figure if one already exists with + overwrite (bool): Whether to overwrite the figure if one already exists with the same name. - save_figure: Whether to save the figure in the database. If ``None``, + save_figure (bool): Whether to save the figure in the database. If ``None``, the ``auto-save`` attribute is used. Returns: - Figure names. + str or list: + Figure names. Raises: DbExperimentEntryExists: If the figure with the same name already exists, From 539b723c71cb64aff3048fea88e4bef3e2e4e186 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 7 Jul 2021 14:55:18 +0300 Subject: [PATCH 41/70] added meas_level to mock iq backend header --- qiskit_experiments/data_processing/nodes.py | 4 ++-- qiskit_experiments/test/mock_iq_backend.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index cbf44f487b..7a2b689e6c 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -127,13 +127,13 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An if self._validate: if len(datum.shape) not in {2, 3}: raise DataProcessorError( - f"IQ data given to {self.__class__.__name__} must be an 2D array. " + f"IQ data given to {self.__class__.__name__} must be a 2D array. " f"Instead, a {len(datum.shape)}D array was given." ) if error is not None and len(error.shape) not in {2, 3}: raise DataProcessorError( - f"IQ data error given to {self.__class__.__name__} must be an 2D array." + f"IQ data error given to {self.__class__.__name__} must be a 2D array." f"Instead, a {len(error.shape)}D array was given." ) diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index a56b5b471b..d533fff2dc 100644 --- a/qiskit_experiments/test/mock_iq_backend.py +++ b/qiskit_experiments/test/mock_iq_backend.py @@ -106,6 +106,7 @@ def run(self, run_input, **options): "shots": shots, "success": True, "header": {"metadata": circ.metadata}, + "meas_level": meas_level, } prob = self._compute_probability(circ) From 7f6e3703f3213ae96a6e19cf00f042384a697113 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 7 Jul 2021 15:40:48 +0300 Subject: [PATCH 42/70] black --- qiskit_experiments/analysis/curve_analysis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index d71b5aeeab..da465a4013 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -1054,7 +1054,9 @@ def _run_analysis( analysis_result = CurveAnalysisResult( result_data=result_data, result_type=result_data["analysis_type"], - device_components=[Qubit(qubit) for qubit in self._physical_qubits] if self._physical_qubits else [], + device_components=[Qubit(qubit) for qubit in self._physical_qubits] + if self._physical_qubits + else [], experiment_id=experiment_data.experiment_id, quality=result_data.get("quality", None), ) From 724245bfa8a0613fd35ec0fb8cd5d74addbd9056 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 8 Jul 2021 13:37:20 +0300 Subject: [PATCH 43/70] removed the call to get_memory --- qiskit_experiments/database_service/db_experiment_data.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index ef5d1b26c2..2fa13966ce 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -313,8 +313,6 @@ def _add_result_data(self, result: Result) -> None: if "counts" in data: # Format to Counts object rather than hex dict data["counts"] = result.get_counts(i) - if "memory" in data: - data["memory"] = result.get_memory(i) expr_result = result.results[i] if hasattr(expr_result, "header") and hasattr(expr_result.header, "metadata"): data["metadata"] = expr_result.header.metadata From a2163ef894d1db2be6041d6a9165f3d733e1cbb4 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 8 Jul 2021 14:02:54 +0300 Subject: [PATCH 44/70] adjusted test_rb_analysis --- test/RB/test_rb_analysis.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/RB/test_rb_analysis.py b/test/RB/test_rb_analysis.py index 88952134b6..786e0dbf18 100644 --- a/test/RB/test_rb_analysis.py +++ b/test/RB/test_rb_analysis.py @@ -144,11 +144,11 @@ def _validate_fitting_parameters( keys_for_array_data = ["popt", "popt_err", "pcov", "xrange"] keys_for_string_data = ["popt_keys", "analysis_type"] for idx, calculated_analysis_sample_data in enumerate(calculated_analysis_samples_data): - for key in calculated_analysis_sample_data: + for key in calculated_analysis_sample_data.data(): if key in keys_for_array_data: self.assertTrue( matrix_equal( - calculated_analysis_sample_data[key], + calculated_analysis_sample_data.data()[key], expected_analysis_samples_data[idx][key], rtol=RTOL_DEFAULT, atol=ATOL_DEFAULT, @@ -157,27 +157,27 @@ def _validate_fitting_parameters( + key + "', doesn't match the expected value." + "\n {} != {}".format( - calculated_analysis_sample_data[key], + calculated_analysis_sample_data.data()[key], expected_analysis_samples_data[idx][key], ), ) else: if key in keys_for_string_data: self.assertTrue( - calculated_analysis_sample_data[key] + calculated_analysis_sample_data.data()[key] == expected_analysis_samples_data[idx][key], "The analysis_type doesn't match to the one expected.", ) else: if key == "EPG": self._validate_epg( - calculated_analysis_sample_data[key], + calculated_analysis_sample_data.data()[key], expected_analysis_samples_data[idx][key], ) else: self.assertTrue( np.allclose( - np.float64(calculated_analysis_sample_data[key]), + np.float64(calculated_analysis_sample_data.data()[key]), np.float64(expected_analysis_samples_data[idx][key]), ), msg="The calculated value for key '" From e2d5d1db1d7d3629fbc0143f5a134e18ce066a8a Mon Sep 17 00:00:00 2001 From: jessieyu Date: Wed, 7 Jul 2021 09:54:13 -0400 Subject: [PATCH 45/70] fix analysis result __str__ --- qiskit_experiments/base_analysis.py | 2 +- qiskit_experiments/database_service/db_analysis_result.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 7d0ac7405e..02b6267786 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -98,7 +98,7 @@ def run( analysis_results = [ DbAnalysisResultV1( result_data={"success": False, "error_message": ex}, - result_type=experiment_data.metadata()["experiment_type"], + result_type=experiment_data.experiment_type, device_components=[ Qubit(qubit) for qubit in experiment_data.metadata()["physical_qubits"] ], diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index d6b7ce5a96..a4f4501f56 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -66,6 +66,8 @@ class DbAnalysisResultV1(DbAnalysisResult): _json_decoder = NumpyDecoder _extra_data = {} + __keys_not_shown__ = tuple() + """Data keys of analysis result which are not directly shown in `__str__` method""" def __init__( self, @@ -374,5 +376,7 @@ def __str__(self): ret += f"\nTags: {self.tags()}" ret += "\nResult Data:" for key, val in self.data().items(): + if key in self.__keys_not_shown__: + continue ret += f"\n - {key}: {val}" return ret From 18ce8aba1f1bbe56c9c2b2f3936b3ab74fa05d80 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 8 Jul 2021 09:21:30 -0400 Subject: [PATCH 46/70] add callable to encoder --- .../database_service/db_analysis_result.py | 6 ++--- .../database_service/db_experiment_data.py | 8 +++---- qiskit_experiments/database_service/json.py | 24 +++++++++++-------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index a4f4501f56..d4d0652271 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -20,7 +20,7 @@ from functools import wraps from .database_service import DatabaseServiceV1 -from .json import NumpyEncoder, NumpyDecoder +from .json import ExperimentEncoder, ExperimentDecoder from .utils import save_data, qiskit_version from .exceptions import DbExperimentDataError from .device_component import DeviceComponent, to_component @@ -62,8 +62,8 @@ class DbAnalysisResultV1(DbAnalysisResult): version = 1 _data_version = 1 - _json_encoder = NumpyEncoder - _json_decoder = NumpyDecoder + _json_encoder = ExperimentEncoder + _json_decoder = ExperimentDecoder _extra_data = {} __keys_not_shown__ = tuple() diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 2fa13966ce..645fe38fef 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -33,7 +33,7 @@ from .database_service import DatabaseServiceV1 from .exceptions import DbExperimentDataError, DbExperimentEntryNotFound, DbExperimentEntryExists from .db_analysis_result import DbAnalysisResultV1 as DbAnalysisResult -from .json import NumpyEncoder, NumpyDecoder +from .json import ExperimentEncoder, ExperimentDecoder from .utils import ( save_data, qiskit_version, @@ -93,8 +93,8 @@ class DbExperimentDataV1(DbExperimentData): _executor = futures.ThreadPoolExecutor() """Threads used for asynchronous processing.""" - _json_encoder = NumpyEncoder - _json_decoder = NumpyDecoder + _json_encoder = ExperimentEncoder + _json_decoder = ExperimentDecoder def __init__( self, @@ -614,7 +614,7 @@ def analysis_results( experiment_id=self.experiment_id, limit=None ) for result in retrieved_results: - self._analysis_results[result["result_id"]] = result + self._analysis_results[result["result_id"]] = DbAnalysisResult.from_data(**result) if index is None: return self._analysis_results.values() diff --git a/qiskit_experiments/database_service/json.py b/qiskit_experiments/database_service/json.py index 8dbb37f2cc..b55e8f2666 100644 --- a/qiskit_experiments/database_service/json.py +++ b/qiskit_experiments/database_service/json.py @@ -14,23 +14,25 @@ """Experiment serialization methods.""" import json -from typing import Any +from typing import Any, Callable import numpy as np -class NumpyEncoder(json.JSONEncoder): +class ExperimentEncoder(json.JSONEncoder): """JSON Encoder for Numpy arrays and complex numbers.""" def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ if hasattr(obj, "tolist"): - return {"type": "array", "value": obj.tolist()} + return {"__type__": "array", "__value__": obj.tolist()} if isinstance(obj, complex): - return {"type": "complex", "value": [obj.real, obj.imag]} + return {"__type__": "complex", "__value__": [obj.real, obj.imag]} + if isinstance(obj, Callable): + return {"__type__": "callable", "__value__": obj.__name__} return super().default(obj) -class NumpyDecoder(json.JSONDecoder): +class ExperimentDecoder(json.JSONDecoder): """JSON Decoder for Numpy arrays and complex numbers.""" def __init__(self, *args, **kwargs): @@ -38,10 +40,12 @@ def __init__(self, *args, **kwargs): def object_hook(self, obj): """Object hook.""" - if "type" in obj: - if obj["type"] == "complex": - val = obj["value"] + if "__type__" in obj: + if obj["__type__"] == "complex": + val = obj["__value__"] return val[0] + 1j * val[1] - if obj["type"] == "array": - return np.array(obj["value"]) + if obj["__type__"] == "array": + return np.array(obj["__value__"]) + if obj["__type__"] == "callable": + return obj["__value__"] return obj From 01ce0677cb88345e44633e9f02128fb462bde9b0 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 8 Jul 2021 09:49:42 -0400 Subject: [PATCH 47/70] fix lint --- qiskit_experiments/database_service/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/database_service/json.py b/qiskit_experiments/database_service/json.py index b55e8f2666..cfcb0a19a5 100644 --- a/qiskit_experiments/database_service/json.py +++ b/qiskit_experiments/database_service/json.py @@ -27,7 +27,7 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ return {"__type__": "array", "__value__": obj.tolist()} if isinstance(obj, complex): return {"__type__": "complex", "__value__": [obj.real, obj.imag]} - if isinstance(obj, Callable): + if callable(obj): return {"__type__": "callable", "__value__": obj.__name__} return super().default(obj) From 7c3a27465613f769bf8e7912910a0e407d16531d Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 8 Jul 2021 11:04:35 -0400 Subject: [PATCH 48/70] fix lint again --- qiskit_experiments/database_service/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/database_service/json.py b/qiskit_experiments/database_service/json.py index cfcb0a19a5..3b50d1780c 100644 --- a/qiskit_experiments/database_service/json.py +++ b/qiskit_experiments/database_service/json.py @@ -14,7 +14,7 @@ """Experiment serialization methods.""" import json -from typing import Any, Callable +from typing import Any import numpy as np From 9933648d62867dc26bc3e45fa25af25a38582c2b Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 8 Jul 2021 13:49:47 -0400 Subject: [PATCH 49/70] log all job failure --- .../database_service/db_experiment_data.py | 6 +++--- .../database_service/test_db_experiment_data.py | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 645fe38fef..b211e16298 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -288,9 +288,9 @@ def _wait_for_job( with self._data.lock: # Hold the lock so we add the block of results together. self._add_result_data(job_result) - except JobError as err: - LOG.warning("Job %s failed: %s", job.job_id(), str(err)) - return + except Exception: # pylint: disable=broad-except + LOG.warning("Job %s failed:\n%s", job.job_id(), traceback.format_exc()) + raise try: if job_done_callback: diff --git a/test/database_service/test_db_experiment_data.py b/test/database_service/test_db_experiment_data.py index b1c9b6bb46..3e3902e75b 100644 --- a/test/database_service/test_db_experiment_data.py +++ b/test/database_service/test_db_experiment_data.py @@ -546,6 +546,11 @@ def test_status_job_pending(self): exp_data.add_data(job2, lambda *args, **kwargs: event.wait()) self.assertEqual("RUNNING", exp_data.status()) + # Cleanup + with self.assertLogs("qiskit_experiments", "WARNING"): + event.set() + exp_data.block_for_results() + def test_status_job_error(self): """Test experiment status when job failed.""" job1 = mock.create_autospec(Job, instance=True) @@ -613,15 +618,25 @@ def test_set_metadata(self): def test_cancel_jobs(self): """Test canceling experiment jobs.""" + + def _job_result(): + event.wait() + raise ValueError("Job was cancelled.") + exp_data = DbExperimentData(experiment_type="qiskit_test") event = threading.Event() self.addCleanup(event.set) job = mock.create_autospec(Job, instance=True) - job.result = lambda *args, **kwargs: event.wait() + job.result = _job_result exp_data.add_data(job) exp_data.cancel_jobs() job.cancel.assert_called_once() + # Cleanup + with self.assertLogs("qiskit_experiments", "WARNING"): + event.set() + exp_data.block_for_results() + def test_metadata_serialization(self): """Test experiment metadata serialization.""" metadata = {"complex": 2 + 3j, "numpy": np.zeros(2)} From 859e83f5d7ee755d1f44f30338ca86ca4a07230b Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 8 Jul 2021 14:37:08 -0400 Subject: [PATCH 50/70] lint --- qiskit_experiments/database_service/db_experiment_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index b211e16298..5003565e83 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -27,7 +27,6 @@ from qiskit.providers import Job, BaseJob, Backend, BaseBackend, Provider from qiskit.result import Result from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES -from qiskit.providers.exceptions import JobError from qiskit.visualization import HAS_MATPLOTLIB from .database_service import DatabaseServiceV1 From 7dc8bbc0cfc185357bbe20afe65b714a14dc6f0a Mon Sep 17 00:00:00 2001 From: jessieyu Date: Fri, 9 Jul 2021 17:15:44 -0400 Subject: [PATCH 51/70] fix test --- test/calibration/experiments/test_rabi.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index b143599810..56161b7e9a 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -109,10 +109,11 @@ def test_wrong_processor(self): rabi.set_analysis_options(data_processor=DataProcessor(fail_key, [])) rabi.set_run_options(shots=2) data = rabi.run(backend) - result = data.analysis_result(0) + data.block_for_results() + result = data.analysis_results(0) self.assertTrue( - f"The input key {fail_key} was not found" in result["error_message"].message + f"The input key {fail_key} was not found" in result.data()["error_message"].message ) From 8a50a45c274d4ae7279adbd2238ac5cc5e943f86 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Fri, 9 Jul 2021 17:24:14 -0400 Subject: [PATCH 52/70] return DbAnalysisResultV1 for t1 t2 --- qiskit_experiments/characterization/t1_analysis.py | 3 +-- qiskit_experiments/characterization/t2ramsey_analysis.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/characterization/t1_analysis.py b/qiskit_experiments/characterization/t1_analysis.py index 1a0d8fbe88..76d718d734 100644 --- a/qiskit_experiments/characterization/t1_analysis.py +++ b/qiskit_experiments/characterization/t1_analysis.py @@ -25,7 +25,6 @@ from qiskit_experiments.analysis.curve_fitting import ( process_curve_data, curve_fit, - CurveAnalysisResult, ) from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting @@ -162,7 +161,7 @@ def fit_fun(x, a, tau, c): else: figures = None - res_v1 = CurveAnalysisResult( + res_v1 = DbAnalysisResultV1( result_data=result_data, result_type="T1", device_components=[Qubit(data[0]["metadata"]["qubit"])], diff --git a/qiskit_experiments/characterization/t2ramsey_analysis.py b/qiskit_experiments/characterization/t2ramsey_analysis.py index bc88d0bca6..3dc39540d9 100644 --- a/qiskit_experiments/characterization/t2ramsey_analysis.py +++ b/qiskit_experiments/characterization/t2ramsey_analysis.py @@ -22,7 +22,6 @@ from qiskit_experiments.analysis.curve_fitting import ( curve_fit, process_curve_data, - CurveAnalysisResult, ) from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting @@ -130,7 +129,7 @@ def _format_plot(ax, unit): if unit == "dt": result_data["fit"]["dt"] = conversion_factor - analysis_result = CurveAnalysisResult( + analysis_result = DbAnalysisResultV1( result_data=result_data, result_type="T2Ramsey", device_components=[Qubit(circ_metadata["qubit"])], From 0d6e3c91ee026af2ee656f33cce405ed18c5119a Mon Sep 17 00:00:00 2001 From: jessieyu Date: Tue, 13 Jul 2021 21:17:49 -0400 Subject: [PATCH 53/70] review comments --- qiskit_experiments/analysis/__init__.py | 1 + qiskit_experiments/analysis/curve_analysis.py | 25 +++++++++++-------- qiskit_experiments/analysis/curve_fitting.py | 18 ++++++++----- .../calibration/analysis/drag_analysis.py | 4 +-- .../analysis/oscillation_analysis.py | 3 ++- .../characterization/resonance_analysis.py | 3 ++- .../database_service/db_experiment_data.py | 2 +- qiskit_experiments/experiment_data.py | 2 +- .../interleaved_rb_analysis.py | 3 ++- .../randomized_benchmarking/rb_analysis.py | 3 ++- 10 files changed, 39 insertions(+), 25 deletions(-) diff --git a/qiskit_experiments/analysis/__init__.py b/qiskit_experiments/analysis/__init__.py index d17b7bfcdc..44e27df6cd 100644 --- a/qiskit_experiments/analysis/__init__.py +++ b/qiskit_experiments/analysis/__init__.py @@ -79,6 +79,7 @@ from .curve_fitting import ( CurveAnalysisResult, + CurveAnalysisResultData, curve_fit, multi_curve_fit, process_curve_data, diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 0d7463256d..c33baba38d 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -24,7 +24,11 @@ from qiskit.providers.options import Options from qiskit_experiments.analysis import plotting -from qiskit_experiments.analysis.curve_fitting import multi_curve_fit, CurveAnalysisResult +from qiskit_experiments.analysis.curve_fitting import ( + multi_curve_fit, + CurveAnalysisResult, + CurveAnalysisResultData, +) from qiskit_experiments.analysis.utils import get_opt_value, get_opt_error from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.data_processing import DataProcessor @@ -365,17 +369,15 @@ def data_processor(data: Dict[str, Any]) -> Tuple[float, float] ) @requires_matplotlib - def _create_figures(self, result_data: Dict) -> List["Figure"]: + def _create_figures(self, result_data: CurveAnalysisResultData) -> List["Figure"]: """Create new figures with the fit result and raw data. - Subclass can override this method to create different type of figures. - - Note: - The ``requires_matplotlib`` decorator is needed to ensure this method - works with ``DbExperimentData``. + Subclass can override this method to create different type of figures, but + the ``requires_matplotlib`` decorator is needed to ensure this method + works with ``DbExperimentData``. Args: - result_data: Result containing fit parameters. + result_data: Result data containing fit parameters. Returns: List of figures. @@ -566,7 +568,7 @@ def _format_data(self, data: CurveData) -> CurveData: metadata=data.metadata, ) - def _post_analysis(self, result_data: Dict) -> Dict: + def _post_analysis(self, result_data: CurveAnalysisResultData) -> CurveAnalysisResultData: """Calculate new quantity from the fit result. Subclasses can override this method to do post analysis. @@ -575,7 +577,7 @@ def _post_analysis(self, result_data: Dict) -> Dict: result_data: Result containing fit result. Returns: - New CurveAnalysisResult instance containing the result of post analysis. + Updated result data containing the result of post analysis. """ return result_data @@ -915,7 +917,8 @@ def _run_analysis( Raises: AnalysisError: if the analysis fails. """ - result_data = {"analysis_type": self.__class__.__name__} + result_data = CurveAnalysisResultData() + result_data["analysis_type"] = self.__class__.__name__ figures = list() # diff --git a/qiskit_experiments/analysis/curve_fitting.py b/qiskit_experiments/analysis/curve_fitting.py index dac76073aa..b78ffa1eca 100644 --- a/qiskit_experiments/analysis/curve_fitting.py +++ b/qiskit_experiments/analysis/curve_fitting.py @@ -23,8 +23,14 @@ from qiskit_experiments.database_service import DbAnalysisResultV1 +class CurveAnalysisResultData(dict): + """Analysis data container for curve fit analysis.""" + + pass + + class CurveAnalysisResult(DbAnalysisResultV1): - """Analysis data container for curve fit analysis. + """Curve fit analysis result. Class Attributes: __keys_not_shown__: Data keys of analysis result which are not directly shown @@ -80,7 +86,7 @@ def curve_fit( sigma: Optional[np.ndarray] = None, bounds: Optional[Union[Dict[str, Tuple[float, float]], Tuple[np.ndarray, np.ndarray]]] = None, **kwargs, -) -> Dict: +) -> CurveAnalysisResultData: r"""Perform a non-linear least squares to fit This solves the optimization problem @@ -201,7 +207,7 @@ def fit_func(x, *params): "xrange": xdata_range, } - return result + return CurveAnalysisResultData(result) def multi_curve_fit( @@ -214,7 +220,7 @@ def multi_curve_fit( weights: Optional[np.ndarray] = None, bounds: Optional[Union[Dict[str, Tuple[float, float]], Tuple[np.ndarray, np.ndarray]]] = None, **kwargs, -) -> Dict: +) -> CurveAnalysisResultData: r"""Perform a linearized multi-objective non-linear least squares fit. This solves the optimization problem @@ -294,9 +300,9 @@ def f(x, *args, **kwargs): return y # Run linearized curve_fit - analysis_result = curve_fit(f, xdata, ydata, p0, sigma=wsigma, bounds=bounds, **kwargs) + result_data = curve_fit(f, xdata, ydata, p0, sigma=wsigma, bounds=bounds, **kwargs) - return analysis_result + return result_data def process_curve_data( diff --git a/qiskit_experiments/calibration/analysis/drag_analysis.py b/qiskit_experiments/calibration/analysis/drag_analysis.py index 77e0a4bf60..a3c699b901 100644 --- a/qiskit_experiments/calibration/analysis/drag_analysis.py +++ b/qiskit_experiments/calibration/analysis/drag_analysis.py @@ -17,7 +17,7 @@ from qiskit_experiments.analysis import ( CurveAnalysis, - CurveAnalysisResult, + CurveAnalysisResultData, SeriesDef, get_opt_value, get_opt_error, @@ -182,7 +182,7 @@ def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any] return fit_options - def _post_analysis(self, result_data: CurveAnalysisResult) -> CurveAnalysisResult: + def _post_analysis(self, result_data: CurveAnalysisResultData) -> CurveAnalysisResultData: """Algorithmic criteria for whether the fit is good or bad. A good fit has: diff --git a/qiskit_experiments/calibration/analysis/oscillation_analysis.py b/qiskit_experiments/calibration/analysis/oscillation_analysis.py index 4ca93d5a84..09b44eed08 100644 --- a/qiskit_experiments/calibration/analysis/oscillation_analysis.py +++ b/qiskit_experiments/calibration/analysis/oscillation_analysis.py @@ -17,6 +17,7 @@ from qiskit_experiments.analysis import ( CurveAnalysis, + CurveAnalysisResultData, SeriesDef, fit_function, guess, @@ -119,7 +120,7 @@ def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any] return fit_options - def _post_analysis(self, result_data: Dict) -> Dict: + def _post_analysis(self, result_data: CurveAnalysisResultData) -> CurveAnalysisResultData: """Algorithmic criteria for whether the fit is good or bad. A good fit has: diff --git a/qiskit_experiments/characterization/resonance_analysis.py b/qiskit_experiments/characterization/resonance_analysis.py index de7a85a1f5..2a213b7a1a 100644 --- a/qiskit_experiments/characterization/resonance_analysis.py +++ b/qiskit_experiments/characterization/resonance_analysis.py @@ -18,6 +18,7 @@ from qiskit_experiments.analysis import ( CurveAnalysis, + CurveAnalysisResultData, SeriesDef, fit_function, guess, @@ -123,7 +124,7 @@ def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any] return fit_option - def _post_analysis(self, result_data: Dict) -> Dict: + def _post_analysis(self, result_data: CurveAnalysisResultData) -> CurveAnalysisResultData: """Algorithmic criteria for whether the fit is good or bad. A good fit has: diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 5003565e83..15ea7efee7 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -97,7 +97,7 @@ class DbExperimentDataV1(DbExperimentData): def __init__( self, - experiment_type: str, + experiment_type: Optional[str] = "Unknown", backend: Optional[Union[Backend, BaseBackend]] = None, experiment_id: Optional[str] = None, tags: Optional[List[str]] = None, diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index 5c1aa855f2..47f3fa58af 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -43,7 +43,7 @@ def __init__( """ self._experiment = experiment super().__init__( - experiment_type=experiment.experiment_type if experiment else "unknown", + experiment_type=experiment.experiment_type if experiment else None, backend=backend, job_ids=job_ids, metadata=experiment._metadata() if experiment else {}, diff --git a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py index 1f968821fd..75ca44bb9c 100644 --- a/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/interleaved_rb_analysis.py @@ -18,6 +18,7 @@ from qiskit_experiments.analysis import ( SeriesDef, + CurveAnalysisResultData, fit_function, get_opt_value, get_opt_error, @@ -166,7 +167,7 @@ def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any] return fit_option - def _post_analysis(self, result_data: Dict) -> Dict: + def _post_analysis(self, result_data: CurveAnalysisResultData) -> CurveAnalysisResultData: """Calculate EPC.""" # Add EPC data nrb = 2 ** self._num_qubits diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index e2ba8e089d..74870c83a3 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -19,6 +19,7 @@ from qiskit_experiments.analysis import ( CurveAnalysis, + CurveAnalysisResultData, SeriesDef, CurveData, fit_function, @@ -163,7 +164,7 @@ def _run_analysis(self, experiment_data, **options): ) return super()._run_analysis(experiment_data, **options) - def _post_analysis(self, result_data: Dict) -> Dict: + def _post_analysis(self, result_data: CurveAnalysisResultData) -> CurveAnalysisResultData: """Calculate EPC.""" alpha = get_opt_value(result_data, "alpha") alpha_err = get_opt_error(result_data, "alpha") From 2a1981c16f4308bf7d51d3cd1bf6721398aecc52 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Wed, 14 Jul 2021 18:37:40 -0400 Subject: [PATCH 54/70] Add `load` method to `DbExperimentDataV1` and `DbAnalysisResultV1` This method parses the dicts currently returned by the IBMQ service to allow loading saved experiment data and analysis results. --- .../database_service/db_analysis_result.py | 67 ++++------ .../database_service/db_experiment_data.py | 118 +++++++++--------- 2 files changed, 85 insertions(+), 100 deletions(-) diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index d4d0652271..357287e907 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -133,55 +133,36 @@ def __init__( pass self._extra_data = kwargs - def serialize_data(self) -> str: - """Serialize result data into JSON string. - - Returns: - Serialized JSON string. - """ - return json.dumps(self._result_data, cls=self._json_encoder) - - @classmethod - def deserialize_data(cls, data: str) -> Dict: - """Deserialize experiment from JSON string. - - Args: - data: Data to be deserialized. - - Returns: - Deserialized data. - """ - return json.loads(data, cls=cls._json_decoder) - @classmethod - def from_data( - cls, - result_data: Dict, - result_type: str, - device_components: List[Union[DeviceComponent, str]], - experiment_id: str, - **kwargs, - ) -> "DbAnalysisResultV1": - """Reconstruct the analysis result from input data. + def load(cls, result_id: str, service: DatabaseServiceV1) -> "DbAnalysisResultV1": + """Load a saved analysis result from a database service. Args: - result_data: Analysis result data. - result_type: Analysis result type. - device_components: Target device components this analysis is for. - experiment_id: ID of the experiment. - **kwargs: Additional analysis result attributes. + result_id: Analysis result ID. + service: the database service. Returns: - Reconstructed analysis result. + The loaded analysis result. """ + # Load data from the service + data = service.analysis_result(result_id) + result_data = data["result_data"] + # Parse serialized data if result_data: - result_data = cls.deserialize_data(json.dumps(result_data)) - return cls( - result_data=result_data, - result_type=result_type, - device_components=device_components, - experiment_id=experiment_id, - **kwargs, + result_data = json.loads( + json.dumps(result_data, cls=cls._json_encoder), cls=cls._json_decoder + ) + # Initialize the result object + return DbAnalysisResultV1( + result_data, + result_type=data["result_type"], + device_components=data["device_components"], + experiment_id=data["experiment_id"], + result_id=data["result_id"], + quality=data["quality"], + verified=data["verified"], + tags=data["tags"], + service=data["service"], ) def save(self) -> None: @@ -196,7 +177,7 @@ def save(self) -> None: ) return - _result_data = json.loads(self.serialize_data()) + _result_data = json.loads(json.dumps(self._result_data, cls=self._json_encoder)) _result_data["_source"] = self._source new_data = { diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 15ea7efee7..9acbe28fa6 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -329,6 +329,20 @@ def _add_single_data(self, data: Dict[str, any]) -> None: """ self._data.append(data) + def _retrieve_data(self): + """Retrieve job data if missing experiment data.""" + # Get job results if missing experiment data. + if (not self._data) and self._provider: + with self._jobs.lock: + for jid in self._jobs: + if self._jobs[jid] is None: + try: + self._jobs[jid] = self._provider.retrieve_job(jid) + except Exception: # pylint: disable=broad-except + pass + if self._jobs[jid] is not None: + self._add_result_data(self._jobs[jid].result()) + def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, List[Dict]]: """Return the experiment data at the specified index. @@ -347,18 +361,7 @@ def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, Li Raises: TypeError: If the input `index` has an invalid type. """ - # Get job results if missing experiment data. - if (not self._data) and self._provider: - with self._jobs.lock: - for jid in self._jobs: - if self._jobs[jid] is None: - try: - self._jobs[jid] = self._provider.retrieve_job(jid) - except Exception: # pylint: disable=broad-except - pass - if self._jobs[jid] is not None: - self._add_result_data(self._jobs[jid].result()) - + self._retrieve_data() if index is None: return self._data.copy() if isinstance(index, (int, slice)): @@ -585,6 +588,21 @@ def delete_analysis_result( return result_key + def _retrieve_analysis_results(self, refresh: bool = False): + """Retrieve service analysis results. + + Args: + refresh: Retrieve the latest analysis results from the server, if + an experiment service is available. + """ + # Get job results if missing experiment data. + if self.service and (not self._analysis_results or refresh): + retrieved_results = self.service.analysis_results( + experiment_id=self.experiment_id, limit=None + ) + for result in retrieved_results: + self._analysis_results[result["result_id"]] = DbAnalysisResult.from_data(**result) + def analysis_results( self, index: Optional[Union[int, slice, str]] = None, refresh: bool = False ) -> Union[DbAnalysisResult, List[DbAnalysisResult]]: @@ -608,13 +626,7 @@ def analysis_results( TypeError: If the input `index` has an invalid type. DbExperimentEntryNotFound: If the entry cannot be found. """ - if self.service and (not self._analysis_results or refresh): - retrieved_results = self.service.analysis_results( - experiment_id=self.experiment_id, limit=None - ) - for result in retrieved_results: - self._analysis_results[result["result_id"]] = DbAnalysisResult.from_data(**result) - + self._retrieve_analysis_results(refresh=refresh) if index is None: return self._analysis_results.values() if isinstance(index, (int, slice)): @@ -646,7 +658,7 @@ def save(self) -> None: LOG.warning("Experiment cannot be saved because backend is missing.") return - metadata = json.loads(self.serialize_metadata()) + metadata = json.loads(json.dumps(self._metadata, cls=self._json_encoder)) metadata["_source"] = self._source update_data = { @@ -710,54 +722,46 @@ def save_all(self) -> None: self._service.delete_figure(experiment_id=self.experiment_id, figure_name=name) self._deleted_figures.remove(name) - def serialize_metadata(self) -> str: - """Serialize experiment metadata into a JSON string. - - Returns: - Serialized JSON string. - """ - return json.dumps(self._metadata, cls=self._json_encoder) - @classmethod - def deserialize_metadata(cls, data: str) -> Any: - """Deserialize experiment metadata from a JSON string. + def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "DbExperimentDataV1": + """Load a saved experiment data from a database service. Args: - data: Data to be deserialized. + experiment_id: Experiment ID. + service: the database service. Returns: - Deserialized data. + The loaded experiment data. """ - return json.loads(data, cls=cls._json_decoder) - - @classmethod - def from_data( - cls, - experiment_type: str, - experiment_id: str, - metadata: Optional[Dict] = None, - **kwargs, - ) -> "DbExperimentDataV1": - """Reconstruct a DbExperimentDataV1 using the input data. + # Load data from the service + data = service.aexperiment(experiment_id) - Args: - experiment_type: Experiment type. - experiment_id: Experiment ID. One will be generated if not supplied. - metadata: Additional experiment metadata. - **kwargs: Additional experiment attributes. - - Returns: - An DbExperimentDataV1 instance. - """ + # Parse serialized metadata + metadata = data["metadata"] if metadata: - metadata = cls.deserialize_metadata(json.dumps(metadata)) - return cls( - experiment_type=experiment_type, - experiment_id=experiment_id, + metadata = json.loads(json.dumps(metadata), cls=cls._json_decoder) + + # Initialize container + # NOTE: not sure how to parse extra_args from service data + expdata = DbExperimentDataV1( + experiment_type=data["experiment_data"], + backend=data["backend"], + experiment_id=data["experiment_id"], + tags=data["tags"], + job_ids=data["job_ids"], + share_level=data["share_level"], metadata=metadata, - **kwargs, + figure_names=data["figure_names"], + notes=data["notes"], ) + # Retrieve analysis results + # Maybe this isn't necessary but the repr of the class should + # be updated to show correct number of results including remote ones + expdata._retrieve_analysis_results() + + return expdata + def cancel_jobs(self) -> None: """Cancel any running jobs.""" for job, fut in self._job_futures.copy(): From 63e5ab744a1f48c9297bc1516282ddad4c677539 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 15 Jul 2021 11:06:13 +0300 Subject: [PATCH 55/70] Revert "Merge pull request #2 from chriseclectic/deserialize" This reverts commit e150dad50a405b5e51e4d49181db525458c524b3, reversing changes made to 0d6e3c91ee026af2ee656f33cce405ed18c5119a. --- .../database_service/db_analysis_result.py | 67 ++++++---- .../database_service/db_experiment_data.py | 118 +++++++++--------- 2 files changed, 100 insertions(+), 85 deletions(-) diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index 357287e907..d4d0652271 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -133,36 +133,55 @@ def __init__( pass self._extra_data = kwargs + def serialize_data(self) -> str: + """Serialize result data into JSON string. + + Returns: + Serialized JSON string. + """ + return json.dumps(self._result_data, cls=self._json_encoder) + + @classmethod + def deserialize_data(cls, data: str) -> Dict: + """Deserialize experiment from JSON string. + + Args: + data: Data to be deserialized. + + Returns: + Deserialized data. + """ + return json.loads(data, cls=cls._json_decoder) + @classmethod - def load(cls, result_id: str, service: DatabaseServiceV1) -> "DbAnalysisResultV1": - """Load a saved analysis result from a database service. + def from_data( + cls, + result_data: Dict, + result_type: str, + device_components: List[Union[DeviceComponent, str]], + experiment_id: str, + **kwargs, + ) -> "DbAnalysisResultV1": + """Reconstruct the analysis result from input data. Args: - result_id: Analysis result ID. - service: the database service. + result_data: Analysis result data. + result_type: Analysis result type. + device_components: Target device components this analysis is for. + experiment_id: ID of the experiment. + **kwargs: Additional analysis result attributes. Returns: - The loaded analysis result. + Reconstructed analysis result. """ - # Load data from the service - data = service.analysis_result(result_id) - result_data = data["result_data"] - # Parse serialized data if result_data: - result_data = json.loads( - json.dumps(result_data, cls=cls._json_encoder), cls=cls._json_decoder - ) - # Initialize the result object - return DbAnalysisResultV1( - result_data, - result_type=data["result_type"], - device_components=data["device_components"], - experiment_id=data["experiment_id"], - result_id=data["result_id"], - quality=data["quality"], - verified=data["verified"], - tags=data["tags"], - service=data["service"], + result_data = cls.deserialize_data(json.dumps(result_data)) + return cls( + result_data=result_data, + result_type=result_type, + device_components=device_components, + experiment_id=experiment_id, + **kwargs, ) def save(self) -> None: @@ -177,7 +196,7 @@ def save(self) -> None: ) return - _result_data = json.loads(json.dumps(self._result_data, cls=self._json_encoder)) + _result_data = json.loads(self.serialize_data()) _result_data["_source"] = self._source new_data = { diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 9acbe28fa6..15ea7efee7 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -329,20 +329,6 @@ def _add_single_data(self, data: Dict[str, any]) -> None: """ self._data.append(data) - def _retrieve_data(self): - """Retrieve job data if missing experiment data.""" - # Get job results if missing experiment data. - if (not self._data) and self._provider: - with self._jobs.lock: - for jid in self._jobs: - if self._jobs[jid] is None: - try: - self._jobs[jid] = self._provider.retrieve_job(jid) - except Exception: # pylint: disable=broad-except - pass - if self._jobs[jid] is not None: - self._add_result_data(self._jobs[jid].result()) - def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, List[Dict]]: """Return the experiment data at the specified index. @@ -361,7 +347,18 @@ def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, Li Raises: TypeError: If the input `index` has an invalid type. """ - self._retrieve_data() + # Get job results if missing experiment data. + if (not self._data) and self._provider: + with self._jobs.lock: + for jid in self._jobs: + if self._jobs[jid] is None: + try: + self._jobs[jid] = self._provider.retrieve_job(jid) + except Exception: # pylint: disable=broad-except + pass + if self._jobs[jid] is not None: + self._add_result_data(self._jobs[jid].result()) + if index is None: return self._data.copy() if isinstance(index, (int, slice)): @@ -588,21 +585,6 @@ def delete_analysis_result( return result_key - def _retrieve_analysis_results(self, refresh: bool = False): - """Retrieve service analysis results. - - Args: - refresh: Retrieve the latest analysis results from the server, if - an experiment service is available. - """ - # Get job results if missing experiment data. - if self.service and (not self._analysis_results or refresh): - retrieved_results = self.service.analysis_results( - experiment_id=self.experiment_id, limit=None - ) - for result in retrieved_results: - self._analysis_results[result["result_id"]] = DbAnalysisResult.from_data(**result) - def analysis_results( self, index: Optional[Union[int, slice, str]] = None, refresh: bool = False ) -> Union[DbAnalysisResult, List[DbAnalysisResult]]: @@ -626,7 +608,13 @@ def analysis_results( TypeError: If the input `index` has an invalid type. DbExperimentEntryNotFound: If the entry cannot be found. """ - self._retrieve_analysis_results(refresh=refresh) + if self.service and (not self._analysis_results or refresh): + retrieved_results = self.service.analysis_results( + experiment_id=self.experiment_id, limit=None + ) + for result in retrieved_results: + self._analysis_results[result["result_id"]] = DbAnalysisResult.from_data(**result) + if index is None: return self._analysis_results.values() if isinstance(index, (int, slice)): @@ -658,7 +646,7 @@ def save(self) -> None: LOG.warning("Experiment cannot be saved because backend is missing.") return - metadata = json.loads(json.dumps(self._metadata, cls=self._json_encoder)) + metadata = json.loads(self.serialize_metadata()) metadata["_source"] = self._source update_data = { @@ -722,46 +710,54 @@ def save_all(self) -> None: self._service.delete_figure(experiment_id=self.experiment_id, figure_name=name) self._deleted_figures.remove(name) + def serialize_metadata(self) -> str: + """Serialize experiment metadata into a JSON string. + + Returns: + Serialized JSON string. + """ + return json.dumps(self._metadata, cls=self._json_encoder) + @classmethod - def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "DbExperimentDataV1": - """Load a saved experiment data from a database service. + def deserialize_metadata(cls, data: str) -> Any: + """Deserialize experiment metadata from a JSON string. Args: - experiment_id: Experiment ID. - service: the database service. + data: Data to be deserialized. Returns: - The loaded experiment data. + Deserialized data. """ - # Load data from the service - data = service.aexperiment(experiment_id) + return json.loads(data, cls=cls._json_decoder) + + @classmethod + def from_data( + cls, + experiment_type: str, + experiment_id: str, + metadata: Optional[Dict] = None, + **kwargs, + ) -> "DbExperimentDataV1": + """Reconstruct a DbExperimentDataV1 using the input data. - # Parse serialized metadata - metadata = data["metadata"] + Args: + experiment_type: Experiment type. + experiment_id: Experiment ID. One will be generated if not supplied. + metadata: Additional experiment metadata. + **kwargs: Additional experiment attributes. + + Returns: + An DbExperimentDataV1 instance. + """ if metadata: - metadata = json.loads(json.dumps(metadata), cls=cls._json_decoder) - - # Initialize container - # NOTE: not sure how to parse extra_args from service data - expdata = DbExperimentDataV1( - experiment_type=data["experiment_data"], - backend=data["backend"], - experiment_id=data["experiment_id"], - tags=data["tags"], - job_ids=data["job_ids"], - share_level=data["share_level"], + metadata = cls.deserialize_metadata(json.dumps(metadata)) + return cls( + experiment_type=experiment_type, + experiment_id=experiment_id, metadata=metadata, - figure_names=data["figure_names"], - notes=data["notes"], + **kwargs, ) - # Retrieve analysis results - # Maybe this isn't necessary but the repr of the class should - # be updated to show correct number of results including remote ones - expdata._retrieve_analysis_results() - - return expdata - def cancel_jobs(self) -> None: """Cancel any running jobs.""" for job, fut in self._job_futures.copy(): From fcace07b4738ed54f6b20cf0d9e0dd16ab6925c0 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 15 Jul 2021 11:56:41 +0300 Subject: [PATCH 56/70] updated tomography qubits --- qiskit_experiments/tomography/tomography_analysis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/tomography/tomography_analysis.py b/qiskit_experiments/tomography/tomography_analysis.py index ec2a245745..fc0879f973 100644 --- a/qiskit_experiments/tomography/tomography_analysis.py +++ b/qiskit_experiments/tomography/tomography_analysis.py @@ -26,6 +26,7 @@ from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.database_service import DbAnalysisResultV1 +from qiskit_experiments.database_service.device_component import Qubit from qiskit_experiments.base_analysis import BaseAnalysis, Options from .fitters import ( linear_inversion, @@ -127,7 +128,9 @@ def _run_analysis(self, experiment_data, **options): analysis_result = DbAnalysisResultV1( result_data=result, result_type="Tomography", - device_components=[], + device_components=[ + Qubit(qubit) for qubit in experiment_data.metadata()["physical_qubits"] + ], experiment_id=experiment_data.experiment_id, quality=None, ) From 89391c28c2f8b9fcb88a53f8dc55ddd8b5b2effc Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Wed, 14 Jul 2021 18:37:40 -0400 Subject: [PATCH 57/70] Add `load` method to `DbExperimentDataV1` and `DbAnalysisResultV1` This method parses the dicts currently returned by the IBMQ service to allow loading saved experiment data and analysis results. --- .../database_service/db_analysis_result.py | 80 ++++++------ .../database_service/db_experiment_data.py | 117 +++++++++--------- .../test_db_analysis_result.py | 2 +- .../test_db_experiment_data.py | 4 +- 4 files changed, 98 insertions(+), 105 deletions(-) diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index d4d0652271..b2b8190a81 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -133,56 +133,19 @@ def __init__( pass self._extra_data = kwargs - def serialize_data(self) -> str: - """Serialize result data into JSON string. - - Returns: - Serialized JSON string. - """ - return json.dumps(self._result_data, cls=self._json_encoder) - - @classmethod - def deserialize_data(cls, data: str) -> Dict: - """Deserialize experiment from JSON string. - - Args: - data: Data to be deserialized. - - Returns: - Deserialized data. - """ - return json.loads(data, cls=cls._json_decoder) - @classmethod - def from_data( - cls, - result_data: Dict, - result_type: str, - device_components: List[Union[DeviceComponent, str]], - experiment_id: str, - **kwargs, - ) -> "DbAnalysisResultV1": - """Reconstruct the analysis result from input data. + def load(cls, result_id: str, service: DatabaseServiceV1) -> "DbAnalysisResultV1": + """Load a saved analysis result from a database service. Args: - result_data: Analysis result data. - result_type: Analysis result type. - device_components: Target device components this analysis is for. - experiment_id: ID of the experiment. - **kwargs: Additional analysis result attributes. + result_id: Analysis result ID. + service: the database service. Returns: - Reconstructed analysis result. + The loaded analysis result. """ - if result_data: - result_data = cls.deserialize_data(json.dumps(result_data)) - return cls( - result_data=result_data, - result_type=result_type, - device_components=device_components, - experiment_id=experiment_id, - **kwargs, - ) + # Load data from the service + return cls._from_data(service.analysis_result(result_id)) def save(self) -> None: """Save this analysis result in the database. @@ -196,7 +159,7 @@ def save(self) -> None: ) return - _result_data = json.loads(self.serialize_data()) + _result_data = json.loads(json.dumps(self._result_data, cls=self._json_encoder)) _result_data["_source"] = self._source new_data = { @@ -220,6 +183,33 @@ def save(self) -> None: update_data=update_data, ) + @classmethod + def _from_service_data(cls, service_data: Dict) -> "DbAnalysisResultV1": + """Construct an analysis result from saved database service data. + + Args: + service_data: Analysis result data. + + Returns: + The loaded analysis result. + """ + # Parse serialized data + result_data = service_data["result_data"] + if result_data: + result_data = json.loads(json.dumps(result_data), cls=cls._json_decoder) + # Initialize the result object + return DbAnalysisResultV1( + result_data, + result_type=service_data["result_type"], + device_components=service_data["device_components"], + experiment_id=service_data["experiment_id"], + result_id=service_data["result_id"], + quality=service_data["quality"], + verified=service_data["verified"], + tags=service_data["tags"], + service=service_data["service"], + ) + def data(self) -> Dict: """Return analysis result data. diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 15ea7efee7..56bb136a2c 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -329,6 +329,20 @@ def _add_single_data(self, data: Dict[str, any]) -> None: """ self._data.append(data) + def _retrieve_data(self): + """Retrieve job data if missing experiment data.""" + # Get job results if missing experiment data. + if (not self._data) and self._provider: + with self._jobs.lock: + for jid in self._jobs: + if self._jobs[jid] is None: + try: + self._jobs[jid] = self._provider.retrieve_job(jid) + except Exception: # pylint: disable=broad-except + pass + if self._jobs[jid] is not None: + self._add_result_data(self._jobs[jid].result()) + def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, List[Dict]]: """Return the experiment data at the specified index. @@ -347,18 +361,7 @@ def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, Li Raises: TypeError: If the input `index` has an invalid type. """ - # Get job results if missing experiment data. - if (not self._data) and self._provider: - with self._jobs.lock: - for jid in self._jobs: - if self._jobs[jid] is None: - try: - self._jobs[jid] = self._provider.retrieve_job(jid) - except Exception: # pylint: disable=broad-except - pass - if self._jobs[jid] is not None: - self._add_result_data(self._jobs[jid].result()) - + self._retrieve_data() if index is None: return self._data.copy() if isinstance(index, (int, slice)): @@ -585,6 +588,23 @@ def delete_analysis_result( return result_key + def _retrieve_analysis_results(self, refresh: bool = False): + """Retrieve service analysis results. + + Args: + refresh: Retrieve the latest analysis results from the server, if + an experiment service is available. + """ + # Get job results if missing experiment data. + if self.service and (not self._analysis_results or refresh): + retrieved_results = self.service.analysis_results( + experiment_id=self.experiment_id, limit=None + ) + for result in retrieved_results: + self._analysis_results[result["result_id"]] = DbAnalysisResult._from_service_data( + result + ) + def analysis_results( self, index: Optional[Union[int, slice, str]] = None, refresh: bool = False ) -> Union[DbAnalysisResult, List[DbAnalysisResult]]: @@ -608,13 +628,7 @@ def analysis_results( TypeError: If the input `index` has an invalid type. DbExperimentEntryNotFound: If the entry cannot be found. """ - if self.service and (not self._analysis_results or refresh): - retrieved_results = self.service.analysis_results( - experiment_id=self.experiment_id, limit=None - ) - for result in retrieved_results: - self._analysis_results[result["result_id"]] = DbAnalysisResult.from_data(**result) - + self._retrieve_analysis_results(refresh=refresh) if index is None: return self._analysis_results.values() if isinstance(index, (int, slice)): @@ -646,7 +660,7 @@ def save(self) -> None: LOG.warning("Experiment cannot be saved because backend is missing.") return - metadata = json.loads(self.serialize_metadata()) + metadata = json.loads(json.dumps(self._metadata, cls=self._json_encoder)) metadata["_source"] = self._source update_data = { @@ -710,53 +724,42 @@ def save_all(self) -> None: self._service.delete_figure(experiment_id=self.experiment_id, figure_name=name) self._deleted_figures.remove(name) - def serialize_metadata(self) -> str: - """Serialize experiment metadata into a JSON string. - - Returns: - Serialized JSON string. - """ - return json.dumps(self._metadata, cls=self._json_encoder) - @classmethod - def deserialize_metadata(cls, data: str) -> Any: - """Deserialize experiment metadata from a JSON string. + def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "DbExperimentDataV1": + """Load a saved experiment data from a database service. Args: - data: Data to be deserialized. + experiment_id: Experiment ID. + service: the database service. Returns: - Deserialized data. + The loaded experiment data. """ - return json.loads(data, cls=cls._json_decoder) - - @classmethod - def from_data( - cls, - experiment_type: str, - experiment_id: str, - metadata: Optional[Dict] = None, - **kwargs, - ) -> "DbExperimentDataV1": - """Reconstruct a DbExperimentDataV1 using the input data. - - Args: - experiment_type: Experiment type. - experiment_id: Experiment ID. One will be generated if not supplied. - metadata: Additional experiment metadata. - **kwargs: Additional experiment attributes. + service_data = service.experiment(experiment_id) - Returns: - An DbExperimentDataV1 instance. - """ + # Parse serialized metadata + metadata = service_data["metadata"] if metadata: - metadata = cls.deserialize_metadata(json.dumps(metadata)) - return cls( - experiment_type=experiment_type, - experiment_id=experiment_id, + metadata = json.loads(json.dumps(metadata), cls=cls._json_decoder) + + # Initialize container + # NOTE: not sure how to parse extra_args from service data + expdata = DbExperimentDataV1( + experiment_type=service_data["experiment_data"], + backend=service_data["backend"], + experiment_id=service_data["experiment_id"], + tags=service_data["tags"], + job_ids=service_data["job_ids"], + share_level=service_data["share_level"], metadata=metadata, - **kwargs, + figure_names=service_data["figure_names"], + notes=service_data["notes"], ) + # Retrieve analysis results + # Maybe this isn't necessary but the repr of the class should + # be updated to show correct number of results including remote ones + expdata._retrieve_analysis_results() + return expdata def cancel_jobs(self) -> None: """Cancel any running jobs.""" diff --git a/test/database_service/test_db_analysis_result.py b/test/database_service/test_db_analysis_result.py index c0b7829816..2f2782bbb2 100644 --- a/test/database_service/test_db_analysis_result.py +++ b/test/database_service/test_db_analysis_result.py @@ -122,7 +122,7 @@ def test_additional_attr(self): def test_data_serialization(self): """Test result data serialization.""" result = self._new_analysis_result(result_data={"complex": 2 + 3j, "numpy": np.zeros(2)}) - serialized = result.serialize_data() + serialized = json.dumps(result._result_data, cls=result._json_encoder) self.assertIsInstance(serialized, str) self.assertTrue(json.loads(serialized)) diff --git a/test/database_service/test_db_experiment_data.py b/test/database_service/test_db_experiment_data.py index 3e3902e75b..2cc0196648 100644 --- a/test/database_service/test_db_experiment_data.py +++ b/test/database_service/test_db_experiment_data.py @@ -641,11 +641,11 @@ def test_metadata_serialization(self): """Test experiment metadata serialization.""" metadata = {"complex": 2 + 3j, "numpy": np.zeros(2)} exp_data = DbExperimentData(experiment_type="qiskit_test", metadata=metadata) - serialized = exp_data.serialize_metadata() + serialized = json.dumps(exp_data._metadata, cls=exp_data._json_encoder) self.assertIsInstance(serialized, str) self.assertTrue(json.loads(serialized)) - deserialized = DbExperimentData.deserialize_metadata(serialized) + deserialized = json.loads(serialized, cls=exp_data._json_decoder) self.assertEqual(metadata["complex"], deserialized["complex"]) self.assertEqual(metadata["numpy"].all(), deserialized["numpy"].all()) From fed136d14b1513a8c42533f7fbcb0e3e5d073ba4 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Thu, 15 Jul 2021 11:00:43 -0400 Subject: [PATCH 58/70] Rename `save` to `save_metadata`, `save_all` to `save` --- .../database_service/db_experiment_data.py | 37 ++++++++++--------- .../test_db_experiment_data.py | 20 +++++----- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 56bb136a2c..8beb5bfdda 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -51,7 +51,7 @@ def auto_save(func: Callable): def _wrapped(self, *args, **kwargs): return_val = func(self, *args, **kwargs) if self.auto_save: - self.save() + self.save_metadata() return return_val return _wrapped @@ -244,7 +244,7 @@ def add_data( ) ) if self.auto_save: - self.save() + self.save_metadata() return if isinstance(data, dict): @@ -640,17 +640,15 @@ def analysis_results( raise TypeError(f"Invalid index type {type(index)}.") - def save(self) -> None: - """Save this experiment in the database. + def save_metadata(self) -> None: + """Save this experiments metadata to a database service. + + .. note:: + This method does not save analysis results nor figures. + Use :meth:`save` for general saving of all experiment data. - Note: - Not all experiment properties are saved. See :meth:`qiskit.providers.experiment.DatabaseServiceV1.create_experiment` for fields that are saved. - - Note: - Note that this method does not save analysis results nor figures. Use - ``save_all()`` if you want to save those. """ if not self._service: LOG.warning("Experiment cannot be saved because no experiment service is available.") @@ -682,18 +680,23 @@ def save(self) -> None: update_data=update_data, ) - def save_all(self) -> None: - """Save this experiment and its analysis results and figures in the database. + def save(self) -> None: + """Save the experiment data to a database service. + + .. note:: + This saves the experiment metadata, all analysis results, and all + figures. Depending on the number of figures and analysis results this + operation could take a while. - Note: - Depending on the amount of data, this operation could take a while. + To only update a previously saved experiments metadata (eg for + additional tags or notes) use :meth:`save_metadata`. """ # TODO - track changes if not self._service: LOG.warning("Experiment cannot be saved because no experiment service is available.") return - self.save() + self.save_metadata() for result in self._analysis_results.values(): result.save() @@ -964,7 +967,7 @@ def share_level(self, new_level: str) -> None: """ self._share_level = new_level if self.auto_save: - self.save() + self.save_metadata() @property def notes(self) -> str: @@ -984,7 +987,7 @@ def notes(self, new_notes: str) -> None: """ self._notes = new_notes if self.auto_save: - self.save() + self.save_metadata() @property def service(self) -> Optional[DatabaseServiceV1]: diff --git a/test/database_service/test_db_experiment_data.py b/test/database_service/test_db_experiment_data.py index 2cc0196648..2620c87dbc 100644 --- a/test/database_service/test_db_experiment_data.py +++ b/test/database_service/test_db_experiment_data.py @@ -362,7 +362,7 @@ def test_delayed_backend(self): exp_data = DbExperimentData(experiment_type="qiskit_test") self.assertIsNone(exp_data.backend) self.assertIsNone(exp_data.service) - exp_data.save() + exp_data.save_metadata() a_job = mock.create_autospec(Job, instance=True) exp_data.add_data(a_job) self.assertIsNotNone(exp_data.backend) @@ -418,21 +418,21 @@ def test_delete_analysis_result(self): exp_data.delete_analysis_result(del_key) self.assertRaises(DbExperimentEntryNotFound, exp_data.analysis_results, res_id) - def test_save(self): - """Test saving experiment data.""" + def test_save_metadata(self): + """Test saving experiment metadata.""" exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") service = mock.create_autospec(DatabaseServiceV1, instance=True) exp_data.service = service - exp_data.save() + exp_data.save_metadata() service.create_experiment.assert_called_once() _, kwargs = service.create_experiment.call_args self.assertEqual(exp_data.experiment_id, kwargs["experiment_id"]) - exp_data.save() + exp_data.save_metadata() service.update_experiment.assert_called_once() _, kwargs = service.update_experiment.call_args self.assertEqual(exp_data.experiment_id, kwargs["experiment_id"]) - def test_save_all(self): + def test_save(self): """Test saving all experiment related data.""" exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") service = mock.create_autospec(DatabaseServiceV1, instance=True) @@ -440,12 +440,12 @@ def test_save_all(self): analysis_result = mock.MagicMock() exp_data.add_analysis_results(analysis_result) exp_data.service = service - exp_data.save_all() + exp_data.save() service.create_experiment.assert_called_once() service.create_figure.assert_called_once() analysis_result.save.assert_called_once() - def test_save_all_delete(self): + def test_save_delete(self): """Test saving all deletion.""" exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") service = mock.create_autospec(DatabaseServiceV1, instance=True) @@ -455,7 +455,7 @@ def test_save_all_delete(self): exp_data.delete_figure(0) exp_data.service = service - exp_data.save_all() + exp_data.save() service.create_experiment.assert_called_once() service.delete_figure.assert_called_once() service.delete_analysis_result.assert_called_once() @@ -507,7 +507,7 @@ def test_auto_save(self): exp_data.auto_save = True mock_result = mock.MagicMock() - exp_data.save() + exp_data.save_metadata() subtests = [ # update function, update parameters, service called From 5bbd07f8a623b267fd1bab62c47f3fa73a7bd646 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 15 Jul 2021 18:05:54 +0300 Subject: [PATCH 59/70] changed analysis result classes --- qiskit_experiments/analysis/__init__.py | 1 - qiskit_experiments/analysis/curve_analysis.py | 18 ++-------- qiskit_experiments/analysis/curve_fitting.py | 13 +++---- qiskit_experiments/base_analysis.py | 34 ++++++++++++++----- .../characterization/t1_analysis.py | 11 +----- .../characterization/t2ramsey_analysis.py | 10 +----- .../composite/composite_analysis.py | 17 ++++------ .../database_service/db_analysis_result.py | 8 ++--- qiskit_experiments/experiment_data.py | 15 ++++++++ .../tomography/tomography_analysis.py | 12 +------ test/analysis/test_curve_fit.py | 10 +++--- test/test_t1.py | 6 ++-- 12 files changed, 66 insertions(+), 89 deletions(-) diff --git a/qiskit_experiments/analysis/__init__.py b/qiskit_experiments/analysis/__init__.py index 44e27df6cd..fbce8f1f2e 100644 --- a/qiskit_experiments/analysis/__init__.py +++ b/qiskit_experiments/analysis/__init__.py @@ -78,7 +78,6 @@ from .curve_analysis import CurveAnalysis, SeriesDef, CurveData from .curve_fitting import ( - CurveAnalysisResult, CurveAnalysisResultData, curve_fit, multi_curve_fit, diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index c33baba38d..376c57dce2 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -26,7 +26,6 @@ from qiskit_experiments.analysis import plotting from qiskit_experiments.analysis.curve_fitting import ( multi_curve_fit, - CurveAnalysisResult, CurveAnalysisResultData, ) from qiskit_experiments.analysis.utils import get_opt_value, get_opt_error @@ -35,7 +34,6 @@ from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ExperimentData -from qiskit_experiments.database_service import DbAnalysisResultV1 from qiskit_experiments.database_service.device_component import Qubit from qiskit_experiments.matplotlib import requires_matplotlib from qiskit_experiments.data_processing.processor_library import get_processor @@ -329,7 +327,7 @@ def curve_fitter( bounds: Optional[ Union[Dict[str, Tuple[float, float]], Tuple[ndarray, ndarray]] ], - ) -> CurveAnalysisResult: + ) -> CurveAnalysisResultData: See :func:`~qiskit_experiment.analysis.multi_curve_fit` for example. data_processor: A callback function to format experiment data. @@ -901,7 +899,7 @@ def _get_option(self, arg_name: str) -> Any: def _run_analysis( self, experiment_data: ExperimentData, **options - ) -> Tuple[List[DbAnalysisResultV1], List["pyplot.Figure"]]: + ) -> Tuple[List["AnalysisResult"], List["pyplot.Figure"]]: """Run analysis on circuit data. Args: @@ -1080,14 +1078,4 @@ def _run_analysis( } result_data["raw_data"] = raw_data_dict - analysis_result = CurveAnalysisResult( - result_data=result_data, - result_type=result_data["analysis_type"], - device_components=[Qubit(qubit) for qubit in self._physical_qubits] - if self._physical_qubits - else [], - experiment_id=experiment_data.experiment_id, - quality=result_data.get("quality", None), - ) - - return [analysis_result], figures + return [result_data], figures diff --git a/qiskit_experiments/analysis/curve_fitting.py b/qiskit_experiments/analysis/curve_fitting.py index b78ffa1eca..4dc95b3425 100644 --- a/qiskit_experiments/analysis/curve_fitting.py +++ b/qiskit_experiments/analysis/curve_fitting.py @@ -21,16 +21,11 @@ from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.analysis.data_processing import filter_data from qiskit_experiments.database_service import DbAnalysisResultV1 +from qiskit_experiments.experiment_data import AnalysisResultData -class CurveAnalysisResultData(dict): - """Analysis data container for curve fit analysis.""" - - pass - - -class CurveAnalysisResult(DbAnalysisResultV1): - """Curve fit analysis result. +class CurveAnalysisResultData(AnalysisResultData): + """Analysis data container for curve fit analysis. Class Attributes: __keys_not_shown__: Data keys of analysis result which are not directly shown @@ -73,7 +68,7 @@ def __str__(self): for key, value, error in zip(popt_keys, popt, popt_err): out += f"\n - {key}: {value} \u00B1 {error}" - out = super().__str__() + out + out = str(super()) + out return out diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 02b6267786..5f085c34c1 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -89,20 +89,36 @@ def run( analysis_options = analysis_options.__dict__ # Run analysis + analysis_result_parameters = { + "result_type": experiment_data.experiment_type, + "experiment_id": experiment_data.experiment_id, + } + if "physical_qubits" in experiment_data.metadata(): + analysis_result_parameters["device_components"] = [ + Qubit(qubit) for qubit in experiment_data.metadata()["physical_qubits"] + ] + else: + analysis_result_parameters["device_components"] = [] try: - analysis_results, figures = self._run_analysis(experiment_data, **analysis_options) - for res in analysis_results: - if "success" not in res.data(): - res._result_data["success"] = True + result_datum, figures = self._run_analysis(experiment_data, **analysis_options) + analysis_results = [] + for res in result_datum: + if "success" not in res: + res["success"] = True + analysis_result = DbAnalysisResultV1( + result_data=res, + **analysis_result_parameters + ) + if "quality" in res: + analysis_result.quality = res["quality"] + analysis_result.verified = True + analysis_results.append(analysis_result) + except AnalysisError as ex: analysis_results = [ DbAnalysisResultV1( result_data={"success": False, "error_message": ex}, - result_type=experiment_data.experiment_type, - device_components=[ - Qubit(qubit) for qubit in experiment_data.metadata()["physical_qubits"] - ], - experiment_id=experiment_data.experiment_id, + **analysis_result_parameters ) ] figures = None diff --git a/qiskit_experiments/characterization/t1_analysis.py b/qiskit_experiments/characterization/t1_analysis.py index 12a5e73fd0..71f7de6482 100644 --- a/qiskit_experiments/characterization/t1_analysis.py +++ b/qiskit_experiments/characterization/t1_analysis.py @@ -161,16 +161,7 @@ def fit_fun(x, a, tau, c): else: figures = None - res_v1 = DbAnalysisResultV1( - result_data=result_data, - result_type="T1", - device_components=[Qubit(data[0]["metadata"]["qubit"])], - experiment_id=experiment_data.experiment_id, - quality=result_data["quality"], - verified=True, - ) - - return [res_v1], figures + return [result_data], figures @staticmethod def _fit_quality(fit_out, fit_err, reduced_chisq): diff --git a/qiskit_experiments/characterization/t2ramsey_analysis.py b/qiskit_experiments/characterization/t2ramsey_analysis.py index 8fe8447c3a..b1303fc2e6 100644 --- a/qiskit_experiments/characterization/t2ramsey_analysis.py +++ b/qiskit_experiments/characterization/t2ramsey_analysis.py @@ -146,15 +146,7 @@ def _format_plot(ax, unit, fit_result, conversion_factor): if unit == "dt": result_data["fit"]["dt"] = conversion_factor - analysis_result = DbAnalysisResultV1( - result_data=result_data, - result_type="T2Ramsey", - device_components=[Qubit(circ_metadata["qubit"])], - experiment_id=experiment_data.experiment_id, - quality=result_data["quality"], - ) - - return [analysis_result], figures + return [result_data], figures def _t2ramsey_default_params( self, diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 2cb99debab..23f0cc1ddd 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -67,15 +67,10 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): sub_ids.append(expdata.experiment_id) sub_qubits.append(expdata.experiment.physical_qubits) - analysis_result = DbAnalysisResultV1( - result_data={ - "experiment_types": sub_types, - "experiment_ids": sub_ids, - "experiment_qubits": sub_qubits, - }, - result_type="composite", - device_components=[Qubit(qidx) for qidx in sub_qubits], - experiment_id=experiment_data.experiment_id, - ) + result_data = { + "experiment_types": sub_types, + "experiment_ids": sub_ids, + "experiment_qubits": sub_qubits, + } - return [analysis_result], None + return [result_data], None diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index b2b8190a81..d3fff7a4f8 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -66,8 +66,6 @@ class DbAnalysisResultV1(DbAnalysisResult): _json_decoder = ExperimentDecoder _extra_data = {} - __keys_not_shown__ = tuple() - """Data keys of analysis result which are not directly shown in `__str__` method""" def __init__( self, @@ -365,8 +363,6 @@ def __str__(self): if self.tags(): ret += f"\nTags: {self.tags()}" ret += "\nResult Data:" - for key, val in self.data().items(): - if key in self.__keys_not_shown__: - continue - ret += f"\n - {key}: {val}" + ret += str(self.data()) + return ret diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py index 47f3fa58af..f674aa6c6e 100644 --- a/qiskit_experiments/experiment_data.py +++ b/qiskit_experiments/experiment_data.py @@ -22,6 +22,21 @@ LOG = logging.getLogger(__name__) +class AnalysisResultData(dict): + """Placeholder class""" + + __keys_not_shown__ = tuple() + """Data keys of analysis result which are not directly shown in `__str__` method""" + + def __str__(self): + out = "" + for key, value in self.items(): + if key in self.__keys_not_shown__: + continue + out += f"\n- {key}: {value}" + return out + + class ExperimentData(DbExperimentDataV1): """Qiskit Experiments Data container class""" diff --git a/qiskit_experiments/tomography/tomography_analysis.py b/qiskit_experiments/tomography/tomography_analysis.py index fc0879f973..393ea26faf 100644 --- a/qiskit_experiments/tomography/tomography_analysis.py +++ b/qiskit_experiments/tomography/tomography_analysis.py @@ -125,17 +125,7 @@ def _run_analysis(self, experiment_data, **options): except AnalysisError as ex: raise AnalysisError(f"Tomography fitter failed with error: {str(ex)}") from ex - analysis_result = DbAnalysisResultV1( - result_data=result, - result_type="Tomography", - device_components=[ - Qubit(qubit) for qubit in experiment_data.metadata()["physical_qubits"] - ], - experiment_id=experiment_data.experiment_id, - quality=None, - ) - - return [analysis_result], [None] + return [result], [None] @classmethod def _postprocess_fit( diff --git a/test/analysis/test_curve_fit.py b/test/analysis/test_curve_fit.py index 72ef2eb496..79345a5640 100644 --- a/test/analysis/test_curve_fit.py +++ b/test/analysis/test_curve_fit.py @@ -308,7 +308,7 @@ def test_run_single_curve_analysis(self): default_opts.p0 = {"p0": ref_p0, "p1": ref_p1, "p2": ref_p2, "p3": ref_p3} results, _ = analysis._run_analysis(test_data, **default_opts.__dict__) - result = results[0].data() + result = results[0] ref_popt = np.asarray([ref_p0, ref_p1, ref_p2, ref_p3]) @@ -347,7 +347,7 @@ def test_run_single_curve_fail(self): # Try to fit with infeasible parameter boundary. This should fail. results, _ = analysis._run_analysis(test_data, **default_opts.__dict__) - result = results[0].data() + result = results[0] self.assertFalse(result["success"]) @@ -402,7 +402,7 @@ def test_run_two_curves_with_same_fitfunc(self): default_opts.p0 = {"p0": ref_p0, "p1": ref_p1, "p2": ref_p2, "p3": ref_p3, "p4": ref_p4} results, _ = analysis._run_analysis(test_data0, **default_opts.__dict__) - result = results[0].data() + result = results[0] ref_popt = np.asarray([ref_p0, ref_p1, ref_p2, ref_p3, ref_p4]) @@ -456,7 +456,7 @@ def test_run_two_curves_with_two_fitfuncs(self): default_opts.p0 = {"p0": ref_p0, "p1": ref_p1, "p2": ref_p2, "p3": ref_p3} results, _ = analysis._run_analysis(test_data0, **default_opts.__dict__) - result = results[0].data() + result = results[0] ref_popt = np.asarray([ref_p0, ref_p1, ref_p2, ref_p3]) @@ -493,7 +493,7 @@ def test_run_fixed_parameters(self): default_opts.fixed_p2 = ref_p2 results, _ = analysis._run_analysis(test_data, **default_opts.__dict__) - result = results[0].data() + result = results[0] ref_popt = np.asarray([ref_p0, ref_p1, ref_p3]) diff --git a/test/test_t1.py b/test/test_t1.py index db3de8ff25..87104dd8a3 100644 --- a/test/test_t1.py +++ b/test/test_t1.py @@ -126,8 +126,8 @@ def test_t1_analysis(self): ) res = T1Analysis()._run_analysis(data)[0][0] - self.assertEqual(res.quality, "good") - self.assertAlmostEqual(res.data()["value"], 25e-9, delta=3) + self.assertEqual(res["quality"], "good") + self.assertAlmostEqual(res["value"], 25e-9, delta=3) def test_t1_metadata(self): """ @@ -173,4 +173,4 @@ def test_t1_low_quality(self): ) res = T1Analysis()._run_analysis(data)[0][0] - self.assertEqual(res.quality, "bad") + self.assertEqual(res["quality"], "bad") From 38799a6fb748c8dc68b462eab6ff304c510e7aec Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 15 Jul 2021 18:11:14 +0300 Subject: [PATCH 60/70] black --- qiskit_experiments/base_analysis.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 5f085c34c1..9bbab7ef98 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -92,7 +92,7 @@ def run( analysis_result_parameters = { "result_type": experiment_data.experiment_type, "experiment_id": experiment_data.experiment_id, - } + } if "physical_qubits" in experiment_data.metadata(): analysis_result_parameters["device_components"] = [ Qubit(qubit) for qubit in experiment_data.metadata()["physical_qubits"] @@ -105,10 +105,7 @@ def run( for res in result_datum: if "success" not in res: res["success"] = True - analysis_result = DbAnalysisResultV1( - result_data=res, - **analysis_result_parameters - ) + analysis_result = DbAnalysisResultV1(result_data=res, **analysis_result_parameters) if "quality" in res: analysis_result.quality = res["quality"] analysis_result.verified = True @@ -118,7 +115,7 @@ def run( analysis_results = [ DbAnalysisResultV1( result_data={"success": False, "error_message": ex}, - **analysis_result_parameters + **analysis_result_parameters, ) ] figures = None From 71d3a3c78c522477c5346cdd0d568d33a6914e58 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 15 Jul 2021 18:16:21 +0300 Subject: [PATCH 61/70] lint --- qiskit_experiments/analysis/curve_analysis.py | 1 - qiskit_experiments/analysis/curve_fitting.py | 10 ++++------ qiskit_experiments/characterization/t1_analysis.py | 1 - .../characterization/t2ramsey_analysis.py | 1 - qiskit_experiments/composite/composite_analysis.py | 2 -- qiskit_experiments/tomography/tomography_analysis.py | 2 -- 6 files changed, 4 insertions(+), 13 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index 376c57dce2..cd8a4765a7 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -34,7 +34,6 @@ from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.experiment_data import ExperimentData -from qiskit_experiments.database_service.device_component import Qubit from qiskit_experiments.matplotlib import requires_matplotlib from qiskit_experiments.data_processing.processor_library import get_processor diff --git a/qiskit_experiments/analysis/curve_fitting.py b/qiskit_experiments/analysis/curve_fitting.py index 4dc95b3425..92bdeabdb1 100644 --- a/qiskit_experiments/analysis/curve_fitting.py +++ b/qiskit_experiments/analysis/curve_fitting.py @@ -20,7 +20,6 @@ import scipy.optimize as opt from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.analysis.data_processing import filter_data -from qiskit_experiments.database_service import DbAnalysisResultV1 from qiskit_experiments.experiment_data import AnalysisResultData @@ -60,11 +59,10 @@ class CurveAnalysisResultData(AnalysisResultData): def __str__(self): out = "" - result_data = self.data() - if result_data.get("success"): - popt_keys = result_data.get("popt_keys") - popt = result_data.get("popt") - popt_err = result_data.get("popt_err") + if self.get("success"): + popt_keys = self.get("popt_keys") + popt = self.get("popt") + popt_err = self.get("popt_err") for key, value, error in zip(popt_keys, popt, popt_err): out += f"\n - {key}: {value} \u00B1 {error}" diff --git a/qiskit_experiments/characterization/t1_analysis.py b/qiskit_experiments/characterization/t1_analysis.py index 71f7de6482..cedc830a82 100644 --- a/qiskit_experiments/characterization/t1_analysis.py +++ b/qiskit_experiments/characterization/t1_analysis.py @@ -20,7 +20,6 @@ from qiskit.utils import apply_prefix from qiskit_experiments.database_service import DbAnalysisResultV1 -from qiskit_experiments.database_service.device_component import Qubit from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.analysis.curve_fitting import ( process_curve_data, diff --git a/qiskit_experiments/characterization/t2ramsey_analysis.py b/qiskit_experiments/characterization/t2ramsey_analysis.py index b1303fc2e6..fb2d1b348c 100644 --- a/qiskit_experiments/characterization/t2ramsey_analysis.py +++ b/qiskit_experiments/characterization/t2ramsey_analysis.py @@ -27,7 +27,6 @@ from qiskit_experiments.analysis import plotting from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.database_service import DbAnalysisResultV1 -from qiskit_experiments.database_service.device_component import Qubit # pylint: disable = invalid-name diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 23f0cc1ddd..32cdd3e43a 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -16,8 +16,6 @@ from qiskit.exceptions import QiskitError from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.composite.composite_experiment_data import CompositeExperimentData -from qiskit_experiments.database_service import DbAnalysisResultV1 -from qiskit_experiments.database_service.device_component import Qubit class CompositeAnalysis(BaseAnalysis): diff --git a/qiskit_experiments/tomography/tomography_analysis.py b/qiskit_experiments/tomography/tomography_analysis.py index 393ea26faf..69a4b60e23 100644 --- a/qiskit_experiments/tomography/tomography_analysis.py +++ b/qiskit_experiments/tomography/tomography_analysis.py @@ -25,8 +25,6 @@ from qiskit.quantum_info.operators.channel.quantum_channel import QuantumChannel from qiskit_experiments.exceptions import AnalysisError -from qiskit_experiments.database_service import DbAnalysisResultV1 -from qiskit_experiments.database_service.device_component import Qubit from qiskit_experiments.base_analysis import BaseAnalysis, Options from .fitters import ( linear_inversion, From 6e656a6888dfbfa401c8f89a4ea88fde9fdea54c Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 15 Jul 2021 21:08:09 +0300 Subject: [PATCH 62/70] fixes related to AnalysisResultData --- qiskit_experiments/analysis/curve_analysis.py | 4 ++-- qiskit_experiments/base_analysis.py | 4 ++-- qiskit_experiments/characterization/t1_analysis.py | 6 +++--- qiskit_experiments/characterization/t2ramsey_analysis.py | 7 +++---- qiskit_experiments/composite/composite_analysis.py | 5 +++-- qiskit_experiments/tomography/tomography_analysis.py | 3 ++- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index cd8a4765a7..b012326dad 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -908,7 +908,7 @@ def _run_analysis( Returns: tuple: A pair ``(analysis_results, figures)`` where ``analysis_results`` may be a single or list of - DbAnalysisResultV1 objects, and ``figures`` is a list of any + CurveAnalysisResultData objects, and ``figures`` is a list of any figures for the experiment. Raises: @@ -1077,4 +1077,4 @@ def _run_analysis( } result_data["raw_data"] = raw_data_dict - return [result_data], figures + return [CurveAnalysisResultData(result_data)], figures diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 9bbab7ef98..749cd333be 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -135,7 +135,7 @@ def run( def _run_analysis( self, experiment_data: ExperimentData, **options ) -> Tuple[ - Union[DbAnalysisResultV1, List[DbAnalysisResultV1]], List["matplotlib.figure.Figure"] + Union["AnalysisResultData", List["AnalysisResultData"]], List["matplotlib.figure.Figure"] ]: """Run analysis on circuit data. @@ -147,7 +147,7 @@ def _run_analysis( Returns: A pair ``(analysis_results, figures)`` where ``analysis_results`` - may be a single or list of DbAnalysisResultV1 objects, and ``figures`` + may be a single or list of AnalysisResultData objects, and ``figures`` is a list of any figures for the experiment. Raises: diff --git a/qiskit_experiments/characterization/t1_analysis.py b/qiskit_experiments/characterization/t1_analysis.py index cedc830a82..ba78d5790d 100644 --- a/qiskit_experiments/characterization/t1_analysis.py +++ b/qiskit_experiments/characterization/t1_analysis.py @@ -19,7 +19,6 @@ from qiskit.providers.options import Options from qiskit.utils import apply_prefix -from qiskit_experiments.database_service import DbAnalysisResultV1 from qiskit_experiments.base_analysis import BaseAnalysis from qiskit_experiments.analysis.curve_fitting import ( process_curve_data, @@ -27,6 +26,7 @@ ) from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting +from qiskit_experiments.experiment_data import AnalysisResultData class T1Analysis(BaseAnalysis): @@ -78,7 +78,7 @@ def _run_analysis( offset_bounds=None, plot=True, ax=None, - ) -> Tuple[List[DbAnalysisResultV1], Optional[List["matplotlib.figure.Figure"]]]: + ) -> Tuple[List[AnalysisResultData], Optional[List["matplotlib.figure.Figure"]]]: """ Calculate T1 @@ -160,7 +160,7 @@ def fit_fun(x, a, tau, c): else: figures = None - return [result_data], figures + return [AnalysisResultData(result_data)], figures @staticmethod def _fit_quality(fit_out, fit_err, reduced_chisq): diff --git a/qiskit_experiments/characterization/t2ramsey_analysis.py b/qiskit_experiments/characterization/t2ramsey_analysis.py index fb2d1b348c..5a22fd2bea 100644 --- a/qiskit_experiments/characterization/t2ramsey_analysis.py +++ b/qiskit_experiments/characterization/t2ramsey_analysis.py @@ -25,8 +25,7 @@ ) from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting -from qiskit_experiments.experiment_data import ExperimentData -from qiskit_experiments.database_service import DbAnalysisResultV1 +from qiskit_experiments.experiment_data import ExperimentData, AnalysisResultData # pylint: disable = invalid-name @@ -46,7 +45,7 @@ def _run_analysis( plot: bool = False, ax: Optional["AxesSubplot"] = None, **kwargs, - ) -> Tuple[List[DbAnalysisResultV1], List["matplotlib.figure.Figure"]]: + ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: r"""Calculate T2Ramsey experiment. The probability of measuring `+` is assumed to be of the form @@ -145,7 +144,7 @@ def _format_plot(ax, unit, fit_result, conversion_factor): if unit == "dt": result_data["fit"]["dt"] = conversion_factor - return [result_data], figures + return [AnalysisResultData(result_data)], figures def _t2ramsey_default_params( self, diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 32cdd3e43a..cdc0d75ab0 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -15,6 +15,7 @@ from qiskit.exceptions import QiskitError from qiskit_experiments.base_analysis import BaseAnalysis +from qiskit_experiments.experiment_data import AnalysisResultData from qiskit_experiments.composite.composite_experiment_data import CompositeExperimentData @@ -34,7 +35,7 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): Returns: tuple: A pair ``(analysis_results, figures)`` where ``analysis_results`` may be a single or list of - AnalysisResult objects, and ``figures`` may be + AnalysisResultData objects, and ``figures`` may be None, a single figure, or a list of figures. Raises: @@ -71,4 +72,4 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): "experiment_qubits": sub_qubits, } - return [result_data], None + return [AnalysisResultData(result_data)], None diff --git a/qiskit_experiments/tomography/tomography_analysis.py b/qiskit_experiments/tomography/tomography_analysis.py index 69a4b60e23..f45eb9152b 100644 --- a/qiskit_experiments/tomography/tomography_analysis.py +++ b/qiskit_experiments/tomography/tomography_analysis.py @@ -26,6 +26,7 @@ from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.base_analysis import BaseAnalysis, Options +from qiskit_experiments.experiment_data import AnalysisResultData from .fitters import ( linear_inversion, scipy_linear_lstsq, @@ -123,7 +124,7 @@ def _run_analysis(self, experiment_data, **options): except AnalysisError as ex: raise AnalysisError(f"Tomography fitter failed with error: {str(ex)}") from ex - return [result], [None] + return [AnalysisResultData(result)], [None] @classmethod def _postprocess_fit( From 09538f7b281fe05496be2d948c6a9820670ccf47 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Thu, 15 Jul 2021 20:54:40 -0400 Subject: [PATCH 63/70] allow all objects in encoder --- .../database_service/db_experiment_data.py | 10 +++++++--- qiskit_experiments/database_service/json.py | 7 ++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 8beb5bfdda..2020d58bb3 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -658,6 +658,13 @@ def save_metadata(self) -> None: LOG.warning("Experiment cannot be saved because backend is missing.") return + with self._job_futures.lock: + if any(not fut.done() for _, fut in self._job_futures) and not self.auto_save: + LOG.warning( + "Not all post-processing has finished. Consider calling " + "save() again after all post-processing is done to save any newly " + "generated data." + ) metadata = json.loads(json.dumps(self._metadata, cls=self._json_encoder)) metadata["_source"] = self._source @@ -1027,9 +1034,6 @@ def __str__(self): ret += f"\nExperiment: {self.experiment_type}" ret += f"\nExperiment ID: {self.experiment_id}" ret += f"\nStatus: {status}" - if status == "ERROR": - ret += "\n " - ret += "\n ".join(self._errors) if self.backend: ret += f"\nBackend: {self.backend}" if self.tags(): diff --git a/qiskit_experiments/database_service/json.py b/qiskit_experiments/database_service/json.py index 3b50d1780c..945a777d2e 100644 --- a/qiskit_experiments/database_service/json.py +++ b/qiskit_experiments/database_service/json.py @@ -29,7 +29,10 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ return {"__type__": "complex", "__value__": [obj.real, obj.imag]} if callable(obj): return {"__type__": "callable", "__value__": obj.__name__} - return super().default(obj) + try: + return super().default(obj) + except TypeError: + return {"__type__": "__class_name__", "__value__": obj.__class__.__name__} class ExperimentDecoder(json.JSONDecoder): @@ -48,4 +51,6 @@ def object_hook(self, obj): return np.array(obj["__value__"]) if obj["__type__"] == "callable": return obj["__value__"] + if obj["__type__"] == "__class_name__": + return obj["__value__"] return obj From bb19b66b7ebd4b72dc0b7cc5c8bc6fdbc473c252 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Fri, 16 Jul 2021 14:24:29 -0400 Subject: [PATCH 64/70] call save if auto save set --- .../database_service/db_analysis_result.py | 49 +++++++++---- .../database_service/db_experiment_data.py | 71 +++++++++++++------ 2 files changed, 83 insertions(+), 37 deletions(-) diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index d3fff7a4f8..b10cafd9a3 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -28,7 +28,7 @@ LOG = logging.getLogger(__name__) -def auto_save(func: Callable): +def do_auto_save(func: Callable): """Decorate the input function to auto save data.""" @wraps(func) @@ -143,7 +143,7 @@ def load(cls, result_id: str, service: DatabaseServiceV1) -> "DbAnalysisResultV1 The loaded analysis result. """ # Load data from the service - return cls._from_data(service.analysis_result(result_id)) + return cls._from_service_data(service.analysis_result(result_id)) def save(self) -> None: """Save this analysis result in the database. @@ -192,20 +192,21 @@ def _from_service_data(cls, service_data: Dict) -> "DbAnalysisResultV1": The loaded analysis result. """ # Parse serialized data - result_data = service_data["result_data"] + result_data = service_data.pop("result_data") if result_data: result_data = json.loads(json.dumps(result_data), cls=cls._json_decoder) # Initialize the result object - return DbAnalysisResultV1( + return cls( result_data, - result_type=service_data["result_type"], - device_components=service_data["device_components"], - experiment_id=service_data["experiment_id"], - result_id=service_data["result_id"], - quality=service_data["quality"], - verified=service_data["verified"], - tags=service_data["tags"], - service=service_data["service"], + result_type=service_data.pop("result_type"), + device_components=service_data.pop("device_components"), + experiment_id=service_data.pop("experiment_id"), + result_id=service_data.pop("result_id"), + quality=service_data.pop("quality"), + verified=service_data.pop("verified"), + tags=service_data.pop("tags"), + service=service_data.pop("service"), + **service_data, ) def data(self) -> Dict: @@ -216,7 +217,7 @@ def data(self) -> Dict: """ return self._result_data - @auto_save + @do_auto_save def set_data(self, new_data: Dict) -> None: """Set result data. @@ -229,7 +230,7 @@ def tags(self): """Return tags associated with this result.""" return self._tags - @auto_save + @do_auto_save def set_tags(self, new_tags: List[str]) -> None: """Set tags for this result. @@ -337,6 +338,26 @@ def service(self, service: DatabaseServiceV1) -> None: raise DbExperimentDataError("An experiment service is already being used.") self._service = service + @property + def auto_save(self) -> bool: + """Return current auto-save option. + + Returns: + Whether changes will be automatically saved. + """ + return self._auto_save + + @auto_save.setter + def auto_save(self, save_val: bool) -> None: + """Set auto save preference. + + Args: + save_val: Whether to do auto-save. + """ + if save_val and not self._auto_save: + self.save() + self._auto_save = save_val + @property def device_components(self) -> List[DeviceComponent]: """Return target device components for this analysis result. diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 2020d58bb3..a0e31928c9 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -44,7 +44,7 @@ LOG = logging.getLogger(__name__) -def auto_save(func: Callable): +def do_auto_save(func: Callable): """Decorate the input function to auto save data.""" @wraps(func) @@ -137,7 +137,7 @@ def __init__( self._service = None self._backend = backend - self.auto_save = False + self._auto_save = False self._set_service_from_backend(backend) self._id = experiment_id or str(uuid.uuid4()) @@ -370,7 +370,7 @@ def data(self, index: Optional[Union[int, slice, str]] = None) -> Union[Dict, Li return [data for data in self._data if data.get("job_id") == index] raise TypeError(f"Invalid index type {type(index)}.") - @auto_save + @do_auto_save def add_figures( self, figures, @@ -465,7 +465,7 @@ def add_figures( return added_figs if len(added_figs) > 1 else added_figs[0] - @auto_save + @do_auto_save def delete_figure( self, figure_key: Union[str, int], @@ -532,7 +532,7 @@ def figure( return num_bytes return figure_data - @auto_save + @do_auto_save def add_analysis_results( self, results: Union[DbAnalysisResult, List[DbAnalysisResult]], @@ -548,14 +548,14 @@ def add_analysis_results( for result in results: self._analysis_results[result.result_id] = result + if self.auto_save and self._service: + result.save() + with contextlib.suppress(DbExperimentDataError): result.service = self.service result.auto_save = self.auto_save - if self.auto_save and self._service: - result.save() - - @auto_save + @do_auto_save def delete_analysis_result( self, result_key: Union[int, str], @@ -601,7 +601,8 @@ def _retrieve_analysis_results(self, refresh: bool = False): experiment_id=self.experiment_id, limit=None ) for result in retrieved_results: - self._analysis_results[result["result_id"]] = DbAnalysisResult._from_service_data( + result_id = result["result_id"] + self._analysis_results[result_id] = DbAnalysisResult._from_service_data( result ) @@ -748,22 +749,22 @@ def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "DbExperimentDa service_data = service.experiment(experiment_id) # Parse serialized metadata - metadata = service_data["metadata"] + metadata = service_data.pop("metadata") if metadata: metadata = json.loads(json.dumps(metadata), cls=cls._json_decoder) # Initialize container - # NOTE: not sure how to parse extra_args from service data - expdata = DbExperimentDataV1( - experiment_type=service_data["experiment_data"], - backend=service_data["backend"], - experiment_id=service_data["experiment_id"], - tags=service_data["tags"], - job_ids=service_data["job_ids"], - share_level=service_data["share_level"], + expdata = cls( + experiment_type=service_data.pop("experiment_type"), + backend=service_data.pop("backend"), + experiment_id=service_data.pop("experiment_id"), + tags=service_data.pop("tags"), + job_ids=service_data.pop("job_ids"), + share_level=service_data.pop("share_level"), metadata=metadata, - figure_names=service_data["figure_names"], - notes=service_data["notes"], + figure_names=service_data.pop("figure_names"), + notes=service_data.pop("notes"), + **service_data, ) # Retrieve analysis results # Maybe this isn't necessary but the repr of the class should @@ -873,7 +874,7 @@ def tags(self) -> List[str]: """ return self._tags - @auto_save + @do_auto_save def set_tags(self, new_tags: List[str]) -> None: """Set tags for this experiment. @@ -890,7 +891,7 @@ def metadata(self) -> Dict: """ return self._metadata - @auto_save + @do_auto_save def set_metadata(self, metadata: Dict) -> None: """Set metadata for this experiment. @@ -1021,6 +1022,30 @@ def service(self, service: DatabaseServiceV1) -> None: with contextlib.suppress(Exception): self.auto_save = self._service.options.get("auto_save", False) + @property + def auto_save(self) -> bool: + """Return current auto-save option. + + Returns: + Whether changes will be automatically saved. + """ + return self._auto_save + + @auto_save.setter + def auto_save(self, save_val: bool) -> None: + """Set auto save preference. + + Args: + save_val: Whether to do auto-save. + """ + if save_val and not self._auto_save: + self.save() + self._auto_save = save_val + for res in self._analysis_results.values(): + # Setting private variable directly to avoid duplicate save. This + # can be removed when we start tracking changes. + res._auto_save = save_val + @property def source(self) -> Dict: """Return the class name and version.""" From 793c20e51ac75e4100425b2f547b62911a7c2a07 Mon Sep 17 00:00:00 2001 From: jessieyu Date: Fri, 16 Jul 2021 18:25:49 -0400 Subject: [PATCH 65/70] fix tests --- qiskit_experiments/analysis/curve_analysis.py | 4 ++-- qiskit_experiments/base_analysis.py | 6 ++---- qiskit_experiments/characterization/t1_analysis.py | 7 +++---- .../database_service/db_experiment_data.py | 6 ++---- test/database_service/test_db_analysis_result.py | 1 - test/database_service/test_db_experiment_data.py | 11 ++++------- 6 files changed, 13 insertions(+), 22 deletions(-) diff --git a/qiskit_experiments/analysis/curve_analysis.py b/qiskit_experiments/analysis/curve_analysis.py index b012326dad..379b8538a9 100644 --- a/qiskit_experiments/analysis/curve_analysis.py +++ b/qiskit_experiments/analysis/curve_analysis.py @@ -898,7 +898,7 @@ def _get_option(self, arg_name: str) -> Any: def _run_analysis( self, experiment_data: ExperimentData, **options - ) -> Tuple[List["AnalysisResult"], List["pyplot.Figure"]]: + ) -> Tuple[List["AnalysisResultData"], List["pyplot.Figure"]]: """Run analysis on circuit data. Args: @@ -1077,4 +1077,4 @@ def _run_analysis( } result_data["raw_data"] = raw_data_dict - return [CurveAnalysisResultData(result_data)], figures + return [result_data], figures diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index 749cd333be..8de1664325 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -14,7 +14,7 @@ """ from abc import ABC, abstractmethod -from typing import List, Tuple, Union +from typing import List, Tuple from qiskit.exceptions import QiskitError from qiskit.providers.options import Options @@ -134,9 +134,7 @@ def run( @abstractmethod def _run_analysis( self, experiment_data: ExperimentData, **options - ) -> Tuple[ - Union["AnalysisResultData", List["AnalysisResultData"]], List["matplotlib.figure.Figure"] - ]: + ) -> Tuple[List["AnalysisResultData"], List["matplotlib.figure.Figure"]]: """Run analysis on circuit data. Args: diff --git a/qiskit_experiments/characterization/t1_analysis.py b/qiskit_experiments/characterization/t1_analysis.py index ba78d5790d..8c4ca63d1a 100644 --- a/qiskit_experiments/characterization/t1_analysis.py +++ b/qiskit_experiments/characterization/t1_analysis.py @@ -78,7 +78,7 @@ def _run_analysis( offset_bounds=None, plot=True, ax=None, - ) -> Tuple[List[AnalysisResultData], Optional[List["matplotlib.figure.Figure"]]]: + ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: """ Calculate T1 @@ -152,13 +152,12 @@ def fit_fun(x, a, tau, c): result_data["fit"]["dt"] = conversion_factor # Generate fit plot + figures = [] if plot and plotting.HAS_MATPLOTLIB: ax = plotting.plot_curve_fit(fit_fun, fit_result, ax=ax, fit_uncertainty=True) ax = plotting.plot_errorbar(xdata, ydata, sigma, ax=ax) self._format_plot(ax, fit_result, qubit=qubit) - figures = [ax.get_figure()] - else: - figures = None + figures.append(ax.get_figure()) return [AnalysisResultData(result_data)], figures diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index a0e31928c9..a7945d9c47 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -602,9 +602,7 @@ def _retrieve_analysis_results(self, refresh: bool = False): ) for result in retrieved_results: result_id = result["result_id"] - self._analysis_results[result_id] = DbAnalysisResult._from_service_data( - result - ) + self._analysis_results[result_id] = DbAnalysisResult._from_service_data(result) def analysis_results( self, index: Optional[Union[int, slice, str]] = None, refresh: bool = False @@ -1038,7 +1036,7 @@ def auto_save(self, save_val: bool) -> None: Args: save_val: Whether to do auto-save. """ - if save_val and not self._auto_save: + if save_val is True and not self._auto_save: self.save() self._auto_save = save_val for res in self._analysis_results.values(): diff --git a/test/database_service/test_db_analysis_result.py b/test/database_service/test_db_analysis_result.py index 2f2782bbb2..5ec7d9d3fe 100644 --- a/test/database_service/test_db_analysis_result.py +++ b/test/database_service/test_db_analysis_result.py @@ -58,7 +58,6 @@ def test_auto_save(self): mock_service = mock.create_autospec(DatabaseServiceV1) result = self._new_analysis_result(service=mock_service) result.auto_save = True - result.save() subtests = [ # update function, update parameters, service called diff --git a/test/database_service/test_db_experiment_data.py b/test/database_service/test_db_experiment_data.py index 2620c87dbc..0a9fb2b5ca 100644 --- a/test/database_service/test_db_experiment_data.py +++ b/test/database_service/test_db_experiment_data.py @@ -505,9 +505,7 @@ def test_auto_save(self): service = self._set_mock_service() exp_data = DbExperimentData(backend=self.backend, experiment_type="qiskit_test") exp_data.auto_save = True - mock_result = mock.MagicMock() - exp_data.save_metadata() subtests = [ # update function, update parameters, service called @@ -515,16 +513,15 @@ def test_auto_save(self): (exp_data.add_figures, (str.encode("hello world"),), service.create_figure), (exp_data.delete_figure, (0,), service.delete_figure), (exp_data.delete_analysis_result, (0,), service.delete_analysis_result), - (exp_data.set_tags, (["foo"],), None), - (exp_data.set_metadata, ({"foo": "bar"},), None), - (setattr, (exp_data, "notes", "foo"), None), - (setattr, (exp_data, "share_level", "hub"), None), + (exp_data.set_tags, (["foo"],), service.update_experiment), + (exp_data.set_metadata, ({"foo": "bar"},), service.update_experiment), + (setattr, (exp_data, "notes", "foo"), service.update_experiment), + (setattr, (exp_data, "share_level", "hub"), service.update_experiment), ] for func, params, called in subtests: with self.subTest(func=func): func(*params) - service.update_experiment.assert_called_once() if called: called.assert_called_once() service.reset_mock() From 5f300f89036ed7579817b9a2c5823210f6216018 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Sun, 18 Jul 2021 09:17:19 +0300 Subject: [PATCH 66/70] fixed test_fine_amplitude --- test/calibration/test_update_library.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/calibration/test_update_library.py b/test/calibration/test_update_library.py index 5a196cce65..24cabf575c 100644 --- a/test/calibration/test_update_library.py +++ b/test/calibration/test_update_library.py @@ -111,6 +111,7 @@ def test_fine_amplitude(self): backend = MockFineAmp(error, np.pi, "xp") exp_data = amp_cal.run(backend) + exp_data.block_for_results() self.assertEqual(self.cals.get_parameter_value("amp", self.qubit, "xp"), 0.2) From 955b7f658ee11ec318d413303a5dc888e22db304 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Sun, 18 Jul 2021 09:48:06 +0300 Subject: [PATCH 67/70] lint --- qiskit_experiments/characterization/t1_analysis.py | 2 +- qiskit_experiments/database_service/db_analysis_result.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/characterization/t1_analysis.py b/qiskit_experiments/characterization/t1_analysis.py index 8c4ca63d1a..b8934952d0 100644 --- a/qiskit_experiments/characterization/t1_analysis.py +++ b/qiskit_experiments/characterization/t1_analysis.py @@ -13,7 +13,7 @@ T1 Analysis class. """ -from typing import Tuple, List, Optional +from typing import Tuple, List import numpy as np from qiskit.providers.options import Options diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index b10cafd9a3..d06ccfa9ca 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -123,7 +123,7 @@ def __init__( # Other attributes. self._service = service self._created_in_db = False - self.auto_save = False + self._auto_save = False if self._service: try: self.auto_save = self._service.option("auto_save") From f96ce739c254f5e3f49959e4f69150921e3b1fdb Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Sun, 18 Jul 2021 10:39:10 +0300 Subject: [PATCH 68/70] black --- test/calibration/experiments/test_fine_amplitude.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/calibration/experiments/test_fine_amplitude.py b/test/calibration/experiments/test_fine_amplitude.py index 5574da1b78..4fb3a290a6 100644 --- a/test/calibration/experiments/test_fine_amplitude.py +++ b/test/calibration/experiments/test_fine_amplitude.py @@ -71,7 +71,7 @@ def test_end_to_end_over_rotation(self): expdata = amp_cal.run(backend) expdata.block_for_results() - result = expdata.analysis_results(-1) + result = expdata.analysis_results(-1) result_data = result.data() d_theta = result_data["popt"][result_data["popt_keys"].index("d_theta")] From 5f967e9e5468b12d27c3296238542338813e1521 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Tue, 20 Jul 2021 09:34:06 +0300 Subject: [PATCH 69/70] adjust to recent changes in main --- .../quantum_volume/qv_analysis.py | 10 +++---- test/fake_backend.py | 4 +-- test/quantum_volume/test_qv.py | 27 +++++++++++-------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/qiskit_experiments/quantum_volume/qv_analysis.py b/qiskit_experiments/quantum_volume/qv_analysis.py index bae5c6d77d..cc2e97e3ee 100644 --- a/qiskit_experiments/quantum_volume/qv_analysis.py +++ b/qiskit_experiments/quantum_volume/qv_analysis.py @@ -19,7 +19,7 @@ import numpy as np from qiskit_experiments.base_analysis import BaseAnalysis -from qiskit_experiments.base_analysis import AnalysisResult +from qiskit_experiments.experiment_data import AnalysisResultData from qiskit_experiments.analysis import plotting @@ -39,9 +39,9 @@ def _run_analysis( plot: If True generate a plot of fitted data. ax: Optional, matplotlib axis to add plot to. Returns: - tuple: A pair ``(analysis_result, figures)`` where - ``analysis_results`` may be a single or list of - AnalysisResult objects, and ``figures`` may be + tuple: A pair ``(result_data figures)`` where + ``result_data`` may be a single or list of + AnalysisResultData objects, and ``figures`` may be None, a single figure, or a list of figures. """ depth = experiment_data.experiment.num_qubits @@ -57,7 +57,7 @@ def _run_analysis( self._calc_exp_heavy_output_probability(data_trial, heavy_output) ) - analysis_result = AnalysisResult( + analysis_result = AnalysisResultData( self._calc_quantum_volume(heavy_output_prob_exp, depth, num_trials) ) diff --git a/test/fake_backend.py b/test/fake_backend.py index 44ecd400f2..9612827740 100644 --- a/test/fake_backend.py +++ b/test/fake_backend.py @@ -17,7 +17,7 @@ from qiskit.result import Result -from qiskit_experiments.test.mock_job import MockJob +from qiskit_experiments.test.utils import FakeJob class FakeBackend(BackendV1): @@ -55,4 +55,4 @@ def run(self, run_input, **options): "success": True, "results": [], } - return MockJob(backend=self, result=Result.from_dict(result)) + return FakeJob(backend=self, result=Result.from_dict(result)) diff --git a/test/quantum_volume/test_qv.py b/test/quantum_volume/test_qv.py index 76fc22b3d3..35ad42a305 100644 --- a/test/quantum_volume/test_qv.py +++ b/test/quantum_volume/test_qv.py @@ -103,15 +103,20 @@ def test_qv_sigma_decreasing(self): # set number of trials to a low number to make the test faster qv_exp.set_experiment_options(trials=2) expdata = qv_exp.run(backend) + expdata.block_for_results() expdata = qv_exp.run(backend, experiment_data=expdata) + expdata.block_for_results() - self.assertTrue(expdata.analysis_result(0)["trials"] == 2, "number of trials is incorrect") self.assertTrue( - expdata.analysis_result(1)["trials"] == 4, + expdata.analysis_results(0).data()["trials"] == 2, "number of trials is incorrect" + ) + self.assertTrue( + expdata.analysis_results(1).data()["trials"] == 4, "number of trials is incorrect" " after adding more trials", ) self.assertTrue( - expdata.analysis_result(1)["sigma"] <= expdata.analysis_result(0)["sigma"], + expdata.analysis_results(1).data()["sigma"] + <= expdata.analysis_results(0).data()["sigma"], "sigma did not decreased after adding more trials", ) @@ -134,8 +139,8 @@ def test_qv_failure_insufficient_trials(self): qv_exp.run_analysis(exp_data) self.assertTrue( - exp_data.analysis_result(-1)["qv success"] is False - and exp_data.analysis_result(-1)["quantum volume"] == 1, + exp_data.analysis_results(-1).data()["qv success"] is False + and exp_data.analysis_results(-1).data()["quantum volume"] == 1, "quantum volume is successful with less than 100 trials", ) @@ -158,8 +163,8 @@ def test_qv_failure_insufficient_hop(self): qv_exp.run_analysis(exp_data) self.assertTrue( - exp_data.analysis_result(-1)["qv success"] is False - and exp_data.analysis_result(-1)["quantum volume"] == 1, + exp_data.analysis_results(-1).data()["qv success"] is False + and exp_data.analysis_results(-1).data()["quantum volume"] == 1, "quantum volume is successful with heavy output probability less than 2/3", ) @@ -183,8 +188,8 @@ def test_qv_failure_insufficient_confidence(self): qv_exp.run_analysis(exp_data) self.assertTrue( - exp_data.analysis_result(-1)["qv success"] is False - and exp_data.analysis_result(-1)["quantum volume"] == 1, + exp_data.analysis_results(-1).data()["qv success"] is False + and exp_data.analysis_results(-1).data()["quantum volume"] == 1, "quantum volume is successful with insufficient confidence", ) @@ -212,12 +217,12 @@ def test_qv_success(self): for key, value in successful_results.items(): if isinstance(value, float): self.assertAlmostEqual( - exp_data.analysis_result(-1)[key], + exp_data.analysis_results(-1).data()[key], value, msg="result " + str(key) + " is not the same as the " "pre-calculated analysis", ) else: self.assertTrue( - exp_data.analysis_result(-1)[key] == value, + exp_data.analysis_results(-1).data()[key] == value, "result " + str(key) + " is not the same as the " "pre-calculated analysis", ) From c5a3a64bd94d88ea933520156c32bf2ca1eb5594 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Tue, 20 Jul 2021 09:47:19 +0300 Subject: [PATCH 70/70] fixes related to AnalysisResultData --- qiskit_experiments/characterization/t1_analysis.py | 6 +++--- qiskit_experiments/characterization/t2ramsey_analysis.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/characterization/t1_analysis.py b/qiskit_experiments/characterization/t1_analysis.py index b8934952d0..cfa5822057 100644 --- a/qiskit_experiments/characterization/t1_analysis.py +++ b/qiskit_experiments/characterization/t1_analysis.py @@ -23,10 +23,10 @@ from qiskit_experiments.analysis.curve_fitting import ( process_curve_data, curve_fit, + CurveAnalysisResultData, ) from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting -from qiskit_experiments.experiment_data import AnalysisResultData class T1Analysis(BaseAnalysis): @@ -78,7 +78,7 @@ def _run_analysis( offset_bounds=None, plot=True, ax=None, - ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: + ) -> Tuple[List[CurveAnalysisResultData], List["matplotlib.figure.Figure"]]: """ Calculate T1 @@ -159,7 +159,7 @@ def fit_fun(x, a, tau, c): self._format_plot(ax, fit_result, qubit=qubit) figures.append(ax.get_figure()) - return [AnalysisResultData(result_data)], figures + return [CurveAnalysisResultData(result_data)], figures @staticmethod def _fit_quality(fit_out, fit_err, reduced_chisq): diff --git a/qiskit_experiments/characterization/t2ramsey_analysis.py b/qiskit_experiments/characterization/t2ramsey_analysis.py index 5a22fd2bea..238421f2c5 100644 --- a/qiskit_experiments/characterization/t2ramsey_analysis.py +++ b/qiskit_experiments/characterization/t2ramsey_analysis.py @@ -22,10 +22,11 @@ from qiskit_experiments.analysis.curve_fitting import ( curve_fit, process_curve_data, + CurveAnalysisResultData, ) from qiskit_experiments.analysis.data_processing import level2_probability from qiskit_experiments.analysis import plotting -from qiskit_experiments.experiment_data import ExperimentData, AnalysisResultData +from qiskit_experiments.experiment_data import ExperimentData # pylint: disable = invalid-name @@ -45,7 +46,7 @@ def _run_analysis( plot: bool = False, ax: Optional["AxesSubplot"] = None, **kwargs, - ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: + ) -> Tuple[List[CurveAnalysisResultData], List["matplotlib.figure.Figure"]]: r"""Calculate T2Ramsey experiment. The probability of measuring `+` is assumed to be of the form @@ -144,7 +145,7 @@ def _format_plot(ax, unit, fit_result, conversion_factor): if unit == "dt": result_data["fit"]["dt"] = conversion_factor - return [AnalysisResultData(result_data)], figures + return [CurveAnalysisResultData(result_data)], figures def _t2ramsey_default_params( self,