Files
Raptor/validation/tests/test_pim_plots.py
T
NiccoloN a77edafcbf
Validate Operations / validate-operations (push) Has been cancelled
even better more structured throughput scheduling
plot adjustments
validation adjustments
2026-10-27 17:46:46 +01:00

971 lines
51 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import csv
from dataclasses import replace
import importlib.util
import math
from pathlib import Path
import subprocess
import sys
import pytest
TOOLS = Path(__file__).resolve().parents[1] / "tools/pim"
@pytest.fixture
def comparison(monkeypatch):
directory = TOOLS / "pimcomp/compare"
monkeypatch.syspath_prepend(str(directory))
spec = importlib.util.spec_from_file_location("comparison_plots", directory / "generate_plots.py")
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, spec.name, module)
spec.loader.exec_module(module)
return module
@pytest.fixture
def comparison_csv():
with (TOOLS / "pimcomp/compare/results_comparison.csv").open(newline="") as stream:
reader = csv.DictReader(stream)
return reader.fieldnames, list(reader)
def write_csv(path, fields, rows):
with path.open("w", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
return path
@pytest.mark.parametrize("custom", [False, True])
def test_comparison_plot_cli_input_paths(comparison, tmp_path, monkeypatch, custom):
expected = tmp_path / "custom.csv" if custom else TOOLS / "pimcomp/compare/results_comparison.csv"
seen = []
class InputCaptured(Exception):
pass
def capture(path):
seen.append(path)
raise InputCaptured
monkeypatch.setattr(comparison, "load_results", capture)
monkeypatch.setattr(comparison, "configure_style", lambda **kwargs: None)
monkeypatch.setattr(sys, "argv", ["generate_plots.py", *(["--input", str(expected)] if custom else [])])
with pytest.raises(InputCaptured):
comparison.main()
assert seen == [expected]
@pytest.mark.parametrize("custom", [False, True])
def test_comparison_plot_cli_output_paths(comparison, tmp_path, monkeypatch, custom):
expected = tmp_path / "custom" if custom else TOOLS / "pimcomp/compare/plots/default"
seen = []
class OutputCaptured(Exception):
pass
def capture(rows, output):
seen.append(output)
raise OutputCaptured
monkeypatch.setattr(comparison, "draw_validation", capture)
monkeypatch.setattr(sys, "argv", ["generate_plots.py", *(["--output-dir", str(expected)] if custom else [])])
with pytest.raises(OutputCaptured):
comparison.main()
assert seen == [expected]
@pytest.mark.parametrize("flags,colors,anonymize", [([], True, False),
(["--colors", "--no-anonymize"], True, False), (["--no-colors"], False, False),
(["--anonymize"], True, True), (["--no-colors", "--anonymize"], False, True),
(["--no-colors", "--colors", "--anonymize", "--no-anonymize"], True, False)])
def test_plot_display_flags(comparison, monkeypatch, flags, colors, anonymize):
captured = []
class StyleCaptured(Exception):
pass
def capture(**options):
captured.append(options)
raise StyleCaptured
monkeypatch.setattr(comparison, "configure_style", capture)
monkeypatch.setattr(sys, "argv", ["generate_plots.py", *flags])
with pytest.raises(StyleCaptured):
comparison.main()
assert captured == [{"colors": colors, "anonymize": anonymize}]
@pytest.mark.parametrize("flags,borders", [([], True), (["--marker-borders"], True),
(["--no-marker-borders"], False), (["--no-marker-borders", "--marker-borders"], True)])
def test_marker_border_cli_flags(comparison, monkeypatch, flags, borders):
captured = []
def capture(*args, marker_borders):
captured.append(marker_borders)
for name in ("draw_validation", "draw_metric", "draw_absolute"):
monkeypatch.setattr(comparison, name, lambda *args, **kwargs: None)
for name in ("draw_compact_summary", "draw_pipeline_effect"):
monkeypatch.setattr(comparison, name, capture)
monkeypatch.setattr(sys, "argv", ["generate_plots.py", *flags])
comparison.main()
assert captured == [borders, borders]
@pytest.mark.parametrize("borders", [True, False])
def test_marker_borders_reach_data_legends_and_svg(comparison, tmp_path, monkeypatch, borders):
from xml.etree import ElementTree as ET
from matplotlib.lines import Line2D
from matplotlib.colors import to_rgba
comparison.configure_style()
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
save = comparison.save_figure
def capture(fig, name, output):
try:
size = 6.5 if name == "performance_energy_compact" else 7
for ax in fig.axes:
points = [line for line in ax.lines if line.get_marker() in ("o", "s", "^", "D")]
assert points
for point in points:
assert point.get_markersize() == size
assert (point.get_markeredgewidth() > 0) == borders
assert to_rgba(point.get_markerfacecolor())[3] == 1
if not borders:
assert to_rgba(point.get_markerfacecolor()) == to_rgba(point.get_markeredgecolor())
handles = [handle for handle in fig.legends[0].legend_handles if isinstance(handle, Line2D)]
assert handles
for handle in handles:
assert handle.get_markersize() == size
assert (handle.get_markeredgewidth() > 0) == borders
if not borders:
assert to_rgba(handle.get_markerfacecolor()) not in ((1, 1, 1, 1), (0, 0, 0, 0))
assert to_rgba(handle.get_markerfacecolor()) == to_rgba(handle.get_markeredgecolor())
save(fig, name, output)
finally:
comparison.plt.close(fig)
root = ET.parse(output / f"svg/{name}.svg")
paths = [node for node in root.findall('.//{http://www.w3.org/2000/svg}defs/{http://www.w3.org/2000/svg}path')
if node.get("id", "").startswith("m")]
assert paths
assert all(("stroke:" in node.get("style", "")) == borders for node in paths)
monkeypatch.setattr(comparison, "save_figure", capture)
comparison.draw_compact_summary(rows, tmp_path, marker_borders=borders)
comparison.draw_pipeline_effect(rows, tmp_path, marker_borders=borders)
@pytest.mark.parametrize("colors", [True, False])
@pytest.mark.parametrize("anonymize", [True, False])
def test_plot_display_variant_exports(comparison, tmp_path, colors, anonymize):
import re
from PIL import Image, ImageChops
try:
comparison.configure_style(colors=colors, anonymize=anonymize)
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
comparison.draw_compact_summary(rows, tmp_path)
selected = comparison.select_compact_speedups([row for row in rows if row.model != "yolo11n"])
comparison.draw_metric(selected, "speedup", "Speedup (x)", "speedup_compact_comparison",
tmp_path, compact=True)
for name in ("performance_energy_compact", "speedup_compact_comparison"):
svg = (tmp_path / f"svg/{name}.svg").read_text()
if name == "performance_energy_compact":
assert ("Anonimized better" in svg) == anonymize
assert ("Raptor better" in svg) == (not anonymize)
svg_colors = re.findall(r"(?:fill|stroke):\s*#([0-9a-fA-F]{6})", svg)
assert svg_colors
assert any(color[:2] != color[2:4] or color[2:4] != color[4:] for color in svg_colors) == colors
with Image.open(tmp_path / f"png/{name}.png") as picture:
red, green, blue = picture.convert("RGB").split()
chromatic = (ImageChops.difference(red, green).getbbox() is not None or
ImageChops.difference(green, blue).getbbox() is not None)
assert chromatic == colors
assert (tmp_path / f"pdf/{name}.pdf").read_bytes().startswith(b"%PDF-")
finally:
comparison.configure_style()
def test_anonymization_reaches_all_plot_labels(comparison, monkeypatch):
from matplotlib.text import Text
captured = {}
def capture(fig, name, output):
captured[name] = [text.get_text() for text in fig.findobj(Text)]
comparison.plt.close(fig)
monkeypatch.setattr(comparison, "save_figure", capture)
monkeypatch.setattr(sys, "argv", ["generate_plots.py", "--anonymize"])
try:
comparison.main()
assert len(captured) == 30
assert all("Raptor" not in label for labels in captured.values() for label in labels)
assert "Anonimized pipeline stages" in captured["raptor_pipeline_throughput"]
assert "Anonimized better →" in captured["performance_energy_compact"]
assert "Anonimized\n2 stages" in captured["validation_status"]
finally:
comparison.configure_style()
def test_exact_comparison_configuration_matrix(comparison, comparison_csv, tmp_path):
fields, rows = comparison_csv
results = comparison.load_results(write_csv(tmp_path / "results.csv", fields, rows))
keys = {(row.arch, row.model, row.mode, row.pipeline) for row in results}
assert keys == {(arch, model, mode, stage) for arch in comparison.ARCHITECTURES
for model in comparison.MODELS for mode, stage in comparison.CONFIGURATIONS}
@pytest.mark.parametrize("damage,message", [("missing", "Missing"), ("duplicate", "Duplicate"),
("arch", "Unexpected"), ("model", "Unexpected"),
("pipeline", "Unexpected"), ("mode", "Unexpected")])
def test_comparison_rejects_bad_matrix_keys(comparison, comparison_csv, tmp_path, damage, message):
fields, rows = comparison_csv
if damage == "missing":
rows.pop()
elif damage == "duplicate":
rows[-1] = dict(rows[-2]) # Still 40 rows and 20 per architecture.
else:
field = "raptor_pipeline" if damage == "pipeline" else damage
rows[-1][field] = {"arch": "arch-c", "model": "other", "pipeline": "3", "mode": "other"}[damage]
with pytest.raises(ValueError, match=message):
comparison.load_results(write_csv(tmp_path / "results.csv", fields, rows))
def test_matrix_validation_survives_optimized_python(comparison_csv, tmp_path):
fields, rows = comparison_csv
path = write_csv(tmp_path / "duplicate.csv", fields, [*rows[:-1], rows[-2]])
program = """import sys
from pathlib import Path
sys.path.insert(0, sys.argv[1])
import generate_plots
try:
generate_plots.load_results(Path(sys.argv[2]))
except ValueError as exc:
if 'Duplicate configuration' not in str(exc): raise
else:
raise RuntimeError('Invalid matrix accepted with Python -O')
"""
subprocess.run([sys.executable, "-O", "-c", program, str(TOOLS / "pimcomp/compare"), str(path)], check=True)
@pytest.mark.parametrize("field,value", [("pimcomp_latency_ms", "999"),
("pimcomp_throughput_samples_s", "999"), ("pimcomp_power_mw", "999"),
("pimcomp_energy_pj", "999"), ("pimcomp_functional_validation", "FAIL")])
def test_repeated_pimcomp_batch_results_must_agree(comparison, comparison_csv, tmp_path, field, value):
fields, rows = comparison_csv
row = next(row for row in rows if row["arch"] == "arch-a" and row["model"] == "vgg8"
and row["raptor_pipeline"] == "4")
row[field] = value
with pytest.raises(ValueError, match="inconsistent shared PIMCOMP batch results"):
comparison.load_results(write_csv(tmp_path / "results.csv", fields, rows))
@pytest.mark.parametrize("status,expected", [("PASS", ["1.0"]),
("FAIL", ["1.0", "INVALID"]), ("SKIPPED", ["1.0", "SKIPPED"]),
("NA", ["1.0", "NA"])])
@pytest.mark.parametrize("direction", [1, -1])
def test_value_label_validation_text(comparison, status, expected, direction):
fig, ax = comparison.plt.subplots()
try:
comparison.value_label(ax, 0, 1, "1.0", status, direction)
assert [text.get_text() for text in ax.texts] == expected
assert ax.texts[-1].get_color() == (comparison.ERROR_COLOR if status == "FAIL" else comparison.TEXT_COLOR)
if status != "PASS":
value, validation = ax.texts
assert value.xy == validation.xy
assert value.get_position()[1] == validation.get_position()[1] == 0
assert value.get_position()[0] == -validation.get_position()[0] != 0
finally:
comparison.plt.close(fig)
@pytest.mark.parametrize("values", [(1, 2), (-1, -2), (-1, 2), (0, 0)])
def test_value_label_limits_are_tight_shared_and_unclipped(comparison, values):
fig, axes = comparison.plt.subplots(1, 2, sharey=True, figsize=(10, 4.4), layout="constrained")
try:
for ax, value in zip(axes, values):
ax.set_ylim(-10, 10)
ax.bar(0, value)
direction = -1 if value < 0 else 1
comparison.value_label(ax, 0, value + direction * 0.05, f"{value:+.2f}%", "FAIL", direction)
comparison.fit_value_labels(fig)
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
assert axes[0].get_ylim() == axes[1].get_ylim()
lower, upper = axes[0].get_ylim()
assert upper - lower < 20
assert lower <= min(0, *values) and upper >= max(0, *values)
assert axes[0].get_window_extent(renderer).height == pytest.approx(axes[1].get_window_extent(renderer).height)
for ax in axes:
bounds = ax.get_window_extent(renderer)
for text in ax.texts:
extent = text.get_window_extent(renderer)
assert bounds.y0 <= extent.y0 <= extent.y1 <= bounds.y1
finally:
comparison.plt.close(fig)
@pytest.mark.parametrize("statuses,expected", [
(("PASS", "PASS", "PASS"), "PASS"),
(("FAIL", "PASS", "PASS"), "FAIL"),
(("PASS", "FAIL", "PASS"), "FAIL"),
(("PASS", "PASS", "FAIL"), "FAIL"),
(("PASS", "SKIPPED", "NA"), "SKIPPED"),
(("PASS", "PASS", "NA"), "N/A"),
])
def test_compact_validation_combines_pipeline_statuses(comparison, tmp_path, monkeypatch, statuses, expected):
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
stages = dict(zip((2, 4, 8), statuses))
rows = [replace(row, raptor_validation=stages[row.pipeline])
if row.arch == "arch-a" and row.model == "vgg8" and row.mode == "throughput"
else row for row in rows]
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, name, path: captured.append((fig, name)))
comparison.draw_validation(rows, tmp_path, compact=True)
fig, name = captured[0]
try:
assert name == "validation_status_compact"
assert fig.get_size_inches() == pytest.approx((8.5, 3.3))
for index, ax in enumerate(fig.axes):
assert [label.get_text() for label in ax.get_xticklabels(minor=True)] == [
"Raptor", "PIMCOMP", "Raptor", "PIMCOMP"]
assert [label.get_text() for label in ax.get_yticklabels()] == ([
"VGG-8", "ResNet-18", "ResNet-34", "GoogLeNet", "YOLO11n"] if index == 0 else [])
assert len(ax.patches) == len(ax.texts) == 4 * len(comparison.MODELS)
assert [label.get_text() for label in ax.get_xticklabels()] == ["Latency-mode", "Throughput-mode"]
assert all(text.get_fontsize() == 12 and text.get_weight() == "normal" for text in ax.texts)
assert all(text.get_color() == "#1D2530" for text in ax.texts)
ax = fig.axes[0]
throughput_x = ax.xaxis.get_minorticklocs()[2]
assert [text.get_text() for text in ax.texts if text.get_position() == (throughput_x, 0)] == [expected]
finally:
comparison.plt.close(fig)
@pytest.mark.parametrize("rates,stage", [((300, 200, 100), 2), ((100, 300, 200), 4),
((100, 200, 300), 8), ((math.nan, 200, 300), 8), ((300, 300, 200), 2),
((math.nan, math.nan, math.nan), 2)])
def test_compact_speedups_select_best_pipeline(comparison, rates, stage):
template = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")[0]
rows = [replace(template, latency_speedup=0.5)]
rows += [replace(template, mode="throughput", pipeline=depth, raptor_throughput=rate,
pimcomp_throughput=100, raptor_validation="FAIL")
for depth, rate in zip((2, 4, 8), rates)]
latency, throughput = comparison.select_compact_speedups(rows)
assert latency is rows[0] and latency.speedup == 0.5
assert throughput.pipeline == stage and throughput.raptor_validation == "FAIL"
@pytest.mark.parametrize("diverging", [False, True])
@pytest.mark.parametrize("metric,ylabel", [("speedup", "Speedup (x)"),
("energy", "Energy reduction (x)"), ("power", "Average power reduction (x)")])
def test_compact_metric_bars_and_units(comparison, tmp_path, monkeypatch, diverging, metric, ylabel):
from matplotlib.colors import to_rgba
template = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")[0]
rows = [replace(template, model=model, arch=arch, mode=mode, pipeline=stage,
latency_speedup=0.5 if arch == "arch-a" else 0.25,
energy=(0.5 if arch == "arch-a" else 0.25) if mode == "latency" else
(3 if arch == "arch-a" else 4),
power=(0.5 if arch == "arch-a" else 0.25) if mode == "latency" else
(3 if arch == "arch-a" else 4),
raptor_throughput=300 if arch == "arch-a" else 400,
pimcomp_throughput=100, pimcomp_validation="FAIL")
for arch in reversed(comparison.ARCHITECTURES) for model in ("resnet18", "vgg8")
for mode, stage in (("throughput", 4), ("latency", 1))]
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, name, path: captured.append(fig))
comparison.draw_metric(rows, metric, ylabel, "compact", tmp_path,
diverging=diverging, compact=True)
fig = captured[0]
try:
assert fig.get_size_inches() == pytest.approx((8.5, 4.4))
expected_panels = {
"speedup": ["Latency-mode", "Throughput-mode"],
"energy": ["Latency-mode energy", "Throughput-mode energy"],
"power": ["Latency-mode power", "Throughput-mode power"],
}
assert [text.get_text() for text in fig.legends[0].get_texts()] == ["Arch-A", "Arch-B"]
for index, ax in enumerate(fig.axes):
bars = [container.patches[0] for container in ax.containers]
assert len(bars) == 4
assert ax.get_xlabel() == expected_panels[metric][index]
assert [text.get_text() for text in ax.get_xticklabels()] == ["VGG-8", "ResNet-18"]
assert list(ax.get_xticks()) == [0.5, 3.5]
lower, upper = ax.get_xlim()
assert bars[0].get_x() - lower == pytest.approx(0.4)
assert upper - bars[-1].get_x() - bars[-1].get_width() == pytest.approx(0.4)
for offset in (0, 2):
arch_a, arch_b = bars[offset:offset + 2]
assert arch_a.get_x() + arch_a.get_width() == arch_b.get_x()
for bar, arch in zip((arch_a, arch_b), comparison.ARCHITECTURES):
fill, border, pattern = comparison.ARCH_STYLES[arch]
assert bar.get_facecolor() == to_rgba(fill)
assert bar.get_edgecolor() == to_rgba(border)
assert bar.get_hatch() == pattern
assert arch_a.get_height() == ((-1 if diverging else 0.5) if index == 0 else (2 if diverging else 3))
assert arch_b.get_height() == ((-3 if diverging else 0.25) if index == 0 else (3 if diverging else 4))
assert bars[1].get_x() + bars[1].get_width() < bars[2].get_x()
assert ax.get_ylabel() == (ylabel if index == 0 else "")
assert bool(ax.get_yticklabels()) == (index == 0)
assert all("×" not in label.get_text() for label in ax.get_yticklabels())
values = (["2.00×", "4.00×"] if diverging else ["0.50×", "0.25×"]) if index == 0 else ["3.00×", "4.00×"]
assert [text.get_text() for text in ax.texts] == [value for _ in range(2) for number in values
for value in (number, "P. INVALID")]
finally:
comparison.plt.close(fig)
@pytest.mark.parametrize("minimum,expected", [(0.4, 0.25), (0.5, 0.5), (0.8, 0.5)])
@pytest.mark.parametrize("compact", [False, True])
def test_ratio_bar_minimum_keeps_values_and_labels_visible(comparison, tmp_path, monkeypatch,
minimum, expected, compact):
template = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")[0]
rows = [replace(template, arch=arch, mode=mode, pipeline=stage,
power=minimum if arch == "arch-a" else 2 if mode == "latency" else math.nan)
for arch in comparison.ARCHITECTURES for mode, stage in (("latency", 1), ("throughput", 4))]
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, *args: captured.append(fig))
comparison.draw_metric(rows, "power", "Average power reduction (x)", "power", tmp_path, compact=compact)
fig = captured[0]
try:
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
assert fig.axes[0].get_ylim() == fig.axes[1].get_ylim()
for index, ax in enumerate(fig.axes):
assert ax.get_ylim()[0] == expected
if index == 0:
assert f"{expected:g}" in [text.get_text() for text in ax.get_yticklabels(which="both")]
else:
assert not ax.get_yticklabels(which="both")
bounds = ax.get_window_extent(renderer)
for text in ax.texts:
extent = text.get_window_extent(renderer)
assert bounds.y0 <= extent.y0 <= extent.y1 <= bounds.y1
assert any(text.get_text() == "N/A" for ax in fig.axes for text in ax.texts)
finally:
comparison.plt.close(fig)
def test_compact_summary_uses_lower_minimum_when_needed(comparison, tmp_path, monkeypatch):
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
rows = [replace(row, energy=0.4) if row.model == "vgg8" and row.mode == "latency" else row for row in rows]
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, *args: captured.append(fig))
comparison.draw_compact_summary(rows, tmp_path)
fig = captured[0]
try:
fig.canvas.draw()
assert all(ax.get_xlim()[0] == 0.25 for ax in fig.axes)
assert "0.25" in [text.get_text() for text in fig.axes[0].get_xticklabels()]
finally:
comparison.plt.close(fig)
@pytest.mark.parametrize("missing_energy", [False, True])
def test_compact_metrics_use_speedup_pipeline_selection(comparison, monkeypatch, missing_energy):
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
cases = {2: (100, 9, 8), 4: (300, math.nan if missing_energy else 0.5, 0.6), 8: (200, 7, 6)}
rows = [replace(row, raptor_throughput=cases[row.pipeline][0],
energy=cases[row.pipeline][1], power=cases[row.pipeline][2])
if row.arch == "arch-a" and row.model == "vgg8" and row.mode == "throughput"
else row for row in rows]
captured = []
def capture(selected, field, ylabel, basename, output_dir, diverging=False, compact=False):
if compact:
captured.append((selected, field, basename, diverging))
monkeypatch.setattr(comparison, "load_results", lambda path: rows)
for name in ("configure_style", "draw_validation", "draw_pipeline_effect", "draw_absolute", "draw_compact_summary"):
monkeypatch.setattr(comparison, name, lambda *args, **kwargs: None)
monkeypatch.setattr(comparison, "draw_metric", capture)
monkeypatch.setattr(sys, "argv", ["generate_plots.py"])
comparison.main()
assert {(field, basename, diverging) for _, field, basename, diverging in captured} == {
(metric, f"{metric}_compact_{suffix}", diverging)
for metric in ("speedup", "energy", "power")
for suffix, diverging in (("comparison", False), ("diverging", True))}
assert len(captured) == 6
selected = captured[0][0]
assert all(plotted is selected for plotted, _, _, _ in captured)
throughput = next(row for row in selected if row.arch == "arch-a" and row.model == "vgg8"
and row.mode == "throughput")
assert throughput.pipeline == 4 and throughput.power == 0.6
assert math.isnan(throughput.energy) if missing_energy else throughput.energy == 0.5
def test_compiler_palette_and_grayscale_patterns(comparison, tmp_path, monkeypatch):
from matplotlib.colors import to_rgba
assert comparison.COMPILER_STYLES == {
"raptor": ("#B3D5CA", "#4B7C6A"), "pimcomp": ("#E1BC8B", "#856A44")}
assert comparison.ARCH_STYLES == {
"arch-a": ("#B3CCE3", "#456B90", ""), "arch-b": ("#B3D5CA", "#4B7C6A", "//")}
assert comparison.VALIDATION_STYLES["PASS"] == ("#B3D5CA", "#4B7C6A")
assert comparison.TEXT_COLOR == "#222222"
assert comparison.NETWORK_STYLES["yolo11n"] == ("#FF9494", "#C52020")
assert comparison.NETWORK_STYLES["googlenet"] == ("#D1C0E0", "#77558F")
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
selected = [replace(row, power=2 if row.arch == "arch-a" else 0.5)
for row in rows if row.model == "vgg8" and row.mode == "latency"]
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, *args: captured.append(fig))
comparison.draw_metric(selected, "power", "Average power reduction (x)", "power", tmp_path, diverging=True)
comparison.draw_absolute(selected, "latency", "Single-inference latency (ms)", "latency", tmp_path)
try:
for ax, compiler in zip(captured[0].axes, ("raptor", "pimcomp")):
bar = ax.containers[0].patches[0]
assert bar.get_facecolor() == to_rgba(comparison.COMPILER_STYLES[compiler][0])
assert bar.get_edgecolor() == to_rgba(comparison.COMPILER_STYLES[compiler][1])
assert not bar.get_hatch()
for ax in captured[1].axes:
assert [container.patches[0].get_hatch() for container in ax.containers] == ["", ""]
finally:
for fig in captured:
comparison.plt.close(fig)
@pytest.mark.parametrize("hatch", ["//", "\\\\", ".."])
@pytest.mark.parametrize("width,height", [(20, 130), (160, 15), (20, 1)])
@pytest.mark.parametrize("clipped", [False, True])
def test_hatch_geometry_stays_inside_bar(comparison, monkeypatch, hatch, width, height, clipped):
from matplotlib.backends.backend_agg import RendererAgg
from matplotlib.path import Path as PlotPath
from matplotlib.transforms import Affine2D, Bbox
from plot_style import CenteredHatch
renderer = RendererAgg(400, 300, 100)
gc = renderer.new_gc()
gc.set_hatch(hatch)
gc.set_hatch_color((0, 0, 0, 1))
path = PlotPath.unit_rectangle()
transform = Affine2D().scale(width, height).translate(50, 70)
bar = transform.transform_path(path).get_extents()
if clipped:
gc.set_clip_rectangle(Bbox.from_extents(55, 70.25, 180, 180))
bar = Bbox.intersection(bar, gc.get_clip_rectangle())
drawn = []
def capture(context, geometry, affine, face=None):
drawn.append((context.get_hatch(), affine.transform_path(geometry).get_extents()))
def capture_markers(context, marker, marker_transform, centers, affine, face=None):
for x, y in affine.transform_path(centers).vertices:
capture(context, marker, marker_transform + Affine2D().translate(x, y), face)
monkeypatch.setattr(renderer, "draw_path", capture)
monkeypatch.setattr(renderer, "draw_markers", capture_markers)
CenteredHatch().draw_path(renderer, gc, path, transform, (1, 1, 1, 1))
assert len(drawn) > 1
assert all(pattern is None for pattern, _ in drawn)
for _, bounds in drawn[1:]:
assert bar.x0 - 1e-8 <= bounds.x0 <= bounds.x1 <= bar.x1 + 1e-8
assert bar.y0 - 1e-8 <= bounds.y0 <= bounds.y1 <= bar.y1 + 1e-8
def test_tick_rotation_changes_only_overlapping_labels(comparison):
fig, ax = comparison.plt.subplots(figsize=(4, 3), layout="constrained")
try:
ax.set_xlim(-1, 6)
ax.set_xticks([0, 0.5, 5], ["Single-inference", "Throughput", "8"])
comparison.fit_x_tick_labels(fig)
assert [text.get_rotation() for text in ax.get_xticklabels()] == [330, 330, 0]
assert [text.get_ha() for text in ax.get_xticklabels()] == ["left", "left", "center"]
finally:
comparison.plt.close(fig)
def test_all_plot_terminology(comparison, monkeypatch):
from matplotlib.path import Path as PlotPath
from matplotlib.text import Text
from matplotlib.transforms import Affine2D
plotter = comparison
labels = set()
def capture(fig, name, output):
fig.canvas.draw()
if "compact" in name:
compact_text = "\n".join(text.get_text().lower() for text in fig.findobj(Text))
assert "single-inference" not in compact_text
assert "best-case throughput" not in compact_text
if name == "performance_energy_compact":
labels.update(text.get_text() for text in fig.findobj(Text))
plotter.plt.close(fig)
return
if name != "raptor_pipeline_throughput":
assert fig.axes[1].get_ylabel() == ""
assert fig.axes[1].get_yticklabels() == []
renderer = fig.canvas.get_renderer()
for ax in fig.axes:
for container in ax.containers:
for bar in container.patches:
center = bar.get_x() + bar.get_width() / 2
bounds = bar.get_window_extent(renderer)
for text in ax.texts:
if hasattr(text, "xy") and text.xy[0] == center:
extent = text.get_window_extent(renderer)
assert bounds.x0 <= extent.x0 <= extent.x1 <= bounds.x1, (name, text.get_text())
for legend in fig.legends:
bounds = legend.get_window_extent(renderer)
assert fig.bbox.x0 <= bounds.x0 <= bounds.x1 <= fig.bbox.x1, name
tick_paths = []
for ax in fig.axes:
for minor in (False, True):
for text in ax.get_xticklabels(minor=minor):
if not text.get_visible() or not text.get_text():
continue
angle = text.get_rotation()
assert angle in (0, 330), (name, text.get_text(), angle)
bounds = text.get_window_extent(renderer)
text.set_rotation(0)
unrotated = text.get_window_extent(renderer)
text.set_rotation(angle)
path = PlotPath.unit_rectangle().transformed(
Affine2D().scale(unrotated.width, unrotated.height).rotate_deg(angle))
rotated = path.get_extents()
path = path.transformed(Affine2D().translate(bounds.x0 - rotated.x0, bounds.y0 - rotated.y0))
assert not any(path.intersects_path(other, filled=True) for other in tick_paths), name
tick_paths.append(path)
if text.get_text() in comparison.MODEL_NAMES.values():
assert angle == 0, (name, text.get_text())
heights = [ax.get_window_extent(renderer).height for ax in fig.axes]
assert max(heights) - min(heights) < 1, name
for ax in fig.axes:
bounds = ax.get_window_extent(renderer)
for text in ax.texts:
extent = text.get_window_extent(renderer)
assert bounds.y0 - 1 <= extent.y0 <= extent.y1 <= bounds.y1 + 1, (name, text.get_text())
assert fig.bbox.x0 <= extent.x0 <= extent.x1 <= fig.bbox.x1, (name, text.get_text())
labels.update(text.get_text() for text in fig.findobj(Text))
plotter.plt.close(fig)
monkeypatch.setattr(plotter, "save_figure", capture)
monkeypatch.setattr(sys, "argv", ["generate_plots.py"])
plotter.main()
text = "\n".join(labels)
for old in ("Pimcomp", "ARCHITECTURE", "VGG8", "RESNET", "GOOGLENET", "YOLO11N",
"Single inference", "Best case", "Latency, 1 stage", "Energy per inference advantage",
"Energy-per-inference advantage", "Energy consumption reduction", "power advantage"):
assert old not in text
assert {"VGG-8", "ResNet-18", "ResNet-34", "GoogLeNet"} <= labels
assert "Arch-A" in text and "Arch-B" in text
assert {"Raptor", "PIMCOMP", "YOLO11n", "Single-inference", "2 stages", "4 stages", "8 stages",
"Single-inference latency (ms)", "Throughput (inferences/s)", "Energy per inference (mJ)",
"Energy reduction (x)", "Average power reduction (x)", "Derived as 1 / latency",
"Throughput measurement"} <= labels
@pytest.mark.parametrize("width,one_row", [(12, True), (4.5, False), (3, False)])
def test_legend_uses_available_width_and_balances_rows(comparison, width, one_row):
from collections import Counter
from matplotlib.patches import Patch
fig = comparison.plt.figure(figsize=(width, 4), layout="constrained")
try:
handles = [Patch(label=f"Entry {index}") for index in range(7)]
comparison.add_legend(fig, handles)
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
legend = fig.legends[0]
counts = Counter(round(text.get_window_extent(renderer).y0, 3)
for text in legend.get_texts() if text.get_visible())
assert sum(counts.values()) == len(handles)
assert (len(counts) == 1) == one_row
assert max(counts.values()) - min(counts.values()) <= 1
bounds = legend.get_window_extent(renderer)
assert 0 <= bounds.x0 <= bounds.x1 <= fig.bbox.width
finally:
comparison.plt.close(fig)
@pytest.mark.parametrize("diverging", [False, True])
def test_advantage_throughput_groups_touch(comparison, tmp_path, monkeypatch, diverging):
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
selected = [row for row in rows if row.model in ("vgg8", "resnet18")]
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, *args: captured.append(fig))
comparison.draw_metric(selected, "energy", "Energy reduction (x)", "energy", tmp_path,
diverging=diverging)
fig = captured[0]
try:
for ax in fig.axes:
bars = [container.patches[0] for container in ax.containers]
lower, upper = ax.get_xlim()
assert bars[0].get_x() - lower == pytest.approx(0.4)
assert upper - bars[-1].get_x() - bars[-1].get_width() == pytest.approx(0.4)
for index, (left, right) in enumerate(zip(bars, bars[1:])):
edge = left.get_x() + left.get_width()
if index % 4 in (1, 2):
assert edge == pytest.approx(right.get_x())
else:
assert edge < right.get_x()
finally:
comparison.plt.close(fig)
def test_grouped_absolute_plot_scheduling_labels(comparison, tmp_path, monkeypatch):
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, *args: captured.append(fig))
comparison.draw_absolute(rows, "energy", "Energy per inference (mJ)", "energy", tmp_path, scale=1e-9)
fig = captured[0]
try:
for ax in fig.axes:
assert {text.get_text() for text in ax.get_xticklabels(minor=True)} == {"Latency", "Throughput"}
labels = [text.get_text() for text in fig.legends[0].get_texts()]
assert "1 stage (latency)" in labels and "Single-inference" not in labels
finally:
comparison.plt.close(fig)
@pytest.mark.parametrize("status", ["PASS", "FAIL", "SKIPPED", "NA"])
def test_pipeline_plot_connects_derived_and_measured_rates(comparison, tmp_path, monkeypatch, status):
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
rows = [replace(row, raptor_validation=status, pimcomp_validation=status) for row in rows]
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, *args: captured.append(fig))
comparison.draw_pipeline_effect(rows, tmp_path)
fig = captured[0]
try:
assert fig.get_size_inches() == pytest.approx((13, 4.3))
assert fig._supylabel.get_text() == "Throughput (inferences/s)"
for index, (ax, (arch, model)) in enumerate(zip(fig.axes, ((arch, model) for arch in comparison.ARCHITECTURES
for model in comparison.MODELS))):
selected = [row for row in rows if row.arch == arch and row.model == model]
measured_line, derived_marker, *measured_markers = ax.lines
assert measured_line.get_ydata()[0] == pytest.approx(1000 / selected[0].raptor_latency)
assert list(measured_line.get_ydata()[1:]) == [row.raptor_throughput for row in selected[1:]]
assert derived_marker.get_marker() == "s"
assert derived_marker.get_ydata()[0] == pytest.approx(1000 / selected[0].raptor_latency)
assert derived_marker.get_color() == comparison.NETWORK_STYLES[model][1]
assert all(marker.get_marker() == "o" for marker in measured_markers)
assert measured_line.get_color() == comparison.NETWORK_STYLES[model][1]
assert [label.get_text() for label in ax.get_xticklabels()] == (
["1", "2", "4", "8"] if arch == "arch-b" else [])
assert ax.get_xlabel() == ""
assert ax.get_title() == (comparison.MODEL_NAMES[model] if arch == "arch-a" else "")
assert ax.title.get_weight() == "normal"
if model == comparison.MODELS[-1]:
assert ax.get_ylabel() == comparison.ARCH_NAMES[arch]
assert ax.yaxis.get_label_position() == "right"
assert ax.yaxis.label.get_rotation() == 270
assert ax.yaxis.label.get_weight() == "bold"
else:
assert ax.get_ylabel() == ""
assert all(label.get_rotation() == 0 for label in ax.get_xticklabels())
lower, upper = ax.get_ylim()
values = measured_line.get_ydata()
assert 0 < lower < min(values) <= max(values) < upper
assert ax.get_yticklabels()
assert ax.get_ylim() == fig.axes[index % len(comparison.MODELS)].get_ylim()
assert [text.get_text() for text in fig.legends[0].get_texts()] == [
"Derived as 1 / latency", "Throughput measurement"]
assert len({ax.lines[0].get_color() for ax in fig.axes[:len(comparison.MODELS)]}) == len(comparison.MODELS)
finally:
comparison.plt.close(fig)
def test_plotter_exports_png_svg_and_pdf(comparison, tmp_path):
comparison.configure_style()
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
selected = [replace(row, raptor_validation="FAIL", pimcomp_latency=math.nan)
for row in rows if row.model == "vgg8" and row.mode == "latency"]
comparison.draw_absolute(selected, "latency", "Latency (ms)", "comparison-smoke", tmp_path)
comparison.draw_validation(rows, tmp_path, compact=True)
comparison.draw_compact_summary(rows, tmp_path)
compact_names = []
for metric in ("speedup", "energy", "power"):
for suffix, diverging in (("comparison", False), ("diverging", True)):
name = f"{metric}_compact_{suffix}"
compact_names.append(name)
comparison.draw_metric(comparison.select_compact_speedups(rows), metric, "Advantage (x)",
name, tmp_path, diverging=diverging, compact=True)
for name in ("comparison-smoke", "validation_status_compact", "performance_energy_compact", *compact_names):
assert (tmp_path / f"png/{name}.png").read_bytes().startswith(b"\x89PNG\r\n\x1a\n")
svg = (tmp_path / f"svg/{name}.svg").read_text()
assert "<svg" in svg
assert (tmp_path / f"pdf/{name}.pdf").read_bytes().startswith(b"%PDF-")
assert "N/A" in (tmp_path / "svg/comparison-smoke.svg").read_text()
assert "P. INVALID" in (tmp_path / "svg/comparison-smoke.svg").read_text()
def test_compact_summary_ratios_validity_and_layout(comparison, tmp_path, monkeypatch):
from matplotlib.path import Path as PlotPath
from matplotlib.transforms import Affine2D, Bbox
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
selected = comparison.select_compact_speedups([row for row in rows if row.model != "yolo11n"])
lookup = {(row.model, row.arch, row.mode): row for row in selected}
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, name, path: captured.append((fig, name)))
comparison.draw_compact_summary(rows, tmp_path)
fig, name = captured[0]
try:
assert name == "performance_energy_compact"
assert len(fig.axes) == 4
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
keys = [(model, arch) for model in comparison.MODELS[:-1] for arch in comparison.ARCHITECTURES]
for ax, mode, field in zip(fig.axes, ("latency", "throughput", "latency", "throughput"),
("latency_speedup", "throughput_speedup", "energy", "energy")):
assert ax.get_xscale() == "linear"
low, high = ax.get_xlim()
assert low == 0.5 < 1 < high
points = [line for line in ax.lines if line.get_marker() in ("o", "s", "^", "D")]
point_keys = [(*key, mode) for key in keys]
assert len(points) == len(point_keys)
expected, invalid_flags = [], []
for point, key in zip(points, point_keys):
row = lookup[key]
ratio = getattr(row, field)
expected.append(ratio)
assert list(point.get_xdata()) == pytest.approx([ratio])
assert list(point.get_ydata()) == [0 if row.arch == "arch-a" else 2]
assert point.get_marker() == {"vgg8": "o", "resnet18": "s", "resnet34": "^", "googlenet": "D"}[row.model]
fill, border = comparison.NETWORK_STYLES[row.model]
assert point.get_markerfacecolor() == fill
assert point.get_markeredgecolor() == border
invalid = row.mode == "throughput" or row.pimcomp_validation == "FAIL"
invalid_flags.append(invalid)
labels = [text for text in ax.texts if getattr(text, "xycoords", None) == "data"]
groups = []
for text, key, ratio, invalid in zip(labels, point_keys, expected, invalid_flags):
assert text.get_text() == f"{ratio:.2f}×"
assert text.get_math_fontfamily() == "cm"
assert text.get_color() == comparison.TEXT_COLOR
assert text.get_rotation() == 90
assert text.arrow_patch is None
assert abs(text.get_position()[0]) <= 8
assert (text.get_position()[1] > 0) == (key[1] == "arch-a")
children = [child for child in ax.texts if getattr(child, "xycoords", None) is text]
assert len(children) == 1
for dagger in children:
assert dagger.get_text() == r"$\boldsymbol{\dagger}$"
assert dagger.get_color() == comparison.DAGGER_COLOR == "#AA1C1C"
assert dagger.get_fontsize() == 9
assert dagger.get_fontsize() > text.get_fontsize()
assert dagger.get_alpha() == int(invalid)
groups.append([text, *children])
for lane in (0, 2):
offsets = [text.get_window_extent(renderer).y1 - ax.transData.transform(text.xy)[1]
for text in labels if text.xy[1] == lane]
assert max(offsets) == pytest.approx(min(offsets))
assert len(labels) == 8
assert list(ax.get_yticks()) == [0, 2]
assert [text.get_text() for text in ax.get_yticklabels()] == ["Arch-A", "Arch-B"]
assert ax.get_title() == ""
assert ax.yaxis.label.get_rotation() == 90
assert ax.yaxis.label._get_multialignment() == "center"
assert ax.yaxis.label.get_fontsize() == 8.5
assert len(ax.get_ylabel().splitlines()) <= 2
assert ax.yaxis.label.get_window_extent(renderer).x1 < ax.get_window_extent(renderer).x0
assert bool(ax.get_xticklabels()) == (ax is fig.axes[0])
if ax is fig.axes[0]:
assert "0.5" in [text.get_text() for text in ax.get_xticklabels()]
assert "0" not in [text.get_text() for text in ax.get_xticklabels()]
assert all(tick.label2.get_visible() and not tick.label1.get_visible()
for tick in ax.xaxis.get_major_ticks())
assert not any(text.get_text() in ("Raptor / PIMCOMP", "PIMCOMP / Raptor") for text in ax.texts)
baseline = next(line for line in ax.lines if list(line.get_xdata()) == [1, 1])
assert baseline.get_linestyle() == "--"
bounds = ax.get_window_extent(renderer)
paths = []
for line in ax.lines:
box = line.get_window_extent(renderer).padded(1)
paths.append(PlotPath.unit_rectangle().transformed(
Affine2D().scale(box.width, box.height).translate(box.x0, box.y0)))
for group in groups:
text = group[0]
box = Bbox.union([part.get_window_extent(renderer) for part in group])
assert bounds.x0 <= box.x0 <= box.x1 <= bounds.x1
assert bounds.y0 <= box.y0 <= box.y1 <= bounds.y1
path = PlotPath.unit_rectangle().transformed(
Affine2D().scale(box.width, box.height).translate(box.x0, box.y0))
assert not any(path.intersects_path(other, filled=True) for other in paths), text.get_text()
paths.append(path)
assert "YOLO11n" not in [text.get_text() for text in ax.get_yticklabels()]
assert {text.get_position()[1] > 0 for ax in fig.axes for text in ax.texts
if getattr(text, "xycoords", None) == "data"} == {False, True}
assert all(ax.get_xlim() == fig.axes[0].get_xlim() for ax in fig.axes)
assert [ax.get_ylabel() for ax in fig.axes] == [
"Latency-mode\nspeedup", "Throughput-mode\nspeedup",
"Latency-mode\nenergy reduction", "Throughput-mode\nenergy reduction"]
assert fig.axes[0].xaxis.get_label_position() == "top"
xlabel = fig.axes[0].xaxis.label.get_window_extent(renderer)
legend = fig.legends[0].get_window_extent(renderer)
assert fig.axes[0].get_window_extent(renderer).y1 < xlabel.y0 < xlabel.y1 < legend.y0
tick_top = max(text.get_window_extent(renderer).y1 for text in fig.axes[0].get_xticklabels())
assert 0 < xlabel.y0 - tick_top < renderer.points_to_pixels(12)
axis_bounds = fig.axes[0].get_window_extent(renderer)
assert (xlabel.x0 + xlabel.x1) / 2 == pytest.approx((axis_bounds.x0 + axis_bounds.x1) / 2)
assert fig.axes[0].get_xlabel() == "Ratio (x)"
assert {"← PIMCOMP better", "Raptor better →"} <= {text.get_text() for text in fig.axes[0].texts}
left, right = [text.get_window_extent(renderer) for text in fig.axes[0].texts if not hasattr(text, "xy")]
assert left.y0 == right.y0 == pytest.approx(xlabel.y0)
assert left.x1 < xlabel.x0 < xlabel.x1 < right.x0
legend_labels = {text.get_text() for text in fig.legends[0].get_texts() if text.get_visible()}
assert legend_labels == {"VGG-8", "ResNet-18", "ResNet-34", "GoogLeNet", "PIMCOMP invalid"}
for handle, text in zip(fig.legends[0].legend_handles, fig.legends[0].get_texts()):
if text.get_text() in comparison.MODEL_NAMES.values():
model = next(model for model, name in comparison.MODEL_NAMES.items() if name == text.get_text())
assert handle.get_marker() == {"vgg8": "o", "resnet18": "s", "resnet34": "^", "googlenet": "D"}[model]
assert handle.get_markerfacecolor() == comparison.NETWORK_STYLES[model][0]
elif text.get_text() == "PIMCOMP invalid":
assert handle.get_text() == r"$\boldsymbol{\dagger}$"
assert handle.get_fontsize() == 9
assert handle.get_math_fontfamily() == "cm"
assert handle.get_color() == comparison.DAGGER_COLOR
result_dagger = next(child for child in fig.axes[0].texts
if getattr(getattr(child, "xycoords", None), "xycoords", None) == "data"
and child.get_alpha() == 1)
result_box = result_dagger.get_window_extent(renderer)
legend_box = handle.get_window_extent(renderer)
assert legend_box.width == pytest.approx(result_box.height)
assert legend_box.height == pytest.approx(result_box.width)
ratios = [line.get_xdata()[0] for ax in fig.axes for line in ax.lines
if line.get_marker() in ("o", "s", "^", "D")]
lower, upper = fig.axes[0].get_xlim()
padding = (max(ratios) - min(ratios)) * 0.04
assert lower == 0.5
assert upper - max(ratios) == pytest.approx(padding)
finally:
comparison.plt.close(fig)
def test_compact_summary_rejects_missing_ratio(comparison, tmp_path):
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
rows = [replace(row, energy=math.nan) if row.mode == "throughput" and row.model == "vgg8"
else row for row in rows]
with pytest.raises(ValueError, match="requires a positive energy ratio"):
comparison.draw_compact_summary(rows, tmp_path)
@pytest.mark.parametrize("compiler,status,label", [("pimcomp", "FAIL", r"$\boldsymbol{\dagger}$"), ("raptor", "FAIL", "R. INVALID"),
("pimcomp", "SKIPPED", "NOT VALIDATED"), ("pimcomp", "NA", "NOT VALIDATED")])
def test_compact_summary_validation_labels(comparison, tmp_path, monkeypatch, compiler, status, label):
rows = comparison.load_results(TOOLS / "pimcomp/compare/results_comparison.csv")
rows = [replace(row, **{f"{compiler}_validation": status})
if row.model == "vgg8" and row.arch == "arch-a" and row.mode == "latency" else row for row in rows]
captured = []
monkeypatch.setattr(comparison, "save_figure", lambda fig, *args: captured.append(fig))
comparison.draw_compact_summary(rows, tmp_path)
fig = captured[0]
try:
ax = fig.axes[0]
if compiler == "pimcomp" and status == "FAIL":
assert any(label in text.get_text() and text.get_color() == comparison.DAGGER_COLOR for text in ax.texts)
else:
assert label in ax.texts[0].get_text()
assert ax.texts[0].get_color() == comparison.TEXT_COLOR
finally:
comparison.plt.close(fig)