From 183c18b13550173e65b1d248d813fee1c68438fd Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Thu, 15 Feb 2024 12:52:07 +0200 Subject: [PATCH 01/13] Update base_experiment.py --- qiskit_experiments/framework/base_experiment.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 41240df41c..47eeacbb88 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -475,9 +475,18 @@ def _metadata(self) -> Dict[str, any]: By default, this assumes the experiment is running on qubits only. Subclasses can override this method to add custom experiment metadata to the returned experiment result data. """ + from qiskit.version import get_version_info as get_qiskit_version + from qiskit_experiments.version import get_version_info as get_qiskit_experiments_version + + experiment_class = self._metadata.__qualname__[: -len("._metadata")] metadata = { "physical_qubits": list(self.physical_qubits), "device_components": list(map(Qubit, self.physical_qubits)), + "environment_data": { + "Qiskit_version": get_qiskit_version(), + "Qiskit_Experiment_version": get_qiskit_experiments_version(), + "experiment_class": experiment_class, + }, } return metadata From 343eed51ba1b828ac5be0d23e3ae1c04c42d5c10 Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Thu, 14 Mar 2024 14:53:16 +0200 Subject: [PATCH 02/13] added `load` method to base experiment --- .../framework/base_experiment.py | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 47eeacbb88..17e6fbc7e0 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -16,10 +16,12 @@ from abc import ABC, abstractmethod import copy from collections import OrderedDict +import logging +import traceback from typing import Sequence, Optional, Tuple, List, Dict, Union from qiskit import transpile, QuantumCircuit -from qiskit.providers import Job, Backend +from qiskit.providers import Provider, Job, Backend from qiskit.exceptions import QiskitError from qiskit.qobj.utils import MeasLevel from qiskit.providers.options import Options @@ -28,7 +30,12 @@ from qiskit_experiments.framework.base_analysis import BaseAnalysis from qiskit_experiments.framework.experiment_data import ExperimentData from qiskit_experiments.framework.configs import ExperimentConfig +from qiskit_experiments.framework.json import ExperimentEncoder, ExperimentDecoder from qiskit_experiments.database_service import Qubit +from qiskit_experiments.database_service.utils import objs_to_zip, zip_to_objs +from qiskit_ibm_experiment import IBMExperimentService + +LOG = logging.getLogger(__name__) class BaseExperiment(ABC, StoreInitArgs): @@ -185,7 +192,10 @@ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": """Initialize an experiment from experiment config""" if isinstance(config, dict): config = ExperimentConfig(**dict) - ret = cls(*config.args, **config.kwargs) + if isinstance(cls, type(BaseExperiment)): + ret = config.cls(*config.args, **config.kwargs) + else: + ret = cls(*config.args, **config.kwargs) if config.experiment_options: ret.set_experiment_options(**config.experiment_options) if config.transpile_options: @@ -194,6 +204,61 @@ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": ret.set_run_options(**config.run_options) return ret + @classmethod + def load( + cls, + experiment_id: str, + service: Optional[IBMExperimentService] = None, + provider: Optional[Provider] = None, + ) -> "BaseExperiment": + """Load a saved experiment from a database service. + + Args: + experiment_id: Experiment ID. + service: the database service. + provider: an IBMProvider required for loading the experiment data and + can be used to initialize the service. When using + :external+qiskit_ibm_runtime:doc:`qiskit-ibm-runtime `, + this is the :class:`~qiskit_ibm_runtime.QiskitRuntimeService` and should + not be confused with the experiment database service + :meth:`qiskit_ibm_experiment.IBMExperimentService`. + + Returns: + The reconstructed experiment. + Raises: + ExperimentError: If not service nor provider were given. + """ + if service is None: + if provider is None: + raise QiskitError( + "Loading an experiment requires a valid Qiskit provider or experiment service." + ) + service = ExperimentData.get_service_from_provider(provider) + + # here we load the data + data = service.experiment(experiment_id, json_decoder=ExperimentDecoder) + if service.experiment_has_file(experiment_id, ExperimentData._metadata_filename): + metadata = service.file_download( + experiment_id, ExperimentData._metadata_filename, json_decoder=ExperimentDecoder + ) + data.metadata.update(metadata) + + # Recreate artifacts + experiment_config_filename = "experiment_config" + try: + if experiment_config_filename in data.metadata: + if service.experiment_has_file(experiment_id, experiment_config_filename): + artifact_file = service.file_download(experiment_id, experiment_config_filename) + + experiment_config = zip_to_objs(artifact_file, json_decoder=ExperimentDecoder)[ + 0 + ] + reconstructed_experiment = cls.from_config(experiment_config) + return reconstructed_experiment + + except Exception: # pylint: disable=broad-except: + LOG.error("Unable to load artifacts: %s", traceback.format_exc()) + def run( self, backend: Optional[Backend] = None, From 872ac460b719aa9dbc2c170d6795669ac402f7d7 Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Thu, 14 Mar 2024 15:24:00 +0200 Subject: [PATCH 03/13] removed metadata env details --- qiskit_experiments/framework/base_experiment.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 17e6fbc7e0..bc5dedebdc 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -540,18 +540,10 @@ def _metadata(self) -> Dict[str, any]: By default, this assumes the experiment is running on qubits only. Subclasses can override this method to add custom experiment metadata to the returned experiment result data. """ - from qiskit.version import get_version_info as get_qiskit_version - from qiskit_experiments.version import get_version_info as get_qiskit_experiments_version - experiment_class = self._metadata.__qualname__[: -len("._metadata")] metadata = { "physical_qubits": list(self.physical_qubits), "device_components": list(map(Qubit, self.physical_qubits)), - "environment_data": { - "Qiskit_version": get_qiskit_version(), - "Qiskit_Experiment_version": get_qiskit_experiments_version(), - "experiment_class": experiment_class, - }, } return metadata From c172cac16bceb9c2007ab4e5293b48a2215e656c Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Sun, 17 Mar 2024 20:56:15 +0200 Subject: [PATCH 04/13] added `load` method and load analysis config Added load method to `BaseExperiment` Added backend initialization to the experiment if available. `from_config` now also loads analysis if available. --- .../framework/base_experiment.py | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index bc5dedebdc..25028114dd 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -12,7 +12,7 @@ """ Base Experiment class. """ - +import warnings from abc import ABC, abstractmethod import copy from collections import OrderedDict @@ -25,15 +25,15 @@ from qiskit.exceptions import QiskitError from qiskit.qobj.utils import MeasLevel from qiskit.providers.options import Options +from qiskit_ibm_experiment import IBMExperimentService from qiskit_experiments.framework import BackendData from qiskit_experiments.framework.store_init_args import StoreInitArgs from qiskit_experiments.framework.base_analysis import BaseAnalysis from qiskit_experiments.framework.experiment_data import ExperimentData from qiskit_experiments.framework.configs import ExperimentConfig -from qiskit_experiments.framework.json import ExperimentEncoder, ExperimentDecoder +from qiskit_experiments.framework.json import ExperimentDecoder from qiskit_experiments.database_service import Qubit -from qiskit_experiments.database_service.utils import objs_to_zip, zip_to_objs -from qiskit_ibm_experiment import IBMExperimentService +from qiskit_experiments.database_service.utils import zip_to_objs LOG = logging.getLogger(__name__) @@ -178,6 +178,12 @@ def config(self) -> ExperimentConfig: (key, getattr(self._transpile_options, key)) for key in self._set_transpile_options ) run_options = dict((key, getattr(self._run_options, key)) for key in self._set_run_options) + + if isinstance(self.analysis, BaseAnalysis): + analysis_config = self.analysis.config() + else: + analysis_config = None + return ExperimentConfig( cls=type(self), args=args, @@ -185,6 +191,7 @@ def config(self) -> ExperimentConfig: experiment_options=experiment_options, transpile_options=transpile_options, run_options=run_options, + analysis=analysis_config, ) @classmethod @@ -192,16 +199,15 @@ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": """Initialize an experiment from experiment config""" if isinstance(config, dict): config = ExperimentConfig(**dict) - if isinstance(cls, type(BaseExperiment)): - ret = config.cls(*config.args, **config.kwargs) - else: - ret = cls(*config.args, **config.kwargs) + ret = cls(*config.args, **config.kwargs) if config.experiment_options: ret.set_experiment_options(**config.experiment_options) if config.transpile_options: ret.set_transpile_options(**config.transpile_options) if config.run_options: ret.set_run_options(**config.run_options) + if config.analysis: + ret.analysis = config.analysis.analysis() return ret @classmethod @@ -226,7 +232,7 @@ def load( Returns: The reconstructed experiment. Raises: - ExperimentError: If not service nor provider were given. + QiskitError: If not service nor provider were given. """ if service is None: if provider is None: @@ -235,8 +241,8 @@ def load( ) service = ExperimentData.get_service_from_provider(provider) - # here we load the data data = service.experiment(experiment_id, json_decoder=ExperimentDecoder) + # loading metadata artifact if exist if service.experiment_has_file(experiment_id, ExperimentData._metadata_filename): metadata = service.file_download( experiment_id, ExperimentData._metadata_filename, json_decoder=ExperimentDecoder @@ -253,8 +259,22 @@ def load( experiment_config = zip_to_objs(artifact_file, json_decoder=ExperimentDecoder)[ 0 ] + backend_name = data.backend reconstructed_experiment = cls.from_config(experiment_config) + if backend_name: + reconstructed_experiment.backend = provider.get_backend(backend_name) + else: + warnings.warn("No backend for loaded data.") return reconstructed_experiment + else: + raise QiskitError( + "The experiment doesn't have saved experiment in the DB to load." + ) + else: + raise QiskitError( + f"No '{experiment_config_filename}' field in the experiment metadata. can't load " + f"the experiment." + ) except Exception: # pylint: disable=broad-except: LOG.error("Unable to load artifacts: %s", traceback.format_exc()) From 20670a24f0736712474ebf5b1f6d11cda019f6ef Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Sun, 17 Mar 2024 20:56:55 +0200 Subject: [PATCH 05/13] added `config` and `from_config` method --- .../curve_analysis/base_curve_analysis.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/qiskit_experiments/curve_analysis/base_curve_analysis.py b/qiskit_experiments/curve_analysis/base_curve_analysis.py index 36cab0ef4a..be05ddd71f 100644 --- a/qiskit_experiments/curve_analysis/base_curve_analysis.py +++ b/qiskit_experiments/curve_analysis/base_curve_analysis.py @@ -14,6 +14,7 @@ Base class of curve analysis. """ +from collections import OrderedDict import warnings from abc import ABC, abstractmethod from typing import Dict, List, Union @@ -29,6 +30,7 @@ BaseAnalysis, ExperimentData, Options, + AnalysisConfig, ) from qiskit_experiments.visualization import ( BaseDrawer, @@ -422,3 +424,36 @@ def _initialize( DeprecationWarning, ) self.set_options(data_subfit_map=data_subfit_map) + + def config(self) -> AnalysisConfig: + """Return the config dataclass for this analysis. We replicate the method from `BaseAnalysis` + because we cannot directly modify the returned object. This limitation arises from our use of + it hashable we use `dataclasses.dataclass(frozen=True)` to ensure its hashability. + Additionally, we include the `plotter` and `drawer` options, which are specific to + this context.""" + args = tuple(getattr(self, "__init_args__", OrderedDict()).values()) + kwargs = dict(getattr(self, "__init_kwargs__", OrderedDict())) + # Only store non-default valued options + options = dict((key, getattr(self._options, key)) for key in self._set_options) + if isinstance(self.plotter, BasePlotter): + plotter = self.plotter.config() + else: + plotter = None + + return AnalysisConfig( + cls=type(self), + args=args, + kwargs=kwargs, + options=options, + plotter=plotter, + ) + + @classmethod + def from_config(cls, config: Union[AnalysisConfig, Dict]) -> "BaseCurveAnalysis": + """Initialize a curve analysis class from analysis config""" + if isinstance(config, dict): + config = AnalysisConfig(**config) + ret = super().from_config(config) + if config.plotter: + ret.set_options(plotter=config.plotter.plotter()) + return ret From c0e3ec6bcef9393276b28ff12aebe9337d9263a4 Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Sun, 17 Mar 2024 20:57:06 +0200 Subject: [PATCH 06/13] added `config` and `from_config` method --- .../visualization/plotters/base_plotter.py | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/qiskit_experiments/visualization/plotters/base_plotter.py b/qiskit_experiments/visualization/plotters/base_plotter.py index 2810184b8d..a67749f145 100644 --- a/qiskit_experiments/visualization/plotters/base_plotter.py +++ b/qiskit_experiments/visualization/plotters/base_plotter.py @@ -16,6 +16,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union from qiskit_experiments.framework import Options +from qiskit_experiments.framework.configs import PlotterConfig from qiskit_experiments.visualization.drawers import BaseDrawer, SeriesName from ..style import PlotStyle @@ -553,36 +554,45 @@ def _configure_drawer(self): # Use drawer.set_figure_options so figure options are serialized. self.drawer.set_figure_options(**_drawer_figure_options) - def config(self) -> Dict: + def config(self) -> PlotterConfig: """Return the config dictionary for this drawing.""" options = dict((key, getattr(self._options, key)) for key in self._set_options) figure_options = dict( (key, getattr(self._figure_options, key)) for key in self._set_figure_options ) - drawer = self.drawer.__json_encode__() + drawer = self.drawer.config() + return PlotterConfig( + cls=type(self), + options=options, + figure_options=figure_options, + drawer=drawer, + ) + # return { + # "cls": type(self), + # "options": options, + # "figure_options": figure_options, + # "drawer": drawer, + # } + + @classmethod + def from_config(cls, config: Union[PlotterConfig, Dict]) -> "BasePlotter": + """Initialize a plotter class from analysis config""" + if isinstance(config, dict): + config = PlotterConfig(**config) - return { - "cls": type(self), - "options": options, - "figure_options": figure_options, - "drawer": drawer, - } + # Create plotter instance + ret = cls(config.drawer.drawer()) + # if "options" in config: + if config.options: + ret.set_options(**config.options) + # if "figure_options" in config: + if config.figure_options: + ret.set_figure_options(**config.figure_options) + return ret def __json_encode__(self): return self.config() @classmethod def __json_decode__(cls, value): - ## Process drawer as it's needed to create a plotter - drawer_values = value["drawer"] - # We expect a subclass of BaseDrawer - drawer_cls: BaseDrawer = drawer_values["cls"] - drawer = drawer_cls.__json_decode__(drawer_values) - - # Create plotter instance - instance = cls(drawer) - if "options" in value: - instance.set_options(**value["options"]) - if "figure_options" in value: - instance.set_figure_options(**value["figure_options"]) - return instance + return cls.from_config(value) From 41bf46c488e111129216d26f6f1487bb19b3aea7 Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Sun, 17 Mar 2024 20:57:10 +0200 Subject: [PATCH 07/13] added `config` and `from_config` method --- .../visualization/drawers/base_drawer.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/qiskit_experiments/visualization/drawers/base_drawer.py b/qiskit_experiments/visualization/drawers/base_drawer.py index d9fb1af075..ed0e6d0f9a 100644 --- a/qiskit_experiments/visualization/drawers/base_drawer.py +++ b/qiskit_experiments/visualization/drawers/base_drawer.py @@ -19,6 +19,8 @@ from qiskit_experiments.framework import Options +from qiskit_experiments.framework.configs import DrawerConfig + from ..style import PlotStyle from ..utils import ExtentTuple @@ -552,27 +554,34 @@ def image( def figure(self): """Return figure object handler to be saved in the database.""" - def config(self) -> Dict: + def config(self) -> DrawerConfig: """Return the config dictionary for this drawer.""" options = dict((key, getattr(self._options, key)) for key in self._set_options) figure_options = dict( (key, getattr(self._figure_options, key)) for key in self._set_figure_options ) + return DrawerConfig( + cls=type(self), + options=options, + figure_options=figure_options, + ) - return { - "cls": type(self), - "options": options, - "figure_options": figure_options, - } + @classmethod + def from_config(cls, config: Union[DrawerConfig, Dict]) -> "BaseDrawer": + """Initialize a drawer class from analysis config""" + if isinstance(config, dict): + config = DrawerConfig(**config) + # Create Drawer instance + ret = cls() + if config.options: + ret.set_options(**config.options) + if config.figure_options: + ret.set_figure_options(**config.figure_options) + return ret def __json_encode__(self): return self.config() @classmethod def __json_decode__(cls, value): - instance = cls() - if "options" in value: - instance.set_options(**value["options"]) - if "figure_options" in value: - instance.set_figure_options(**value["figure_options"]) - return instance + return cls.from_config(value) From ab2e162b38e49f05bf53534d22d2f18976915ed3 Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Sun, 17 Mar 2024 20:57:45 +0200 Subject: [PATCH 08/13] Added `DrawerConfig` and `PlotterConfig` dataclasses --- qiskit_experiments/framework/configs.py | 136 ++++++++++++++++++++---- 1 file changed, 116 insertions(+), 20 deletions(-) diff --git a/qiskit_experiments/framework/configs.py b/qiskit_experiments/framework/configs.py index 9e114584e3..e9137f3126 100644 --- a/qiskit_experiments/framework/configs.py +++ b/qiskit_experiments/framework/configs.py @@ -21,46 +21,90 @@ @dataclasses.dataclass(frozen=True) -class ExperimentConfig: - """Store configuration settings for an Experiment class. +class DrawerConfig: + """Store configuration settings for a Drawer class. - This stores the current configuration of a :class:`.BaseExperiment` and can be used to - reconstruct the experiment using either the :meth:`experiment` property if the experiment class - type is currently stored, or the :meth:`~.BaseExperiment.from_config` class method of the - appropriate experiment. + This stores the current configuration of a :class:`.BaseDrawer` and can be used to reconstruct + the drawer class using either the :meth:`drawer` property if the drawer class type is + currently stored, or the :meth:`~.BaseDrawer.from_config` class method. """ cls: type = None - args: Tuple[Any] = dataclasses.field(default_factory=tuple) - kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) - experiment_options: Dict[str, Any] = dataclasses.field(default_factory=dict) - transpile_options: Dict[str, Any] = dataclasses.field(default_factory=dict) - run_options: Dict[str, Any] = dataclasses.field(default_factory=dict) + options: Dict[str, Any] = dataclasses.field(default_factory=dict) + figure_options: Dict[str, Any] = dataclasses.field(default_factory=dict) version: str = __version__ - def experiment(self): - """Return the experiment constructed from this config. + def drawer(self): + """Return the drawer class constructed from this config. Returns: - BaseExperiment: The experiment reconstructed from the config. + BaseDrawer: The drawer reconstructed from the config. Raises: - QiskitError: If the experiment class is not stored, + QiskitError: If the drawer class is not stored, was not successful deserialized, or reconstruction - of the experiment fails. + of the drawer class fails. """ cls = self.cls if cls is None: - raise QiskitError("No experiment class in experiment config") + raise QiskitError("No drawer class in drawer config") if isinstance(cls, dict): raise QiskitError( - "Unable to load experiment class. Try manually loading " - "experiment using `Experiment.from_config(config)` instead." + "Unable to load analysis class. Try manually loading " + "analysis using `Analysis.plotter.drawer.from_config(config)` instead." ) try: return cls.from_config(self) except Exception as ex: - msg = "Unable to construct experiments from config." + msg = "Unable to construct analysis from config." + if cls.version != __version__: + msg += ( + f" Note that config version ({cls.version}) differs from the current" + f" qiskit-experiments version ({__version__}). You could try" + " installing a compatible qiskit-experiments version." + ) + raise QiskitError(f"{msg}\nError Message:\n{str(ex)}") from ex + + +@dataclasses.dataclass(frozen=True) +class PlotterConfig: + """Store configuration settings for a Drawer class. + + This stores the current configuration of a :class:`.BasePlotter` and can be used to reconstruct + the plotter class using either the :meth:`plotter` property if the plotter class type is + currently stored, or the :meth:`~.BasePlotter.from_config` class method. + """ + + cls: type = None + options: Dict[str, Any] = dataclasses.field(default_factory=dict) + figure_options: Dict[str, Any] = dataclasses.field(default_factory=dict) + # drawer: Dict[str, Any] = dataclasses.field(default_factory=dict) + drawer: DrawerConfig = None + version: str = __version__ + + def plotter(self): + """Return the plotter of the analysis class constructed from this config. + + Returns: + BasePlotter: The plotter reconstructed from the config. + + Raises: + QiskitError: If the plotter class is not stored, + was not successful deserialized, or reconstruction + of the plotter class fails. + """ + cls = self.cls + if cls is None: + raise QiskitError("No plotter class in plotter config") + if isinstance(cls, dict): + raise QiskitError( + "Unable to load plotter class. Try manually loading " + "analysis using `Analysis.plotter.from_config(config)` instead." + ) + try: + return cls.from_config(self) + except Exception as ex: + msg = "Unable to construct plotter from config." if cls.version != __version__: msg += ( f" Note that config version ({cls.version}) differs from the current" @@ -84,6 +128,7 @@ class AnalysisConfig: args: Tuple[Any] = dataclasses.field(default_factory=tuple) kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) options: Dict[str, Any] = dataclasses.field(default_factory=dict) + plotter: PlotterConfig = None version: str = __version__ def analysis(self): @@ -116,3 +161,54 @@ def analysis(self): " installing a compatible qiskit-experiments version." ) raise QiskitError(f"{msg}\nError Message:\n{str(ex)}") from ex + + +@dataclasses.dataclass(frozen=True) +class ExperimentConfig: + """Store configuration settings for an Experiment class. + + This stores the current configuration of a :class:`.BaseExperiment` and can be used to + reconstruct the experiment using either the :meth:`experiment` property if the experiment class + type is currently stored, or the :meth:`~.BaseExperiment.from_config` class method of the + appropriate experiment. + """ + + cls: type = None + args: Tuple[Any] = dataclasses.field(default_factory=tuple) + kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) + experiment_options: Dict[str, Any] = dataclasses.field(default_factory=dict) + transpile_options: Dict[str, Any] = dataclasses.field(default_factory=dict) + run_options: Dict[str, Any] = dataclasses.field(default_factory=dict) + analysis: AnalysisConfig = None + version: str = __version__ + + def experiment(self): + """Return the experiment constructed from this config. + + Returns: + BaseExperiment: The experiment reconstructed from the config. + + Raises: + QiskitError: If the experiment class is not stored, + was not successful deserialized, or reconstruction + of the experiment fails. + """ + cls = self.cls + if cls is None: + raise QiskitError("No experiment class in experiment config") + if isinstance(cls, dict): + raise QiskitError( + "Unable to load experiment class. Try manually loading " + "experiment using `Experiment.from_config(config)` instead." + ) + try: + return cls.from_config(self) + except Exception as ex: + msg = "Unable to construct experiments from config." + if cls.version != __version__: + msg += ( + f" Note that config version ({cls.version}) differs from the current" + f" qiskit-experiments version ({__version__}). You could try" + " installing a compatible qiskit-experiments version." + ) + raise QiskitError(f"{msg}\nError Message:\n{str(ex)}") from ex From ef18ad9a14a9250a4a17b9b17d68cde9c4825d9c Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Sun, 17 Mar 2024 22:10:51 +0200 Subject: [PATCH 09/13] removed comment --- qiskit_experiments/visualization/plotters/base_plotter.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/qiskit_experiments/visualization/plotters/base_plotter.py b/qiskit_experiments/visualization/plotters/base_plotter.py index a67749f145..223327dbc7 100644 --- a/qiskit_experiments/visualization/plotters/base_plotter.py +++ b/qiskit_experiments/visualization/plotters/base_plotter.py @@ -567,12 +567,6 @@ def config(self) -> PlotterConfig: figure_options=figure_options, drawer=drawer, ) - # return { - # "cls": type(self), - # "options": options, - # "figure_options": figure_options, - # "drawer": drawer, - # } @classmethod def from_config(cls, config: Union[PlotterConfig, Dict]) -> "BasePlotter": From 3cb91bbbba5eeb24721f0dbe3b73af824dce0e5a Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Sun, 17 Mar 2024 22:11:26 +0200 Subject: [PATCH 10/13] assign plotter not trough `set_options` --- qiskit_experiments/curve_analysis/base_curve_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/curve_analysis/base_curve_analysis.py b/qiskit_experiments/curve_analysis/base_curve_analysis.py index be05ddd71f..5c16a8fd0c 100644 --- a/qiskit_experiments/curve_analysis/base_curve_analysis.py +++ b/qiskit_experiments/curve_analysis/base_curve_analysis.py @@ -455,5 +455,5 @@ def from_config(cls, config: Union[AnalysisConfig, Dict]) -> "BaseCurveAnalysis" config = AnalysisConfig(**config) ret = super().from_config(config) if config.plotter: - ret.set_options(plotter=config.plotter.plotter()) + ret.options.plotter = config.plotter.plotter() return ret From d095a82a514fe80f50408d6ce1090732f853e1c9 Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Sun, 17 Mar 2024 22:23:53 +0200 Subject: [PATCH 11/13] fixed error message for DrawerConfig --- qiskit_experiments/framework/configs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/framework/configs.py b/qiskit_experiments/framework/configs.py index e9137f3126..c55a336758 100644 --- a/qiskit_experiments/framework/configs.py +++ b/qiskit_experiments/framework/configs.py @@ -50,13 +50,13 @@ def drawer(self): raise QiskitError("No drawer class in drawer config") if isinstance(cls, dict): raise QiskitError( - "Unable to load analysis class. Try manually loading " - "analysis using `Analysis.plotter.drawer.from_config(config)` instead." + "Unable to load drawer class. Try manually loading " + "drawer using `Analysis.plotter.drawer.from_config(config)` instead." ) try: return cls.from_config(self) except Exception as ex: - msg = "Unable to construct analysis from config." + msg = "Unable to construct drawer from config." if cls.version != __version__: msg += ( f" Note that config version ({cls.version}) differs from the current" From 0e80d6408aed8cad26ca6e7db5daf068bb8239f9 Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Wed, 1 May 2024 12:46:24 +0300 Subject: [PATCH 12/13] added default reconstruction value and fixed loading --- qiskit_experiments/curve_analysis/base_curve_analysis.py | 2 ++ qiskit_experiments/framework/base_experiment.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/curve_analysis/base_curve_analysis.py b/qiskit_experiments/curve_analysis/base_curve_analysis.py index f5db8039c9..7327b034e9 100644 --- a/qiskit_experiments/curve_analysis/base_curve_analysis.py +++ b/qiskit_experiments/curve_analysis/base_curve_analysis.py @@ -456,4 +456,6 @@ def from_config(cls, config: Union[AnalysisConfig, Dict]) -> "BaseCurveAnalysis" ret = super().from_config(config) if config.plotter: ret.options.plotter = config.plotter.plotter() + else: + ret.options.plotter = None return ret diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 25028114dd..cab7ee47f0 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -208,6 +208,8 @@ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": ret.set_run_options(**config.run_options) if config.analysis: ret.analysis = config.analysis.analysis() + else: + ret.analysis = None return ret @classmethod @@ -256,11 +258,9 @@ def load( if service.experiment_has_file(experiment_id, experiment_config_filename): artifact_file = service.file_download(experiment_id, experiment_config_filename) - experiment_config = zip_to_objs(artifact_file, json_decoder=ExperimentDecoder)[ - 0 - ] + experiment_config_artifact = zip_to_objs(artifact_file, json_decoder=ExperimentDecoder)[0] backend_name = data.backend - reconstructed_experiment = cls.from_config(experiment_config) + reconstructed_experiment = cls.from_config(experiment_config_artifact.data) if backend_name: reconstructed_experiment.backend = provider.get_backend(backend_name) else: From 566bed40880b54942baff4a5453bb83f15b0c884 Mon Sep 17 00:00:00 2001 From: ItamarGoldman Date: Wed, 1 May 2024 12:52:23 +0300 Subject: [PATCH 13/13] added test --- test/framework/test_framework.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/framework/test_framework.py b/test/framework/test_framework.py index 1a2ae395d3..fff9b946dd 100644 --- a/test/framework/test_framework.py +++ b/test/framework/test_framework.py @@ -17,6 +17,7 @@ from test.fake_experiment import FakeExperiment, FakeAnalysis from test.base import QiskitExperimentsTestCase +import json import ddt from qiskit import QuantumCircuit @@ -33,6 +34,8 @@ BaseAnalysis, AnalysisResultData, AnalysisStatus, + ExperimentEncoder, + ExperimentDecoder, ) from qiskit_experiments.test.fake_backend import FakeBackend from qiskit_experiments.test.utils import FakeJob @@ -426,3 +429,16 @@ def circuits(self): self.assertEqual(exp2.experiment_type, "MyExp") exp2.experiment_type = "suieee" self.assertEqual(exp2.experiment_type, "suieee") + + def test_experiment_serialization_and_deserialization(self): + """Check if the serialization of the reconstructed analysis and the original + analysis are the same.""" + exp = FakeExperiment((0, 2)) + exp.analysis.set_options(dummyoption="dummy") + exp_config = exp.config() + encode = json.dumps(exp_config, cls=ExperimentEncoder) + decode = json.loads(encode, cls=ExperimentDecoder) + + reconstructed_exp_from_config = decode.experiment() + # checking the config method output as there is no __equal__ op for plotter and figure. + self.assertEqual(exp.analysis.config(), reconstructed_exp_from_config.analysis.config())