Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix pickle deserialization of ExperimentData #1326

Merged
merged 10 commits into from
Jan 9, 2024
5 changes: 3 additions & 2 deletions qiskit_experiments/framework/experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def _repr_svg_(self):
return None


FigureT = Union[str, bytes, MatplotlibFigure, FigureData]
FigureType = Union[str, bytes, MatplotlibFigure, FigureData]


class ExperimentData:
Expand Down Expand Up @@ -1142,7 +1142,7 @@ def data(
@do_auto_save
def add_figures(
self,
figures: Union[FigureT, List[FigureT]],
figures: Union[FigureType, List[FigureType]],
figure_names: Optional[Union[str, List[str]]] = None,
overwrite: bool = False,
save_figure: Optional[bool] = None,
Expand Down Expand Up @@ -2551,6 +2551,7 @@ def __setstate__(self, state):
self._job_futures = ThreadSafeOrderedDict()
self._analysis_futures = ThreadSafeOrderedDict()
self._analysis_executor = futures.ThreadPoolExecutor(max_workers=1)
self._monitor_executor = futures.ThreadPoolExecutor()

def __str__(self):
line = 51 * "-"
Expand Down
9 changes: 9 additions & 0 deletions releasenotes/notes/exp-data-pickle-61511b6e926e3198.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
fixes:
- |
Fixed :mod:`pickle` deserialization of :class:`.ExperimentData` objects.
Previously, :class:`.ExperimentData` objects could be serialized and
deserialized using Python's ``pickle`` module, but deserialized objects
were not completely restored and an exception would be raised when doing
some operations like running analysis on the restored object. See `#1326
<https://github.com/Qiskit-Extensions/qiskit-experiments/pull/1326/files>`__.
32 changes: 29 additions & 3 deletions test/extended_equality.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@ def _check_service_analysis_results(
"value",
"extra",
"device_components",
"result_id",
"experiment_id",
"chisq",
"quality",
Expand Down Expand Up @@ -295,9 +294,36 @@ def _check_result_table(
**kwargs,
):
"""Check equality of data frame which may involve Qiskit Experiments class value."""
table1 = data1.copy().to_dict(orient="index")
table2 = data2.copy().to_dict(orient="index")
for table in (table1, table2):
for result in table.values():
result.pop("created_time")
# Must ignore result ids because they are internally generated with
# random values by the ExperimentData wrapping object.
result.pop("result_id")
# Keys of the dict are based on the result ids so they must be ignored
# as well. Try to sort entries so equivalent entries will be in the same
# order.
table1 = sorted(
table1.values(),
key=lambda x: (
x["name"],
() if x["components"] is None else tuple(repr(d) for d in x["components"]),
x["value"],
),
)
table2 = sorted(
table2.values(),
key=lambda x: (
x["name"],
() if x["components"] is None else tuple(repr(d) for d in x["components"]),
x["value"],
),
)
return is_equivalent(
data1.copy().to_dict(orient="index"),
data2.copy().to_dict(orient="index"),
table1,
table2,
**kwargs,
)

Expand Down
17 changes: 17 additions & 0 deletions test/framework/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

"""Tests for base experiment framework."""

import pickle
from test.fake_experiment import FakeExperiment, FakeAnalysis
from test.base import QiskitExperimentsTestCase
from itertools import product
Expand Down Expand Up @@ -116,6 +117,22 @@ def circuits(self):
num_jobs += 1
self.assertEqual(len(job_ids), num_jobs)

def test_run_analysis_experiment_data_pickle_roundtrip(self):
"""Test running analysis on ExperimentData after pickle roundtrip"""
analysis = FakeAnalysis()
expdata1 = ExperimentData()
# Set physical qubit for more complete comparison
expdata1.metadata["physical_qubits"] = (1,)
expdata1 = analysis.run(expdata1, seed=54321)
self.assertExperimentDone(expdata1)

expdata2 = ExperimentData(experiment_id=expdata1.experiment_id)
expdata2.metadata["physical_qubits"] = (1,)
expdata2 = pickle.loads(pickle.dumps(expdata2))
expdata2 = analysis.run(expdata2, replace_results=True, seed=54321)
self.assertExperimentDone(expdata2)
self.assertEqualExtended(expdata1, expdata2)

def test_analysis_replace_results_true(self):
"""Test running analysis with replace_results=True"""
analysis = FakeAnalysis()
Expand Down