nsight-python 0.9.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nsight/__init__.py +12 -0
- nsight/analyze.py +363 -0
- nsight/annotation.py +80 -0
- nsight/collection/__init__.py +10 -0
- nsight/collection/core.py +399 -0
- nsight/collection/ncu.py +268 -0
- nsight/exceptions.py +51 -0
- nsight/extraction.py +224 -0
- nsight/thermovision.py +115 -0
- nsight/transformation.py +167 -0
- nsight/utils.py +320 -0
- nsight/visualization.py +470 -0
- nsight_python-0.9.4.dist-info/METADATA +254 -0
- nsight_python-0.9.4.dist-info/RECORD +16 -0
- nsight_python-0.9.4.dist-info/WHEEL +4 -0
- nsight_python-0.9.4.dist-info/licenses/LICENSE +202 -0
nsight/visualization.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Visualization utilities for Nsight Python profiling and tensor difference analysis.
|
|
6
|
+
|
|
7
|
+
This module provides:
|
|
8
|
+
- Plotting functions for profiling results with configurable layout and annotation.
|
|
9
|
+
"""
|
|
10
|
+
from collections.abc import Callable, Sequence
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import matplotlib
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
from nsight import exceptions, utils
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def visualize(
|
|
22
|
+
agg_df: str | pd.DataFrame,
|
|
23
|
+
row_panels: Sequence[str] | None,
|
|
24
|
+
col_panels: Sequence[str] | None,
|
|
25
|
+
x_keys: Sequence[str] | None = None,
|
|
26
|
+
print_data: bool = False,
|
|
27
|
+
title: str = "",
|
|
28
|
+
filename: str = "plot.png",
|
|
29
|
+
ylabel: str = "",
|
|
30
|
+
annotate_points: bool = True,
|
|
31
|
+
show_avg: bool = True,
|
|
32
|
+
plot_type: str = "line",
|
|
33
|
+
plot_width: int = 6,
|
|
34
|
+
plot_height: int = 4,
|
|
35
|
+
show_geomean: bool = True,
|
|
36
|
+
show_grid: bool = True,
|
|
37
|
+
variant_fields: Sequence[Any] | None = None,
|
|
38
|
+
variant_annotations: Sequence[Any] | None = None,
|
|
39
|
+
plot_callback: Callable[[matplotlib.figure.Figure], None] | None = None,
|
|
40
|
+
) -> pd.DataFrame:
|
|
41
|
+
"""
|
|
42
|
+
Plots profiling results using line or bar plots in a subplot grid.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
agg_df: Aggregated profiling data or path to CSV file.
|
|
46
|
+
row_panels: List of fields for whose unique values
|
|
47
|
+
to create a new subplot along the vertical axis.
|
|
48
|
+
col_panels: List of fields for whose unique values
|
|
49
|
+
to create a new subplot along the horizontal axis.
|
|
50
|
+
x_keys: List of fields to use for the x-axis. By
|
|
51
|
+
default, we use all parameters of the decorated function except those
|
|
52
|
+
specified in `row_panels` and `col_panels`.
|
|
53
|
+
print_data: Whether to print aggregated profiling data to stdout.
|
|
54
|
+
title: Main plot title.
|
|
55
|
+
filename: Output filename for the saved plot.
|
|
56
|
+
ylabel: Label for the y-axis (typically the metric name).
|
|
57
|
+
annotate_points: Whether to annotate data points with values.
|
|
58
|
+
show_avg: Whether to add an "Avg" column with average metric values.
|
|
59
|
+
plot_type: Type of plot: "line" or "bar".
|
|
60
|
+
show_geomean: Whether to show geometric mean values.
|
|
61
|
+
show_grid: Whether to display grid lines on the plot.
|
|
62
|
+
variant_fields: List of config fields to use as variant fields (lines).
|
|
63
|
+
variant_annotations: List of annotated range names for which to apply variant splitting. The provided strings must each match one of the names defined using nsight.annotate.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(agg_df, str):
|
|
67
|
+
agg_df = pd.read_csv(agg_df)
|
|
68
|
+
assert isinstance(
|
|
69
|
+
agg_df, pd.DataFrame
|
|
70
|
+
), f"agg_df must be a pandas DataFrame or a CSV file path, not {type(agg_df)}"
|
|
71
|
+
|
|
72
|
+
row_panels = row_panels or []
|
|
73
|
+
col_panels = col_panels or []
|
|
74
|
+
|
|
75
|
+
# --- Annotation Variants Expansion ---
|
|
76
|
+
if variant_fields and variant_annotations:
|
|
77
|
+
# Remove variant_fields from Configuration for all annotations
|
|
78
|
+
config_exclude = set(variant_fields)
|
|
79
|
+
else:
|
|
80
|
+
config_exclude = set()
|
|
81
|
+
|
|
82
|
+
# Build Configuration field excluding variant_fields
|
|
83
|
+
annotation_idx = agg_df.columns.get_loc("AvgValue")
|
|
84
|
+
func_fields = list(agg_df.columns[1:annotation_idx])
|
|
85
|
+
subplot_fields = row_panels + col_panels # type: ignore[operator]
|
|
86
|
+
non_panel_fields = [
|
|
87
|
+
field
|
|
88
|
+
for field in func_fields
|
|
89
|
+
if field not in subplot_fields and field not in config_exclude
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
agg_df["Configuration"] = agg_df[non_panel_fields].apply(
|
|
93
|
+
lambda row: ", ".join(
|
|
94
|
+
f"{field_name}={value}" for field_name, value in row.items()
|
|
95
|
+
),
|
|
96
|
+
axis=1,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Expand variant annotations into separate lines, but keep x-ticks shared
|
|
100
|
+
if variant_fields and variant_annotations:
|
|
101
|
+
df = agg_df.copy()
|
|
102
|
+
new_rows = []
|
|
103
|
+
for annotation in variant_annotations:
|
|
104
|
+
annotation_mask = df["Annotation"] == annotation
|
|
105
|
+
if not annotation_mask.any():
|
|
106
|
+
continue
|
|
107
|
+
annotation_df = df[annotation_mask]
|
|
108
|
+
# For each unique combination of variant_fields fields
|
|
109
|
+
unique_combos = annotation_df[variant_fields].drop_duplicates()
|
|
110
|
+
for _, combo in unique_combos.iterrows():
|
|
111
|
+
combo_mask = (annotation_df[variant_fields] == combo.values).all(axis=1)
|
|
112
|
+
combo_df = annotation_df[combo_mask].copy()
|
|
113
|
+
# Create new annotation label
|
|
114
|
+
variant_label = (
|
|
115
|
+
annotation
|
|
116
|
+
+ " "
|
|
117
|
+
+ ", ".join(
|
|
118
|
+
f"{k}={v}" for k, v in zip(variant_fields, combo.values)
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
combo_df["Annotation"] = variant_label
|
|
122
|
+
new_rows.append(combo_df)
|
|
123
|
+
# Remove the original annotation rows
|
|
124
|
+
df = df[~annotation_mask]
|
|
125
|
+
# Add all new variant rows
|
|
126
|
+
if new_rows:
|
|
127
|
+
df = pd.concat([df] + new_rows, ignore_index=True)
|
|
128
|
+
agg_df = df
|
|
129
|
+
|
|
130
|
+
# --- End Annotation Variants Expansion ---
|
|
131
|
+
|
|
132
|
+
gpu_model = agg_df["GPU"].unique()[0]
|
|
133
|
+
host = agg_df["Host"].unique()[0]
|
|
134
|
+
hw_info_subtitle = f"{gpu_model}, {host}"
|
|
135
|
+
title_with_hardware_info = f"{title}\n{hw_info_subtitle}"
|
|
136
|
+
|
|
137
|
+
# Ensure that all fields in x_keys are present in non_panel_fields
|
|
138
|
+
if x_keys:
|
|
139
|
+
for field in x_keys:
|
|
140
|
+
if field not in non_panel_fields:
|
|
141
|
+
raise exceptions.ProfilerException(
|
|
142
|
+
f"Field '{field}' is not present in the DataFrame. "
|
|
143
|
+
f"Available fields: {non_panel_fields}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
unique_rows = (
|
|
147
|
+
agg_df[row_panels].drop_duplicates()
|
|
148
|
+
if row_panels
|
|
149
|
+
else pd.DataFrame({"dummy": [0]})
|
|
150
|
+
)
|
|
151
|
+
unique_cols = (
|
|
152
|
+
agg_df[col_panels].drop_duplicates()
|
|
153
|
+
if col_panels
|
|
154
|
+
else pd.DataFrame({"dummy": [0]})
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
nrows, ncols = max(len(unique_rows), 1), max(len(unique_cols), 1)
|
|
158
|
+
# --- Ensure each subplot has a minimum size ---
|
|
159
|
+
min_width_per_subplot = plot_width # inches
|
|
160
|
+
min_height_per_subplot = plot_height # inches
|
|
161
|
+
fig_width = max(min_width_per_subplot * ncols, 8)
|
|
162
|
+
fig_height = max(min_height_per_subplot * nrows, 6)
|
|
163
|
+
fig, axes = plt.subplots(
|
|
164
|
+
nrows,
|
|
165
|
+
ncols,
|
|
166
|
+
figsize=(fig_width, fig_height),
|
|
167
|
+
constrained_layout=True, # Use constrained layout
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if nrows == 1:
|
|
171
|
+
axes = np.expand_dims(axes, axis=0)
|
|
172
|
+
if ncols == 1:
|
|
173
|
+
axes = np.expand_dims(axes, axis=1)
|
|
174
|
+
|
|
175
|
+
for row_idx, (_, row_values) in enumerate(unique_rows.iterrows()):
|
|
176
|
+
for col_idx, (_, col_values) in enumerate(unique_cols.iterrows()):
|
|
177
|
+
ax = axes[row_idx, col_idx]
|
|
178
|
+
subplot_df = agg_df
|
|
179
|
+
if row_panels:
|
|
180
|
+
subplot_df = subplot_df[
|
|
181
|
+
(subplot_df[row_panels] == row_values).all(axis=1)
|
|
182
|
+
]
|
|
183
|
+
if col_panels:
|
|
184
|
+
subplot_df = subplot_df[
|
|
185
|
+
(subplot_df[col_panels] == col_values).all(axis=1)
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
local_df = subplot_df.copy()
|
|
189
|
+
|
|
190
|
+
# --- x_keys validation and Configuration building ---
|
|
191
|
+
used_fields = (
|
|
192
|
+
set(row_panels or [])
|
|
193
|
+
| set(col_panels or [])
|
|
194
|
+
| set(variant_fields or [])
|
|
195
|
+
)
|
|
196
|
+
if x_keys:
|
|
197
|
+
overlap = used_fields & set(x_keys)
|
|
198
|
+
if overlap:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"x_keys cannot contain fields used in row_panels, col_panels, or variant_fields: {overlap}"
|
|
201
|
+
)
|
|
202
|
+
config_fields = x_keys
|
|
203
|
+
else:
|
|
204
|
+
annotation_idx = local_df.columns.get_loc("AvgValue")
|
|
205
|
+
func_fields = list(local_df.columns[1:annotation_idx])
|
|
206
|
+
subplot_fields = row_panels + col_panels # type: ignore[operator]
|
|
207
|
+
config_exclude = set(variant_fields or [])
|
|
208
|
+
config_fields = [
|
|
209
|
+
field
|
|
210
|
+
for field in func_fields
|
|
211
|
+
if field not in subplot_fields and field not in config_exclude
|
|
212
|
+
]
|
|
213
|
+
local_df["Configuration"] = local_df[config_fields].apply(
|
|
214
|
+
lambda row: ", ".join(
|
|
215
|
+
f"{field_name}={value}" for field_name, value in row.items()
|
|
216
|
+
),
|
|
217
|
+
axis=1,
|
|
218
|
+
)
|
|
219
|
+
# --- End x_keys validation and Configuration building ---
|
|
220
|
+
|
|
221
|
+
# --- Annotation Variants logic per subplot ---
|
|
222
|
+
if variant_fields and variant_annotations:
|
|
223
|
+
df = local_df.copy()
|
|
224
|
+
new_rows = []
|
|
225
|
+
for annotation in variant_annotations:
|
|
226
|
+
annotation_mask = df["Annotation"] == annotation
|
|
227
|
+
if not annotation_mask.any():
|
|
228
|
+
continue
|
|
229
|
+
annotation_df = df[annotation_mask]
|
|
230
|
+
unique_combos = annotation_df[variant_fields].drop_duplicates()
|
|
231
|
+
for _, combo in unique_combos.iterrows():
|
|
232
|
+
combo_mask = (
|
|
233
|
+
annotation_df[variant_fields] == combo.values
|
|
234
|
+
).all(axis=1)
|
|
235
|
+
combo_df = annotation_df[combo_mask].copy()
|
|
236
|
+
variant_label = (
|
|
237
|
+
annotation
|
|
238
|
+
+ " "
|
|
239
|
+
+ ", ".join(
|
|
240
|
+
f"{k}={v}" for k, v in zip(variant_fields, combo.values)
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
combo_df["Annotation"] = variant_label
|
|
244
|
+
new_rows.append(combo_df)
|
|
245
|
+
df = df[~annotation_mask]
|
|
246
|
+
if new_rows:
|
|
247
|
+
df = pd.concat([df] + new_rows, ignore_index=True)
|
|
248
|
+
local_df = df
|
|
249
|
+
# --- End Annotation Variants logic per subplot ---
|
|
250
|
+
|
|
251
|
+
annotations = local_df["Annotation"].unique()
|
|
252
|
+
nvidia_colors = [
|
|
253
|
+
"#76B900", # Green
|
|
254
|
+
"#0070C5", # Blue
|
|
255
|
+
"#5C1682", # Purple
|
|
256
|
+
"#890C57", # Red
|
|
257
|
+
"#FAC200", # Yellow
|
|
258
|
+
"#008564", # Dark Green
|
|
259
|
+
"#FF5733", # Orange
|
|
260
|
+
"#C70039", # Crimson
|
|
261
|
+
"#900C3F", # Dark Red
|
|
262
|
+
"#581845", # Dark Purple
|
|
263
|
+
"#1F618D", # Dark Blue
|
|
264
|
+
"#28B463", # Light Green
|
|
265
|
+
"#F39C12", # Bright Yellow
|
|
266
|
+
"#D35400", # Dark Orange
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
unique_configs = local_df["Configuration"].dropna().unique()
|
|
270
|
+
|
|
271
|
+
x_ticks = np.arange(len(unique_configs))
|
|
272
|
+
n_annotations = len(annotations)
|
|
273
|
+
width = (
|
|
274
|
+
0.8 / n_annotations if n_annotations > 0 else 0.2
|
|
275
|
+
) # Adjust width for grouped bars
|
|
276
|
+
avg_values = {}
|
|
277
|
+
|
|
278
|
+
for i, (annotation, color) in enumerate(zip(annotations, nvidia_colors)):
|
|
279
|
+
annotation_data = local_df[local_df["Annotation"] == annotation]
|
|
280
|
+
# Keep annotation_data in original order (no sorting)
|
|
281
|
+
|
|
282
|
+
# Compute valid average (for show_avg only)
|
|
283
|
+
valid_values = annotation_data["AvgValue"].dropna()
|
|
284
|
+
avg_value = valid_values.mean() if not valid_values.empty else np.nan
|
|
285
|
+
avg_values[annotation] = avg_value
|
|
286
|
+
|
|
287
|
+
# Map annotation_data to x positions (may be multiple points per x-tick)
|
|
288
|
+
x_pos_map = {label: idx for idx, label in enumerate(unique_configs)}
|
|
289
|
+
x_positions = annotation_data["Configuration"].map(x_pos_map)
|
|
290
|
+
|
|
291
|
+
# Adjust x positions for grouped bars
|
|
292
|
+
if plot_type == "bar" and n_annotations > 1:
|
|
293
|
+
# Center the group of bars around each x-tick
|
|
294
|
+
x_offset = (i - (n_annotations - 1) / 2) * width
|
|
295
|
+
x_positions = x_positions + x_offset
|
|
296
|
+
|
|
297
|
+
if plot_type == "line":
|
|
298
|
+
ax.plot(
|
|
299
|
+
annotation_data["Configuration"].astype(str),
|
|
300
|
+
annotation_data["AvgValue"],
|
|
301
|
+
marker="o",
|
|
302
|
+
label=annotation,
|
|
303
|
+
color=color,
|
|
304
|
+
)
|
|
305
|
+
elif plot_type == "bar":
|
|
306
|
+
ax.bar(
|
|
307
|
+
x_positions,
|
|
308
|
+
annotation_data["AvgValue"],
|
|
309
|
+
width=width,
|
|
310
|
+
label=annotation,
|
|
311
|
+
color=color,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Annotate each point with its value (formatted to 2 decimal places)
|
|
315
|
+
if annotate_points:
|
|
316
|
+
for x_pos, y in zip(x_positions, annotation_data["AvgValue"]):
|
|
317
|
+
ax.text(
|
|
318
|
+
x_pos,
|
|
319
|
+
y + (0.02 if plot_type == "line" else 0.03),
|
|
320
|
+
f"{y:.2f}",
|
|
321
|
+
fontsize=8,
|
|
322
|
+
color=color,
|
|
323
|
+
ha="center",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if show_avg:
|
|
327
|
+
# Add vertical separation line for average values
|
|
328
|
+
sep_index = len(unique_configs)
|
|
329
|
+
ax.axvline(
|
|
330
|
+
x=sep_index - 0.5,
|
|
331
|
+
color="black",
|
|
332
|
+
linestyle="dashed",
|
|
333
|
+
linewidth=1,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
for i, (annotation, color) in enumerate(
|
|
337
|
+
zip(annotations, nvidia_colors)
|
|
338
|
+
):
|
|
339
|
+
avg_value = avg_values.get(annotation, np.nan)
|
|
340
|
+
if not np.isnan(avg_value): # Only plot if valid
|
|
341
|
+
avg_x_pos = sep_index + (i - (n_annotations - 1) / 2) * width
|
|
342
|
+
# Plot absolute value as bar on primary axis
|
|
343
|
+
ax.bar(avg_x_pos, avg_value, width=width, color=color, zorder=3)
|
|
344
|
+
# Always annotate the average values after the bar, with higher zorder
|
|
345
|
+
ax.text(
|
|
346
|
+
avg_x_pos,
|
|
347
|
+
avg_value + 0.02,
|
|
348
|
+
f"{avg_value:.2f}",
|
|
349
|
+
fontsize=9,
|
|
350
|
+
color=color,
|
|
351
|
+
ha="center",
|
|
352
|
+
fontweight="bold",
|
|
353
|
+
zorder=4, # Ensure text is above the bar
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if show_geomean:
|
|
357
|
+
# Add vertical separation line for geomean values
|
|
358
|
+
sep_index = len(unique_configs) + (1 if show_avg else 0)
|
|
359
|
+
ax.axvline(
|
|
360
|
+
x=sep_index - 0.5,
|
|
361
|
+
color="black",
|
|
362
|
+
linestyle="dashed",
|
|
363
|
+
linewidth=1,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
for i, (annotation, color) in enumerate(
|
|
367
|
+
zip(annotations, nvidia_colors)
|
|
368
|
+
):
|
|
369
|
+
geomean = agg_df[agg_df["Annotation"] == annotation][
|
|
370
|
+
"Geomean"
|
|
371
|
+
].iloc[0]
|
|
372
|
+
if not np.isnan(geomean):
|
|
373
|
+
geomean_x_pos = (
|
|
374
|
+
sep_index + (i - (n_annotations - 1) / 2) * width
|
|
375
|
+
)
|
|
376
|
+
ax.bar(
|
|
377
|
+
geomean_x_pos, geomean, width=width, color=color, zorder=3
|
|
378
|
+
)
|
|
379
|
+
ax.text(
|
|
380
|
+
geomean_x_pos,
|
|
381
|
+
geomean + 0.02,
|
|
382
|
+
f"{geomean:.2f}",
|
|
383
|
+
fontsize=9,
|
|
384
|
+
color=color,
|
|
385
|
+
ha="center",
|
|
386
|
+
fontweight="bold",
|
|
387
|
+
zorder=4, # Ensure text is above the bar
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Ensure all x-axis labels are strings
|
|
391
|
+
x_labels = list(map(str, unique_configs))
|
|
392
|
+
if show_avg:
|
|
393
|
+
x_labels.append("Avg")
|
|
394
|
+
if show_geomean:
|
|
395
|
+
x_labels.append("Geomean")
|
|
396
|
+
|
|
397
|
+
ax.set_xticks(np.arange(len(x_labels)))
|
|
398
|
+
ax.set_xticklabels(
|
|
399
|
+
x_labels,
|
|
400
|
+
ha="right", # Align to the right to prevent overlap
|
|
401
|
+
rotation=45, # Rotate for better readability
|
|
402
|
+
fontsize=9, # Reduce font size slightly
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
ax.set_ylim(0, max(agg_df["AvgValue"].max(skipna=True) * 1.1, 1))
|
|
406
|
+
|
|
407
|
+
# Add grid
|
|
408
|
+
if show_grid:
|
|
409
|
+
ax.grid(True, linestyle="--", alpha=0.7)
|
|
410
|
+
ax.set_axisbelow(True) # Put grid behind the plot elements
|
|
411
|
+
|
|
412
|
+
ylabel = ylabel or agg_df["Metric"].unique()[0]
|
|
413
|
+
if col_idx == 0:
|
|
414
|
+
ax.set_ylabel(f"{ylabel} (avg: {agg_df['NumRuns'].max()} runs)")
|
|
415
|
+
|
|
416
|
+
# Generate combined subplot title with both row and col fields
|
|
417
|
+
row_label = "\n".join(
|
|
418
|
+
f"{field}={row_values[field]}" for field in row_panels
|
|
419
|
+
)
|
|
420
|
+
col_label = "\n".join(
|
|
421
|
+
f"{field}={col_values[field]}" for field in col_panels
|
|
422
|
+
)
|
|
423
|
+
full_title = (
|
|
424
|
+
f"{row_label}\n{col_label}"
|
|
425
|
+
if row_label and col_label
|
|
426
|
+
else row_label or col_label
|
|
427
|
+
)
|
|
428
|
+
# Count the number of linebreaks to adjust padding
|
|
429
|
+
num_lines = full_title.count("\n") + 1 if full_title else 1
|
|
430
|
+
ax.set_title(full_title, pad=18 + 6 * (num_lines - 1))
|
|
431
|
+
|
|
432
|
+
# Use 'best' legend location for each axis
|
|
433
|
+
for ax in axes.flat:
|
|
434
|
+
ax.legend(
|
|
435
|
+
title="Annotation",
|
|
436
|
+
loc="best", # Let matplotlib pick the best spot
|
|
437
|
+
fontsize=9,
|
|
438
|
+
title_fontsize=10,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Set subplot titles with smaller font and more padding
|
|
442
|
+
n_axes = len(axes.flat)
|
|
443
|
+
for i, ax in enumerate(axes.flat):
|
|
444
|
+
ax.set_title(ax.get_title(), fontsize=11, pad=18)
|
|
445
|
+
# Only show x-axis labels and tick labels on the bottom row
|
|
446
|
+
if nrows > 1:
|
|
447
|
+
# If this axis is not in the last row, hide x labels
|
|
448
|
+
if i < (nrows - 1) * ncols:
|
|
449
|
+
ax.set_xlabel("")
|
|
450
|
+
ax.set_xticklabels([])
|
|
451
|
+
else:
|
|
452
|
+
# If only one row, show all x labels
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
# Set the main title, move it up a bit
|
|
456
|
+
fig.suptitle(title_with_hardware_info, fontsize=14, fontweight="bold", y=1.08)
|
|
457
|
+
|
|
458
|
+
if plot_callback:
|
|
459
|
+
plot_callback(fig)
|
|
460
|
+
|
|
461
|
+
# Save with tight bounding box to avoid clipping
|
|
462
|
+
fig.savefig(filename, bbox_inches="tight")
|
|
463
|
+
|
|
464
|
+
if print_data:
|
|
465
|
+
agg_df["AvgValue"] = agg_df["AvgValue"].map("{:.4f}".format)
|
|
466
|
+
print("Aggregated Data (Average value and Number of Runs):")
|
|
467
|
+
print(agg_df.to_string(index=False))
|
|
468
|
+
|
|
469
|
+
plt.close()
|
|
470
|
+
return agg_df
|