Skip to content

Commit a6c9e53

Browse files
authored
Fix pickle deserialization of ExperimentData (#1326)
Previously, `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.
1 parent 729014b commit a6c9e53

File tree

4 files changed

+58
-5
lines changed

4 files changed

+58
-5
lines changed

qiskit_experiments/framework/experiment_data.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def _repr_svg_(self):
197197
return None
198198

199199

200-
FigureT = Union[str, bytes, MatplotlibFigure, FigureData]
200+
FigureType = Union[str, bytes, MatplotlibFigure, FigureData]
201201

202202

203203
class ExperimentData:
@@ -1142,7 +1142,7 @@ def data(
11421142
@do_auto_save
11431143
def add_figures(
11441144
self,
1145-
figures: Union[FigureT, List[FigureT]],
1145+
figures: Union[FigureType, List[FigureType]],
11461146
figure_names: Optional[Union[str, List[str]]] = None,
11471147
overwrite: bool = False,
11481148
save_figure: Optional[bool] = None,
@@ -2551,6 +2551,7 @@ def __setstate__(self, state):
25512551
self._job_futures = ThreadSafeOrderedDict()
25522552
self._analysis_futures = ThreadSafeOrderedDict()
25532553
self._analysis_executor = futures.ThreadPoolExecutor(max_workers=1)
2554+
self._monitor_executor = futures.ThreadPoolExecutor()
25542555

25552556
def __str__(self):
25562557
line = 51 * "-"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed :mod:`pickle` deserialization of :class:`.ExperimentData` objects.
5+
Previously, :class:`.ExperimentData` objects could be serialized and
6+
deserialized using Python's ``pickle`` module, but deserialized objects
7+
were not completely restored and an exception would be raised when doing
8+
some operations like running analysis on the restored object. See `#1326
9+
<https://github.com/Qiskit-Extensions/qiskit-experiments/pull/1326/files>`__.

test/extended_equality.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ def _check_service_analysis_results(
249249
"value",
250250
"extra",
251251
"device_components",
252-
"result_id",
253252
"experiment_id",
254253
"chisq",
255254
"quality",
@@ -295,9 +294,36 @@ def _check_result_table(
295294
**kwargs,
296295
):
297296
"""Check equality of data frame which may involve Qiskit Experiments class value."""
297+
table1 = data1.copy().to_dict(orient="index")
298+
table2 = data2.copy().to_dict(orient="index")
299+
for table in (table1, table2):
300+
for result in table.values():
301+
result.pop("created_time")
302+
# Must ignore result ids because they are internally generated with
303+
# random values by the ExperimentData wrapping object.
304+
result.pop("result_id")
305+
# Keys of the dict are based on the result ids so they must be ignored
306+
# as well. Try to sort entries so equivalent entries will be in the same
307+
# order.
308+
table1 = sorted(
309+
table1.values(),
310+
key=lambda x: (
311+
x["name"],
312+
() if x["components"] is None else tuple(repr(d) for d in x["components"]),
313+
x["value"],
314+
),
315+
)
316+
table2 = sorted(
317+
table2.values(),
318+
key=lambda x: (
319+
x["name"],
320+
() if x["components"] is None else tuple(repr(d) for d in x["components"]),
321+
x["value"],
322+
),
323+
)
298324
return is_equivalent(
299-
data1.copy().to_dict(orient="index"),
300-
data2.copy().to_dict(orient="index"),
325+
table1,
326+
table2,
301327
**kwargs,
302328
)
303329

test/framework/test_framework.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
"""Tests for base experiment framework."""
1414

15+
import pickle
1516
from test.fake_experiment import FakeExperiment, FakeAnalysis
1617
from test.base import QiskitExperimentsTestCase
1718
from itertools import product
@@ -127,6 +128,22 @@ def circuits(self):
127128
num_jobs += 1
128129
self.assertEqual(len(job_ids), num_jobs)
129130

131+
def test_run_analysis_experiment_data_pickle_roundtrip(self):
132+
"""Test running analysis on ExperimentData after pickle roundtrip"""
133+
analysis = FakeAnalysis()
134+
expdata1 = ExperimentData()
135+
# Set physical qubit for more complete comparison
136+
expdata1.metadata["physical_qubits"] = (1,)
137+
expdata1 = analysis.run(expdata1, seed=54321)
138+
self.assertExperimentDone(expdata1)
139+
140+
expdata2 = ExperimentData(experiment_id=expdata1.experiment_id)
141+
expdata2.metadata["physical_qubits"] = (1,)
142+
expdata2 = pickle.loads(pickle.dumps(expdata2))
143+
expdata2 = analysis.run(expdata2, replace_results=True, seed=54321)
144+
self.assertExperimentDone(expdata2)
145+
self.assertEqualExtended(expdata1, expdata2)
146+
130147
def test_analysis_replace_results_true(self):
131148
"""Test running analysis with replace_results=True"""
132149
analysis = FakeAnalysis()

0 commit comments

Comments
 (0)