update plots
Validate Operations / validate-operations (push) Has been cancelled

This commit is contained in:
NiccoloN
2026-10-26 12:18:36 +01:00
parent 4b53f6cf10
commit 49d6fb42cf
8 changed files with 918 additions and 370 deletions
+124 -22
View File
@@ -1,4 +1,4 @@
"""Shared appearance and PNG/SVG export for PIM result plots."""
"""Shared appearance and PNG/SVG/PDF export for PIM result plots."""
from pathlib import Path
from math import ceil
@@ -12,9 +12,32 @@ from matplotlib.patheffects import AbstractPathEffect
from matplotlib.transforms import Affine2D, IdentityTransform, TransformedPath
TEXT_COLOR = "#243449"
HATCH_COLOR = "#C62828"
COLORS = ("#2B65A1", "#168C8B", "#D18D32", "#8062A5", "#B65D60")
PALETTE = {
"sage": ("#B3D5CA", "#4B7C6A"),
"sand": ("#E1BC8B", "#856A44"),
"pale_blue": ("#EDF3FA", "#456B90"),
"blue": ("#B3CCE3", "#456B90"),
"pale_green": ("#EAF5F1", "#3E8271"),
"pale_sand": ("#FCF1E5", "#A87940"),
"red": ("#FF9494", "#C52020"),
"purple": ("#D1C0E0", "#77558F"),
}
TEXT_COLOR = "#222222"
MATRIX_TEXT_COLOR = "#1D2530"
ERROR_COLOR = PALETTE["red"][1]
GRID_COLOR = "#E6E9EC"
LINE_COLOR = "#C9CED3"
BAND_COLOR = "#F7F9FA"
COMPILER_STYLES = {"raptor": PALETTE["sage"], "pimcomp": PALETTE["sand"]}
METRIC_STYLES = {"latency": PALETTE["blue"], "throughput": PALETTE["sage"]}
NETWORK_STYLES = {"vgg8": PALETTE["blue"], "resnet18": PALETTE["sage"],
"resnet34": PALETTE["sand"], "googlenet": PALETTE["purple"],
"yolo11n": PALETTE["red"]}
COMPILER_NAMES = {"raptor": "Raptor", "pimcomp": "PIMCOMP"}
ARCH_NAMES = {"arch-a": "Arch-A", "arch-b": "Arch-B"}
MODEL_NAMES = {"vgg8": "VGG-8", "resnet18": "ResNet-18", "resnet34": "ResNet-34",
"googlenet": "GoogLeNet", "yolo11n": "YOLO11n"}
MODE_NAMES = {"latency": "Single-inference", "throughput": "Throughput"}
class CenteredHatch(AbstractPathEffect):
@@ -77,51 +100,130 @@ def configure_style() -> None:
"text.color": TEXT_COLOR,
"xtick.color": TEXT_COLOR,
"ytick.color": TEXT_COLOR,
"axes.edgecolor": "#CFD6DF",
"axes.edgecolor": LINE_COLOR,
"grid.color": GRID_COLOR,
"grid.linewidth": 0.8,
"hatch.color": TEXT_COLOR,
"savefig.facecolor": "white",
"figure.facecolor": "white",
})
def add_legend(fig, handles) -> None:
"""Use the fewest balanced legend rows that fit the figure width."""
renderer = fig.canvas.get_renderer()
for rows in range(1, len(handles) + 1):
columns = ceil(len(handles) / rows)
grid = []
start = 0
for row in range(rows):
count = len(handles) // rows + (row < len(handles) % rows)
grid.append(handles[start:start + count] + [None] * (columns - count))
start += count
ordered = [handle for column in zip(*grid) for handle in column]
legend = fig.legend(handles=[handle if handle is not None else Patch(visible=False) for handle in ordered],
labels=[handle.get_label() if handle is not None else "" for handle in ordered],
loc="outside upper center", ncols=columns, frameon=False, fontsize=9.5,
handlelength=1.7, columnspacing=1.5, handletextpad=0.6, labelspacing=0.4)
for handle, patch, text in zip(ordered, legend.legend_handles, legend.get_texts()):
if handle is None:
patch.set_visible(False)
text.set_visible(False)
if legend.get_window_extent(renderer).width <= fig.bbox.width - 20:
return
legend.remove()
def value_label(ax, x, y, label: str, status: str = "PASS", direction: int = 1,
fontsize: float = 8) -> None:
fontsize: float = 8, invalid_label: str = "INVALID") -> None:
alignment = "bottom" if direction > 0 else "top"
rotation = 90 if direction > 0 else -90
text = ax.text(x, y, label, rotation=rotation, ha="center", va=alignment,
fontsize=fontsize, color=TEXT_COLOR)
offset = direction * fontsize * 0.65 if status != "PASS" else 0
ax.annotate(label, (x, y), xytext=(-offset, 0), textcoords="offset points",
rotation=rotation, ha="center", va=alignment, fontsize=fontsize,
color=TEXT_COLOR)
if status != "PASS":
height = text.get_window_extent(ax.figure.canvas.get_renderer()).height
offset = direction * (height * 72 / ax.figure.dpi + 3)
ax.annotate("INVALID" if status == "FAIL" else status, (x, y),
xytext=(0, offset), textcoords="offset points",
ax.annotate(invalid_label if status == "FAIL" else status, (x, y),
xytext=(offset, 0), textcoords="offset points",
rotation=rotation, ha="center", va=alignment, fontsize=fontsize,
color=HATCH_COLOR)
color=ERROR_COLOR if status == "FAIL" else TEXT_COLOR)
def fit_x_tick_labels(fig) -> None:
"""Rotate only overlapping labels, clockwise by 30 degrees."""
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
for minor in (False, True):
labels = [(text, text.get_window_extent(renderer))
for ax in fig.axes for text in ax.get_xticklabels(minor=minor)
if text.get_visible() and text.get_text()]
crowded = set()
for index, (text, bounds) in enumerate(labels):
for other_text, other_bounds in labels[:index]:
if bounds.overlaps(other_bounds):
crowded.update((text, other_text))
for text in crowded:
text.set_rotation(-30)
text.set_rotation_mode("anchor")
text.set_ha("right" if text.get_va() == "bottom" else "left")
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
for ax in fig.axes:
bottom_minor = [text for text in ax.get_xticklabels(minor=True)
if text.get_va() == "top" and text.get_visible() and text.get_text()]
if bottom_minor:
height = max(text.get_window_extent(renderer).height for text in bottom_minor)
ax.tick_params(axis="x", which="major", pad=height * 72 / fig.dpi + 6)
fig.canvas.draw()
def fit_bar_widths(fig) -> None:
"""Reserve enough horizontal space for each bar's rendered annotations."""
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
scale = 1.0
for ax in fig.axes:
labels = {}
for text in ax.texts:
if hasattr(text, "xy"):
labels.setdefault(text.xy[0], []).append(text.get_window_extent(renderer))
for container in ax.containers:
for bar in container.patches:
bounds = labels.get(bar.get_x() + bar.get_width() / 2, [])
if bounds:
width = max(box.x1 for box in bounds) - min(box.x0 for box in bounds)
scale = max(scale, (width + renderer.points_to_pixels(2)) / bar.get_window_extent(renderer).width)
if scale > 1:
fig.set_size_inches(fig.get_figwidth() * scale, fig.get_figheight())
def fit_value_labels(fig) -> None:
"""Reserve vertical space for rotated labels using their rendered size."""
"""Fit common vertical limits to the data and rendered value labels."""
for ax in fig.axes:
for text in ax.texts:
text.set_in_layout(False)
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
limits = []
labels = []
for ax in fig.axes:
bounds = ax.get_window_extent(renderer)
labels = []
for text in ax.texts:
x, y = text.xy if hasattr(text, "xy") else text.get_position()
anchor = ax.transData.transform((x, y))[1]
extent = text.get_window_extent(renderer)
labels.append((y, max(0, anchor - extent.y0 + 3) / bounds.height,
max(0, extent.y1 - anchor + 3) / bounds.height))
lower, upper = ax.get_ylim()
for _ in range(12):
lower = min([lower, *((y - below * upper) / (1 - below) for y, below, _ in labels)])
upper = max([upper, *((y - above * lower) / (1 - above) for y, _, above in labels)])
limits.append((lower, upper))
limits.append(ax.dataLim.intervaly if ax.has_data() else ax.get_ylim())
lower = min(0, *(lower for lower, _ in limits))
upper = max(0, *(upper for _, upper in limits))
if lower == upper:
upper = lower + 1
for _ in range(12):
lower = min([lower, *((y - below * upper) / (1 - below) for y, below, _ in labels)])
upper = max([upper, *((y - above * lower) / (1 - above) for y, _, above in labels)])
for ax in fig.axes:
ax.set_ylim(min(lower for lower, _ in limits), max(upper for _, upper in limits))
ax.set_ylim(lower, upper)
def save_figure(fig, basename: str, output_dir: Path) -> None:
@@ -129,7 +231,7 @@ def save_figure(fig, basename: str, output_dir: Path) -> None:
hatch = patch.get_hatch() or ""
if any(symbol in hatch for symbol in "./\\"):
patch.set_path_effects([CenteredHatch()])
for extension in ("png", "svg"):
for extension in ("png", "svg", "pdf"):
target = output_dir / extension / f"{basename}.{extension}"
target.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(target, dpi=220, bbox_inches="tight", pad_inches=0.04)