Source code for ppbcc.plot.time_barplot

"""Bar chart of benchmark runtimes per paradigm and platform."""

from __future__ import annotations

import math

import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.ticker import NullFormatter

from ppbcc.constants import APPLICATION, HARDWARE
from ppbcc.plot.styles import (
    _application_legend_handles,
    _display_application,
    _framework_colors,
    format_relative_log_axis,
)

#: Column of the plotted value: nanoseconds, or FLOP when normalized.
RUNTIME_NS = "Runtime (ns)"
PEAK_FLOP = "Runtime x Peak Performance (FLOP)"
#: Display units for runtimes, largest first, with their size in nanoseconds.
TIME_UNITS = (("s", 1e9), ("ms", 1e6), (r"$\mu$s", 1e3), ("ns", 1.0))
#: Share of one platform group's width filled with bars.
GROUP_FILL = 0.84
#: Below this many decades a runtime axis gets plain decimal ticks, since
#: Matplotlib would otherwise label it with cluttered minor ticks.
DECIMAL_TICK_DECADES = 1.5


def _time_unit(values_ns: pd.Series) -> tuple[str, float]:
    """Pick the largest unit in which the median runtime is at least one.

    Args:
        values_ns: Plotted runtimes in nanoseconds.

    Returns:
        The unit label and its size in nanoseconds.
    """
    median = float(values_ns.median())
    for unit, factor in TIME_UNITS:
        if median >= factor:
            return unit, factor
    return TIME_UNITS[-1]


[docs] def plot_time_barplot( runtimes: pd.DataFrame, problem_title: str, time_label: str, selected_size: float, precision: int, normalized: bool = False, remove_description: bool = False, show_legends: bool = True, ) -> plt.Figure: """Plot one runtime bar per paradigm, grouped by platform. With a single platform the bars stand side by side and are labeled with their paradigm. With several platforms every platform is one labeled group holding a slot per paradigm, in the same order in every group, so a paradigm keeps its position; a slot stays empty where the paradigm has no result on that platform. The runtime axis is logarithmic, because runtimes of one problem differ by orders of magnitude between paradigms. Args: runtimes: One row per application and hardware with ``Runtime (ns)``, and ``Runtime x Peak Performance (FLOP)`` when ``normalized``. problem_title: Benchmark problem shown in the title. time_label: Name of the plotted runtime, e.g. ``Kernel Time``. selected_size: Problem size the runtimes were measured at. precision: Floating-point precision in bits. normalized: Whether the runtime is multiplied by the platform's peak performance, i.e. plotted as the FLOPs the platform could have executed in that time. remove_description: Whether bracketed descriptions are hidden. show_legends: Whether the paradigm legend is drawn into the figure. Returns: The Matplotlib figure. Raises: ValueError: If there is nothing to plot. """ if runtimes.empty: raise ValueError("No runtimes available for the time bar plot.") applications = sorted( runtimes[APPLICATION].astype(str).unique(), key=lambda application: (application.casefold(), application), ) platforms = sorted(runtimes[HARDWARE].astype(str).unique()) colors = _framework_colors(applications, remove_description) lookup = runtimes.set_index([APPLICATION, HARDWARE]) if normalized: column, factor = PEAK_FLOP, 1.0 y_label = f"{time_label} × Peak Performance [FLOP]" else: unit, factor = _time_unit(runtimes[RUNTIME_NS]) column = RUNTIME_NS y_label = f"{time_label} [{unit}]" single = len(platforms) == 1 if single: width = max(8.0, 2.5 + 0.62 * len(applications)) else: width = max(9.0, 2.5 + len(platforms) * (0.16 * len(applications) + 0.35)) figure, axis = plt.subplots(figsize=(width, 6.0)) bar_width = 0.7 if single else GROUP_FILL / len(applications) plotted: list[float] = [] for group, platform in enumerate(platforms): for slot, application in enumerate(applications): if (application, platform) not in lookup.index: continue value = float(lookup.loc[(application, platform), column]) / factor plotted.append(value) if single: position = slot else: position = group + (slot - (len(applications) - 1) / 2) * bar_width axis.bar( position, value, width=bar_width, color=colors[application], edgecolor="black", linewidth=0.5, zorder=3, ) # Bars on a log axis have no natural base, so the axis starts a little # below the shortest bar to keep every bar visible as a bar. low, high = min(plotted), max(plotted) axis.set_yscale("log") axis.set_ylim(low / 1.6, high * 1.6) if not normalized and math.log10(high / low) < DECIMAL_TICK_DECADES: format_relative_log_axis(axis.yaxis) else: axis.yaxis.set_minor_formatter(NullFormatter()) axis.set_ylabel(y_label) axis.grid(True, axis="y", which="major", linestyle="--", alpha=0.45, zorder=0) if single: axis.set_xticks(range(len(applications))) axis.set_xticklabels( [ _display_application(application, remove_description) for application in applications ], rotation=35, ha="right", ) axis.set_xlim(-0.6, len(applications) - 0.4) axis.set_xlabel("Paradigm") else: axis.set_xticks(range(len(platforms))) axis.set_xticklabels(platforms) axis.set_xlim(-0.5, len(platforms) - 0.5) for boundary in range(1, len(platforms)): axis.axvline(boundary - 0.5, color="0.75", linewidth=0.8, zorder=1) axis.set_xlabel("Platform") axis.grid(False, axis="x") size_text = ( f"{selected_size:,.0f}" if float(selected_size).is_integer() else f"{selected_size:g}" ) subtitle = f"Problem size: {size_text}, FP{precision}" if single: subtitle += f", {platforms[0]}" axis.set_title(f"{problem_title} {time_label}\n{subtitle}") if show_legends: axis.legend( handles=_application_legend_handles( applications, colors, remove_description ), title="Paradigm", loc="center left", bbox_to_anchor=(1.01, 0.5), frameon=True, ) figure.tight_layout() return figure