Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/tutorials/quantum_volume.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)"
Expand Down
11 changes: 4 additions & 7 deletions docs/tutorials/randomized_benchmarking.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)"
Expand Down Expand Up @@ -832,7 +829,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.11"
"version": "3.7.5"
}
},
"nbformat": 4,
Expand Down
3 changes: 1 addition & 2 deletions docs/tutorials/state_tomography.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions docs/tutorials/t1.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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`"
]
},
{
Expand Down Expand Up @@ -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)"
Expand Down
21 changes: 0 additions & 21 deletions qiskit_experiments/database_service/db_experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1403,27 +1403,6 @@ def __repr__(self):
out += ")"
return out

def __str__(self):
Comment thread
yaelbh marked this conversation as resolved.
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]
Expand Down
5 changes: 5 additions & 0 deletions qiskit_experiments/database_service/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
2 changes: 0 additions & 2 deletions qiskit_experiments/framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@
ParallelExperiment
BatchExperiment
CompositeAnalysis
CompositeExperimentData

Base Classes
************
Expand All @@ -244,5 +243,4 @@
ParallelExperiment,
BatchExperiment,
CompositeAnalysis,
CompositeExperimentData,
)
12 changes: 1 addition & 11 deletions qiskit_experiments/framework/base_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -84,17 +79,12 @@ 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()

Expand Down
7 changes: 1 addition & 6 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion qiskit_experiments/framework/composite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"""Composite Experiments"""

# Base classes
from .composite_experiment_data import CompositeExperimentData
from .composite_analysis import CompositeAnalysis

# Composite experiment classes
Expand Down
18 changes: 17 additions & 1 deletion qiskit_experiments/framework/composite/batch_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
129 changes: 114 additions & 15 deletions qiskit_experiments/framework/composite/composite_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,39 @@
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


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 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: 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.
Expand All @@ -40,15 +60,94 @@ 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()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
component_exps = composite_exp.component_experiment()
component_exps = composite_exp.component_experiments()

?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the current name of the method in the CompositeExperiment class

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is suggestion from @wshanks #460 (comment)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be changed, would just be an API change.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

component_experiment() could be kept if desired. It seems to me like making component_experiments a property that gives back _experiments would be best. My point in that comment was that the API of a function with a singular name that takes an index and returns one value and also takes no input and gives back all values is confusing, so I would just make a separate plural API point that gives back all the values and has a plural name for that case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also be fine with just having the plural version and allowing it to take None, int or slice to return all, single, or subset like we do with analysis_resultsand child_data, but that can be done in separate PR

if "component_job_metadata" in experiment_data.metadata:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand these lines. What does the existence of component_job_metadata in the metadata mean, and why, if it exists, are you taking the last of experiment_data.metadata["component_job_metadata"][-1] ? Please write inline documentation to explain.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly the component_job_metadata is another set of metadata to initialize child experiment data. This is generated per job run so this means the last set of children. The metadata also contains job_metadata but this represents configurations of a full set of composite experiment (i.e. parent) so this doesn't help analysis. I don't think job_metadata is really necessary for composite experiment (more specifically, only run options is necessary to reconstruct experiment because other configs are usually unique to children).

@nkanazawa1989 nkanazawa1989 Nov 4, 2021

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The important point we need to understand here is children is not generated by job execution, but this is generated on the fly by analysis. This is because experiment data child is not limited only to the composite, but also, for example, we can create pseudo RDB of experiment data. i.e.

my_all_result_for_writing_prx_paper = ExperimentData()
my_all_result_for_writing_prx_paper.add_child_data(my_data_of_calibration)
my_all_result_for_writing_prx_paper.add_child_data(my_data_of_rb)
my_all_result_for_writing_prx_paper.add_child_data(my_data_of_quantum_volume)
etc...

then we can write my_all_result_for_writing_prx_paper id in paper for reproducibility.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to deal with how regular ExperimentData stores job metadata, which is used by some analysis (curve analysis in particular i think). If its not there certain experiments analysis wont run (note I think this should be changed, but it's beyond the scope of this PR).

Since each component experiment might need this for its respective analysis to work this is just storing the list of these component job metadata in the parent experiment so it can be added later along with the marginalized data. Previously this was all done when calling run which initialized all the child containers, but this is just moving it to analysis.

The [-1] was because this structure is a list for each run job, and you want the most recent one (same as job_metadata). Since #463 this isn't really necessary since you can't re-run more experiment jobs into the same container.

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
for i, (sub_data, sub_exp) in enumerate(zip(marginalized_data, component_exps)):
sub_exp_data = experiment_data.child_data(component_ids[i])

comp_exp = experiment_data.experiment
# Clear any previously stored data and add marginalized data
sub_exp_data._data.clear()
sub_exp_data.add_data(sub_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.component_experiment_data(i)
exp.run_analysis(expdata, **options)
# 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)

return [], []

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

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", {})

# 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)

# Sort by index
return [marginalized_data[i] for i in sorted(marginalized_data.keys())]
Loading