Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/apidocs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Package Modules
curve_analysis
calibration_management
database_service
visualization
test

Experiment Modules
Expand Down
6 changes: 6 additions & 0 deletions docs/apidocs/visualization.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.. _qiskit-experiments-visualization:

.. automodule:: qiskit_experiments.visualization
:no-members:
:no-inherited-members:
:no-special-members:
22 changes: 13 additions & 9 deletions qiskit_experiments/visualization/drawers/base_drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,18 @@ def _default_figure_options(cls) -> Options:
If not provided, it is automatically scaled based on the input data points.
ylim (Tuple[float, float]): Min and max value of the vertical axis.
If not provided, it is automatically scaled based on the input data points.
xval_unit (str): SI unit of x values. No prefix is needed here.
For example, when the x values represent time, this option will be just "s"
rather than "ms". In the output figure, the prefix is automatically selected
based on the maximum value in this axis. If your x values are in [1e-3, 1e-4],
they are displayed as [1 ms, 10 ms]. This option is likely provided by the
analysis class rather than end-users. However, users can still override
if they need different unit notation. By default, this option is set to ``None``,
and no scaling is applied. If nothing is provided, the axis numbers will be
displayed in the scientific notation.
xval_unit (str): Unit of x values. No scaling prefix is needed here as this is controlled by
``xval_unit_scale``.
yval_unit (str): Unit of y values. See ``xval_unit`` for details.
xval_unit_scale (bool): Whether to add an SI unit prefix to ``xval_unit`` if needed.
For example, when the x values represent time and ``xval_unit="s"``,
``xval_unit_scale=True`` adds an SI unit prefix to ``"s"`` based on X values of plotted
data. In the output figure, the prefix is automatically selected based on the maximum
value in this axis. If your x values are in [1e-3, 1e-4], they are displayed as [1 ms, 10
ms]. By default, this option is set to ``True``. If ``False`` is provided, the axis
numbers will be displayed in the scientific notation.
yval_unit_scale (bool): Whether to add an SI unit prefix to ``yval_unit`` if needed. See
``xval_unit_scale`` for details.
figure_title (str): Title of the figure. Defaults to None, i.e. nothing is shown.
series_params (Dict[str, Dict[str, Any]]): A dictionary of parameters for each series.
This is keyed on the name for each series. Sub-dictionary is expected to have the
Expand All @@ -215,6 +217,8 @@ def _default_figure_options(cls) -> Options:
ylim=None,
xval_unit=None,
yval_unit=None,
xval_unit_scale=True,
yval_unit_scale=True,
figure_title=None,
series_params={},
custom_style=PlotStyle(),
Expand Down
16 changes: 13 additions & 3 deletions qiskit_experiments/visualization/drawers/mpl_drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,11 @@ def format_canvas(self):
if ax_type == "x":
lim = self.figure_options.xlim
unit = self.figure_options.xval_unit
unit_scale = self.figure_options.xval_unit_scale
else:
lim = self.figure_options.ylim
unit = self.figure_options.yval_unit
unit_scale = self.figure_options.yval_unit_scale

# Compute data range from auto scale
if not lim:
Expand All @@ -173,7 +175,7 @@ def format_canvas(self):
lim = (v0, v1)

# Format axis number notation
if unit:
if unit and unit_scale:
# If value is specified, automatically scale axis magnitude
# and write prefix to axis label, i.e. 1e3 Hz -> 1 kHz
maxv = max(np.abs(lim[0]), np.abs(lim[1]))
Expand All @@ -192,7 +194,7 @@ def format_canvas(self):
formatter.set_scientific(True)
formatter.set_powerlimits((-3, 3))

units_str = ""
units_str = f" [{unit}]" if unit else ""

for sub_ax in all_axes:
if ax_type == "x":
Expand Down Expand Up @@ -342,6 +344,12 @@ def scatter(
draw_options.update(**options)

if x_err is None and y_err is None:
# Size of symbols is defined by the `s` kwarg for scatter(). Check if `s` exists in
# `draw_options`, if not set to the default style. Square the `symbol_size` as `s` for MPL
# scatter is proportional to the width and not the area of the marker, but `symbol_size` is
# proportional to the area.
if "s" not in draw_options:
draw_options["s"] = self.style["symbol_size"] ** 2
self._get_axis(axis).scatter(x_data, y_data, **draw_options)
else:
# Check for invalid error values.
Expand All @@ -354,7 +362,9 @@ def scatter(
# `options`, and thus draw_options.
errorbar_options = {
"linestyle": "",
"markersize": 9,
# `markersize` is equivalent to `symbol_size`.
"markersize": self.style["symbol_size"],
"capsize": self.style["errorbar_capsize"],
}
errorbar_options.update(draw_options)

Expand Down
21 changes: 13 additions & 8 deletions qiskit_experiments/visualization/plotters/base_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,15 +397,18 @@ def _default_figure_options(cls) -> Options:
If not provided, it is automatically scaled based on the input data points.
ylim (Tuple[float, float]): Min and max value of the vertical axis.
If not provided, it is automatically scaled based on the input data points.
xval_unit (str): SI unit of x values. No prefix is needed here.
For example, when the x values represent time, this option will be just "s" rather than
"ms". In the output figure, the prefix is automatically selected based on the maximum
value in this axis. If your x values are in [1e-3, 1e-4], they are displayed as [1 ms, 10
ms]. This option is likely provided by the analysis class rather than end-users. However,
users can still override if they need different unit notation. By default, this option is
set to ``None``, and no scaling is applied. If nothing is provided, the axis numbers will
be displayed in the scientific notation.
xval_unit (str): Unit of x values. No scaling prefix is needed here as this is controlled by
``xval_unit_scale``.
yval_unit (str): Unit of y values. See ``xval_unit`` for details.
xval_unit_scale (bool): Whether to add an SI unit prefix to ``xval_unit`` if needed.
For example, when the x values represent time and ``xval_unit="s"``,
``xval_unit_scale=True`` adds an SI unit prefix to ``"s"`` based on X values of plotted
data. In the output figure, the prefix is automatically selected based on the maximum
value in this axis. If your x values are in [1e-3, 1e-4], they are displayed as [1 ms, 10
ms]. By default, this option is set to ``True``. If ``False`` is provided, the axis
numbers will be displayed in the scientific notation.
yval_unit_scale (bool): Whether to add an SI unit prefix to ``yval_unit`` if needed. See
``xval_unit_scale`` for details.
figure_title (str): Title of the figure. Defaults to None, i.e. nothing is shown.
series_params (Dict[SeriesName, Dict[str, Any]]): A dictionary of plot parameters for each
series. This is keyed on the name for each series. Sub-dictionary is expected to have
Expand All @@ -420,6 +423,8 @@ def _default_figure_options(cls) -> Options:
ylim=None,
xval_unit=None,
yval_unit=None,
xval_unit_scale=True,
yval_unit_scale=True,
figure_title=None,
series_params={},
)
Expand Down
32 changes: 26 additions & 6 deletions qiskit_experiments/visualization/plotters/curve_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@ def _default_options(cls) -> Options:
options.plot_sigma = [(1.0, 0.7), (3.0, 0.3)]
return options

@classmethod
def _default_figure_options(cls) -> Options:
"""Return curve-plotter specific default figure options.

Figure Options:
report_red_chi2_label (str): The label for the reduced-chi squared entry of the fit
report. Defaults to "reduced-$\\chi^2$`.
"""
fig_opts = super()._default_figure_options()
fig_opts.report_red_chi2_label = "reduced-$\\chi^2$"

return fig_opts

def _plot_figure(self):
"""Plots a curve-fit figure."""
for ser in self.series:
Expand Down Expand Up @@ -154,9 +167,9 @@ def _plot_figure(self):
def _write_report(self) -> str:
"""Write fit report with supplementary_data.

Subclass can override this method to customize fit report.
By default, this writes important fit parameters and chi-squared value of the
fit in the fit report.
Subclass can override this method to customize fit report. By default, this writes important fit
parameters and chi-squared value of the fit in the fit report. The ``report_red_chi2_label``
figure-option controls the label for the chi-squared entries in the report.

Returns:
Fit report.
Expand All @@ -173,13 +186,20 @@ def _write_report(self) -> str:
if "fit_red_chi" in self.supplementary_data:
red_chi = self.supplementary_data["fit_red_chi"]
if len(report) > 0:
report += "\n\n"
report += "\n"
if isinstance(red_chi, float):
report += f"reduced-chi2 = {red_chi: .4g}"
report += f"{self.figure_options.report_red_chi2_label} = {red_chi: .4g}"
else:
# Composite curve analysis reporting multiple chi-sq values.
# This is usually given by a dict keyed on fit group name.
report += "reduced-chi2 per fit\n"

# Add gap between primary-results and reduced-chi squared as
# we have multiple values to display. This is easier to read.
if len(report) > 0:
report += "\n"

# Created indented text of reduced-chi squared results.
report += f"{self.figure_options.report_red_chi2_label} per fit\n"
lines = []
for mod_name, mod_chi in red_chi.items():
lines.append(f" * {mod_name}: {mod_chi: .4g}")
Expand Down
2 changes: 2 additions & 0 deletions qiskit_experiments/visualization/plotters/iq_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ def _default_figure_options(cls) -> Options:
fig_opts.ylabel = "Quadrature"
fig_opts.xval_unit = "arb."
fig_opts.yval_unit = "arb."
fig_opts.xval_unit_scale = False
fig_opts.yval_unit_scale = False
return fig_opts

def _misclassified_points(self, series_name: str, points: np.ndarray) -> Optional[np.ndarray]:
Expand Down
18 changes: 13 additions & 5 deletions qiskit_experiments/visualization/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ def default_style(cls) -> "PlotStyle":

Style Parameters:
figsize (Tuple[int,int]): The size of the figure ``(width, height)``, in inches.
legend_loc (str): The location of the legend.
legend_loc (Optional[str]): The location of the legend in axis coordinates. If None, location
is automatically determined by the drawer.
tick_label_size (int): The font size for tick labels.
axis_label_size (int): The font size for axis labels.
symbol_size (float): The size of symbols for points/markers, proportional to the area of the
drawn graphic.
errorbar_capsize (float): The size of end-caps for error-bars.
textbox_rel_pos (Tuple[float,float]): The relative position ``(horizontal, vertical)`` of
textboxes, as a percentage of the canvas dimensions.
textbox_text_size (int): The font size for textboxes.
Expand All @@ -61,16 +65,20 @@ def default_style(cls) -> "PlotStyle":
style = {
# size of figure (width, height)
"figsize": (8, 5), # Tuple[int, int]
# legend location (vertical, horizontal)
"legend_loc": "center right", # str
# legend location (vertical, horizontal) or None.
"legend_loc": None, # str
# size of tick label
"tick_label_size": 14, # int
# size of axis label
"axis_label_size": 16, # int
# relative position of a textbox
"textbox_rel_pos": (0.6, 0.95), # Tuple[float, float]
"textbox_rel_pos": (0.5, -0.25), # Tuple[float, float]
# size of textbox text
"textbox_text_size": 14, # int
"textbox_text_size": 12, # int
# size of caps for error-bars
"errorbar_capsize": 4, # float
# Default size of symbols, used for graphics where symbols are drawn for points.
"symbol_size": 6.0, # float
}
return cls(**style)

Expand Down
4 changes: 3 additions & 1 deletion test/visualization/test_iq_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ def _dummy_data(
is_trained: bool = True,
n_series: int = 3,
raise_predict_not_trained: bool = False,
factor: float = 1,
) -> Tuple[List, List, BaseDiscriminator]:
"""Create dummy data for the tests.

Args:
is_trained: Whether the discriminator should be trained or not. Defaults to True.
n_series: The number of series to generate dummy data for. Defaults to 3.
raise_predict_not_trained: Passed to the discriminator :class:`MockDiscriminator` class.
factor: A scaler factor by which to multipl all data.


Returns:
Expand All @@ -99,7 +101,7 @@ def _dummy_data(
points = []
labels = []
for i in range(n_series):
points.append(np.random.rand(128, 2))
points.append(np.random.rand(128, 2) * factor)
labels.append(f"{i}")
mock_discrim = MockDiscriminator(
is_trained, n_states=n_series, raise_predict_not_trained=raise_predict_not_trained
Expand Down
47 changes: 47 additions & 0 deletions test/visualization/test_plotter_mpldrawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,53 @@ def test_end_to_end_short(self):
# Expect a specific type
self.assertTrue(isinstance(fig, matplotlib.pyplot.Figure))

@ddt.data(
*list(product([(-3, "m"), (0, ""), (3, "k"), (6, "M")], [True, False], [True, False]))
)
def test_unit_scale(self, args):
"""Test whether axis labels' unit-prefixes scale correctly."""
(exponent, prefix), xval_unit_scale, yval_unit_scale = args
input_unit_x = "DUMMYX"
input_unit_y = "DUMMYY"
plotter = MockPlotter(MplDrawer(), plotting_enabled=True)
plotter.set_figure_options(
xlabel=" ", # Dummy labels to force drawing of units.
ylabel=" ", #
xval_unit=input_unit_x,
yval_unit=input_unit_y,
xval_unit_scale=xval_unit_scale,
yval_unit_scale=yval_unit_scale,
)

n_points = 128
plotter.set_series_data(
"seriesA",
x=np.random.rand(n_points) * 2 * (10**exponent),
y=np.random.rand(n_points) * 2 * (10**exponent),
z=np.random.rand(128) * (10**exponent),
)

plotter.figure()

expected_unit_x = prefix + input_unit_x if xval_unit_scale else input_unit_x
expected_unit_y = prefix + input_unit_y if yval_unit_scale else input_unit_y

# Get actual labels
ax = plotter.drawer._axis
xlabel = ax.get_xlabel()
ylabel = ax.get_ylabel()

# Check if expected units exist in the axis labels.
for axis, actual_label, expected_units in zip(
["X", "Y"], [xlabel, ylabel], [expected_unit_x, expected_unit_y]
):
self.assertTrue(actual_label is not None)
self.assertTrue(
actual_label.find(expected_units) > 0,
msg=f"{axis} axis label does not contain unit: Could not find '{expected_units}' "
f"in '{actual_label}'.",
)

@ddt.data(
{str: ["0", "1", "2"], int: [0, 1, 2]},
{str: [str(0.0), str(1.0), str(2.0)], float: [0.0, 1.0, 2.0]},
Expand Down
14 changes: 11 additions & 3 deletions test/visualization/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_default_only_contains_expected_fields(self):
"""Test that only expected fields are set in the default style.

This enforces two things:
1. The expected style fields are not None.
1. The expected style fields are not None, unless otherwise stated.
2. No extra fields are set.

The second property being enforced is to make sure that this test fails if new default style
Expand All @@ -62,16 +62,24 @@ def test_default_only_contains_expected_fields(self):
default = PlotStyle.default_style()
expected_not_none_fields = [
"figsize",
"legend_loc",
"tick_label_size",
"axis_label_size",
"textbox_rel_pos",
"textbox_text_size",
"errorbar_capsize",
"symbol_size",
]
expected_none_fields = [
"legend_loc",
]
for field in expected_not_none_fields:
self.assertIsNotNone(default.get(field, None))
for field in expected_none_fields:
self.assertIsNone(default.get(field, 0))
# Check that default style keys are as expected, ignoring order.
self.assertCountEqual(expected_not_none_fields, list(default.keys()))
self.assertCountEqual(
[*expected_not_none_fields, *expected_none_fields], list(default.keys())
)

def test_update(self):
"""Test that styles can be updated."""
Expand Down