Skip to content
82 changes: 55 additions & 27 deletions qiskit_experiments/framework/experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
from qiskit_experiments.database_service.exceptions import (
ExperimentDataError,
ExperimentEntryNotFound,
ExperimentEntryExists,
ExperimentDataSaveFailed,
)

Expand Down Expand Up @@ -1111,31 +1110,32 @@ def data(
@do_auto_save
def add_figures(
self,
figures,
figure_names=None,
overwrite=False,
save_figure=None,
figures: Union[str, bytes, pyplot.Figure, list],
figure_names: Optional[Union[str, list]] = None,
overwrite: Optional[bool] = False,
save_figure: Optional[bool] = None,
) -> Union[str, List[str]]:
"""Add the experiment figure.

Args:
figures (str or bytes or pyplot.Figure or list): Paths of the figure
files or figure data.
figure_names (str or list): Names of the figures. If ``None``, use the figure file
names, if given, or a generated name. If `figures` is a list, then
`figure_names` must also be a list of the same length or ``None``.
overwrite (bool): Whether to overwrite the figure if one already exists with
the same name.
save_figure (bool): Whether to save the figure in the database. If ``None``,
figures: Paths of the figure files or figure data.
figure_names: Names of the figures. If ``None``, use the figure file
names, if given, or a generated name of the format ``experiment_type``, figure
index, first 5 elements of ``device_components``, and first 8 digits of the
experiment ID connected by underscores, such as ``T1_Q0_0123abcd.svg``. If `figures`
is a list, then `figure_names` must also be a list of the same length or ``None``.
overwrite: Whether to overwrite the figure if one already exists with
the same name. By default, overwrite is ``False`` and the figure will be renamed
with an incrementing numerical suffix. For example, trying to save ``figure.svg`` when
``figure.svg`` already exists will save it as ``figure-1.svg``, and trying to save
``figure-1.svg`` when ``figure-1.svg`` already exists will save it as ``figure-2.svg``.
save_figure: Whether to save the figure in the database. If ``None``,
the ``auto-save`` attribute is used.

Returns:
str or list:
Figure names.
Figure names in SVG format.

Raises:
ExperimentEntryExists: If the figure with the same name already exists,
and `overwrite=True` is not specified.
ValueError: If an input parameter has an invalid value.
"""
if figure_names is not None and not isinstance(figure_names, list):
Expand All @@ -1152,27 +1152,51 @@ def add_figures(
for idx, figure in enumerate(figures):
if figure_names is None:
if isinstance(figure, str):
# figure is a filename, so we use it as the name
fig_name = figure
else:
elif not isinstance(figure, FigureData):
# Generate a name in the form StandardRB_Q0_Q1_Q2_b4f1d8ad-1.svg
fig_name = (
f"{self.experiment_type}_"
f"Fig-{len(self._figures)}_"
f"Exp-{self.experiment_id[:8]}.svg"
f'{"_".join(str(i) for i in self.metadata.get("device_components", [])[:5])}_'
f"{self.experiment_id[:8]}.svg"
)
else:
# Keep the existing figure name if there is one
fig_name = figure.name
else:
fig_name = figure_names[idx]

if not fig_name.endswith(".svg"):
LOG.info("File name %s does not have an SVG extension. A '.svg' is added.")
fig_name += ".svg"

existing_figure = fig_name in self._figures
if existing_figure and not overwrite:
raise ExperimentEntryExists(
f"A figure with the name {fig_name} for this experiment "
f"already exists. Specify overwrite=True if you "
f"want to overwrite it."
)
# Remove any existing suffixes then generate new figure name
# StandardRB_Q0_Q1_Q2_b4f1d8ad.svg becomes StandardRB_Q0_Q1_Q2_b4f1d8ad
fig_name_chunked = fig_name.rsplit("-", 1)
if len(fig_name_chunked) != 1: # Figure name already has a suffix
# This extracts StandardRB_Q0_Q1_Q2_b4f1d8ad as the prefix from
# StandardRB_Q0_Q1_Q2_b4f1d8ad-1.svg
fig_name_prefix = fig_name_chunked[0]
try:
fig_name_suffix = int(fig_name_chunked[1].rsplit(".", 1)[0])
except ValueError: # the suffix is not an int, add our own suffix
# my-custom-figure-name will be the prefix of my-custom-figure-name.svg
fig_name_prefix = fig_name.rsplit(".", 1)[0]
fig_name_suffix = 0
else:
# StandardRB_Q0_Q1_Q2_b4f1d8ad.svg has no hyphens so
# StandardRB_Q0_Q1_Q2_b4f1d8ad would be its prefix
fig_name_prefix = fig_name.rsplit(".", 1)[0]
fig_name_suffix = 0
fig_name = f"{fig_name_prefix}-{fig_name_suffix + 1}.svg"
while fig_name in self._figures: # Increment suffix until the name isn't taken
# If StandardRB_Q0_Q1_Q2_b4f1d8ad-1.svg already exists,
# StandardRB_Q0_Q1_Q2_b4f1d8ad-2.svg will be the name of this figure
fig_name_suffix += 1
fig_name = f"{fig_name_prefix}-{fig_name_suffix + 1}.svg"

# figure_data = None
if isinstance(figure, str):
with open(figure, "rb") as file:
Expand All @@ -1184,7 +1208,11 @@ def add_figures(
figure = figure_data.figure

else:
figure_metadata = {"qubits": self.metadata.get("physical_qubits")}
figure_metadata = {
"qubits": self.metadata.get("physical_qubits"),
"device_components": self.metadata.get("device_components"),
"experiment_type": self.experiment_type,
}
figure_data = FigureData(figure=figure, name=fig_name, metadata=figure_metadata)

self._figures[fig_name] = figure_data
Expand Down
12 changes: 12 additions & 0 deletions releasenotes/notes/update-figure-name-2db258c30ffe9912.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
other:
- |
Figure names have been updated to include qubit indices up to the first five device components in
the experiment, with format ``StandardRB_Q0_Q1_Q2_Q3_Q5_b4f1d8ad.svg``. For composite
experiments where ``flatten_results`` is set to ``True``, the head of the figure name is now the
class name of the experiment instead of ``ParallelExperiment`` or ``BatchExperiment``, such that
the figure name is the same when ``flatten_results`` is ``False``. The behavior when a figure
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the code I don't see a change for composite experiments

Copy link
Collaborator Author

@coruscating coruscating Jul 26, 2023

Choose a reason for hiding this comment

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

This is because of the new naming scheme as well as this addition:
https://github.com/Qiskit-Extensions/qiskit-experiments/blob/1fa7ed3ce07acc80dd0bde80d0918b464b76522b/qiskit_experiments/framework/experiment_data.py#L1164-L1166

Before, figures of composite experiments overwrote child experiment figure names with ParallelExperiment or BatchExperiment, but now we use the actual child experiment class to generate the figure name and don't overwrite in a composite experiment.

name is repeated and ``overwrite`` is ``False`` has changed from throwing an exception to
appending a numerical suffix to the figure name like ``StandardRB_Q0_Q1_Q2_Q3_Q5_b4f1d8ad-1.svg``.
- |
Figure metadata now includes ``experiment_type`` and ``device_components``.
26 changes: 20 additions & 6 deletions test/database_service/test_db_experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
from qiskit_experiments.database_service.exceptions import (
ExperimentDataError,
ExperimentEntryNotFound,
ExperimentEntryExists,
)
from qiskit_experiments.database_service.device_component import Qubit
from qiskit_experiments.framework.experiment_data import (
Expand Down Expand Up @@ -318,12 +317,26 @@ def test_add_figure_overwrite(self):

exp_data = ExperimentData(backend=self.backend, experiment_type="qiskit_test")
fn = exp_data.add_figures(hello_bytes)
with self.assertRaises(ExperimentEntryExists):
exp_data.add_figures(friend_bytes, fn)

exp_data.add_figures(friend_bytes, fn, overwrite=True)
# pylint: disable=no-member
fn_prefix = fn.rsplit(".", 1)[0]

# Without overwrite on, the filename should have an incrementing suffix
self.assertEqual(exp_data.add_figures(friend_bytes, fn), f"{fn_prefix}-1.svg")

self.assertEqual(
exp_data.add_figures([friend_bytes, friend_bytes], [fn, fn]),
[f"{fn_prefix}-2.svg", f"{fn_prefix}-3.svg"],
)

self.assertEqual(exp_data.add_figures(friend_bytes, fn, overwrite=True), fn)

self.assertEqual(friend_bytes, exp_data.figure(fn).figure)

self.assertEqual(
exp_data.add_figures(friend_bytes, f"{fn_prefix}-a.svg"), f"{fn_prefix}-a.svg"
)

def test_add_figure_save(self):
"""Test saving a figure in the database."""
hello_bytes = str.encode("hello world")
Expand All @@ -343,15 +356,16 @@ def test_add_figure_metadata(self):
exp_data = ExperimentData(
backend=self.backend,
experiment_type="qiskit_test",
metadata={"physical_qubits": qubits},
metadata={"physical_qubits": qubits, "device_components": list(map(Qubit, qubits))},
)
exp_data.add_figures(hello_bytes)
exp_data.figure(0).metadata["foo"] = "bar"
figure_data = exp_data.figure(0)

self.assertEqual(figure_data.metadata["qubits"], qubits)
self.assertEqual(figure_data.metadata["device_components"], list(map(Qubit, qubits)))
self.assertEqual(figure_data.metadata["foo"], "bar")
expected_name_prefix = "qiskit_test_Fig-0_Exp-"
expected_name_prefix = "qiskit_test_Q0_Q1_Q2_"
self.assertEqual(figure_data.name[: len(expected_name_prefix)], expected_name_prefix)

exp_data2 = ExperimentData(
Expand Down