ror HF Staff commited on
Commit
ef39cc6
·
1 Parent(s): 9a96f61

Refactor of model page

Browse files
Files changed (2) hide show
  1. app.py +7 -213
  2. model_page.py +216 -0
app.py CHANGED
@@ -2,11 +2,12 @@ import matplotlib.pyplot as plt
2
  import matplotlib
3
  import pandas as pd
4
  import gradio as gr
5
- import threading
6
 
7
  from data import CIResults
8
- from utils import logger, generate_underlined_line
9
  from summary_page import create_summary_page
 
 
10
 
11
  # Configure matplotlib to prevent memory warnings and set dark background
12
  matplotlib.rcParams['figure.facecolor'] = '#000000'
@@ -22,216 +23,6 @@ Ci_results.load_data()
22
  Ci_results.schedule_data_reload()
23
 
24
 
25
- def plot_model_stats(model_name: str) -> tuple[plt.Figure, str, str]:
26
- """Draws a pie chart of model's passed, failed, skipped, and error stats."""
27
- if Ci_results.df.empty or model_name not in Ci_results.df.index:
28
- # Handle case where model data is not available
29
- fig, ax = plt.subplots(figsize=(10, 8), facecolor='#000000')
30
- ax.set_facecolor('#000000')
31
- ax.text(0.5, 0.5, f'No data available for {model_name}',
32
- horizontalalignment='center', verticalalignment='center',
33
- transform=ax.transAxes, fontsize=16, color='#888888',
34
- fontfamily='monospace', weight='normal')
35
- ax.set_xlim(0, 1)
36
- ax.set_ylim(0, 1)
37
- ax.axis('off')
38
- return fig, "No data available", "No data available"
39
-
40
- row = Ci_results.df.loc[model_name]
41
-
42
- # Handle missing values and get counts directly from dataframe
43
- success_amd = int(row.get('success_amd', 0)) if pd.notna(row.get('success_amd', 0)) else 0
44
- success_nvidia = int(row.get('success_nvidia', 0)) if pd.notna(row.get('success_nvidia', 0)) else 0
45
- failed_multi_amd = int(row.get('failed_multi_no_amd', 0)) if pd.notna(row.get('failed_multi_no_amd', 0)) else 0
46
- failed_multi_nvidia = int(row.get('failed_multi_no_nvidia', 0)) if pd.notna(row.get('failed_multi_no_nvidia', 0)) else 0
47
- failed_single_amd = int(row.get('failed_single_no_amd', 0)) if pd.notna(row.get('failed_single_no_amd', 0)) else 0
48
- failed_single_nvidia = int(row.get('failed_single_no_nvidia', 0)) if pd.notna(row.get('failed_single_no_nvidia', 0)) else 0
49
-
50
- # Calculate total failures
51
- total_failed_amd = failed_multi_amd + failed_single_amd
52
- total_failed_nvidia = failed_multi_nvidia + failed_single_nvidia
53
-
54
- # Softer color palette - less pastel, more vibrant
55
- colors = {
56
- 'passed': '#4CAF50', # Medium green
57
- 'failed': '#E53E3E', # More red
58
- 'skipped': '#FFD54F', # Medium yellow
59
- 'error': '#8B0000' # Dark red
60
- }
61
-
62
- # Create stats dictionaries directly from dataframe values
63
- amd_stats = {
64
- 'passed': success_amd,
65
- 'failed': total_failed_amd,
66
- 'skipped': 0, # Not available in this dataset
67
- 'error': 0 # Not available in this dataset
68
- }
69
-
70
- nvidia_stats = {
71
- 'passed': success_nvidia,
72
- 'failed': total_failed_nvidia,
73
- 'skipped': 0, # Not available in this dataset
74
- 'error': 0 # Not available in this dataset
75
- }
76
-
77
- # Filter out categories with 0 values for cleaner visualization
78
- amd_filtered = {k: v for k, v in amd_stats.items() if v > 0}
79
- nvidia_filtered = {k: v for k, v in nvidia_stats.items() if v > 0}
80
-
81
- if not amd_filtered and not nvidia_filtered:
82
- # Handle case where all values are 0 - minimal empty state
83
- fig, ax = plt.subplots(figsize=(10, 8), facecolor='#000000')
84
- ax.set_facecolor('#000000')
85
- ax.text(0.5, 0.5, 'No test results available',
86
- horizontalalignment='center', verticalalignment='center',
87
- transform=ax.transAxes, fontsize=16, color='#888888',
88
- fontfamily='monospace', weight='normal')
89
- ax.set_xlim(0, 1)
90
- ax.set_ylim(0, 1)
91
- ax.axis('off')
92
- return fig, "", ""
93
-
94
- # Create figure with two subplots side by side with padding
95
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 9), facecolor='#000000')
96
- ax1.set_facecolor('#000000')
97
- ax2.set_facecolor('#000000')
98
-
99
- def create_pie_chart(ax, device_label, filtered_stats):
100
- if not filtered_stats:
101
- ax.text(0.5, 0.5, 'No test results',
102
- horizontalalignment='center', verticalalignment='center',
103
- transform=ax.transAxes, fontsize=14, color='#888888',
104
- fontfamily='monospace', weight='normal')
105
- ax.set_title(device_label,
106
- fontsize=28, weight='bold', pad=2, color='#FFFFFF',
107
- fontfamily='monospace')
108
- ax.axis('off')
109
- return
110
-
111
- chart_colors = [colors[category] for category in filtered_stats.keys()]
112
-
113
- # Create minimal pie chart - full pie, no donut effect
114
- wedges, texts, autotexts = ax.pie(
115
- filtered_stats.values(),
116
- labels=[label.lower() for label in filtered_stats.keys()], # Lowercase for minimal look
117
- colors=chart_colors,
118
- autopct=lambda pct: f'{int(pct/100*sum(filtered_stats.values()))}',
119
- startangle=90,
120
- explode=None, # No separation
121
- shadow=False,
122
- wedgeprops=dict(edgecolor='#1a1a1a', linewidth=0.5), # Minimal borders
123
- textprops={'fontsize': 12, 'weight': 'normal', 'color': '#CCCCCC', 'fontfamily': 'monospace'}
124
- )
125
-
126
- # Enhanced percentage text styling for better readability
127
- for autotext in autotexts:
128
- autotext.set_color('#000000') # Black text for better contrast
129
- autotext.set_weight('bold')
130
- autotext.set_fontsize(14)
131
- autotext.set_fontfamily('monospace')
132
-
133
- # Minimal category labels
134
- for text in texts:
135
- text.set_color('#AAAAAA')
136
- text.set_weight('normal')
137
- text.set_fontsize(13)
138
- text.set_fontfamily('monospace')
139
-
140
- # Device label closer to chart and bigger
141
- ax.set_title(device_label,
142
- fontsize=28, weight='normal', pad=2, color='#FFFFFF',
143
- fontfamily='monospace')
144
-
145
- # Create both pie charts with device labels
146
- create_pie_chart(ax1, "amd", amd_filtered)
147
- create_pie_chart(ax2, "nvidia", nvidia_filtered)
148
-
149
- # Add subtle separation line between charts - stops at device labels level
150
- line_x = 0.5
151
- fig.add_artist(plt.Line2D([line_x, line_x], [0.0, 0.85],
152
- color='#333333', linewidth=1, alpha=0.5,
153
- transform=fig.transFigure))
154
-
155
- # Add central shared title for model name
156
- fig.suptitle(f'{model_name.lower()}',
157
- fontsize=32, weight='bold', color='#CCCCCC',
158
- fontfamily='monospace', y=1)
159
-
160
- # Clean layout with padding and space for central title
161
- plt.tight_layout()
162
- plt.subplots_adjust(top=0.85, wspace=0.4) # Added wspace for padding between charts
163
-
164
- # Generate failure info directly from dataframe
165
- failures_amd = row.get('failures_amd', {})
166
- failures_nvidia = row.get('failures_nvidia', {})
167
-
168
- amd_failed_info = extract_failure_info(failures_amd, 'AMD', failed_multi_amd, failed_single_amd)
169
- nvidia_failed_info = extract_failure_info(failures_nvidia, 'NVIDIA', failed_multi_nvidia, failed_single_nvidia)
170
-
171
- return fig, amd_failed_info, nvidia_failed_info
172
-
173
- def extract_failure_info(failures_obj, device: str, multi_count: int, single_count: int) -> str:
174
- """Extract failure information from failures object."""
175
- if (not failures_obj or pd.isna(failures_obj)) and multi_count == 0 and single_count == 0:
176
- return f"No failures on {device}"
177
-
178
- info_lines = []
179
-
180
- # Add counts summary
181
- if multi_count > 0 or single_count > 0:
182
- info_lines.append(generate_underlined_line(f"Failure Summary for {device}:"))
183
- if multi_count > 0:
184
- info_lines.append(f"Multi GPU failures: {multi_count}")
185
- if single_count > 0:
186
- info_lines.append(f"Single GPU failures: {single_count}")
187
- info_lines.append("")
188
-
189
- # Try to extract detailed failure information
190
- try:
191
- if isinstance(failures_obj, dict):
192
- # Check for multi and single failure categories
193
- if 'multi' in failures_obj and failures_obj['multi']:
194
- info_lines.append(generate_underlined_line(f"Multi GPU failure details:"))
195
- if isinstance(failures_obj['multi'], list):
196
- # Handle list of failures (could be strings or dicts)
197
- for i, failure in enumerate(failures_obj['multi'][:10]): # Limit to first 10
198
- if isinstance(failure, dict):
199
- # Extract meaningful info from dict (e.g., test name, line, etc.)
200
- failure_str = failure.get('line', failure.get('test', failure.get('name', str(failure))))
201
- info_lines.append(f" {i+1}. {failure_str}")
202
- else:
203
- info_lines.append(f" {i+1}. {str(failure)}")
204
- if len(failures_obj['multi']) > 10:
205
- info_lines.append(f"... and {len(failures_obj['multi']) - 10} more")
206
- else:
207
- info_lines.append(str(failures_obj['multi']))
208
- info_lines.append("")
209
-
210
- if 'single' in failures_obj and failures_obj['single']:
211
- info_lines.append(generate_underlined_line(f"Single GPU failure details:"))
212
- if isinstance(failures_obj['single'], list):
213
- # Handle list of failures (could be strings or dicts)
214
- for i, failure in enumerate(failures_obj['single'][:10]): # Limit to first 10
215
- if isinstance(failure, dict):
216
- # Extract meaningful info from dict (e.g., test name, line, etc.)
217
- failure_str = failure.get('line', failure.get('test', failure.get('name', str(failure))))
218
- info_lines.append(f" {i+1}. {failure_str}")
219
- else:
220
- info_lines.append(f" {i+1}. {str(failure)}")
221
- if len(failures_obj['single']) > 10:
222
- info_lines.append(f"... and {len(failures_obj['single']) - 10} more")
223
- else:
224
- info_lines.append(str(failures_obj['single']))
225
-
226
- return "\n".join(info_lines) if info_lines else f"No detailed failure info for {device}"
227
-
228
- except Exception as e:
229
- if multi_count > 0 or single_count > 0:
230
- return f"Failures detected on {device} (Multi: {multi_count}, Single: {single_count})\nDetails unavailable: {str(e)}"
231
- return f"Error processing failure info for {device}: {str(e)}"
232
-
233
-
234
-
235
  # Load CSS from external file
236
  def load_css():
237
  try:
@@ -241,6 +32,7 @@ def load_css():
241
  logger.warning("styles.css not found, using minimal default styles")
242
  return "body { background: #000; color: #fff; }"
243
 
 
244
  # Create the Gradio interface with sidebar and dark theme
245
  with gr.Blocks(title="Model Test Results Dashboard", css=load_css()) as demo:
246
 
@@ -331,7 +123,7 @@ with gr.Blocks(title="Model Test Results Dashboard", css=load_css()) as demo:
331
  for i, btn in enumerate(model_buttons):
332
  model_name = model_choices[i]
333
  btn.click(
334
- fn=lambda selected_model=model_name: plot_model_stats(selected_model),
335
  outputs=[plot_output, amd_failed_tests_output, nvidia_failed_tests_output]
336
  ).then(
337
  fn=lambda: [gr.update(visible=False), gr.update(visible=True)],
@@ -432,5 +224,7 @@ with gr.Blocks(title="Model Test Results Dashboard", css=load_css()) as demo:
432
  outputs=[ci_links_display]
433
  )
434
 
 
 
435
  if __name__ == "__main__":
436
  demo.launch()
 
2
  import matplotlib
3
  import pandas as pd
4
  import gradio as gr
 
5
 
6
  from data import CIResults
7
+ from utils import logger
8
  from summary_page import create_summary_page
9
+ from model_page import plot_model_stats
10
+
11
 
12
  # Configure matplotlib to prevent memory warnings and set dark background
13
  matplotlib.rcParams['figure.facecolor'] = '#000000'
 
23
  Ci_results.schedule_data_reload()
24
 
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  # Load CSS from external file
27
  def load_css():
28
  try:
 
32
  logger.warning("styles.css not found, using minimal default styles")
33
  return "body { background: #000; color: #fff; }"
34
 
35
+
36
  # Create the Gradio interface with sidebar and dark theme
37
  with gr.Blocks(title="Model Test Results Dashboard", css=load_css()) as demo:
38
 
 
123
  for i, btn in enumerate(model_buttons):
124
  model_name = model_choices[i]
125
  btn.click(
126
+ fn=lambda selected_model=model_name: plot_model_stats(Ci_results.df, selected_model),
127
  outputs=[plot_output, amd_failed_tests_output, nvidia_failed_tests_output]
128
  ).then(
129
  fn=lambda: [gr.update(visible=False), gr.update(visible=True)],
 
224
  outputs=[ci_links_display]
225
  )
226
 
227
+
228
+ # Gradio entrypoint
229
  if __name__ == "__main__":
230
  demo.launch()
model_page.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import pandas as pd
3
+ from utils import generate_underlined_line
4
+
5
+
6
+ def extract_failure_info(failures_obj, device: str, multi_count: int, single_count: int) -> str:
7
+ """Extract failure information from failures object."""
8
+ if (not failures_obj or pd.isna(failures_obj)) and multi_count == 0 and single_count == 0:
9
+ return f"No failures on {device}"
10
+
11
+ info_lines = []
12
+
13
+ # Add counts summary
14
+ if multi_count > 0 or single_count > 0:
15
+ info_lines.append(generate_underlined_line(f"Failure Summary for {device}:"))
16
+ if multi_count > 0:
17
+ info_lines.append(f"Multi GPU failures: {multi_count}")
18
+ if single_count > 0:
19
+ info_lines.append(f"Single GPU failures: {single_count}")
20
+ info_lines.append("")
21
+
22
+ # Try to extract detailed failure information
23
+ try:
24
+ if isinstance(failures_obj, dict):
25
+ # Check for multi and single failure categories
26
+ if 'multi' in failures_obj and failures_obj['multi']:
27
+ info_lines.append(generate_underlined_line(f"Multi GPU failure details:"))
28
+ if isinstance(failures_obj['multi'], list):
29
+ # Handle list of failures (could be strings or dicts)
30
+ for i, failure in enumerate(failures_obj['multi'][:10]): # Limit to first 10
31
+ if isinstance(failure, dict):
32
+ # Extract meaningful info from dict (e.g., test name, line, etc.)
33
+ failure_str = failure.get('line', failure.get('test', failure.get('name', str(failure))))
34
+ info_lines.append(f" {i+1}. {failure_str}")
35
+ else:
36
+ info_lines.append(f" {i+1}. {str(failure)}")
37
+ if len(failures_obj['multi']) > 10:
38
+ info_lines.append(f"... and {len(failures_obj['multi']) - 10} more")
39
+ else:
40
+ info_lines.append(str(failures_obj['multi']))
41
+ info_lines.append("")
42
+
43
+ if 'single' in failures_obj and failures_obj['single']:
44
+ info_lines.append(generate_underlined_line(f"Single GPU failure details:"))
45
+ if isinstance(failures_obj['single'], list):
46
+ # Handle list of failures (could be strings or dicts)
47
+ for i, failure in enumerate(failures_obj['single'][:10]): # Limit to first 10
48
+ if isinstance(failure, dict):
49
+ # Extract meaningful info from dict (e.g., test name, line, etc.)
50
+ failure_str = failure.get('line', failure.get('test', failure.get('name', str(failure))))
51
+ info_lines.append(f" {i+1}. {failure_str}")
52
+ else:
53
+ info_lines.append(f" {i+1}. {str(failure)}")
54
+ if len(failures_obj['single']) > 10:
55
+ info_lines.append(f"... and {len(failures_obj['single']) - 10} more")
56
+ else:
57
+ info_lines.append(str(failures_obj['single']))
58
+
59
+ return "\n".join(info_lines) if info_lines else f"No detailed failure info for {device}"
60
+
61
+ except Exception as e:
62
+ if multi_count > 0 or single_count > 0:
63
+ return f"Failures detected on {device} (Multi: {multi_count}, Single: {single_count})\nDetails unavailable: {str(e)}"
64
+ return f"Error processing failure info for {device}: {str(e)}"
65
+
66
+
67
+ def plot_model_stats(
68
+ df: pd.DataFrame,
69
+ model_name: str,
70
+ ) -> tuple[plt.Figure, str, str]:
71
+ """Draws a pie chart of model's passed, failed, skipped, and error stats."""
72
+ if df.empty or model_name not in df.index:
73
+ # Handle case where model data is not available
74
+ fig, ax = plt.subplots(figsize=(10, 8), facecolor='#000000')
75
+ ax.set_facecolor('#000000')
76
+ ax.text(0.5, 0.5, f'No data available for {model_name}',
77
+ horizontalalignment='center', verticalalignment='center',
78
+ transform=ax.transAxes, fontsize=16, color='#888888',
79
+ fontfamily='monospace', weight='normal')
80
+ ax.set_xlim(0, 1)
81
+ ax.set_ylim(0, 1)
82
+ ax.axis('off')
83
+ return fig, "No data available", "No data available"
84
+
85
+ row = df.loc[model_name]
86
+
87
+ # Handle missing values and get counts directly from dataframe
88
+ success_amd = int(row.get('success_amd', 0)) if pd.notna(row.get('success_amd', 0)) else 0
89
+ success_nvidia = int(row.get('success_nvidia', 0)) if pd.notna(row.get('success_nvidia', 0)) else 0
90
+ failed_multi_amd = int(row.get('failed_multi_no_amd', 0)) if pd.notna(row.get('failed_multi_no_amd', 0)) else 0
91
+ failed_multi_nvidia = int(row.get('failed_multi_no_nvidia', 0)) if pd.notna(row.get('failed_multi_no_nvidia', 0)) else 0
92
+ failed_single_amd = int(row.get('failed_single_no_amd', 0)) if pd.notna(row.get('failed_single_no_amd', 0)) else 0
93
+ failed_single_nvidia = int(row.get('failed_single_no_nvidia', 0)) if pd.notna(row.get('failed_single_no_nvidia', 0)) else 0
94
+
95
+ # Calculate total failures
96
+ total_failed_amd = failed_multi_amd + failed_single_amd
97
+ total_failed_nvidia = failed_multi_nvidia + failed_single_nvidia
98
+
99
+ # Softer color palette - less pastel, more vibrant
100
+ colors = {
101
+ 'passed': '#4CAF50', # Medium green
102
+ 'failed': '#E53E3E', # More red
103
+ 'skipped': '#FFD54F', # Medium yellow
104
+ 'error': '#8B0000' # Dark red
105
+ }
106
+
107
+ # Create stats dictionaries directly from dataframe values
108
+ amd_stats = {
109
+ 'passed': success_amd,
110
+ 'failed': total_failed_amd,
111
+ 'skipped': 0, # Not available in this dataset
112
+ 'error': 0 # Not available in this dataset
113
+ }
114
+
115
+ nvidia_stats = {
116
+ 'passed': success_nvidia,
117
+ 'failed': total_failed_nvidia,
118
+ 'skipped': 0, # Not available in this dataset
119
+ 'error': 0 # Not available in this dataset
120
+ }
121
+
122
+ # Filter out categories with 0 values for cleaner visualization
123
+ amd_filtered = {k: v for k, v in amd_stats.items() if v > 0}
124
+ nvidia_filtered = {k: v for k, v in nvidia_stats.items() if v > 0}
125
+
126
+ if not amd_filtered and not nvidia_filtered:
127
+ # Handle case where all values are 0 - minimal empty state
128
+ fig, ax = plt.subplots(figsize=(10, 8), facecolor='#000000')
129
+ ax.set_facecolor('#000000')
130
+ ax.text(0.5, 0.5, 'No test results available',
131
+ horizontalalignment='center', verticalalignment='center',
132
+ transform=ax.transAxes, fontsize=16, color='#888888',
133
+ fontfamily='monospace', weight='normal')
134
+ ax.set_xlim(0, 1)
135
+ ax.set_ylim(0, 1)
136
+ ax.axis('off')
137
+ return fig, "", ""
138
+
139
+ # Create figure with two subplots side by side with padding
140
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 9), facecolor='#000000')
141
+ ax1.set_facecolor('#000000')
142
+ ax2.set_facecolor('#000000')
143
+
144
+ def create_pie_chart(ax, device_label, filtered_stats):
145
+ if not filtered_stats:
146
+ ax.text(0.5, 0.5, 'No test results',
147
+ horizontalalignment='center', verticalalignment='center',
148
+ transform=ax.transAxes, fontsize=14, color='#888888',
149
+ fontfamily='monospace', weight='normal')
150
+ ax.set_title(device_label,
151
+ fontsize=28, weight='bold', pad=2, color='#FFFFFF',
152
+ fontfamily='monospace')
153
+ ax.axis('off')
154
+ return
155
+
156
+ chart_colors = [colors[category] for category in filtered_stats.keys()]
157
+
158
+ # Create minimal pie chart - full pie, no donut effect
159
+ wedges, texts, autotexts = ax.pie(
160
+ filtered_stats.values(),
161
+ labels=[label.lower() for label in filtered_stats.keys()], # Lowercase for minimal look
162
+ colors=chart_colors,
163
+ autopct=lambda pct: f'{int(pct/100*sum(filtered_stats.values()))}',
164
+ startangle=90,
165
+ explode=None, # No separation
166
+ shadow=False,
167
+ wedgeprops=dict(edgecolor='#1a1a1a', linewidth=0.5), # Minimal borders
168
+ textprops={'fontsize': 12, 'weight': 'normal', 'color': '#CCCCCC', 'fontfamily': 'monospace'}
169
+ )
170
+
171
+ # Enhanced percentage text styling for better readability
172
+ for autotext in autotexts:
173
+ autotext.set_color('#000000') # Black text for better contrast
174
+ autotext.set_weight('bold')
175
+ autotext.set_fontsize(14)
176
+ autotext.set_fontfamily('monospace')
177
+
178
+ # Minimal category labels
179
+ for text in texts:
180
+ text.set_color('#AAAAAA')
181
+ text.set_weight('normal')
182
+ text.set_fontsize(13)
183
+ text.set_fontfamily('monospace')
184
+
185
+ # Device label closer to chart and bigger
186
+ ax.set_title(device_label,
187
+ fontsize=28, weight='normal', pad=2, color='#FFFFFF',
188
+ fontfamily='monospace')
189
+
190
+ # Create both pie charts with device labels
191
+ create_pie_chart(ax1, "amd", amd_filtered)
192
+ create_pie_chart(ax2, "nvidia", nvidia_filtered)
193
+
194
+ # Add subtle separation line between charts - stops at device labels level
195
+ line_x = 0.5
196
+ fig.add_artist(plt.Line2D([line_x, line_x], [0.0, 0.85],
197
+ color='#333333', linewidth=1, alpha=0.5,
198
+ transform=fig.transFigure))
199
+
200
+ # Add central shared title for model name
201
+ fig.suptitle(f'{model_name.lower()}',
202
+ fontsize=32, weight='bold', color='#CCCCCC',
203
+ fontfamily='monospace', y=1)
204
+
205
+ # Clean layout with padding and space for central title
206
+ plt.tight_layout()
207
+ plt.subplots_adjust(top=0.85, wspace=0.4) # Added wspace for padding between charts
208
+
209
+ # Generate failure info directly from dataframe
210
+ failures_amd = row.get('failures_amd', {})
211
+ failures_nvidia = row.get('failures_nvidia', {})
212
+
213
+ amd_failed_info = extract_failure_info(failures_amd, 'AMD', failed_multi_amd, failed_single_amd)
214
+ nvidia_failed_info = extract_failure_info(failures_nvidia, 'NVIDIA', failed_multi_nvidia, failed_single_nvidia)
215
+
216
+ return fig, amd_failed_info, nvidia_failed_info