Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
9d87938
tags of composite experiments
yaelbh Aug 22, 2021
563c07f
CompositeExperimentData.save
yaelbh Aug 26, 2021
1674ebe
store component ids in composite metadata
yaelbh Aug 26, 2021
558a65f
initialize composite experiment data with components
yaelbh Aug 26, 2021
49056e3
CompositeExperimentData.load
yaelbh Aug 29, 2021
ea80555
TestCompositeExperimentData
yaelbh Sep 2, 2021
aeab266
temporarily remove handling of tags
yaelbh Sep 5, 2021
360f04f
tags in composite experiments and their descendants
yaelbh Sep 5, 2021
257eb70
test save and load of composite experiment
yaelbh Sep 5, 2021
0006f1f
bug fixes
yaelbh Sep 5, 2021
c7dc857
bug fix
yaelbh Sep 5, 2021
f399444
bug fixes
yaelbh Sep 5, 2021
db70174
composite load seems to work now
yaelbh Sep 5, 2021
6a81e89
code clean
yaelbh Sep 9, 2021
f251a56
bug fix
yaelbh Sep 9, 2021
1c97c7f
recursive share_level in composite
yaelbh Sep 9, 2021
a7a78e5
black
yaelbh Sep 9, 2021
df3b246
lint
yaelbh Sep 9, 2021
1ad8385
black
yaelbh Sep 9, 2021
29ae28e
lint
yaelbh Sep 9, 2021
92041e3
lint
yaelbh Sep 9, 2021
7f04300
lint
yaelbh Sep 9, 2021
93607ab
fixed comment
yaelbh Sep 9, 2021
42b617a
release notes
yaelbh Sep 19, 2021
8df066f
Merge branch 'main' into compsaveload
yaelbh Sep 19, 2021
14a65e6
Merge branch 'main' into compsaveload
yaelbh Sep 29, 2021
8c9cfc3
update tags of composite
yaelbh Sep 29, 2021
830aa5a
black
yaelbh Sep 29, 2021
9ad7243
Merge branch 'main' into compsaveload
yaelbh Oct 3, 2021
871e361
added verbose flag to save
yaelbh Oct 6, 2021
ee81f54
removed DummyExperiment
yaelbh Oct 6, 2021
c84fdf0
reworked root id, parent id, and tags
yaelbh Oct 7, 2021
3d30308
add parent_id support
kt474 Sep 29, 2021
5472cf0
added property DbExperimentDataV1.parent_id
yaelbh Oct 10, 2021
bee575c
remove root id and tags hack
yaelbh Oct 10, 2021
f7d535b
reverted a recent change in DbExperimentDataV1._copy_metadata
yaelbh Oct 10, 2021
a0b1c55
testing component ids and classes
yaelbh Oct 10, 2021
4148f14
CompositeExperimentData._copy_metadata
yaelbh Oct 10, 2021
11ccf05
black
yaelbh Oct 10, 2021
a7938bb
lint
yaelbh Oct 10, 2021
5b6a6f0
Merge branch 'main' into compsaveload
yaelbh Oct 10, 2021
20263dd
removed job ids from composite tests
yaelbh Oct 10, 2021
6a072ea
black
yaelbh Oct 10, 2021
6c1f5b9
bug fix
yaelbh Oct 11, 2021
e141fe9
lint
yaelbh Oct 11, 2021
d602aae
black
yaelbh Oct 11, 2021
a4a4127
typo
yaelbh Oct 11, 2021
13129c5
Update qiskit_experiments/database_service/db_experiment_data.py
yaelbh Oct 12, 2021
b8ebad8
updated release notes
yaelbh Oct 12, 2021
5e8ba8b
verbose as a class attribute
yaelbh Oct 12, 2021
e347211
black
yaelbh Oct 12, 2021
adcd38d
rewrote share level setter
yaelbh Oct 12, 2021
d540a5b
lint
yaelbh Oct 12, 2021
90240b0
sphinx
yaelbh Oct 12, 2021
fd896ef
changed inheritance structure of save_metadata
yaelbh Oct 13, 2021
3fbc4de
verbose as a class attribute
yaelbh Oct 19, 2021
1ab63c8
Update qiskit_experiments/framework/composite/composite_experiment_da…
yaelbh Oct 19, 2021
a3c685b
save_experiment_metadata -> _save_experiment_metadata
yaelbh Oct 19, 2021
6c909a1
line breaks in release notes
yaelbh Oct 19, 2021
00e57be
Merge branch 'main' into compsaveload
chriseclectic Oct 19, 2021
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
8 changes: 7 additions & 1 deletion qiskit_experiments/database_service/database_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def create_experiment(
backend_name: str,
metadata: Optional[Dict] = None,
experiment_id: Optional[str] = None,
parent_id: Optional[str] = None,
job_ids: Optional[List[str]] = None,
tags: Optional[List[str]] = None,
notes: Optional[str] = None,
Expand All @@ -68,6 +69,9 @@ def create_experiment(
metadata: Experiment metadata.
experiment_id: Experiment ID. It must be in the ``uuid4`` format.
One will be generated if not supplied.
parent_id: The experiment ID of the parent experiment.
The parent experiment must exist, must be on the same backend as the child,
and an experiment cannot be its own parent.
job_ids: IDs of experiment jobs.
tags: Tags to be associated with the experiment.
notes: Freeform notes about the experiment.
Expand All @@ -78,7 +82,7 @@ def create_experiment(
Experiment ID.

Raises:
ExperimentEntryExists: If the experiment already exits.
ExperimentEntryExists: If the experiment already exists.
"""
pass

Expand Down Expand Up @@ -134,6 +138,7 @@ def experiments(
experiment_type: Optional[str] = None,
backend_name: Optional[str] = None,
tags: Optional[List[str]] = None,
parent_id: Optional[str] = None,
tags_operator: Optional[str] = "OR",
**filters: Any,
) -> List[Dict]:
Expand All @@ -148,6 +153,7 @@ def experiments(
backend_name: Backend name used for filtering.
tags: Filter by tags assigned to experiments. This can be used
with `tags_operator` for granular filtering.
parent_id: Filter by parent experiment ID.
tags_operator: Logical operator to use when filtering by tags. Valid
values are "AND" and "OR":

Expand Down
56 changes: 50 additions & 6 deletions qiskit_experiments/database_service/db_experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class DbExperimentDataV1(DbExperimentData):
"""

version = 1
verbose = True # Whether to print messages to the standard output.
_metadata_version = 1
_executor = futures.ThreadPoolExecutor()
"""Threads used for asynchronous processing."""
Expand All @@ -113,6 +114,7 @@ def __init__(
experiment_type: Optional[str] = "Unknown",
backend: Optional[Union[Backend, BaseBackend]] = None,
experiment_id: Optional[str] = None,
parent_id: Optional[str] = None,
tags: Optional[List[str]] = None,
job_ids: Optional[List[str]] = None,
share_level: Optional[str] = None,
Expand All @@ -127,6 +129,7 @@ def __init__(
experiment_type: Experiment type.
backend: Backend the experiment runs on.
experiment_id: Experiment ID. One will be generated if not supplied.
parent_id: The experiment ID of the parent experiment.
tags: Tags to be associated with the experiment.
job_ids: IDs of jobs submitted for the experiment.
share_level: Whether this experiment can be shared with others. This
Expand Down Expand Up @@ -154,6 +157,7 @@ def __init__(
self._set_service_from_backend(backend)

self._id = experiment_id or str(uuid.uuid4())
self._parent_id = parent_id
self._type = experiment_type
self._tags = tags or []
self._share_level = share_level
Expand Down Expand Up @@ -719,6 +723,18 @@ def analysis_results(
def save_metadata(self) -> None:
"""Save this experiments metadata to a database service.

.. note::
This method does not save analysis results nor figures.
Use :meth:`save` for general saving of all experiment data.

See :meth:`qiskit.providers.experiment.DatabaseServiceV1.create_experiment`
for fields that are saved.
"""
self._save_experiment_metadata()

def _save_experiment_metadata(self) -> None:
"""Save this experiments metadata to a database service.

.. note::
This method does not save analysis results nor figures.
Use :meth:`save` for general saving of all experiment data.
Expand Down Expand Up @@ -758,6 +774,8 @@ def save_metadata(self) -> None:
new_data = {"experiment_type": self._type, "backend_name": self._backend.name()}
if self.share_level:
update_data["share_level"] = self.share_level
if self.parent_id:
update_data["parent_id"] = self.parent_id

self._created_in_db, _ = save_data(
is_new=(not self._created_in_db),
Expand Down Expand Up @@ -788,7 +806,7 @@ def save(self) -> None:
)
return

self.save_metadata()
self._save_experiment_metadata()
for result in self._analysis_results.values():
result.save()

Expand Down Expand Up @@ -817,10 +835,11 @@ def save(self) -> None:
self._service.delete_figure(experiment_id=self.experiment_id, figure_name=name)
self._deleted_figures.remove(name)

print(
"You can view the experiment online at https://quantum-computing.ibm.com/experiments/"
+ self.experiment_id
)
if self.verbose:
print(
"You can view the experiment online at https://quantum-computing.ibm.com/experiments/"
+ self.experiment_id
)

@classmethod
def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "DbExperimentDataV1":
Expand All @@ -843,6 +862,7 @@ def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "DbExperimentDa
experiment_type=service_data.pop("experiment_type"),
backend=service_data.pop("backend"),
experiment_id=service_data.pop("experiment_id"),
parent_id=service_data.pop("parent_id"),
tags=service_data.pop("tags"),
job_ids=service_data.pop("job_ids"),
share_level=service_data.pop("share_level"),
Expand Down Expand Up @@ -1103,14 +1123,15 @@ def _copy_metadata(
and metadata but different ID.
"""
if new_instance is None:
# pylint: disable=no-value-for-parameter
new_instance = self.__class__()

new_instance._type = self.experiment_type
new_instance._backend = self._backend
new_instance._tags = self._tags
new_instance._jobs = self._jobs.copy_object()
new_instance._share_level = self._share_level
new_instance._metadata = self._metadata
new_instance._metadata = copy.deepcopy(self._metadata)
new_instance._notes = self._notes
new_instance._auto_save = self._auto_save
new_instance._service = self._service
Expand All @@ -1133,6 +1154,7 @@ def _copy_metadata(
timeout=orig_kwargs["timeout"],
**extra_kwargs,
)

return new_instance

@property
Expand Down Expand Up @@ -1185,6 +1207,15 @@ def experiment_id(self) -> str:
"""
return self._id

@property
def parent_id(self) -> str:
"""Return parent experiment ID

Returns:
Parent ID.
"""
return self._parent_id

@property
def job_ids(self) -> List[str]:
"""Return experiment job IDs.
Expand Down Expand Up @@ -1275,6 +1306,17 @@ def service(self) -> Optional[DatabaseServiceV1]:
def 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.
"""
self._set_service(service)

def _set_service(self, service: DatabaseServiceV1) -> None:
"""Set the service to be used for storing experiment data.

Args:
service: Service to be used.

Expand Down Expand Up @@ -1319,6 +1361,8 @@ def source(self) -> Dict:
def __repr__(self):
out = f"{type(self).__name__}({self.experiment_type}"
out += f", {self.experiment_id}"
if self._parent_id:
out += f", parent_id={self._parent_id}"
if self._tags:
out += f", tags={self._tags}"
if self.job_ids:
Expand Down
113 changes: 98 additions & 15 deletions qiskit_experiments/framework/composite/composite_experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,35 @@
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 DbExperimentDataV1, DatabaseServiceV1


class CompositeExperimentData(ExperimentData):
"""Composite experiment data class"""

def __init__(
self,
experiment,
backend=None,
job_ids=None,
):
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.

Raises:
ExperimentError: If an input argument is invalid.
"""

super().__init__(
experiment,
backend=backend,
job_ids=job_ids,
)
super().__init__(experiment, backend=backend, parent_id=parent_id, job_ids=job_ids)

# Initialize sub experiments
self._components = [
expr.__experiment_data__(expr, backend) for expr in experiment._experiments
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)
Expand Down Expand Up @@ -103,3 +98,91 @@ def _add_single_data(self, data):
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 = DbExperimentDataV1.load(experiment_id, service)
components = []
for comp_id, comp_class in zip(
expdata.metadata["component_ids"], expdata.metadata["component_classes"]
):
load_class = globals()[comp_class]
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's going on here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We have to invoke the correct load method, according to the class of the component. So we figure out the class (in __init__ we have stored the component classes in expdata.metadata["component_classes"]), then we fetch the load method that belongs to this class (e.g. ExperimentData.load), then we call it.

load_func = getattr(load_class, "load")
loaded_comp = load_func(comp_id, service)
components.append(loaded_comp)

expdata.__class__ = CompositeExperimentData
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a little sketchy and what I tried to avoid in #423. But currently composite experiment data has an issue where it requires experiments to not be None on initialization, because it gets components from there.

I think it might be better to modify the init method of htis class to make experiments optional, and to also add an optional components kwarg that you can directly pass in the component experiment data during init.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But, even when CompositeExperimentData accepts components, we will not use it and not call it from CompositeExperimentData.load. Because expdata was already initialized in DbExperimentData.load.

expdata._experiment = None
expdata._components = components

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
15 changes: 5 additions & 10 deletions qiskit_experiments/framework/experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,21 @@
class ExperimentData(DbExperimentDataV1):
"""Qiskit Experiments Data container class"""

def __init__(
self,
experiment=None,
backend=None,
job_ids=None,
):
def __init__(self, experiment=None, backend=None, parent_id=None, job_ids=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
in the setting of a composite experiment
job_ids (list[str]): Optional, IDs of jobs submitted for the experiment.

Raises:
ExperimentError: If an input argument is invalid.
"""
self._experiment = experiment
super().__init__(
experiment_type=experiment.experiment_type if experiment else None,
backend=backend,
parent_id=parent_id,
job_ids=job_ids,
metadata=experiment._metadata() if experiment else {},
)
Expand Down Expand Up @@ -80,7 +75,7 @@ def _copy_metadata(self, new_instance: Optional["ExperimentData"] = None) -> "Ex
and metadata but different ID.
"""
if new_instance is None:
new_instance = ExperimentData(
new_instance = self.__class__(
experiment=self.experiment, backend=self.backend, job_ids=self.job_ids
)
return super()._copy_metadata(new_instance)
Expand Down
12 changes: 12 additions & 0 deletions releasenotes/notes/save-load-composite-cb882d855dd669be.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
features:
- |
Adds support for saving and loading :class:`~qiskit_experiments.framework.ParallelExperiment`
and :class:`~qiskit_experiments.framework.BatchExperiment` experiment data,
including all their component experiment data and results, to the IBM experiments database service.

When saving one of these experiments, each component experiment will be saved under a unique experiment ID,
which references the original parent composite experiment,
via the :meth:`~qiskit_Expeirments.framework.ExperimentData.parent_id`.

Note that changing the share level of the parent composite experiment will also change the share level of all component experiments.

Loading