From 2ef6591e118837bfa8b5b95601ca9b5735c5dcf2 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Fri, 22 Oct 2021 11:42:30 -0400 Subject: [PATCH 01/12] Merge CompositeExperimentData into ExperimentData --- .../database_service/db_experiment_data.py | 21 -- qiskit_experiments/framework/__init__.py | 2 - qiskit_experiments/framework/base_analysis.py | 11 - .../framework/base_experiment.py | 7 +- .../framework/composite/__init__.py | 1 - .../framework/composite/composite_analysis.py | 43 +++- .../composite/composite_experiment.py | 14 +- .../composite/composite_experiment_data.py | 198 ------------------ .../framework/experiment_data.py | 184 +++++++++++++--- .../update_expdata-ab3576f3bdd5057a.yaml | 31 ++- test/calibration/experiments/test_rabi.py | 2 + .../test_db_experiment_data.py | 11 - test/test_composite.py | 47 ++--- test/test_t1.py | 4 +- test/test_t2ramsey.py | 4 +- test/test_tomography.py | 8 +- 16 files changed, 255 insertions(+), 333 deletions(-) delete mode 100644 qiskit_experiments/framework/composite/composite_experiment_data.py diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index 1abb154dde..acd9b9bcf7 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -1403,27 +1403,6 @@ def __repr__(self): out += ")" return out - 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 self.backend: - ret += f"\nBackend: {self.backend}" - if self.tags: - ret += f"\nTags: {self.tags}" - 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] diff --git a/qiskit_experiments/framework/__init__.py b/qiskit_experiments/framework/__init__.py index 32cbd92c6a..40aceadd5c 100644 --- a/qiskit_experiments/framework/__init__.py +++ b/qiskit_experiments/framework/__init__.py @@ -220,7 +220,6 @@ ParallelExperiment BatchExperiment CompositeAnalysis - CompositeExperimentData Base Classes ************ @@ -244,5 +243,4 @@ ParallelExperiment, BatchExperiment, CompositeAnalysis, - CompositeExperimentData, ) diff --git a/qiskit_experiments/framework/base_analysis.py b/qiskit_experiments/framework/base_analysis.py index 256009bb60..bec50e850c 100644 --- a/qiskit_experiments/framework/base_analysis.py +++ b/qiskit_experiments/framework/base_analysis.py @@ -16,8 +16,6 @@ from abc import ABC, abstractmethod from typing import List, Tuple -from qiskit.exceptions import QiskitError - from qiskit_experiments.database_service.device_component import Qubit from qiskit_experiments.framework import Options from qiskit_experiments.framework.experiment_data import ExperimentData @@ -41,9 +39,6 @@ class BaseAnalysis(ABC): run method and passed to the `_run_analysis` function. """ - # Expected experiment data container for analysis - __experiment_data__ = ExperimentData - @classmethod def _default_options(cls) -> Options: return Options() @@ -84,12 +79,6 @@ def run( will be returned containing only the new analysis results and figures. This data can then be saved as its own experiment to a database service. """ - if not isinstance(experiment_data, self.__experiment_data__): - raise QiskitError( - f"Invalid experiment data type, expected {self.__experiment_data__.__name__}" - f" but received {type(experiment_data).__name__}" - ) - # Make a new copy of experiment data if not updating results if not replace_results and ( experiment_data._created_in_db diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 16e6cc663c..3919efc3ac 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -95,16 +95,11 @@ class BaseExperiment(ABC): __analysis_class__: Optional, the default Analysis class to use for data analysis. If None no data analysis will be done on experiment data (Default: None). - __experiment_data__: ExperimentData class that is produced by the - experiment (Default: ExperimentData). """ # Analysis class for experiment __analysis_class__ = None - # ExperimentData class for experiment - __experiment_data__ = ExperimentData - def __init__( self, qubits: Sequence[int], @@ -325,7 +320,7 @@ def run( def _initialize_experiment_data(self) -> ExperimentData: """Initialize the return data container for the experiment run""" - return self.__experiment_data__(experiment=self) + return ExperimentData(experiment=self) def run_analysis( self, experiment_data: ExperimentData, replace_results: bool = False, **options diff --git a/qiskit_experiments/framework/composite/__init__.py b/qiskit_experiments/framework/composite/__init__.py index 9aef51733b..d308f3f38c 100644 --- a/qiskit_experiments/framework/composite/__init__.py +++ b/qiskit_experiments/framework/composite/__init__.py @@ -13,7 +13,6 @@ """Composite Experiments""" # Base classes -from .composite_experiment_data import CompositeExperimentData from .composite_analysis import CompositeAnalysis # Composite experiment classes diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index 437e7c81dd..cf9f2c6a50 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -13,18 +13,15 @@ Composite Experiment Analysis class. """ -from qiskit.exceptions import QiskitError -from qiskit_experiments.framework import BaseAnalysis -from .composite_experiment_data import CompositeExperimentData +from qiskit.result import marginal_counts +from qiskit_experiments.framework import BaseAnalysis, ExperimentData class CompositeAnalysis(BaseAnalysis): """Analysis class for CompositeExperiment""" - __experiment_data__ = CompositeExperimentData - # pylint: disable = arguments-differ - def _run_analysis(self, experiment_data: CompositeExperimentData, **options): + def _run_analysis(self, experiment_data: ExperimentData, **options): """Run analysis on circuit data. Args: @@ -40,15 +37,43 @@ def _run_analysis(self, experiment_data: CompositeExperimentData, **options): QiskitError: if analysis is attempted on non-composite experiment data. """ - if not isinstance(experiment_data, CompositeExperimentData): - raise QiskitError("CompositeAnalysis must be run on CompositeExperimentData.") + # Maginalize data + self._marginalize_data(experiment_data) comp_exp = experiment_data.experiment for i in range(comp_exp.num_experiments): # Run analysis for sub-experiments and add sub-experiment metadata exp = comp_exp.component_experiment(i) - expdata = experiment_data.component_experiment_data(i) + expdata = experiment_data.child_data(i) exp.run_analysis(expdata, **options) return [], [] + + def _marginalize_data(self, experiment_data: ExperimentData): + """Maginalize composite data and store in child experiments""" + # Marginalize data + child_data = {} + for datum in experiment_data.data(): + metadata = datum.get("metadata", {}) + + # Add marginalized data to sub experiments + if "composite_clbits" in metadata: + composite_clbits = metadata["composite_clbits"] + else: + composite_clbits = None + for i, index in enumerate(metadata["composite_index"]): + if index not in child_data: + # Initialize data list for child data + child_data[index] = [] + sub_data = {"metadata": metadata["composite_metadata"][i]} + if "counts" in datum: + if composite_clbits is not None: + sub_data["counts"] = marginal_counts(datum["counts"], composite_clbits[i]) + else: + sub_data["counts"] = datum["counts"] + child_data[index].append(sub_data) + + # Add child data + for index, data in child_data.items(): + experiment_data.child_data(index).add_data(data) diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index db696dcc66..4134ad9333 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -16,11 +16,8 @@ from typing import List, Sequence, Optional from abc import abstractmethod import warnings - from qiskit.providers.backend import Backend -from qiskit_experiments.framework import BaseExperiment - -from .composite_experiment_data import CompositeExperimentData +from qiskit_experiments.framework import BaseExperiment, ExperimentData from .composite_analysis import CompositeAnalysis @@ -28,7 +25,6 @@ class CompositeExperiment(BaseExperiment): """Composite Experiment base class""" __analysis_class__ = CompositeAnalysis - __experiment_data__ = CompositeExperimentData def __init__( self, @@ -85,6 +81,12 @@ def _set_backend(self, backend): for subexp in self._experiments: subexp._set_backend(backend) + def _initialize_experiment_data(self) -> ExperimentData: + expdata = super()._initialize_experiment_data() + for subexp in self._experiments: + expdata.add_child_data(subexp._initialize_experiment_data()) + return expdata + def _add_job_metadata(self, experiment_data, jobs, **run_options): # Add composite metadata super()._add_job_metadata(experiment_data, jobs, **run_options) @@ -103,7 +105,7 @@ def _add_job_metadata(self, experiment_data, jobs, **run_options): "Sub-experiment run and transpile options" " are overridden by composite experiment options." ) - sub_data = experiment_data.component_experiment_data(i) + sub_data = experiment_data.child_data(i) sub_exp._add_job_metadata(sub_data, jobs, **run_options) def _postprocess_transpiled_circuits(self, circuits, **run_options): diff --git a/qiskit_experiments/framework/composite/composite_experiment_data.py b/qiskit_experiments/framework/composite/composite_experiment_data.py deleted file mode 100644 index 1e04c4df84..0000000000 --- a/qiskit_experiments/framework/composite/composite_experiment_data.py +++ /dev/null @@ -1,198 +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. -""" -Composite Experiment data class. -""" - -from typing import Optional, Union, List -from qiskit.result import marginal_counts -from qiskit.exceptions import QiskitError -from qiskit_experiments.framework.experiment_data import ExperimentData -from qiskit_experiments.database_service import DatabaseServiceV1 -from qiskit_experiments.database_service.utils import combined_timeout - - -class CompositeExperimentData(ExperimentData): - """Composite experiment data class""" - - def __init__(self, experiment, backend=None, parent_id=None, job_ids=None): - """Initialize experiment data. - - Args: - 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. - parent_id (str): Optional, ID of the parent experiment data - in the setting of a composite experiment. - job_ids (list[str]): Optional, IDs of jobs submitted for the experiment. - """ - - super().__init__(experiment, backend=backend, parent_id=parent_id, job_ids=job_ids) - - # Initialize sub experiments - self._components = [ - expr.__experiment_data__(expr, backend=backend, parent_id=self.experiment_id) - for expr in experiment._experiments - ] - - self.metadata["component_ids"] = [comp.experiment_id for comp in self._components] - self.metadata["component_classes"] = [comp.__class__.__name__ for comp in self._components] - - 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"\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:" - ret += f"\n{str(self._analysis_results.values()[-1])}" - return ret - - def component_experiment_data( - self, index: Optional[Union[int, slice]] = None - ) -> Union[ExperimentData, List[ExperimentData]]: - """Return component experiment data""" - if index is None: - return self._components - if isinstance(index, (int, slice)): - return self._components[index] - raise QiskitError(f"Invalid index type {type(index)}.") - - def _add_single_data(self, data): - """Add data to the experiment""" - # TODO: Handle optional marginalizing IQ data - metadata = data.get("metadata", {}) - if metadata.get("experiment_type") == self._type: - - # Add parallel data - self._data.append(data) - - # Add marginalized data to sub experiments - if "composite_clbits" in metadata: - composite_clbits = metadata["composite_clbits"] - else: - composite_clbits = None - for i, index in enumerate(metadata["composite_index"]): - sub_data = {"metadata": metadata["composite_metadata"][i]} - if "counts" in data: - if composite_clbits is not None: - sub_data["counts"] = marginal_counts(data["counts"], composite_clbits[i]) - else: - sub_data["counts"] = data["counts"] - self._components[index]._add_single_data(sub_data) - - def save(self) -> None: - super().save() - for comp in self._components: - original_verbose = comp.verbose - comp.verbose = False - comp.save() - comp.verbose = original_verbose - - def save_metadata(self) -> None: - super().save_metadata() - for comp in self._components: - comp.save_metadata() - - @classmethod - def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "CompositeExperimentData": - expdata = ExperimentData.load(experiment_id, service) - expdata.__class__ = CompositeExperimentData - expdata._components = [] - for comp_id, comp_class in zip( - expdata.metadata["component_ids"], expdata.metadata["component_classes"] - ): - load_class = globals()[comp_class] - load_func = getattr(load_class, "load") - loaded_comp = load_func(comp_id, service) - - # Sub-experiments that were saved before parent_id was introduced - - # their parent_id was set to None by the super class load method, - # and has now to be updated to the correct id - loaded_comp._parent_id = expdata.experiment_id - - expdata._components.append(loaded_comp) - - return expdata - - def _set_service(self, service: DatabaseServiceV1) -> None: - """Set the service to be used for storing experiment data. - - Args: - service: Service to be used. - - Raises: - DbExperimentDataError: If an experiment service is already being used. - """ - super()._set_service(service) - for comp in self._components: - comp._set_service(service) - - @ExperimentData.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, IBM Quantum experiment service allows - "public", "hub", "group", "project", and "private". - """ - self._share_level = new_level - for comp in self._components: - original_auto_save = comp.auto_save - comp.auto_save = False - comp.share_level = new_level - comp.auto_save = original_auto_save - if self.auto_save: - self.save_metadata() - - def _copy_metadata( - self, new_instance: Optional["CompositeExperimentData"] = None - ) -> "CompositeExperimentData": - """Make a copy of the composite experiment metadata. - - Note: - This method only copies experiment data and metadata, not its - figures nor analysis results. The copy also contains a different - experiment ID. - - Returns: - A copy of the ``CompositeExperimentData`` object with the same data - and metadata but different ID. - """ - new_instance = super()._copy_metadata(new_instance) - - for original_comp, new_comp in zip( - self.component_experiment_data(), new_instance.component_experiment_data() - ): - original_comp._copy_metadata(new_comp) - - new_instance.metadata["component_ids"] = [ - comp.experiment_id for comp in new_instance.component_experiment_data() - ] - return new_instance - - def block_for_results(self, timeout: Optional[float] = None): - _, timeout = combined_timeout(super().block_for_results, timeout) - for subdata in self._components: - _, timeout = combined_timeout(subdata.block_for_results, timeout) - return self diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index 225a932644..14ff5ac381 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -12,28 +12,42 @@ """ Experiment Data class """ +from __future__ import annotations import logging -from typing import Dict, Optional +from typing import Dict, Optional, List, Union from datetime import datetime - -from qiskit_experiments.database_service import DbExperimentDataV1 -from qiskit_experiments.database_service.database_service import DatabaseServiceV1 +import warnings +from qiskit.exceptions import QiskitError +from qiskit.providers.backend import Backend +from qiskit_experiments.database_service import DbExperimentDataV1 as DbExperimentData +from qiskit_experiments.database_service.database_service import ( + DatabaseServiceV1 as DatabaseService, +) +from qiskit_experiments.database_service.utils import combined_timeout, ThreadSafeList LOG = logging.getLogger(__name__) -class ExperimentData(DbExperimentDataV1): +class ExperimentData(DbExperimentData): """Qiskit Experiments Data container class""" - def __init__(self, experiment=None, backend=None, parent_id=None, job_ids=None): + def __init__( + self, + experiment: Optional["BaseExperiment"] = None, + backend: Optional[Backend] = None, + parent_id: Optional[str] = None, + job_ids: Optional[List[str]] = None, + child_data: Optional[List[ExperimentData]] = None, + ): """Initialize experiment data. Args: - experiment (BaseExperiment): Optional, experiment object that generated the data. - backend (Backend): Optional, Backend the experiment runs on. - parent_id (str): Optional, ID of the parent experiment data + experiment: Optional, experiment object that generated the data. + backend: Optional, Backend the experiment runs on. + parent_id: Optional, ID of the parent experiment data in the setting of a composite experiment - job_ids (list[str]): Optional, IDs of jobs submitted for the experiment. + job_ids: Optional, IDs of jobs submitted for the experiment. + child_data: Optional, list of child experiment data. """ if experiment is not None: backend = backend or experiment.backend @@ -50,6 +64,12 @@ def __init__(self, experiment=None, backend=None, parent_id=None, job_ids=None): metadata=experiment._metadata() if experiment else {}, ) + # Add component data and set parent ID to current container + self._child_data = ThreadSafeList() + self.metadata["child_ids"] = [] + if child_data is not None: + self._set_child_data(child_data) + @property def experiment(self): """Return the experiment for this data. @@ -69,23 +89,110 @@ def completion_times(self) -> Dict[str, datetime]: return job_times + def add_child_data(self, experiment_data: ExperimentData): + """Add child experiment data to the current experiment data""" + experiment_data._parent_id = self.experiment_id + self._child_data.append(experiment_data) + self.metadata["child_ids"].append(experiment_data.experiment_id) + + def child_data( + self, index: Optional[Union[int, slice]] = None + ) -> Union[ExperimentData, List[ExperimentData]]: + """Return child experiment data""" + if index is None: + return self._child_data + if isinstance(index, (int, slice)): + return self._child_data[index] + raise QiskitError(f"Invalid index type {type(index)}.") + + def component_experiment_data( + self, index: Optional[Union[int, slice]] = None + ) -> Union[ExperimentData, List[ExperimentData]]: + """Return child experiment data""" + warnings.warn( + "This method is deprecated and will be removed next release. " + "Use the `child_data` method instead.", + DeprecationWarning, + ) + return self.child_data(index) + + def save(self) -> None: + super().save() + for data in self._child_data: + original_verbose = data.verbose + data.verbose = False + data.save() + data.verbose = original_verbose + + def save_metadata(self) -> None: + super().save_metadata() + for data in self._child_data: + data.save_metadata() + @classmethod - def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "ExperimentData": - """Load a saved experiment data from a database service. + def load(cls, experiment_id: str, service: DatabaseService) -> ExperimentData: + expdata = DbExperimentData.load(experiment_id, service) + expdata.__class__ = ExperimentData + child_data = [ + ExperimentData.load(child_id, service) + for child_id in expdata.metadata.get("child_ids", []) + ] + expdata._set_child_data(child_data) + return expdata + + def _set_child_data(self, child_data: List[ExperimentData]): + """Set child experiment data for the current experiment.""" + self._child_data = ThreadSafeList() + self.metadata["child_ids"] = [] + for data in child_data: + self.add_child_data(data) + + def _set_service(self, service: DatabaseService) -> None: + """Set the service to be used for storing experiment data. + + Args: + service: Service to be used. + + Raises: + DbExperimentDataError: If an experiment service is already being used. + """ + super()._set_service(service) + for data in self._child_data: + data._set_service(service) + + @DbExperimentData.share_level.setter + def share_level(self, new_level: str) -> None: + """Set the experiment share level. Args: - experiment_id: Experiment ID. - service: the database service. + new_level: New experiment share level. Valid share levels are provider- + specified. For example, IBM Quantum experiment service allows + "public", "hub", "group", "project", and "private". + """ + self._share_level = new_level + for data in self._child_data: + original_auto_save = data.auto_save + data.auto_save = False + data.share_level = new_level + data.auto_save = original_auto_save + if self.auto_save: + self.save_metadata() + + def block_for_results(self, timeout: Optional[float] = None) -> ExperimentData: + """Block until all pending jobs and analysis callbacks finish. + + Args: + timeout: Timeout waiting for results. Returns: - The loaded experiment data. + The experiment data with finished jobs and post-processing. """ - expdata = DbExperimentDataV1.load(experiment_id, service) - expdata.__class__ = ExperimentData - expdata._experiment = None - return expdata + _, timeout = combined_timeout(super().block_for_results, timeout) + for subdata in self._child_data: + _, timeout = combined_timeout(subdata.block_for_results, timeout) + return self - def _copy_metadata(self, new_instance: Optional["ExperimentData"] = None) -> "ExperimentData": + def _copy_metadata(self, new_instance: Optional[ExperimentData] = None) -> ExperimentData: """Make a copy of the experiment metadata. Note: @@ -97,11 +204,14 @@ def _copy_metadata(self, new_instance: Optional["ExperimentData"] = None) -> "Ex A copy of the ``ExperimentData`` object with the same data and metadata but different ID. """ - if new_instance is None: - new_instance = self.__class__( - experiment=self.experiment, backend=self.backend, job_ids=self.job_ids - ) - return super()._copy_metadata(new_instance) + new_instance = super()._copy_metadata(new_instance) + new_instance._experiment = self.experiment + new_instance._child_data = self._child_data + + # Recursively copy metadata of child data + child_data = [data._copy_metadata() for data in new_instance._child_data] + new_instance._set_child_data(child_data) + return new_instance def __repr__(self): out = ( @@ -111,3 +221,27 @@ def __repr__(self): f", experiment_id: {self.experiment_id}>" ) return out + + 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}" + if self._parent_id: + ret += f"\nParent ID: {self._parent_id}" + if self._child_data: + ret += f"\nChild Experiment Data: {len(self._child_data)}" + 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: + ret += f"\nTags: {self.tags}" + ret += f"\nData: {len(self._data)}" + ret += f"\nAnalysis Results: {n_res}" + ret += f"\nFigures: {len(self._figures)}" + return ret diff --git a/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml b/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml index 5d181eecd5..68f83dbc9d 100644 --- a/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml +++ b/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml @@ -1,10 +1,27 @@ --- upgrade: - | - The ``experiment_data`` kwarg has been removed from the - :meth:`~qiskit_experiment.framework.BaseExperiment.run` method of - experiments. To combine data from multiple executions you can manually - add data from one :class:`~qiskit_experiments.framework.ExperimentData` - container to another using the - :meth:`~qiskit_experiments.framework.ExperimentData.add_data` method - eg. ``expdata2.add_data(expdata1.data())`` + The ``CompositeExperimentData`` class has been removed and its + functionality integrated into the + :class:`~qiskit_experiments.framework.ExperimentData` class. + A composite :class:`~qiskit_experiments.framework.ExperimentData` + can now be created by initializing with a list of child + ``ExperimentData`` containers using the ``child_data`` kwarg. + - | + :class:`~qiskit_experiments.framework.ParallelExperiment` and + :class:`~qiskit_experiments.framework.BatchExperiment` now return + a :class:`~qiskit_experiments.framework.ExperimentData` object + which no longer contains a ``composite_experiment_data`` method. + This method has been replaced by the + :meth:`~qiskit_experiments.framework.ExperimentData.child_data` + method. +features: + - | + The :class:`~qiskit_experiments.framework.ExperimentData` class + can now store child ``ExperimentData`` containers. + Child data can either be added at initialization using the + ``child_data`` kwarg or added later using the + :meth:`~qiskit_experiments.framework.ExperimentData.add_child_data` + method. Child ``ExperimentData`` can be accessed using the + :meth:`~qiskit_experiments.framework.ExperimentData.child_data` + method. diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 53a6e166b4..43b43c8adc 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -269,6 +269,7 @@ def test_good_analysis(self): .run(experiment_data, data_processor=data_processor, plot=False) .block_for_results() ) + experiment_data.block_for_results() result = experiment_data.analysis_results() self.assertEqual(result[0].quality, "good") self.assertTrue(abs(result[0].value.value[1] - expected_rate) < test_tol) @@ -289,6 +290,7 @@ def test_bad_analysis(self): .run(experiment_data, data_processor=data_processor, plot=False) .block_for_results() ) + experiment_data.block_for_results() result = experiment_data.analysis_results() self.assertEqual(result[0].quality, "bad") diff --git a/test/database_service/test_db_experiment_data.py b/test/database_service/test_db_experiment_data.py index b2364df7cb..860c1238c0 100644 --- a/test/database_service/test_db_experiment_data.py +++ b/test/database_service/test_db_experiment_data.py @@ -700,17 +700,6 @@ def test_additional_attr(self): 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 = DbExperimentData(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 test_copy_metadata(self): """Test copy metadata.""" exp_data = DbExperimentData(experiment_type="qiskit_test") diff --git a/test/test_composite.py b/test/test_composite.py index f31dc21241..29757a79a7 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -25,7 +25,7 @@ from qiskit_experiments.framework import ( ParallelExperiment, Options, - CompositeExperimentData, + ExperimentData, BatchExperiment, ) from qiskit_experiments.database_service import DatabaseServiceV1 @@ -261,7 +261,7 @@ def preferences(self) -> Dict: class TestCompositeExperimentData(QiskitTestCase): """ - Test operations on objects of CompositeExperimentData + Test operations on objects of composit ExperimentData """ def setUp(self): @@ -276,7 +276,7 @@ def setUp(self): exp3 = FakeExperiment(4) batch_exp = BatchExperiment([par_exp, exp3]) - self.rootdata = CompositeExperimentData(batch_exp, backend=self.backend) + self.rootdata = ExperimentData(batch_exp, backend=self.backend) self.rootdata.share_level = self.share_level @@ -287,11 +287,10 @@ def check_attributes(self, expdata): self.assertEqual(expdata.backend, self.backend) self.assertEqual(expdata.share_level, self.share_level) - if isinstance(expdata, CompositeExperimentData): - components = expdata.component_experiment_data() - comp_ids = expdata.metadata["component_ids"] - comp_classes = expdata.metadata["component_classes"] - for childdata, comp_id, comp_class in zip(components, comp_ids, comp_classes): + if isinstance(expdata, ExperimentData): + components = expdata.child_data() + comp_ids = expdata.metadata.get("child_ids", []) + for childdata, comp_id, comp_class in zip(components, comp_ids): self.check_attributes(childdata) self.assertEqual(childdata.parent_id, expdata.experiment_id) self.assertEqual(childdata.experiment_id, comp_id) @@ -309,24 +308,22 @@ def check_if_equal(self, expdata1, expdata2, is_a_copy): metadata1 = copy.copy(expdata1.metadata) metadata2 = copy.copy(expdata2.metadata) if is_a_copy: - comp_ids1 = metadata1.pop("component_ids", None) - comp_ids2 = metadata2.pop("component_ids", None) - if comp_ids1 is None: - self.assertEqual(comp_ids2, None) - else: - self.assertNotEqual(comp_ids1, comp_ids2) + comp_ids1 = metadata1.pop("child_ids", []) + comp_ids2 = metadata2.pop("child_ids", []) + for id1 in comp_ids1: + self.assertNotIn(id1, comp_ids2) + for id2 in comp_ids2: + self.assertNotIn(id2, comp_ids1) if expdata1.parent_id is None: self.assertEqual(expdata2.parent_id, None) else: self.assertNotEqual(expdata1.parent_id, expdata2.parent_id) else: self.assertEqual(expdata1.parent_id, expdata2.parent_id) - self.assertEqual(metadata1, metadata2) + self.assertDictEqual(metadata1, metadata2, msg="metadata not equal") - if isinstance(expdata1, CompositeExperimentData): - for childdata1, childdata2 in zip( - expdata1.component_experiment_data(), expdata2.component_experiment_data() - ): + if isinstance(expdata1, ExperimentData): + for childdata1, childdata2 in zip(expdata1.child_data(), expdata2.child_data()): self.check_if_equal(childdata1, childdata2, is_a_copy) def test_composite_experiment_data_attributes(self): @@ -343,28 +340,22 @@ def test_composite_save_load(self): self.rootdata.service = DummyService() self.rootdata.save() - loaded_data = CompositeExperimentData.load( - self.rootdata.experiment_id, self.rootdata.service - ) - + loaded_data = ExperimentData.load(self.rootdata.experiment_id, self.rootdata.service) self.check_if_equal(loaded_data, self.rootdata, is_a_copy=False) def test_composite_save_metadata(self): """ Verify that saving metadata and loading restores the original composite experiment data object """ - self.rootdata.service = DummyService() self.rootdata.save_metadata() - loaded_data = CompositeExperimentData.load( - self.rootdata.experiment_id, self.rootdata.service - ) + loaded_data = ExperimentData.load(self.rootdata.experiment_id, self.rootdata.service) self.check_if_equal(loaded_data, self.rootdata, is_a_copy=False) def test_composite_copy_metadata(self): """ - Test CompositeExperimentData._copy_metadata + Test composite ExperimentData._copy_metadata """ new_instance = self.rootdata._copy_metadata() self.check_if_equal(new_instance, self.rootdata, is_a_copy=True) diff --git a/test/test_t1.py b/test/test_t1.py index 6b78e56b55..665c08c2b3 100644 --- a/test/test_t1.py +++ b/test/test_t1.py @@ -74,7 +74,7 @@ def test_t1_parallel(self): res.block_for_results() for i in range(2): - sub_res = res.component_experiment_data(i).analysis_results("T1") + sub_res = res.child_data(i).analysis_results("T1") self.assertEqual(sub_res.quality, "good") self.assertAlmostEqual(sub_res.value.value, t1[i], delta=3) @@ -98,7 +98,7 @@ def test_t1_parallel_different_analysis_options(self): sub_res = [] for i in range(2): - sub_res.append(res.component_experiment_data(i).analysis_results("T1")) + sub_res.append(res.child_data(i).analysis_results("T1")) self.assertEqual(sub_res[0].quality, "good") self.assertAlmostEqual(sub_res[0].value.value, t1, delta=3) diff --git a/test/test_t2ramsey.py b/test/test_t2ramsey.py index e06d8f1ac6..c83a79cb40 100644 --- a/test/test_t2ramsey.py +++ b/test/test_t2ramsey.py @@ -120,7 +120,7 @@ def test_t2ramsey_parallel(self): expdata.block_for_results() for i in range(2): - res_t2star = expdata.component_experiment_data(i).analysis_results("T2star") + res_t2star = expdata.child_data(i).analysis_results("T2star") self.assertAlmostEqual( res_t2star.value.value, t2ramsey[i], @@ -129,7 +129,7 @@ def test_t2ramsey_parallel(self): self.assertEqual( res_t2star.quality, "good", "Result quality bad for experiment on qubit " + str(i) ) - res_freq = expdata.component_experiment_data(i).analysis_results("Frequency") + res_freq = expdata.child_data(i).analysis_results("Frequency") self.assertAlmostEqual( res_freq.value.value, estimated_freq[i], diff --git a/test/test_tomography.py b/test/test_tomography.py index 0948d58d78..3bb403d59f 100644 --- a/test/test_tomography.py +++ b/test/test_tomography.py @@ -218,7 +218,7 @@ def test_batch_exp(self): # Check target fidelity of component experiments f_threshold = 0.95 for i in range(batch_exp.num_experiments): - results = batch_data.component_experiment_data(i).analysis_results() + results = batch_data.child_data(i).analysis_results() # Check state is density matrix state = filter_results(results, "state").value @@ -257,7 +257,7 @@ def test_parallel_exp(self): # Check target fidelity of component experiments f_threshold = 0.95 for i in range(par_exp.num_experiments): - results = par_data.component_experiment_data(i).analysis_results() + results = par_data.child_data(i).analysis_results() # Check state is density matrix state = filter_results(results, "state").value @@ -440,7 +440,7 @@ def test_batch_exp_with_measurement_qubits(self): # Check target fidelity of component experiments f_threshold = 0.95 for i in range(batch_exp.num_experiments): - results = batch_data.component_experiment_data(i).analysis_results() + results = batch_data.child_data(i).analysis_results() # Check state is density matrix state = filter_results(results, "state").value @@ -477,7 +477,7 @@ def test_parallel_exp(self): # Check target fidelity of component experiments f_threshold = 0.95 for i in range(par_exp.num_experiments): - results = par_data.component_experiment_data(i).analysis_results() + results = par_data.child_data(i).analysis_results() # Check state is density matrix state = filter_results(results, "state").value From 37a3c247a2f5c72d4d7b03c727dc3e4cff668c19 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Fri, 22 Oct 2021 14:18:29 -0400 Subject: [PATCH 02/12] Update tutorial notebooks --- docs/tutorials/quantum_volume.ipynb | 4 ++-- docs/tutorials/randomized_benchmarking.ipynb | 11 ++++------- docs/tutorials/state_tomography.ipynb | 3 +-- docs/tutorials/t1.ipynb | 5 ++--- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/docs/tutorials/quantum_volume.ipynb b/docs/tutorials/quantum_volume.ipynb index 7ac4c6962d..16195d02b8 100644 --- a/docs/tutorials/quantum_volume.ipynb +++ b/docs/tutorials/quantum_volume.ipynb @@ -281,7 +281,7 @@ ], "source": [ "qv_values = [\n", - " batch_expdata.component_experiment_data(i).analysis_results(\"quantum_volume\").value\n", + " batch_expdata.cchild_data(i).analysis_results(\"quantum_volume\").value\n", " for i in range(batch_exp.num_experiments)\n", "]\n", "\n", @@ -399,7 +399,7 @@ "source": [ "for i in range(batch_exp.num_experiments):\n", " print(f\"\\nComponent experiment {i}\")\n", - " sub_data = batch_expdata.component_experiment_data(i)\n", + " sub_data = batch_expdata.child_data(i)\n", " display(sub_data.figure(0))\n", " for result in sub_data.analysis_results():\n", " print(result)" diff --git a/docs/tutorials/randomized_benchmarking.ipynb b/docs/tutorials/randomized_benchmarking.ipynb index 1841e9f162..815fea4389 100644 --- a/docs/tutorials/randomized_benchmarking.ipynb +++ b/docs/tutorials/randomized_benchmarking.ipynb @@ -580,15 +580,13 @@ "source": [ "### Viewing sub experiment data\n", "\n", - "The experiment data returned from a batched experiment also contains individual experiment data for each sub experiment which can be accessed using `component_experiment_data(index)`" + "The experiment data returned from a batched experiment also contains individual experiment data for each sub experiment which can be accessed using `child_data`" ] }, { "cell_type": "code", "execution_count": 14, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -761,9 +759,8 @@ ], "source": [ "# Print sub-experiment data\n", - "for i in range(par_exp.num_experiments):\n", + "for i, sub_data in enumerate(par_expdata.child_data):\n", " print(f\"Component experiment {i}\")\n", - " sub_data = par_expdata.component_experiment_data(i)\n", " display(sub_data.figure(0))\n", " for result in sub_data.analysis_results():\n", " print(result)" @@ -832,7 +829,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.11" + "version": "3.7.5" } }, "nbformat": 4, diff --git a/docs/tutorials/state_tomography.ipynb b/docs/tutorials/state_tomography.ipynb index 54196da9b9..b069fb3453 100644 --- a/docs/tutorials/state_tomography.ipynb +++ b/docs/tutorials/state_tomography.ipynb @@ -411,8 +411,7 @@ } ], "source": [ - "for i in range(parexp.num_experiments):\n", - " expdata = pardata.component_experiment_data(i)\n", + "for i, expdata in enumerate(pardata.child_data()):\n", " state_result_i = expdata.analysis_results(\"state\")\n", " fid_result_i = expdata.analysis_results(\"state_fidelity\")\n", " \n", diff --git a/docs/tutorials/t1.ipynb b/docs/tutorials/t1.ipynb index 94d8b902fb..0266a02c1d 100644 --- a/docs/tutorials/t1.ipynb +++ b/docs/tutorials/t1.ipynb @@ -177,7 +177,7 @@ "source": [ "### Viewing sub experiment data\n", "\n", - "The experiment data returned from a batched experiment also contains individual experiment data for each sub experiment which can be accessed using `component_experiment_data(index)`" + "The experiment data returned from a batched experiment also contains individual experiment data for each sub experiment which can be accessed using `child_data`" ] }, { @@ -260,9 +260,8 @@ ], "source": [ "# Print sub-experiment data\n", - "for i in range(parallel_exp.num_experiments):\n", + "for i, sub_data in enumerate(parallel_data.child_data()):\n", " print(f\"Component experiment {i}\")\n", - " sub_data = parallel_data.component_experiment_data(i)\n", " display(sub_data.figure(0))\n", " for result in sub_data.analysis_results():\n", " print(result)" From 970ca48b672cfcbe9e57ed017d205cfe5ce427ee Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Tue, 2 Nov 2021 12:40:53 -0400 Subject: [PATCH 03/12] Review comments, rework child_ids Change child data to be stored as a ThreadSafeOrderedDict instead of ThreadSafeList, where the keys are the child experiment ids. This allows child data to also be looked up by experiment id as well as index. The child ids are now only copied to metadata when saving to DB, and popped back off metadata when loading. --- .../framework/experiment_data.py | 64 ++++++++++++------- test/test_composite.py | 14 ++-- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index 14ff5ac381..d994f57e75 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -23,7 +23,7 @@ from qiskit_experiments.database_service.database_service import ( DatabaseServiceV1 as DatabaseService, ) -from qiskit_experiments.database_service.utils import combined_timeout, ThreadSafeList +from qiskit_experiments.database_service.utils import ThreadSafeOrderedDict, combined_timeout LOG = logging.getLogger(__name__) @@ -65,8 +65,7 @@ def __init__( ) # Add component data and set parent ID to current container - self._child_data = ThreadSafeList() - self.metadata["child_ids"] = [] + self._child_data = ThreadSafeOrderedDict() if child_data is not None: self._set_child_data(child_data) @@ -92,16 +91,34 @@ def completion_times(self) -> Dict[str, datetime]: def add_child_data(self, experiment_data: ExperimentData): """Add child experiment data to the current experiment data""" experiment_data._parent_id = self.experiment_id - self._child_data.append(experiment_data) - self.metadata["child_ids"].append(experiment_data.experiment_id) + self._child_data[experiment_data.experiment_id] = experiment_data def child_data( - self, index: Optional[Union[int, slice]] = None + self, index: Optional[Union[int, slice, str]] = None ) -> Union[ExperimentData, List[ExperimentData]]: - """Return child experiment data""" + """Return child experiment data. + + Args: + index: Index of the child experiment data to be returned. + Several types are accepted for convenience: + + * None: Return all child data. + * int: Specific index of the child data. + * slice: A list slice of indexes. + * str: experiment ID of the child data. + + Returns: + The requested single or list of child experiment data. + + Raises: + QiskitError: if the index or ID of the child experiment data + cannot be found. + """ if index is None: - return self._child_data + return self._child_data.values() if isinstance(index, (int, slice)): + return self._child_data.values()[index] + if isinstance(index, str): return self._child_data[index] raise QiskitError(f"Invalid index type {type(index)}.") @@ -118,32 +135,33 @@ def component_experiment_data( def save(self) -> None: super().save() - for data in self._child_data: + for data in self._child_data.values(): original_verbose = data.verbose data.verbose = False data.save() data.verbose = original_verbose def save_metadata(self) -> None: + # Copy child experiment IDs to metadata + if self._child_data: + self._metadata["child_data_ids"] = self._child_data.keys() super().save_metadata() - for data in self._child_data: + for data in self._child_data.values(): data.save_metadata() @classmethod def load(cls, experiment_id: str, service: DatabaseService) -> ExperimentData: expdata = DbExperimentData.load(experiment_id, service) expdata.__class__ = ExperimentData - child_data = [ - ExperimentData.load(child_id, service) - for child_id in expdata.metadata.get("child_ids", []) - ] + expdata._experiment = None + child_data_ids = expdata.metadata.pop("child_data_ids", []) + child_data = [ExperimentData.load(child_id, service) for child_id in child_data_ids] expdata._set_child_data(child_data) return expdata def _set_child_data(self, child_data: List[ExperimentData]): """Set child experiment data for the current experiment.""" - self._child_data = ThreadSafeList() - self.metadata["child_ids"] = [] + self._child_data = ThreadSafeOrderedDict() for data in child_data: self.add_child_data(data) @@ -157,7 +175,7 @@ def _set_service(self, service: DatabaseService) -> None: DbExperimentDataError: If an experiment service is already being used. """ super()._set_service(service) - for data in self._child_data: + for data in self._child_data.values(): data._set_service(service) @DbExperimentData.share_level.setter @@ -170,7 +188,7 @@ def share_level(self, new_level: str) -> None: "public", "hub", "group", "project", and "private". """ self._share_level = new_level - for data in self._child_data: + for data in self._child_data.values(): original_auto_save = data.auto_save data.auto_save = False data.share_level = new_level @@ -188,7 +206,7 @@ def block_for_results(self, timeout: Optional[float] = None) -> ExperimentData: The experiment data with finished jobs and post-processing. """ _, timeout = combined_timeout(super().block_for_results, timeout) - for subdata in self._child_data: + for subdata in self._child_data.values(): _, timeout = combined_timeout(subdata.block_for_results, timeout) return self @@ -205,11 +223,13 @@ def _copy_metadata(self, new_instance: Optional[ExperimentData] = None) -> Exper and metadata but different ID. """ new_instance = super()._copy_metadata(new_instance) - new_instance._experiment = self.experiment - new_instance._child_data = self._child_data + if self.experiment is None: + new_instance._experiment = None + else: + new_instance._experiment = self.experiment.copy() # Recursively copy metadata of child data - child_data = [data._copy_metadata() for data in new_instance._child_data] + child_data = [data._copy_metadata() for data in self._child_data.values()] new_instance._set_child_data(child_data) return new_instance diff --git a/test/test_composite.py b/test/test_composite.py index 29757a79a7..7f207c5b7e 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -287,14 +287,12 @@ def check_attributes(self, expdata): self.assertEqual(expdata.backend, self.backend) self.assertEqual(expdata.share_level, self.share_level) - if isinstance(expdata, ExperimentData): - components = expdata.child_data() - comp_ids = expdata.metadata.get("child_ids", []) - for childdata, comp_id, comp_class in zip(components, comp_ids): - self.check_attributes(childdata) - self.assertEqual(childdata.parent_id, expdata.experiment_id) - self.assertEqual(childdata.experiment_id, comp_id) - self.assertEqual(childdata.__class__.__name__, comp_class) + components = expdata.child_data() + comp_ids = expdata.metadata.get("child_ids", []) + for childdata, comp_id in zip(components, comp_ids): + self.check_attributes(childdata) + self.assertEqual(childdata.parent_id, expdata.experiment_id) + self.assertEqual(childdata.experiment_id, comp_id) def check_if_equal(self, expdata1, expdata2, is_a_copy): """ From 86a665fd8705a000c542906f6da5e5be09491d16 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Tue, 2 Nov 2021 17:12:03 -0400 Subject: [PATCH 04/12] Rework composite analysis This change moves all creation and update of child experiment data for a composite experiment into the `CompositeAnalysis._run_analysis method`. --- qiskit_experiments/database_service/utils.py | 5 ++ .../framework/composite/composite_analysis.py | 69 ++++++++++++++----- .../composite/composite_experiment.py | 33 +++++---- 3 files changed, 73 insertions(+), 34 deletions(-) diff --git a/qiskit_experiments/database_service/utils.py b/qiskit_experiments/database_service/utils.py index 4dcb381c03..a3325c330a 100644 --- a/qiskit_experiments/database_service/utils.py +++ b/qiskit_experiments/database_service/utils.py @@ -240,6 +240,11 @@ def copy_object(self): obj._container = self.copy() return obj + def clear(self): + """Remove all elements from this container.""" + with self.lock: + self._container.clear() + class ThreadSafeOrderedDict(ThreadSafeContainer): """Thread safe OrderedDict.""" diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index cf9f2c6a50..ddc9328f64 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -37,24 +37,56 @@ def _run_analysis(self, experiment_data: ExperimentData, **options): QiskitError: if analysis is attempted on non-composite experiment data. """ - # Maginalize data - self._marginalize_data(experiment_data) + composite_exp = experiment_data.experiment + component_exps = composite_exp.component_experiment() + if "component_job_metadata" in experiment_data.metadata: + component_metadata = experiment_data.metadata["component_job_metadata"][-1] + else: + component_metadata = [{}] * composite_exp.num_experiments - comp_exp = experiment_data.experiment + # Check if component experiment data has already been initialized + components_exist = self._components_initialized(composite_exp, experiment_data) - for i in range(comp_exp.num_experiments): - # Run analysis for sub-experiments and add sub-experiment metadata - exp = comp_exp.component_experiment(i) - expdata = experiment_data.child_data(i) - exp.run_analysis(expdata, **options) + # Compute marginalize data + marginalized_data = self._marginalize_data(experiment_data.data()) + + # Construct component experiment data + for i, (sub_data, sub_exp) in enumerate(zip(marginalized_data, component_exps)): + if components_exist: + # Get existing component ExperimentData and clear any previously + # stored data + sub_exp_data = experiment_data.component_experiment_data(i) + sub_exp_data._data.clear() + else: + # Initialize component ExperimentData and add as child data + sub_exp_data = sub_exp._initialize_experiment_data() + experiment_data.add_child_data(sub_exp_data) + + # Add component job metadata + sub_exp_data._metadata["job_metadata"] = [component_metadata[i]] + + # Add marginalized data + sub_exp_data.add_data(sub_data) + + # Run analysis + sub_exp.run_analysis(sub_exp_data) return [], [] - def _marginalize_data(self, experiment_data: ExperimentData): - """Maginalize composite data and store in child experiments""" + def _components_initialized(self, experiment, experiment_data): + """Return True if component experiment data is initialized""" + if len(experiment_data.child_data()) != experiment.num_experiments: + return False + for data, exp in zip(experiment.composite_experiment(), experiment_data.child_data()): + if exp.experiment_type == data.experiment_type: + return False + return True + + def _marginalize_data(self, composite_data): + """Return marginalized data for component experiments""" # Marginalize data - child_data = {} - for datum in experiment_data.data(): + marginalized_data = {} + for datum in composite_data: metadata = datum.get("metadata", {}) # Add marginalized data to sub experiments @@ -63,17 +95,16 @@ def _marginalize_data(self, experiment_data: ExperimentData): else: composite_clbits = None for i, index in enumerate(metadata["composite_index"]): - if index not in child_data: - # Initialize data list for child data - child_data[index] = [] + if index not in marginalized_data: + # Initialize data list for marginalized + marginalized_data[index] = [] sub_data = {"metadata": metadata["composite_metadata"][i]} if "counts" in datum: if composite_clbits is not None: sub_data["counts"] = marginal_counts(datum["counts"], composite_clbits[i]) else: sub_data["counts"] = datum["counts"] - child_data[index].append(sub_data) + marginalized_data[index].append(sub_data) - # Add child data - for index, data in child_data.items(): - experiment_data.child_data(index).add_data(data) + # Sort by index + return [marginalized_data[i] for i in sorted(marginalized_data.keys())] diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 4134ad9333..9045837067 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -13,11 +13,12 @@ Composite Experiment abstract base class. """ +import copy from typing import List, Sequence, Optional from abc import abstractmethod import warnings from qiskit.providers.backend import Backend -from qiskit_experiments.framework import BaseExperiment, ExperimentData +from qiskit_experiments.framework import BaseExperiment from .composite_analysis import CompositeAnalysis @@ -81,32 +82,34 @@ def _set_backend(self, backend): for subexp in self._experiments: subexp._set_backend(backend) - def _initialize_experiment_data(self) -> ExperimentData: - expdata = super()._initialize_experiment_data() - for subexp in self._experiments: - expdata.add_child_data(subexp._initialize_experiment_data()) - return expdata + def _additional_metadata(self): + return {"component_job_metadata": []} def _add_job_metadata(self, experiment_data, jobs, **run_options): - # Add composite metadata - super()._add_job_metadata(experiment_data, jobs, **run_options) - + # Extract component metadata + component_metadata = [] # Add sub-experiment options - for i in range(self.num_experiments): - sub_exp = self.component_experiment(i) - + for sub_exp in self.component_experiment(): # Run and transpile options are always overridden if ( sub_exp.run_options != sub_exp._default_run_options() or sub_exp.transpile_options != sub_exp._default_transpile_options() ): - warnings.warn( "Sub-experiment run and transpile options" " are overridden by composite experiment options." ) - sub_data = experiment_data.child_data(i) - sub_exp._add_job_metadata(sub_data, jobs, **run_options) + component_metadata.append( + { + "job_ids": [job.job_id() for job in jobs], + "experiment_options": copy.copy(sub_exp.experiment_options.__dict__), + "transpile_options": copy.copy(sub_exp.transpile_options.__dict__), + "analysis_options": copy.copy(sub_exp.analysis_options.__dict__), + "run_options": copy.copy(run_options), + } + ) + super()._add_job_metadata(experiment_data, jobs, **run_options) + experiment_data._metadata["component_job_metadata"].append(component_metadata) def _postprocess_transpiled_circuits(self, circuits, **run_options): for expr in self._experiments: From 8c8a5f347c342ce1c7ca18ea0f6f456ed810225c Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Wed, 3 Nov 2021 14:29:55 -0400 Subject: [PATCH 05/12] Make composite analysis more robust --- qiskit_experiments/framework/base_analysis.py | 1 + .../framework/composite/composite_analysis.py | 46 ++++++++++++------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/qiskit_experiments/framework/base_analysis.py b/qiskit_experiments/framework/base_analysis.py index bec50e850c..792b0624b4 100644 --- a/qiskit_experiments/framework/base_analysis.py +++ b/qiskit_experiments/framework/base_analysis.py @@ -84,6 +84,7 @@ def run( experiment_data._created_in_db or experiment_data._analysis_results or experiment_data._figures + or getattr(experiment_data, "_child_data", None) ): experiment_data = experiment_data._copy_metadata() diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index ddc9328f64..206854748a 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -44,40 +44,54 @@ def _run_analysis(self, experiment_data: ExperimentData, **options): else: component_metadata = [{}] * composite_exp.num_experiments - # Check if component experiment data has already been initialized - components_exist = self._components_initialized(composite_exp, experiment_data) + # Initialize component data for updating and get the experiment IDs for + # the component child experiments + component_ids = self._initialize_components(composite_exp, experiment_data) # Compute marginalize data marginalized_data = self._marginalize_data(experiment_data.data()) # Construct component experiment data for i, (sub_data, sub_exp) in enumerate(zip(marginalized_data, component_exps)): - if components_exist: - # Get existing component ExperimentData and clear any previously - # stored data - sub_exp_data = experiment_data.component_experiment_data(i) - sub_exp_data._data.clear() - else: - # Initialize component ExperimentData and add as child data - sub_exp_data = sub_exp._initialize_experiment_data() - experiment_data.add_child_data(sub_exp_data) + sub_exp_data = experiment_data.child_data(component_ids[i]) + + # Clear any previously stored data and add marginalized data + sub_exp_data._data.clear() + sub_exp_data.add_data(sub_data) # Add component job metadata sub_exp_data._metadata["job_metadata"] = [component_metadata[i]] - # Add marginalized data - sub_exp_data.add_data(sub_data) - # Run analysis - sub_exp.run_analysis(sub_exp_data) + # Since copy for replace result is handled at the parent level + # we always run with replace result on component analysis + sub_exp.run_analysis(sub_exp_data, replace_results=True) return [], [] + def _initialize_components(self, experiment, experiment_data): + """Initialize child data components and return list of child experiment IDs""" + component_index = experiment_data._metadata.get("component_child_index", []) + if not component_index: + # Construct component data and update indices + start_index = len(experiment_data.child_data()) + component_index = [] + for i, sub_exp in enumerate(experiment.component_experiment()): + sub_data = sub_exp._initialize_experiment_data() + experiment_data.add_child_data(sub_data) + component_index.append(start_index + i) + experiment_data._metadata["component_child_index"] = component_index + + # Child components exist so we can get their ID for accessing them + child_ids = experiment_data._child_data.keys() + component_ids = [child_ids[idx] for idx in component_index] + return component_ids + def _components_initialized(self, experiment, experiment_data): """Return True if component experiment data is initialized""" if len(experiment_data.child_data()) != experiment.num_experiments: return False - for data, exp in zip(experiment.composite_experiment(), experiment_data.child_data()): + for data, exp in zip(experiment.component_experiment(), experiment_data.child_data()): if exp.experiment_type == data.experiment_type: return False return True From c5debcfddc9b5774e712dc6009a46e3ba1fc8e96 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Wed, 3 Nov 2021 14:32:23 -0400 Subject: [PATCH 06/12] Fixup --- .../framework/composite/composite_analysis.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index 206854748a..a90c030e1e 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -87,15 +87,6 @@ def _initialize_components(self, experiment, experiment_data): component_ids = [child_ids[idx] for idx in component_index] return component_ids - def _components_initialized(self, experiment, experiment_data): - """Return True if component experiment data is initialized""" - if len(experiment_data.child_data()) != experiment.num_experiments: - return False - for data, exp in zip(experiment.component_experiment(), experiment_data.child_data()): - if exp.experiment_type == data.experiment_type: - return False - return True - def _marginalize_data(self, composite_data): """Return marginalized data for component experiments""" # Marginalize data From 92fba8cbf0857f0cf44e52a6cb1058147e2a1a9f Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Thu, 4 Nov 2021 13:09:48 -0400 Subject: [PATCH 07/12] Update documentation for composite experiments --- .../framework/composite/batch_experiment.py | 18 +++++- .../framework/composite/composite_analysis.py | 58 +++++++++++++++---- .../composite/parallel_experiment.py | 18 +++++- .../framework/experiment_data.py | 6 +- .../update_expdata-ab3576f3bdd5057a.yaml | 2 +- 5 files changed, 86 insertions(+), 16 deletions(-) diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index 74006e5d73..274a72a267 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -24,7 +24,23 @@ @fix_class_docs class BatchExperiment(CompositeExperiment): - """Batch experiment class""" + """Combine multiple experiments into a batch experiment. + + Batch experiments combine individual experiments on any subset of qubits + into a single composite experiment which appends all the circuits from + each component experiment into a single batch of circuits to be executed + as one experiment job. + + Analysis of batch experiments is performed using the + :class:`~qiskit_experiments.framework.CompositeAnalysis` class which handles + sorting the composite experiment circuit data into individual child + :class:`ExperimentData` containers for each component experiment which are + then analyzed using the corresponding analysis class for that component + experiment. + + See :class:`~qiskit_experiments.framework.CompositeAnalysis` + documentation for additional information. + """ def __init__(self, experiments: List[BaseExperiment], backend: Optional[Backend] = None): """Initialize a batch experiment. diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index a90c030e1e..c5747bba3e 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -13,16 +13,39 @@ Composite Experiment Analysis class. """ +from typing import List, Dict from qiskit.result import marginal_counts from qiskit_experiments.framework import BaseAnalysis, ExperimentData class CompositeAnalysis(BaseAnalysis): - """Analysis class for CompositeExperiment""" + """Run analysis for composite experiments. + + Composite experiments consist of several component experiments + run together in a single execution, the results of which are returned + as a single list of circuit result data in the :class:`ExperimentData` + container. Analysis of this composite circuit data involves constructing + a child experiment data container for each component experiment containing + the marginalized circuit result data for that experiment. Each component + child data is then analyzed using the analysis class from the corresponding + component experiment. + + .. note:: + + The child :class:`ExperimentData` for each component experiment is + constructed and added to the parent experiment data the first time + :meth:`run` is called on the composite :class:`ExperimentData`. + + On sub-sequent called to :meth:`run` if `replace_results=True`` + in a addition to replace the analysis results and figures of each + component child experiment any previously stored child experiment + circuit data will be cleared and replaced with the marginalized data + reconstructed from the parent composite experiment data. + """ # pylint: disable = arguments-differ def _run_analysis(self, experiment_data: ExperimentData, **options): - """Run analysis on circuit data. + """Run analysis on composite experiment circuit data. Args: experiment_data: the experiment data to analyze. @@ -37,6 +60,9 @@ def _run_analysis(self, experiment_data: ExperimentData, **options): QiskitError: if analysis is attempted on non-composite experiment data. """ + # Extract job metadata for the component experiments so it can be added + # to the child experiment data incase it is required by the child experiments + # analysis classes composite_exp = experiment_data.experiment component_exps = composite_exp.component_experiment() if "component_job_metadata" in experiment_data.metadata: @@ -45,13 +71,19 @@ def _run_analysis(self, experiment_data: ExperimentData, **options): component_metadata = [{}] * composite_exp.num_experiments # Initialize component data for updating and get the experiment IDs for - # the component child experiments + # the component child experiments in case there are other child experiments + # in the experiment data component_ids = self._initialize_components(composite_exp, experiment_data) - # Compute marginalize data + # Compute marginalize data for each component experiment marginalized_data = self._marginalize_data(experiment_data.data()) - # Construct component experiment data + # Add the marginalized component data and component job metadata + # to each component child experiment. Note that this will clear + # any currently stored data in the experiment. Since copying of + # child data is handled by the `replace_results` kwarg of the + # parent container it is safe to always clear and replace the + # results of child containers in this step for i, (sub_data, sub_exp) in enumerate(zip(marginalized_data, component_exps)): sub_exp_data = experiment_data.child_data(component_ids[i]) @@ -60,7 +92,7 @@ def _run_analysis(self, experiment_data: ExperimentData, **options): sub_exp_data.add_data(sub_data) # Add component job metadata - sub_exp_data._metadata["job_metadata"] = [component_metadata[i]] + sub_exp_data.metadata["job_metadata"] = [component_metadata[i]] # Run analysis # Since copy for replace result is handled at the parent level @@ -71,23 +103,29 @@ def _run_analysis(self, experiment_data: ExperimentData, **options): def _initialize_components(self, experiment, experiment_data): """Initialize child data components and return list of child experiment IDs""" - component_index = experiment_data._metadata.get("component_child_index", []) + # Check if component child experiment data containers have already + # been created. If so the list of indices for their positions in the + # ordered dict should exist. Index is used to extract the experiment + # IDs for each child experiment which can change when re-running analysis + # if replace_results=False, so that we update the correct child data + # for each component experiment + component_index = experiment_data.metadata.get("component_child_index", []) if not component_index: - # Construct component data and update indices + # If the experiment Construct component data and update indices start_index = len(experiment_data.child_data()) component_index = [] for i, sub_exp in enumerate(experiment.component_experiment()): sub_data = sub_exp._initialize_experiment_data() experiment_data.add_child_data(sub_data) component_index.append(start_index + i) - experiment_data._metadata["component_child_index"] = component_index + experiment_data.metadata["component_child_index"] = component_index # Child components exist so we can get their ID for accessing them child_ids = experiment_data._child_data.keys() component_ids = [child_ids[idx] for idx in component_index] return component_ids - def _marginalize_data(self, composite_data): + def _marginalize_data(self, composite_data: List[Dict]) -> List[Dict]: """Return marginalized data for component experiments""" # Marginalize data marginalized_data = {} diff --git a/qiskit_experiments/framework/composite/parallel_experiment.py b/qiskit_experiments/framework/composite/parallel_experiment.py index b3b4c5db3c..c6a842ddac 100644 --- a/qiskit_experiments/framework/composite/parallel_experiment.py +++ b/qiskit_experiments/framework/composite/parallel_experiment.py @@ -22,7 +22,23 @@ @fix_class_docs class ParallelExperiment(CompositeExperiment): - """Parallel Experiment class""" + """Combine multiple experiments into a parallel experiment. + + Parallel experiments combine individual experiments on disjoint subsets + of qubits into a single composite experiment on the union of those qubits. + The component experiment circuits are combined to run in parallel on the + respective qubits. + + Analysis of parallel experiments is performed using the + :class:`~qiskit_experiments.framework.CompositeAnalysis` class which handles + marginalizing the composite experiment circuit data into individual child + :class:`ExperimentData` containers for each component experiment which are + then analyzed using the corresponding analysis class for that component + experiment. + + See :class:`~qiskit_experiments.framework.CompositeAnalysis` + documentation for additional information. + """ def __init__(self, experiments: List[BaseExperiment], backend: Optional[Backend] = None): """Initialize the analysis object. diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index d994f57e75..f8b8aae13a 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -146,7 +146,7 @@ def save_metadata(self) -> None: if self._child_data: self._metadata["child_data_ids"] = self._child_data.keys() super().save_metadata() - for data in self._child_data.values(): + for data in self.child_data(): data.save_metadata() @classmethod @@ -175,7 +175,7 @@ def _set_service(self, service: DatabaseService) -> None: DbExperimentDataError: If an experiment service is already being used. """ super()._set_service(service) - for data in self._child_data.values(): + for data in self.child_data(): data._set_service(service) @DbExperimentData.share_level.setter @@ -229,7 +229,7 @@ def _copy_metadata(self, new_instance: Optional[ExperimentData] = None) -> Exper new_instance._experiment = self.experiment.copy() # Recursively copy metadata of child data - child_data = [data._copy_metadata() for data in self._child_data.values()] + child_data = [data._copy_metadata() for data in self.child_data()] new_instance._set_child_data(child_data) return new_instance diff --git a/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml b/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml index 68f83dbc9d..adc166d933 100644 --- a/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml +++ b/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml @@ -11,7 +11,7 @@ upgrade: :class:`~qiskit_experiments.framework.ParallelExperiment` and :class:`~qiskit_experiments.framework.BatchExperiment` now return a :class:`~qiskit_experiments.framework.ExperimentData` object - which no longer contains a ``composite_experiment_data`` method. + which no longer contains a ``component_experiment_data`` method. This method has been replaced by the :meth:`~qiskit_experiments.framework.ExperimentData.child_data` method. From ecf0a2a954a06e8b32cc9871463e3a837123a192 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Thu, 4 Nov 2021 14:06:39 -0400 Subject: [PATCH 08/12] Return component experiment type and id as results Looking at a component experiment in result DB it is not currently possible to tell if its analysis has run or not yet. This adds a list of analysis results for each component experiment to store the experiment id and experiment type for convenient viewing in the online DB. --- .../framework/composite/composite_analysis.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index c5747bba3e..8982d84512 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -15,7 +15,8 @@ from typing import List, Dict from qiskit.result import marginal_counts -from qiskit_experiments.framework import BaseAnalysis, ExperimentData +from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData +from qiskit_experiments.database_service.device_component import Qubit class CompositeAnalysis(BaseAnalysis): @@ -84,6 +85,7 @@ def _run_analysis(self, experiment_data: ExperimentData, **options): # child data is handled by the `replace_results` kwarg of the # parent container it is safe to always clear and replace the # results of child containers in this step + analysis_results = [] for i, (sub_data, sub_exp) in enumerate(zip(marginalized_data, component_exps)): sub_exp_data = experiment_data.child_data(component_ids[i]) @@ -99,7 +101,18 @@ def _run_analysis(self, experiment_data: ExperimentData, **options): # we always run with replace result on component analysis sub_exp.run_analysis(sub_exp_data, replace_results=True) - return [], [] + # Record the component experiment id and type as an analysis result + # for evidence analysis has started and to display in the service DB + result = AnalysisResultData( + name=sub_exp_data.experiment_type, + value=sub_exp_data.experiment_id, + device_components=[ + Qubit(qubit) for qubit in sub_exp_data.metadata.get("physical_qubits", []) + ], + ) + analysis_results.append(result) + + return analysis_results, [] def _initialize_components(self, experiment, experiment_data): """Initialize child data components and return list of child experiment IDs""" From 9175928caa962ec62778a557fc5bd067cc1b4181 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Thu, 4 Nov 2021 15:28:50 -0400 Subject: [PATCH 09/12] Add `copy` method to ExperimentData --- .../database_service/db_analysis_result.py | 16 +++++++ .../database_service/db_experiment_data.py | 47 +++++++++++++------ qiskit_experiments/framework/base_analysis.py | 5 +- .../framework/experiment_data.py | 37 ++++++--------- .../update_expdata-ab3576f3bdd5057a.yaml | 11 +++++ .../test_db_experiment_data.py | 4 +- test/test_composite.py | 6 +-- 7 files changed, 80 insertions(+), 46 deletions(-) diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index 3ef001389d..4b09ebf13a 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -195,6 +195,22 @@ def save(self) -> None: json_encoder=self._json_encoder, ) + def copy(self) -> "DbAnalysisResultV1": + """Return a copy of the result with a new result ID""" + return DbAnalysisResultV1( + name=self.name, + value=self.value, + device_components=self.device_components, + experiment_id=self.experiment_id, + chisq=self.chisq, + quality=self.quality, + extra=self.extra, + verified=self.verified, + tags=self.tags, + service=self.service, + source=self._source, + ) + @classmethod def _from_service_data(cls, service_data: Dict) -> "DbAnalysisResultV1": """Construct an analysis result from saved database service data. diff --git a/qiskit_experiments/database_service/db_experiment_data.py b/qiskit_experiments/database_service/db_experiment_data.py index acd9b9bcf7..6d4dc8dd40 100644 --- a/qiskit_experiments/database_service/db_experiment_data.py +++ b/qiskit_experiments/database_service/db_experiment_data.py @@ -185,7 +185,7 @@ def _clear_results(self): self._deleted_analysis_results.append(key) self._analysis_results = ThreadSafeOrderedDict() # Schedule existing figures for deletion next save call - for key in self._analysis_results.keys(): + for key in self._figures.keys(): self._deleted_figures.append(key) self._figures = ThreadSafeOrderedDict() @@ -532,7 +532,7 @@ def add_figures( ) added_figs.append(fig_name) - return added_figs if len(added_figs) > 1 else added_figs[0] + return added_figs if len(added_figs) != 1 else added_figs[0] @do_auto_save def delete_figure( @@ -1130,24 +1130,26 @@ def errors(self) -> str: return "\n".join(errors) - def _copy_metadata( - self, new_instance: Optional["DbExperimentDataV1"] = None - ) -> "DbExperimentDataV1": - """Make a copy of the experiment metadata. + def copy(self, copy_results: bool = True) -> "DbExperimentDataV1": + """Make a copy of the experiment data with a new experiment ID. - Note: - This method only copies experiment data and metadata, not its - figures nor analysis results. The copy also contains a different - experiment ID. + Args: + copy_results: If True copy the analysis results and figures + into the returned container, along with the + experiment data and metadata. If False only copy + the experiment data and metadata. Returns: - A copy of the ``DbExperimentDataV1`` object with the same data - and metadata but different ID. + A copy of the experiment data object with the same data + but different IDs. + + .. note: + If analysis results and figures are copied they will also have + new result IDs and figure names generated for the copies. """ - if new_instance is None: - # pylint: disable=no-value-for-parameter - new_instance = self.__class__() + new_instance = self.__class__() + # Copy basic properties and metadata new_instance._type = self.experiment_type new_instance._backend = self._backend new_instance._tags = self._tags @@ -1159,6 +1161,7 @@ def _copy_metadata( new_instance._service = self._service new_instance._extra_data = self._extra_data + # Copy circuit result data and jobs with self._data.lock: # Hold the lock so no new data can be added. new_instance._data = self._data.copy_object() for orig_kwargs, fut in self._job_futures.copy(): @@ -1177,6 +1180,20 @@ def _copy_metadata( **extra_kwargs, ) + # If not copying results return the object + if not copy_results: + return new_instance + + # Copy results and figures. + # This requires analysis callbacks to finish + self._wait_for_callbacks() + with self._analysis_results.lock: + new_instance._analysis_results = ThreadSafeOrderedDict() + new_instance.add_analysis_results([result.copy() for result in self.analysis_results()]) + with self._figures.lock: + new_instance._figures = ThreadSafeOrderedDict() + new_instance.add_figures(self._figures.values()) + return new_instance @property diff --git a/qiskit_experiments/framework/base_analysis.py b/qiskit_experiments/framework/base_analysis.py index 792b0624b4..eea12cb85d 100644 --- a/qiskit_experiments/framework/base_analysis.py +++ b/qiskit_experiments/framework/base_analysis.py @@ -86,7 +86,7 @@ def run( or experiment_data._figures or getattr(experiment_data, "_child_data", None) ): - experiment_data = experiment_data._copy_metadata() + experiment_data = experiment_data.copy() # Get experiment device components if "physical_qubits" in experiment_data.metadata: @@ -109,8 +109,7 @@ def run_analysis(expdata): for result in results ] # Update experiment data with analysis results - if replace_results: - experiment_data._clear_results() + experiment_data._clear_results() if analysis_results: expdata.add_analysis_results(analysis_results) if figures: diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index f8b8aae13a..4831a8a8ce 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -159,6 +159,20 @@ def load(cls, experiment_id: str, service: DatabaseService) -> ExperimentData: expdata._set_child_data(child_data) return expdata + def copy(self, copy_results=True) -> "ExperimentData": + new_instance = super().copy(copy_results=copy_results) + + # Copy additional attributes not in base class + if self.experiment is None: + new_instance._experiment = None + else: + new_instance._experiment = self.experiment.copy() + + # Recursively copy child data + child_data = [data.copy(copy_results=copy_results) for data in self.child_data()] + new_instance._set_child_data(child_data) + return new_instance + def _set_child_data(self, child_data: List[ExperimentData]): """Set child experiment data for the current experiment.""" self._child_data = ThreadSafeOrderedDict() @@ -210,29 +224,6 @@ def block_for_results(self, timeout: Optional[float] = None) -> ExperimentData: _, timeout = combined_timeout(subdata.block_for_results, timeout) return self - def _copy_metadata(self, new_instance: Optional[ExperimentData] = None) -> ExperimentData: - """Make a copy of the experiment metadata. - - Note: - This method only copies experiment data and metadata, not its - figures nor analysis results. The copy also contains a different - experiment ID. - - Returns: - A copy of the ``ExperimentData`` object with the same data - and metadata but different ID. - """ - new_instance = super()._copy_metadata(new_instance) - if self.experiment is None: - new_instance._experiment = None - else: - new_instance._experiment = self.experiment.copy() - - # Recursively copy metadata of child data - child_data = [data._copy_metadata() for data in self.child_data()] - new_instance._set_child_data(child_data) - return new_instance - def __repr__(self): out = ( f" Date: Thu, 4 Nov 2021 15:33:45 -0400 Subject: [PATCH 10/12] Initialize empty child data for composite experiment.run This adds back in functionality to initialize the empty child data containers during CompositeExperiment.run, so that if someone does `expdata.child_data` before analysis has run they can access the empty containers. The current analysis logic still works on this case, but will also be able to initalize these child data if they arent already there, such as when loading experiment data from a composite experiment job ID instead of a saved experiment. --- .../framework/composite/composite_experiment.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 9045837067..652d5cb780 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -18,7 +18,7 @@ from abc import abstractmethod import warnings from qiskit.providers.backend import Backend -from qiskit_experiments.framework import BaseExperiment +from qiskit_experiments.framework import BaseExperiment, ExperimentData from .composite_analysis import CompositeAnalysis @@ -82,6 +82,16 @@ def _set_backend(self, backend): for subexp in self._experiments: subexp._set_backend(backend) + def _initialize_experiment_data(self): + """Initialize the return data container for the experiment run""" + experiment_data = ExperimentData(experiment=self) + # Initialize child experiment data + for sub_exp in self._experiments: + sub_data = sub_exp._initialize_experiment_data() + experiment_data.add_child_data(sub_data) + experiment_data.metadata["component_child_index"] = list(range(self.num_experiments)) + return experiment_data + def _additional_metadata(self): return {"component_job_metadata": []} From 3ec7f537be474a20274eab1d743a07f123632084 Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Thu, 4 Nov 2021 16:14:38 -0400 Subject: [PATCH 11/12] Add some extra tests --- .../framework/composite/composite_analysis.py | 12 +++--- test/test_composite.py | 42 +++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/framework/composite/composite_analysis.py b/qiskit_experiments/framework/composite/composite_analysis.py index 8982d84512..33bdf9e931 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -33,13 +33,13 @@ class CompositeAnalysis(BaseAnalysis): .. note:: - The child :class:`ExperimentData` for each component experiment is - constructed and added to the parent experiment data the first time - :meth:`run` is called on the composite :class:`ExperimentData`. + The the child :class:`ExperimentData` for each component experiment + does not already exist in the experiment data they will be initialized + and added to the experiment data when :meth:`run` is called on the + composite :class:`ExperimentData`. - On sub-sequent called to :meth:`run` if `replace_results=True`` - in a addition to replace the analysis results and figures of each - component child experiment any previously stored child experiment + When calling :meth:`run` on experiment data already containing + initalized component experiment child data, any previously stored circuit data will be cleared and replaced with the marginalized data reconstructed from the parent composite experiment data. """ diff --git a/test/test_composite.py b/test/test_composite.py index 2be7e7dd73..d8bf21ff61 100644 --- a/test/test_composite.py +++ b/test/test_composite.py @@ -358,3 +358,45 @@ def test_composite_copy(self): new_instance = self.rootdata.copy() self.check_if_equal(new_instance, self.rootdata, is_a_copy=True) self.check_attributes(new_instance) + + def test_analysis_replace_results_true(self): + """ + Test replace results when analyzing composite experiment data + """ + exp1 = FakeExperiment([0, 2]) + exp2 = FakeExperiment([1, 3]) + par_exp = ParallelExperiment([exp1, exp2]) + data1 = par_exp.run(FakeBackend()).block_for_results() + + # Additional data not part of composite experiment + exp3 = FakeExperiment([0, 1]) + extra_data = exp3.run(FakeBackend()) + data1.add_child_data(extra_data) + + # Replace results + data2 = par_exp.run_analysis(data1, replace_results=True) + self.assertEqual(data1, data2) + self.assertEqual(len(data1.child_data()), len(data2.child_data())) + for sub1, sub2 in zip(data1.child_data(), data2.child_data()): + self.assertEqual(sub1, sub2) + + def test_analysis_replace_results_false(self): + """ + Test replace_results of composite experiment data + """ + exp1 = FakeExperiment([0, 2]) + exp2 = FakeExperiment([1, 3]) + par_exp = BatchExperiment([exp1, exp2]) + data1 = par_exp.run(FakeBackend()).block_for_results() + + # Additional data not part of composite experiment + exp3 = FakeExperiment([0, 1]) + extra_data = exp3.run(FakeBackend()) + data1.add_child_data(extra_data) + + # Replace results + data2 = par_exp.run_analysis(data1, replace_results=False) + self.assertNotEqual(data1.experiment_id, data2.experiment_id) + self.assertEqual(len(data1.child_data()), len(data2.child_data())) + for sub1, sub2 in zip(data1.child_data(), data2.child_data()): + self.assertNotEqual(sub1.experiment_id, sub2.experiment_id) From 0ee07fab5c597062cda10338bdce2f69bcc95a7b Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Thu, 4 Nov 2021 16:29:10 -0400 Subject: [PATCH 12/12] Fixup qv tut --- docs/tutorials/quantum_volume.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/quantum_volume.ipynb b/docs/tutorials/quantum_volume.ipynb index 16195d02b8..1f4db0e7fc 100644 --- a/docs/tutorials/quantum_volume.ipynb +++ b/docs/tutorials/quantum_volume.ipynb @@ -223,7 +223,7 @@ "qv_exp.set_experiment_options(trials=60)\n", "expdata2 = qv_exp.run(backend, analysis=False).block_for_results()\n", "expdata2.add_data(expdata.data())\n", - "qv_exp.run_analysis(expdata2)\n", + "qv_exp.run_analysis(expdata2).block_for_results()\n", "\n", "# View result data\n", "display(expdata2.figure(0))\n", @@ -281,7 +281,7 @@ ], "source": [ "qv_values = [\n", - " batch_expdata.cchild_data(i).analysis_results(\"quantum_volume\").value\n", + " batch_expdata.child_data(i).analysis_results(\"quantum_volume\").value\n", " for i in range(batch_exp.num_experiments)\n", "]\n", "\n",