-
Notifications
You must be signed in to change notification settings - Fork 127
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
Refactoring _run_analysis in composite_analysis #1268 #1286
Changes from 7 commits
d526f89
09a3c99
24b210c
cbf544a
e0924af
145ae45
5c59e08
cc0b89a
2dbba8a
e7f46c3
1c803ae
9eb2dba
0bd3a18
5e4b9d2
f752a4f
dd257a2
c79e888
8f21278
73db5bd
1ed676e
745669f
fd6df6c
0565a1b
a54a49f
814c22a
229692f
288d18f
a3abf4d
02906dc
daa36dc
fdc276a
b906461
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -38,6 +38,9 @@ | |||||
from matplotlib import pyplot | ||||||
from matplotlib.figure import Figure as MatplotlibFigure | ||||||
from qiskit.result import Result | ||||||
from qiskit.result import marginal_distribution | ||||||
from qiskit.result.postprocess import format_counts_memory | ||||||
from qiskit.result.utils import marginal_memory | ||||||
from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES | ||||||
from qiskit.exceptions import QiskitError | ||||||
from qiskit.providers import Job, Backend, Provider | ||||||
|
@@ -767,6 +770,49 @@ def add_data( | |||||
) -> None: | ||||||
"""Add experiment data. | ||||||
|
||||||
Args: | ||||||
data: Experiment data to add. Several types are accepted for convenience: | ||||||
|
||||||
* Result: Add data from this ``Result`` object. | ||||||
* List[Result]: Add data from the ``Result`` objects. | ||||||
* Dict: Add this data. | ||||||
* List[Dict]: Add this list of data. | ||||||
|
||||||
Raises: | ||||||
TypeError: If the input data type is invalid. | ||||||
""" | ||||||
|
||||||
if any(not future.done() for future in self._analysis_futures.values()): | ||||||
LOG.warning( | ||||||
"Not all analysis has finished running. Adding new data may " | ||||||
"create unexpected analysis results." | ||||||
) | ||||||
if not isinstance(data, list): | ||||||
data = [data] | ||||||
|
||||||
# Directly add non-job data | ||||||
with self._result_data.lock: | ||||||
for datum in data: | ||||||
if isinstance(datum, dict): | ||||||
if datum["metadata"].get("composite_metadata"): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
if value is not necessary |
||||||
tmp_exp_data = ExperimentData() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need another conditional branching here. Note that each element in the list contains the result of composite experiment. Let's assume a nested composite experiment consisting of three inner experiments. Here the
Experiment A and B are batched by Instead, in each loop, you need to check whether corresponding child container is already initialized, make new one if not exist otherwise just call existing one, then add marginalize data to corresponding container. More specifically,
|
||||||
marginalized_data = self._marginalized_component_data([datum]) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this call is always with a single element, it's much simpler to assume |
||||||
for inner_datum in marginalized_data: | ||||||
tmp_exp_data.__add_data(inner_datum) | ||||||
self._set_child_data([tmp_exp_data]) | ||||||
else: | ||||||
self._result_data.append(datum) | ||||||
elif isinstance(datum, Result): | ||||||
self.__add_data(datum) | ||||||
else: | ||||||
raise TypeError(f"Invalid data type {type(datum)}.") | ||||||
|
||||||
def __add_data( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think subroutine is necessary. You can just recursively call |
||||||
self, | ||||||
data: Union[Result, List[Result], Dict, List[Dict]], | ||||||
) -> None: | ||||||
"""Add experiment data. | ||||||
|
||||||
Args: | ||||||
data: Experiment data to add. Several types are accepted for convenience: | ||||||
|
||||||
|
@@ -792,9 +838,128 @@ def add_data( | |||||
if isinstance(datum, dict): | ||||||
self._result_data.append(datum) | ||||||
elif isinstance(datum, Result): | ||||||
self._add_result_data(datum) | ||||||
if datum["metadata"]: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
self._set_child_data(datum["metadata"]._metadata()) | ||||||
else: | ||||||
self._add_result_data(datum) | ||||||
else: | ||||||
raise TypeError(f"Invalid data type {type(datum)}.") | ||||||
|
||||||
def _add_data( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to update this line instead. Having a dedicated method for marginalization is also okey. The returned Qiskit You would check the |
||||||
self, | ||||||
component_expdata: List[ExperimentData], | ||||||
data: Union[Result, List[Result], Dict, List[Dict]], | ||||||
) -> None: | ||||||
"""Add experiment data. | ||||||
|
||||||
Args: | ||||||
data: Experiment data to add. Several types are accepted for convenience: | ||||||
|
||||||
* Result: Add data from this ``Result`` object. | ||||||
* List[Result]: Add data from the ``Result`` objects. | ||||||
* Dict: Add this data. | ||||||
* List[Dict]: Add this list of data. | ||||||
|
||||||
Raises: | ||||||
TypeError: If the input data type is invalid. | ||||||
""" | ||||||
#TODO: Continue from here | ||||||
|
||||||
if not isinstance(data, list): | ||||||
data = [data] | ||||||
|
||||||
# self._marginalized_component_data() | ||||||
# Directly add non-job data | ||||||
marginalized_data = self._marginalized_component_data(data) | ||||||
|
||||||
with self._result_data.lock: | ||||||
for sub_expdata, sub_data in zip(component_expdata, marginalized_data): | ||||||
# Clear any previously stored data and add marginalized data | ||||||
sub_expdata._result_data.clear() | ||||||
sub_expdata.__add_data(sub_data) | ||||||
|
||||||
def _marginalized_component_data(self, composite_data: List[Dict]) -> List[List[Dict]]: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I mentioned before, with new implementation the input data length is always 1. So you can at least remove outer loop. |
||||||
"""Return marginalized data for component experiments. | ||||||
|
||||||
Args: | ||||||
composite_data: a list of composite experiment circuit data. | ||||||
|
||||||
Returns: | ||||||
A List of lists of marginalized circuit data for each component | ||||||
experiment in the composite experiment. | ||||||
""" | ||||||
# 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 | ||||||
|
||||||
# Pre-process the memory if any to avoid redundant calls to format_counts_memory | ||||||
f_memory = None | ||||||
if ( | ||||||
"memory" in datum | ||||||
and composite_clbits is not None | ||||||
and isinstance(datum["memory"][0], str) | ||||||
): | ||||||
f_memory = self._format_memory(datum, composite_clbits) | ||||||
|
||||||
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_distribution( | ||||||
counts=datum["counts"], | ||||||
indices=composite_clbits[i], | ||||||
) | ||||||
else: | ||||||
sub_data["counts"] = datum["counts"] | ||||||
if "memory" in datum: | ||||||
if composite_clbits is not None: | ||||||
# level 2 | ||||||
if f_memory is not None: | ||||||
idx = slice( | ||||||
-1 - composite_clbits[i][-1], -composite_clbits[i][0] or None | ||||||
) | ||||||
sub_data["memory"] = [shot[idx] for shot in f_memory] | ||||||
# level 1 | ||||||
else: | ||||||
mem = np.array(datum["memory"]) | ||||||
|
||||||
# Averaged level 1 data | ||||||
if len(mem.shape) == 2: | ||||||
sub_data["memory"] = mem[composite_clbits[i]].tolist() | ||||||
# Single-shot level 1 data | ||||||
if len(mem.shape) == 3: | ||||||
sub_data["memory"] = mem[:, composite_clbits[i]].tolist() | ||||||
else: | ||||||
sub_data["memory"] = datum["memory"] | ||||||
marginalized_data[index].append(sub_data) | ||||||
|
||||||
# Sort by index | ||||||
return [marginalized_data[i] for i in sorted(marginalized_data.keys())] | ||||||
|
||||||
@staticmethod | ||||||
def _format_memory(datum: Dict, composite_clbits: List): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you also replace this with the merginal_memory function? This is also implemented with Rust, and must be faster. |
||||||
"""A helper method to convert level 2 memory (if it exists) to bit-string format.""" | ||||||
f_memory = None | ||||||
if ( | ||||||
"memory" in datum | ||||||
and composite_clbits is not None | ||||||
and isinstance(datum["memory"][0], str) | ||||||
): | ||||||
num_cbits = 1 + max(cbit for cbit_list in composite_clbits for cbit in cbit_list) | ||||||
header = {"memory_slots": num_cbits} | ||||||
f_memory = list(format_counts_memory(shot, header) for shot in datum["memory"]) | ||||||
|
||||||
return f_memory | ||||||
|
||||||
def add_jobs( | ||||||
self, | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually you should remove all of these lines. You've already marginalized the data and bootstrapped child container preparations (and this is why you needed to append
datum
inself._results_data
). What you are doing here is:(1) Prepare child data with
add_data
, but keep the original (un-marginalized) data in the outer container.(2) Ignore prepared child data and prepare child data again in composite analysis.
Indeed this is very inefficient (you just introduced extra overhead of data preparation).
In the
_run_analysis
, you just need to runIn principle you can remove almost all protected methods of the composite analysis, since you delegate the responsibility of inner data preparation to
ExperimentData
with this PR. This simplification helps refactoring of composite analysis.