Source code for ppbcc.performance_portability.cli

"""Command-line orchestration for P2 and P3 analysis."""

from __future__ import annotations

import argparse
import sys

import pandas as pd
import seaborn as sns
from loguru import logger

from ppbcc.constants import (
    APPLICATION,
    APPLICATION_EFFICIENCY,
    BENCHMARK_PROBLEM,
    DESCRIPTION,
    HARDWARE,
    PARADIGM,
    PERFORMANCE_PORTABILITY,
    PRECISION,
    PROBLEM,
    PROBLEM_SIZE,
    TIME_COLUMNS,
)
from ppbcc.hardware import peak_performance
from ppbcc.performance_portability.complexity import (
    add_complexity_to_export,
    append_cpp_complexity_row,
    export_metrics_to_csv,
    load_complexity_baseline,
    load_complexity_data,
    load_complexity_export_data,
    merge_portability_complexity,
)
from ppbcc.performance_portability.correlation import (
    CORRELATION_PP,
    build_value_table,
    complexity_by_paradigm,
    portability_by_paradigm,
    rank_correlation_matrix,
    rank_table,
    resolve_correlation_variable,
    shared_paradigm_counts,
    write_rank_correlation_csv,
)
from ppbcc.performance_portability.metrics import (
    calculate_average_size_metrics,
    calculate_export_metrics,
    calculate_extreme_size_metrics,
    calculate_metrics,
    calculate_metrics_by_size,
    calculate_runtimes,
    calculate_scaling_metrics,
)
from ppbcc.performance_portability.options import (
    P3_CHARTS,
    RANK_CORRELATION,
    build_p2_parser,
    build_p3_parser,
)
from ppbcc.performance_portability.selection import (
    ALL_SIZE,
    AVERAGE_OVER_PP,
    AVERAGE_SIZE,
    BEST_SIZE,
    WORST_SIZE,
    _application_label,
    _filter_problem_size,
    configure_logging,
    load_benchmark_csvs,
    select_problem_rows,
)
from ppbcc.plot.cascade import plot_cascade
from ppbcc.plot.complexity_comparison import plot_complexity_comparison
from ppbcc.plot.heatmap import (
    plot_efficiency_boxplot,
    plot_efficiency_double_heatmap,
    plot_efficiency_heatmap,
)
from ppbcc.plot.navchart import plot_navchart
from ppbcc.plot.time_barplot import PEAK_FLOP, RUNTIME_NS, plot_time_barplot
from ppbcc.plot.styles import (
    create_separate_legend,
    resolve_output_path,
    save_figure,
)


[docs] def p2analysis_main(argv: list[str] | None = None) -> int: """Run the P2 analysis command-line program (benchmark results only). Args: argv: Command-line arguments, or ``None`` to read ``sys.argv``. Returns: Process exit status: zero on success, one on failure. """ args = build_p2_parser().parse_args(argv) configure_logging(args.verbose) if args.hardware is not None and args.chart not in {"boxplot", "time-barplot"}: logger.error("--hardware is only valid for boxplot and time-barplot charts.") return 1 if args.chart != "time-barplot" and ( args.time is not None or args.normalize_time_to_peak ): logger.error( "--time and --normalize-time-to-peak are only valid for time-barplot." ) return 1 if args.chart == "double-heatmap": if not isinstance(args.size, float) or args.second_size is None: logger.error( "double-heatmap needs two exact numeric sizes: -s/--size for " "the upper-left and --second-size for the lower-right triangles." ) return 1 elif args.second_size is not None: logger.error("--second-size is only valid for double-heatmap.") return 1 if args.sort_alphabetically and args.chart not in {"heatmap", "double-heatmap"}: logger.error( "--sort-alphabetically is only valid for heatmap and double-heatmap." ) return 1 return _analyze(args)
def _double_heatmap_efficiencies( rows: pd.DataFrame, description_is_workload: bool, sizes: tuple[float, float], problem: str, ) -> tuple[pd.DataFrame, pd.DataFrame]: """Calculate application efficiency at the two sizes of a double-heatmap. Both sizes share one application and platform universe, so a paradigm benchmarked at only one of them still gets a (missing) cell at the other. Args: rows: Selected benchmark rows of every size. description_is_workload: Whether Description belongs to the workload key. sizes: Problem sizes of the upper-left and lower-right triangles. problem: Resolved problem name, used in error messages. Returns: Efficiency rows at the first and at the second size. Raises: ValueError: If a size has no rows. """ applications = sorted( { _application_label(paradigm, description, description_is_workload) for paradigm, description in zip(rows[PARADIGM], rows[DESCRIPTION]) } ) platforms = sorted(rows[HARDWARE].unique()) efficiencies = [] for size in sizes: efficiency, _ = calculate_metrics( _filter_problem_size(rows, size, problem), description_is_workload, hardware_universe=platforms, application_universe=applications, ) efficiency.insert(0, PROBLEM, problem) efficiencies.append(efficiency) return efficiencies[0], efficiencies[1] def _filter_hardware( rows: pd.DataFrame, hardware: str | None, problem: str ) -> pd.DataFrame: """Keep only the rows of one hardware label, if one is requested. Args: rows: Selected benchmark rows. hardware: Requested ``Hardware`` label, or ``None`` to keep all rows. problem: Resolved problem name, used in the error message. Returns: The matching rows. Raises: ValueError: If no row matches the requested hardware. """ if hardware is None: return rows hardware_labels = rows[HARDWARE].astype(str) hardware_mask = hardware_labels.eq(hardware) if not hardware_mask.any(): available_hardware = sorted(hardware_labels.unique(), key=str.casefold) raise ValueError( f"Hardware {hardware!r} has no selected rows for {problem}. " f"Available hardware: {available_hardware}" ) return rows.loc[hardware_mask].copy() def _time_barplot(args: argparse.Namespace, benchmark_data: pd.DataFrame) -> None: """Render the runtime bar chart, its separate legend and its CSV export. Args: args: Parsed ``p2analysis`` arguments. benchmark_data: Combined benchmark results. Raises: ValueError: If the selection leaves no plottable runtime. """ if isinstance(args.size, str) and args.size != ALL_SIZE: raise ValueError( "time-barplot requires an exact numeric --size, or 'all' for the " f"largest one; {args.size!r} would mix runtimes of different sizes." ) time_key = args.time or "wall-clock" time_column = TIME_COLUMNS[time_key] problem_query = _resolve_problem_query(benchmark_data, args.name) rows, problem, description_is_workload = select_problem_rows( benchmark_data, problem_query, args.description_include, args.description_exclude, args.size if isinstance(args.size, float) else None, args.precision, ) rows = _filter_hardware(rows, args.hardware, problem) size = float(pd.to_numeric(rows[PROBLEM_SIZE], errors="coerce").max()) rows = _filter_problem_size(rows, size, problem) if time_column not in rows.columns or rows[time_column].isna().all(): available = [ key for key, column in TIME_COLUMNS.items() if column in rows.columns and rows[column].notna().any() ] raise ValueError( f"{problem} has no {time_column!r} results. Available --time " f"choices: {available}" ) precisions = sorted(pd.to_numeric(rows[PRECISION], errors="coerce").unique()) if len(precisions) != 1: raise ValueError( f"{problem} has results in several precisions {precisions}; " "select one with -p/--precision." ) precision = int(precisions[0]) runtimes = calculate_runtimes( rows, description_is_workload, time_column, remove_description=args.remove_description, ) if args.normalize_time_to_peak: runtimes[PEAK_FLOP] = [ runtime_ns * 1e-9 * peak_performance(str(hardware), int(row_precision)) for runtime_ns, hardware, row_precision in zip( runtimes[RUNTIME_NS], runtimes[HARDWARE], runtimes[PRECISION] ) ] output = resolve_output_path(args.output, [problem], f"time-barplot-{time_key}") figure = plot_time_barplot( runtimes, problem, time_column, size, precision, normalized=args.normalize_time_to_peak, remove_description=args.remove_description, show_legends=not args.legend, ) save_figure(figure, output) if args.export_to_csv: export = runtimes.copy() export.insert(0, PROBLEM, problem) export.insert(3, PROBLEM_SIZE, size) csv_output = output.with_suffix(".csv") export.to_csv(csv_output, index=False) logger.success(f"Wrote runtimes: {csv_output.resolve()}") if args.legend: legend_figure = create_separate_legend( list(runtimes[APPLICATION].astype(str).unique()), [problem], [], remove_description=args.remove_description, vertical=args.legend_vertical, ) save_figure(legend_figure, output.with_name(f"{output.stem}_legend.pdf"))
[docs] def p3analysis_main(argv: list[str] | None = None) -> int: """Run the P3 analysis command-line program (benchmarks and complexity). Args: argv: Command-line arguments, or ``None`` to read ``sys.argv``. Returns: Process exit status: zero on success, one on failure. """ args = build_p3_parser().parse_args(argv) configure_logging(args.verbose) if args.chart == "complexity-comparison" and args.complexity_absolute: logger.error( "complexity-comparison compares two metrics on one shared scale " "relative to CPP, which --complexity-metric-absolute removes." ) return 1 if args.chart != RANK_CORRELATION and args.correlation != "pp": logger.error(f"--correlation is only valid for {RANK_CORRELATION}.") return 1 if args.chart == RANK_CORRELATION: if args.legend or args.legend_vertical: logger.error( f"{RANK_CORRELATION} writes a CSV table and draws no figure, " "so it has no legend to separate." ) return 1 if args.remove_description: logger.error( f"{RANK_CORRELATION} always ranks paradigms, combining " "implementation variants; --remove-description is implied." ) return 1 if args.log_complexity or args.complexity_absolute: logger.error( "--log-complexity and --complexity-metric-absolute scale " f"plot axes, which {RANK_CORRELATION} does not have; ranks " "are unaffected by either." ) return 1 return _analyze(args)
def _problem_metrics( args: argparse.Namespace, selected: pd.DataFrame, description_is_workload: bool, ) -> tuple[pd.DataFrame, pd.DataFrame]: """Calculate efficiency and PP under the selected ``--size`` mode. Args: args: Parsed arguments supplying ``size``, ``average_over``, and ``non_zero_pp``. selected: Benchmark rows of one problem. description_is_workload: Whether Description belongs to the workload key. Returns: A pair of application-efficiency and performance-portability rows. """ if args.size == AVERAGE_SIZE: return calculate_average_size_metrics( selected, description_is_workload, non_zero_pp=args.non_zero_pp, average_over=args.average_over, ) if args.size in {BEST_SIZE, WORST_SIZE}: return calculate_extreme_size_metrics( selected, description_is_workload, args.size, non_zero_pp=args.non_zero_pp, ) return calculate_metrics( selected, description_is_workload, non_zero_pp=args.non_zero_pp, ) def _rank_correlation(args: argparse.Namespace, benchmark_data: pd.DataFrame) -> None: """Write the cross-problem rank correlations of one variable as CSV. Args: args: Parsed ``p3analysis`` arguments. benchmark_data: Combined benchmark results. Raises: ValueError: If the selection cannot produce a correlation table. """ variable = resolve_correlation_variable(args.correlation) values: dict[str, pd.Series] = {} value_column = PERFORMANCE_PORTABILITY if variable == CORRELATION_PP else variable for problem_query in _resolve_problem_queries(benchmark_data, args.name): rows, problem, description_is_workload = select_problem_rows( benchmark_data, problem_query, args.description_include, args.description_exclude, args.size if isinstance(args.size, float) else None, args.precision, ) if problem in values: raise ValueError( f"-n/--name selects the problem {problem!r} more than once." ) paradigms = sorted(set(rows[PARADIGM].astype(str))) if variable == CORRELATION_PP: _, portability = _problem_metrics(args, rows, description_is_workload) values[problem] = portability_by_paradigm(portability, paradigms) else: values[problem], value_column = complexity_by_paradigm( args.complexity, problem_query, args.correlation, paradigms ) table = build_value_table(values) logger.debug(f"{value_column} by paradigm:\n{table.to_string()}") logger.info( f"Rank-correlating {value_column} over " f"{int(table.notna().any(axis=1).sum())} paradigm(s) and " f"{len(table.columns)} problem(s)" ) matrix = rank_correlation_matrix(table) logger.debug( "Shared paradigms per pair:\n" f"{shared_paradigm_counts(table).to_string()}" ) output = resolve_output_path( args.output, list(table.columns), RANK_CORRELATION ).with_suffix(".csv") write_rank_correlation_csv(matrix, output) if args.export_to_csv: ranks = rank_table(table, variable, value_column) ranks_output = output.with_name(f"{output.stem}_ranks.csv") ranks.to_csv(ranks_output, index=False) logger.success(f"Wrote paradigm ranks: {ranks_output.resolve()}") def _resolve_problem_queries( benchmark_data: pd.DataFrame, name: str | None ) -> list[str]: """Split ``-n/--name`` into the problem queries of a cross-problem table. Args: benchmark_data: Combined benchmark results. name: Comma-separated ``-n/--name`` value, or ``None`` for every problem present in the data. Returns: At least two problem queries. Raises: ValueError: If fewer than two problems are selected. """ if name is None: available = benchmark_data[BENCHMARK_PROBLEM].dropna().unique() problems = sorted({str(value) for value in available}) if len(problems) < 2: raise ValueError( "rank-correlation compares the paradigm orderings of at least " f"two problems; the CSVs contain {problems}." ) return problems queries = [query.strip() for query in name.split(",") if query.strip()] if len(queries) < 2: raise ValueError( "rank-correlation needs at least two comma-separated problems in " f"-n/--name, or no -n at all to use every problem; got {name!r}." ) return queries def _resolve_problem_query(benchmark_data: pd.DataFrame, name: str | None) -> str: """Return the problem query, defaulting to the only problem in the data. Args: benchmark_data: Combined benchmark results. name: User-supplied ``-n/--name`` query, or ``None``. Returns: The query to resolve against benchmark and complexity data. Raises: ValueError: If no name is given and the data holds several problems. """ if name is not None: return name problems = sorted( {str(value) for value in benchmark_data[BENCHMARK_PROBLEM].dropna().unique()} ) if len(problems) != 1: raise ValueError( "The benchmark CSVs contain several problems; select one with " f"-n/--name. Available problems: {problems}" ) return problems[0] def _analyze(args: argparse.Namespace) -> int: """Compute the metrics and render the chart selected on the command line. Args: args: Parsed arguments of ``p2analysis`` or ``p3analysis``. The complexity options are only read for the P3 charts, the hardware filter only for the boxplot. Returns: Process exit status: zero on success, one on failure. """ sns.set_theme(style="whitegrid", context="talk", font="DejaVu Sans") uses_complexity = args.chart in P3_CHARTS try: if args.legend_vertical and not args.legend: raise ValueError("--legend--vertical requires -l/--legend.") if args.chart == "boxplot" and args.size in { AVERAGE_SIZE, BEST_SIZE, WORST_SIZE, }: raise ValueError( "boxplot requires --size all or an exact numeric size; " f"{args.size!r} collapses the efficiency distribution." ) if args.average_over != AVERAGE_OVER_PP and args.size != AVERAGE_SIZE: raise ValueError( f"--average-over {args.average_over} requires --size avg; the " "other size modes do not average PP over sizes." ) benchmark_data = load_benchmark_csvs(args.csv_files) if args.chart == "time-barplot": _time_barplot(args, benchmark_data) return 0 if args.chart == RANK_CORRELATION: _rank_correlation(args, benchmark_data) return 0 efficiency_frames: list[pd.DataFrame] = [] portability_frames: list[pd.DataFrame] = [] export_efficiency_frames: list[pd.DataFrame] = [] export_portability_frames: list[pd.DataFrame] = [] scaling_frames: list[pd.DataFrame] = [] boxplot_efficiency_frames: list[pd.DataFrame] = [] problem_pairs: list[tuple[str, str]] = [] double_heatmap_efficiencies: tuple[pd.DataFrame, pd.DataFrame] | None = None for problem_query in (_resolve_problem_query(benchmark_data, args.name),): numeric_size = args.size if isinstance(args.size, float) else None selection_size = ( None if args.chart in {"combined", "double-heatmap"} else numeric_size ) all_size_rows, resolved_problem, description_is_workload = ( select_problem_rows( benchmark_data, problem_query, args.description_include, args.description_exclude, selection_size, args.precision, ) ) problem_pairs.append((problem_query, resolved_problem)) selected = all_size_rows if args.chart == "double-heatmap": assert numeric_size is not None double_heatmap_efficiencies = _double_heatmap_efficiencies( all_size_rows, description_is_workload, (numeric_size, args.second_size), resolved_problem, ) if ( args.chart in {"combined", "double-heatmap"} and numeric_size is not None ): selected = _filter_problem_size( all_size_rows, numeric_size, resolved_problem ) problem_efficiency, problem_portability = _problem_metrics( args, selected, description_is_workload ) problem_efficiency.insert(0, PROBLEM, resolved_problem) problem_portability.insert(0, PROBLEM, resolved_problem) efficiency_frames.append(problem_efficiency) portability_frames.append(problem_portability) if args.chart == "boxplot": boxplot_rows = _filter_hardware( selected, args.hardware, resolved_problem ) boxplot_efficiency, _ = calculate_metrics_by_size( boxplot_rows, description_is_workload, non_zero_pp=args.non_zero_pp, ) boxplot_efficiency.insert(0, PROBLEM, resolved_problem) boxplot_efficiency_frames.append(boxplot_efficiency) if args.export_to_csv: export_rows, export_problem, export_description_is_workload = ( select_problem_rows( benchmark_data, problem_query, args.description_include, args.description_exclude, None, None, ) ) export_efficiency, export_portability = calculate_export_metrics( export_rows, export_description_is_workload, non_zero_pp=args.non_zero_pp, average_over=args.average_over, ) export_efficiency.insert(0, PROBLEM, export_problem) export_portability.insert(0, PROBLEM, export_problem) export_efficiency_frames.append(export_efficiency) export_portability_frames.append(export_portability) if args.chart == "combined": problem_scaling = calculate_scaling_metrics( all_size_rows, description_is_workload, non_zero_pp=args.non_zero_pp, ) problem_scaling.insert(0, PROBLEM, resolved_problem) scaling_frames.append(problem_scaling) efficiency = pd.concat(efficiency_frames, ignore_index=True) portability = pd.concat(portability_frames, ignore_index=True) problems = [resolved for _, resolved in problem_pairs] problem_title = " + ".join(problems) mode = args.chart output = resolve_output_path(args.output, problems, mode) navchart_data: pd.DataFrame | None = None metric: str | None = None export_efficiency: pd.DataFrame | None = None export_portability: pd.DataFrame | None = None if args.export_to_csv: export_efficiency = pd.concat(export_efficiency_frames, ignore_index=True) export_portability = pd.concat(export_portability_frames, ignore_index=True) if args.chart in {"navchart", "combined"}: navchart_frames: list[pd.DataFrame] = [] for problem_query, resolved_problem in problem_pairs: complexity, current_metric = load_complexity_data( args.complexity, problem_query, args.complexity_metric, normalize=not args.complexity_absolute, ) if metric is not None and current_metric != metric: raise ValueError( "Complexity metric labels differ between problems: " f"{metric!r} and {current_metric!r}." ) metric = current_metric problem_pp = portability.loc[ portability[PROBLEM] == resolved_problem ].drop(columns=PROBLEM) problem_navchart = merge_portability_complexity( problem_pp, complexity, current_metric ) problem_navchart.insert(0, PROBLEM, resolved_problem) navchart_frames.append(problem_navchart) navchart_data = pd.concat(navchart_frames, ignore_index=True) logger.debug(f"Navchart data:\n{navchart_data.to_string(index=False)}") assert metric is not None if args.log_complexity and navchart_data[metric].le(0.0).any(): raise ValueError( "--log-complexity requires all plotted complexity values " "to be positive." ) comparison_data: pd.DataFrame | None = None comparison_labels: tuple[str, str] | None = None comparison_baselines: tuple[float, float] | None = None if args.chart == "complexity-comparison": comparison_frames: list[pd.DataFrame] = [] baseline_sets: set[tuple[float, float]] = set() for problem_query, resolved_problem in problem_pairs: problem_pp = portability.loc[ portability[PROBLEM] == resolved_problem ].drop(columns=PROBLEM) merged: pd.DataFrame | None = None labels: list[str] = [] for request in (args.compare_metric, args.complexity_metric): # Both metrics are taken relative to the CPP baseline so # they share one dimensionless scale and the identity line # of the comparison chart is meaningful. complexity, label = load_complexity_data( args.complexity, problem_query, request, normalize=True, ) labels.append(label) matched = merge_portability_complexity( problem_pp, complexity, label ).drop(columns=PERFORMANCE_PORTABILITY) merged = ( matched if merged is None else merged.merge(matched, on=APPLICATION, how="inner") ) if labels[0] == labels[1]: raise ValueError( "complexity-comparison needs two different " f"metrics; --compare-metric and --complexity-metric " f"both resolve to {labels[0]!r}." ) assert merged is not None merged.insert(0, PROBLEM, resolved_problem) comparison_frames.append(merged) if comparison_labels is not None and comparison_labels != tuple(labels): raise ValueError( "Complexity metric labels differ between problems: " f"{comparison_labels} and {tuple(labels)}." ) comparison_labels = (labels[0], labels[1]) baseline_sets.add( ( load_complexity_baseline( args.complexity, problem_query, args.compare_metric ), load_complexity_baseline( args.complexity, problem_query, args.complexity_metric, ), ) ) # Percentages of two different baselines cannot be keyed by one # box, so a multi-problem chart states no absolute values at all. if len(baseline_sets) == 1: comparison_baselines = baseline_sets.pop() elif baseline_sets: logger.info( "Problems have different CPP baselines; the comparison " "chart omits the absolute reference values." ) comparison_data = pd.concat(comparison_frames, ignore_index=True) logger.debug( f"Comparison data:\n{comparison_data.to_string(index=False)}" ) if args.export_to_csv and uses_complexity: assert export_efficiency is not None assert export_portability is not None enriched_efficiency_frames: list[pd.DataFrame] = [] enriched_portability_frames: list[pd.DataFrame] = [] for problem_query, resolved_problem in problem_pairs: export_complexity = load_complexity_export_data( args.complexity, problem_query ) portability_mask = export_portability[PROBLEM] == resolved_problem problem_export_portability = add_complexity_to_export( export_portability.loc[portability_mask].copy(), export_complexity, ) efficiency_mask = export_efficiency[PROBLEM] == resolved_problem problem_export_efficiency = add_complexity_to_export( export_efficiency.loc[efficiency_mask].copy(), export_complexity, ) enriched_portability_frames.append( append_cpp_complexity_row( problem_export_portability, export_complexity, resolved_problem, ) ) enriched_efficiency_frames.append( append_cpp_complexity_row( problem_export_efficiency, export_complexity, resolved_problem, ) ) export_efficiency = pd.concat( enriched_efficiency_frames, ignore_index=True, sort=False ) export_portability = pd.concat( enriched_portability_frames, ignore_index=True, sort=False ) if args.chart == "combined": assert navchart_data is not None assert metric is not None scaling_data = pd.concat(scaling_frames, ignore_index=True) figure = plot_cascade( efficiency, portability, problem_title, remove_description=args.remove_description, navchart_data=navchart_data, complexity_metric=metric, scaling_data=scaling_data, log_complexity=args.log_complexity, selected_size=None if args.size == ALL_SIZE else args.size, average_over=args.average_over, show_legends=not args.legend, ) elif args.chart == "navchart": assert navchart_data is not None assert metric is not None figure = plot_navchart( navchart_data, metric, problem_title, remove_description=args.remove_description, log_complexity=args.log_complexity, show_legends=not args.legend, ) elif args.chart == "complexity-comparison": assert comparison_data is not None assert comparison_labels is not None figure = plot_complexity_comparison( comparison_data, comparison_labels[0], comparison_labels[1], problem_title, remove_description=args.remove_description, log_axes=args.log_complexity, show_legends=not args.legend, baselines=comparison_baselines, ) elif args.chart == "heatmap": figure = plot_efficiency_heatmap( efficiency, problem_title, remove_description=args.remove_description, selected_size=args.size, sort_alphabetically=args.sort_alphabetically, ) elif args.chart == "double-heatmap": assert double_heatmap_efficiencies is not None figure = plot_efficiency_double_heatmap( *double_heatmap_efficiencies, problem_title, first_size=args.size, second_size=args.second_size, remove_description=args.remove_description, sort_alphabetically=args.sort_alphabetically, ) elif args.chart == "boxplot": boxplot_efficiency = pd.concat(boxplot_efficiency_frames, ignore_index=True) figure = plot_efficiency_boxplot( boxplot_efficiency, problem_title, remove_description=args.remove_description, selected_size=args.size, ) else: figure = plot_cascade( efficiency, portability, problem_title, remove_description=args.remove_description, selected_size=None if args.size == ALL_SIZE else args.size, average_over=args.average_over, show_legends=not args.legend, ) save_figure(figure, output) if args.export_to_csv: assert export_efficiency is not None assert export_portability is not None export_identity_columns = { PROBLEM, APPLICATION, PROBLEM_SIZE, PRECISION, PERFORMANCE_PORTABILITY, } complexity_columns = [ column for column in export_portability.columns if column not in export_identity_columns ] export_efficiency = export_efficiency[ [ PROBLEM, APPLICATION, PROBLEM_SIZE, PRECISION, *complexity_columns, HARDWARE, APPLICATION_EFFICIENCY, ] ] export_portability = export_portability[ [ PROBLEM, APPLICATION, PROBLEM_SIZE, PRECISION, *complexity_columns, PERFORMANCE_PORTABILITY, ] ] export_metrics_to_csv(export_efficiency, export_portability, output) if args.legend and args.chart in { "cascade", "navchart", "combined", "complexity-comparison", }: legend_source = navchart_data if args.chart == "navchart" else portability assert legend_source is not None legend_applications = list( dict.fromkeys(legend_source[APPLICATION].astype(str)) ) legend_figure = create_separate_legend( legend_applications, problems, sorted(efficiency[HARDWARE].astype(str).unique()), remove_description=args.remove_description, vertical=args.legend_vertical, ) legend_output = output.with_name(f"{output.stem}_legend.pdf") save_figure(legend_figure, legend_output) return 0 except (FileNotFoundError, OSError, ValueError) as error: logger.error(str(error)) return 1 if __name__ == "__main__": sys.exit(p3analysis_main())