diff --git a/docs/tutorials/quantum_volume.ipynb b/docs/tutorials/quantum_volume.ipynb index 7ac4c6962d..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.component_experiment_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", @@ -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)" 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 6cb4359073..c6d8ab29f2 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 @@ -1403,27 +1420,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/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/__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..eea12cb85d 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,19 +79,14 @@ 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 or experiment_data._analysis_results 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: @@ -119,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/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/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 437e7c81dd..33bdf9e931 100644 --- a/qiskit_experiments/framework/composite/composite_analysis.py +++ b/qiskit_experiments/framework/composite/composite_analysis.py @@ -13,19 +13,40 @@ Composite Experiment Analysis class. """ -from qiskit.exceptions import QiskitError -from qiskit_experiments.framework import BaseAnalysis -from .composite_experiment_data import CompositeExperimentData +from typing import List, Dict +from qiskit.result import marginal_counts +from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData +from qiskit_experiments.database_service.device_component import Qubit class CompositeAnalysis(BaseAnalysis): - """Analysis class for CompositeExperiment""" + """Run analysis for composite experiments. - __experiment_data__ = CompositeExperimentData + 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 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`. + + 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. + """ # pylint: disable = arguments-differ - def _run_analysis(self, experiment_data: CompositeExperimentData, **options): - """Run analysis on circuit data. + def _run_analysis(self, experiment_data: ExperimentData, **options): + """Run analysis on composite experiment circuit data. Args: experiment_data: the experiment data to analyze. @@ -40,15 +61,106 @@ 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.") + # 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: + component_metadata = experiment_data.metadata["component_job_metadata"][-1] + else: + component_metadata = [{}] * composite_exp.num_experiments + + # Initialize component data for updating and get the experiment IDs for + # 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 for each component experiment + marginalized_data = self._marginalize_data(experiment_data.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 + 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]) + + # 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]] + + # Run analysis + # 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) + + # 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""" + # 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: + # 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 + + # 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 - comp_exp = experiment_data.experiment + def _marginalize_data(self, composite_data: List[Dict]) -> List[Dict]: + """Return marginalized data for component experiments""" + # Marginalize data + marginalized_data = {} + for datum in composite_data: + metadata = datum.get("metadata", {}) - 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) - exp.run_analysis(expdata, **options) + # 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 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"] + marginalized_data[index].append(sub_data) - return [], [] + # 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 db696dcc66..652d5cb780 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -13,14 +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 - -from .composite_experiment_data import CompositeExperimentData +from qiskit_experiments.framework import BaseExperiment, ExperimentData from .composite_analysis import CompositeAnalysis @@ -28,7 +26,6 @@ class CompositeExperiment(BaseExperiment): """Composite Experiment base class""" __analysis_class__ = CompositeAnalysis - __experiment_data__ = CompositeExperimentData def __init__( self, @@ -85,26 +82,44 @@ def _set_backend(self, backend): for subexp in self._experiments: subexp._set_backend(backend) - def _add_job_metadata(self, experiment_data, jobs, **run_options): - # Add composite metadata - super()._add_job_metadata(experiment_data, jobs, **run_options) + 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 - # Add sub-experiment options - for i in range(self.num_experiments): - sub_exp = self.component_experiment(i) + def _additional_metadata(self): + return {"component_job_metadata": []} + def _add_job_metadata(self, experiment_data, jobs, **run_options): + # Extract component metadata + component_metadata = [] + # Add sub-experiment options + 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.component_experiment_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: 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/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 225a932644..4831a8a8ce 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 ThreadSafeOrderedDict, combined_timeout 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,11 @@ 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 = ThreadSafeOrderedDict() + if child_data is not None: + self._set_child_data(child_data) + @property def experiment(self): """Return the experiment for this data. @@ -69,39 +88,141 @@ def completion_times(self) -> Dict[str, datetime]: return job_times - @classmethod - def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "ExperimentData": - """Load a saved experiment data from a database service. + 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[experiment_data.experiment_id] = experiment_data + + def child_data( + self, index: Optional[Union[int, slice, str]] = None + ) -> Union[ExperimentData, List[ExperimentData]]: + """Return child experiment data. Args: - experiment_id: Experiment ID. - service: the database service. + 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 loaded experiment data. + The requested single or list of child experiment data. + + Raises: + QiskitError: if the index or ID of the child experiment data + cannot be found. """ - expdata = DbExperimentDataV1.load(experiment_id, service) + if index is None: + 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)}.") + + 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.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(): + data.save_metadata() + + @classmethod + def load(cls, experiment_id: str, service: DatabaseService) -> ExperimentData: + expdata = DbExperimentData.load(experiment_id, service) expdata.__class__ = ExperimentData 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 _copy_metadata(self, new_instance: Optional["ExperimentData"] = None) -> "ExperimentData": - """Make a copy of the experiment metadata. + 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() + 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: + 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.values(): + 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() - Note: - This method only copies experiment data and metadata, not its - figures nor analysis results. The copy also contains a different - experiment ID. + 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: - A copy of the ``ExperimentData`` object with the same data - and metadata but different ID. + The experiment data with finished jobs and post-processing. """ - 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) + _, timeout = combined_timeout(super().block_for_results, timeout) + for subdata in self._child_data.values(): + _, timeout = combined_timeout(subdata.block_for_results, timeout) + return self def __repr__(self): out = ( @@ -111,3 +232,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..2ea40587e1 100644 --- a/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml +++ b/releasenotes/notes/update_expdata-ab3576f3bdd5057a.yaml @@ -1,10 +1,38 @@ --- 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 ``component_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. + - | + Added a :meth:`~qiskit_experiments.framework.ExperimentData.copy` + method to :class:`~qiskit_experiments.framework.ExperimentData` which + allows making a copy of an experiment data container with a new + experiment ID, new result IDs, and new figure names, generated for + the copy. + + This method has a kwarg option ``copy_results`` that can be set to + ``False`` to only copy the experiment + :meth:`~qiskit_experiments.framework.ExperimentData.data` and + metadata, but not the analysis results and figures. 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..07bdd71e6f 100644 --- a/test/database_service/test_db_experiment_data.py +++ b/test/database_service/test_db_experiment_data.py @@ -700,24 +700,13 @@ 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") exp_data.add_data(self._get_job_result(1)) result = mock.MagicMock() exp_data.add_analysis_results(result) - copied = exp_data._copy_metadata() + copied = exp_data.copy(copy_results=False) self.assertEqual(exp_data.data(), copied.data()) self.assertFalse(copied.analysis_results()) @@ -740,7 +729,7 @@ def _job2_result(): job.result = _job1_result exp_data.add_data(job) - copied = exp_data._copy_metadata() + copied = exp_data.copy(copy_results=False) job2 = mock.create_autospec(Job, instance=True) job2.result = _job2_result copied.add_data(job2) diff --git a/test/test_composite.py b/test/test_composite.py index f31dc21241..d8bf21ff61 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,15 +287,12 @@ 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): - 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): """ @@ -309,24 +306,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,29 +338,65 @@ 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): + def test_composite_copy(self): """ - Test CompositeExperimentData._copy_metadata + Test composite ExperimentData.copy """ - new_instance = self.rootdata._copy_metadata() + 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) 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