QSBench commited on
Commit
a4d74cf
Β·
verified Β·
1 Parent(s): ffca061

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +320 -66
app.py CHANGED
@@ -2,7 +2,6 @@ import ast
2
  import logging
3
  import re
4
  from typing import Dict, List, Optional, Tuple
5
-
6
  import gradio as gr
7
  import matplotlib.pyplot as plt
8
  import numpy as np
@@ -24,22 +23,47 @@ APP_SUBTITLE = (
24
  )
25
 
26
  REPO_CONFIG = {
27
- "clean": {"label": "clean", "repo": "QSBench/QSBench-Core-v1.0.0-demo"},
28
- "depolarizing": {"label": "depolarizing", "repo": "QSBench/QSBench-Depolarizing-Demo-v1.0.0"},
29
- "amplitude_damping": {"label": "amplitude_damping", "repo": "QSBench/QSBench-Amplitude-v1.0.0-demo"},
30
- "hardware_aware": {"label": "hardware_aware", "repo": "QSBench/QSBench-Transpilation-v1.0.0-demo"},
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
 
33
  CLASS_ORDER = ["clean", "depolarizing", "amplitude_damping", "hardware_aware"]
34
 
35
  NON_FEATURE_COLS = {
36
- "sample_id", "sample_seed", "circuit_hash", "split",
37
- "circuit_qasm", "qasm_raw", "qasm_transpiled",
38
- "circuit_type_resolved", "circuit_type_requested",
39
- "noise_type", "noise_prob", "observable_bases",
40
- "observable_mode", "backend_device", "precision_mode",
41
- "circuit_signature", "entanglement", "meyer_wallach",
42
- "cx_count", "noise_label",
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
 
45
  SOFT_EXCLUDE_PATTERNS = ["ideal_", "noisy_", "error_", "sign_ideal_", "sign_noisy_"]
@@ -49,6 +73,7 @@ _COMBINED_CACHE: Optional[pd.DataFrame] = None
49
 
50
 
51
  def safe_parse(value):
 
52
  if isinstance(value, str):
53
  try:
54
  return ast.literal_eval(value)
@@ -58,9 +83,15 @@ def safe_parse(value):
58
 
59
 
60
  def adjacency_features(adj_value) -> Dict[str, float]:
 
61
  parsed = safe_parse(adj_value)
62
  if not isinstance(parsed, list) or len(parsed) == 0:
63
- return {"adj_edge_count": np.nan, "adj_density": np.nan, "adj_degree_mean": np.nan, "adj_degree_std": np.nan}
 
 
 
 
 
64
  try:
65
  arr = np.array(parsed, dtype=float)
66
  n = arr.shape[0]
@@ -75,17 +106,31 @@ def adjacency_features(adj_value) -> Dict[str, float]:
75
  "adj_degree_std": float(np.std(degrees)),
76
  }
77
  except Exception:
78
- return {"adj_edge_count": np.nan, "adj_density": np.nan, "adj_degree_mean": np.nan, "adj_degree_std": np.nan}
 
 
 
 
 
79
 
80
 
81
  def qasm_features(qasm_value) -> Dict[str, float]:
 
82
  if not isinstance(qasm_value, str) or not qasm_value.strip():
83
- return {"qasm_length": np.nan, "qasm_line_count": np.nan, "qasm_gate_keyword_count": np.nan,
84
- "qasm_measure_count": np.nan, "qasm_comment_count": np.nan}
 
 
 
 
 
85
  text = qasm_value
86
  lines = [line for line in text.splitlines() if line.strip()]
87
- gate_keywords = re.findall(r"\b(cx|h|x|y|z|rx|ry|rz|u1|u2|u3|u|swap|cz|ccx|rxx|ryy|rzz)\b",
88
- text, flags=re.IGNORECASE)
 
 
 
89
  measure_count = len(re.findall(r"\bmeasure\b", text, flags=re.IGNORECASE))
90
  comment_count = sum(1 for line in lines if line.strip().startswith("//"))
91
  return {
@@ -98,6 +143,7 @@ def qasm_features(qasm_value) -> Dict[str, float]:
98
 
99
 
100
  def enrich_dataframe(df: pd.DataFrame) -> pd.DataFrame:
 
101
  df = df.copy()
102
  if "adjacency" in df.columns:
103
  adj_df = df["adjacency"].apply(adjacency_features).apply(pd.Series)
@@ -110,7 +156,9 @@ def enrich_dataframe(df: pd.DataFrame) -> pd.DataFrame:
110
 
111
 
112
  def load_single_dataset(dataset_key: str) -> pd.DataFrame:
 
113
  if dataset_key not in _ASSET_CACHE:
 
114
  ds = load_dataset(REPO_CONFIG[dataset_key]["repo"])
115
  df = pd.DataFrame(ds["train"])
116
  df = enrich_dataframe(df)
@@ -120,29 +168,66 @@ def load_single_dataset(dataset_key: str) -> pd.DataFrame:
120
 
121
 
122
  def load_combined_dataset() -> pd.DataFrame:
 
123
  global _COMBINED_CACHE
124
  if _COMBINED_CACHE is None:
125
- frames = [load_single_dataset(k) for k in REPO_CONFIG.keys()]
126
  combined = pd.concat(frames, ignore_index=True)
127
  combined = combined[combined["noise_label"].isin(CLASS_ORDER)].copy()
128
  _COMBINED_CACHE = combined
129
  return _COMBINED_CACHE
130
 
131
 
 
 
 
 
 
 
 
 
 
132
  def get_available_feature_columns(df: pd.DataFrame) -> List[str]:
 
133
  numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
134
- features = [col for col in numeric_cols if col not in NON_FEATURE_COLS
135
- and all(pattern not in col for pattern in SOFT_EXCLUDE_PATTERNS)]
 
 
 
 
 
136
  return sorted(features)
137
 
138
 
139
  def default_feature_selection(features: List[str]) -> List[str]:
140
- preferred = ["gate_entropy", "adj_density", "adj_degree_mean", "adj_degree_std",
141
- "depth", "total_gates", "cx_count", "qasm_length"]
142
- return [f for f in preferred if f in features]
143
-
144
-
145
- def make_classification_figure(y_true, y_pred, class_names, feature_names=None, importances=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  fig = plt.figure(figsize=(20, 6))
147
  gs = fig.add_gridspec(1, 3)
148
  ax1 = fig.add_subplot(gs[0, 0])
@@ -150,7 +235,7 @@ def make_classification_figure(y_true, y_pred, class_names, feature_names=None,
150
  ax3 = fig.add_subplot(gs[0, 2])
151
 
152
  cm = confusion_matrix(y_true, y_pred, labels=class_names)
153
- im = ax1.imshow(cm, interpolation="nearest")
154
  ax1.set_title("Confusion Matrix")
155
  ax1.set_xlabel("Predicted")
156
  ax1.set_ylabel("Actual")
@@ -161,7 +246,7 @@ def make_classification_figure(y_true, y_pred, class_names, feature_names=None,
161
  for i in range(cm.shape[0]):
162
  for j in range(cm.shape[1]):
163
  ax1.text(j, i, cm[i, j], ha="center", va="center")
164
- fig.colorbar(im, ax=ax1, fraction=0.046, pad=0.04)
165
 
166
  incorrect = (y_true != y_pred).astype(int)
167
  ax2.hist(incorrect, bins=[-0.5, 0.5, 1.5])
@@ -175,64 +260,233 @@ def make_classification_figure(y_true, y_pred, class_names, feature_names=None,
175
  ax3.set_title("Top-10 Feature Importances")
176
  ax3.set_xlabel("Importance")
177
  else:
178
- ax3.text(0.5, 0.5, "Feature importances unavailable", ha="center", va="center")
179
  ax3.set_axis_off()
180
 
181
  fig.tight_layout()
182
  return fig
183
 
184
 
185
- def train_classifier(feature_columns, test_size, max_depth, random_state, n_estimators=200):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  if not feature_columns:
187
  return None, "### ❌ Please select at least one feature."
 
188
  df = load_combined_dataset()
189
- df = df.dropna(subset=feature_columns + ["noise_label"])
190
- X = df[feature_columns]
191
- y = df["noise_label"]
192
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=int(random_state),
193
- stratify=y)
194
- model = Pipeline([
195
- ("imputer", SimpleImputer(strategy="median")),
196
- ("scaler", StandardScaler()),
197
- ("classifier", HistGradientBoostingClassifier(
198
- max_depth=int(max_depth),
199
- max_iter=int(n_estimators),
200
- random_state=int(random_state),
201
- learning_rate=0.05,
202
- ))
203
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  model.fit(X_train, y_train)
205
  y_pred = model.predict(X_test)
 
 
 
 
 
206
  classifier = model.named_steps["classifier"]
207
  importances = getattr(classifier, "feature_importances_", None)
208
- fig = make_classification_figure(y_test.to_numpy(), y_pred, CLASS_ORDER, feature_columns, importances)
209
- report = classification_report(y_test, y_pred, labels=CLASS_ORDER)
210
- results = f"### Classification report\n```\n{report}\n```"
211
- return fig, results
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
- CUSTOM_CSS = ".gradio-container {max-width: 1400px !important;}"
215
 
216
 
 
 
 
 
 
 
 
 
 
217
  with gr.Blocks(title=APP_TITLE) as demo:
218
  gr.Markdown(f"# 🌌 {APP_TITLE}")
219
  gr.Markdown(APP_SUBTITLE)
220
 
221
- with gr.TabItem("🧠 Classification"):
222
- feature_picker = gr.CheckboxGroup(label="Input features", choices=[])
223
- test_size = gr.Slider(0.1, 0.4, value=0.2, step=0.05, label="Test split")
224
- max_depth = gr.Slider(1, 30, value=5, step=1, label="Max depth")
225
- seed = gr.Number(value=42, precision=0, label="Random seed")
226
- n_estimators = gr.Slider(50, 400, value=200, step=10, label="Iterations")
227
- run_btn = gr.Button("Train & Evaluate", variant="primary")
228
- plot = gr.Plot()
229
- metrics = gr.Markdown()
230
-
231
- dataset_dropdown = gr.Dropdown(list(REPO_CONFIG.keys()), value="clean", label="Dataset")
232
- dataset_dropdown.change(lambda _: gr.update(choices=default_feature_selection(get_available_feature_columns(load_combined_dataset()))),
233
- [], [feature_picker])
234
-
235
- run_btn.click(train_classifier, [feature_picker, test_size, max_depth, seed, n_estimators], [plot, metrics])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
  if __name__ == "__main__":
238
  demo.launch(theme=gr.themes.Soft(), css=CUSTOM_CSS)
 
2
  import logging
3
  import re
4
  from typing import Dict, List, Optional, Tuple
 
5
  import gradio as gr
6
  import matplotlib.pyplot as plt
7
  import numpy as np
 
23
  )
24
 
25
  REPO_CONFIG = {
26
+ "clean": {
27
+ "label": "clean",
28
+ "repo": "QSBench/QSBench-Core-v1.0.0-demo",
29
+ },
30
+ "depolarizing": {
31
+ "label": "depolarizing",
32
+ "repo": "QSBench/QSBench-Depolarizing-Demo-v1.0.0",
33
+ },
34
+ "amplitude_damping": {
35
+ "label": "amplitude_damping",
36
+ "repo": "QSBench/QSBench-Amplitude-v1.0.0-demo",
37
+ },
38
+ "hardware_aware": {
39
+ "label": "hardware_aware",
40
+ "repo": "QSBench/QSBench-Transpilation-v1.0.0-demo",
41
+ },
42
  }
43
 
44
  CLASS_ORDER = ["clean", "depolarizing", "amplitude_damping", "hardware_aware"]
45
 
46
  NON_FEATURE_COLS = {
47
+ "sample_id",
48
+ "sample_seed",
49
+ "circuit_hash",
50
+ "split",
51
+ "circuit_qasm",
52
+ "qasm_raw",
53
+ "qasm_transpiled",
54
+ "circuit_type_resolved",
55
+ "circuit_type_requested",
56
+ "noise_type",
57
+ "noise_prob",
58
+ "observable_bases",
59
+ "observable_mode",
60
+ "backend_device",
61
+ "precision_mode",
62
+ "circuit_signature",
63
+ "entanglement",
64
+ "meyer_wallach",
65
+ "cx_count",
66
+ "noise_label",
67
  }
68
 
69
  SOFT_EXCLUDE_PATTERNS = ["ideal_", "noisy_", "error_", "sign_ideal_", "sign_noisy_"]
 
73
 
74
 
75
  def safe_parse(value):
76
+ """Safely parse stringified Python literals."""
77
  if isinstance(value, str):
78
  try:
79
  return ast.literal_eval(value)
 
83
 
84
 
85
  def adjacency_features(adj_value) -> Dict[str, float]:
86
+ """Derive graph statistics from an adjacency matrix."""
87
  parsed = safe_parse(adj_value)
88
  if not isinstance(parsed, list) or len(parsed) == 0:
89
+ return {
90
+ "adj_edge_count": np.nan,
91
+ "adj_density": np.nan,
92
+ "adj_degree_mean": np.nan,
93
+ "adj_degree_std": np.nan,
94
+ }
95
  try:
96
  arr = np.array(parsed, dtype=float)
97
  n = arr.shape[0]
 
106
  "adj_degree_std": float(np.std(degrees)),
107
  }
108
  except Exception:
109
+ return {
110
+ "adj_edge_count": np.nan,
111
+ "adj_density": np.nan,
112
+ "adj_degree_mean": np.nan,
113
+ "adj_degree_std": np.nan,
114
+ }
115
 
116
 
117
  def qasm_features(qasm_value) -> Dict[str, float]:
118
+ """Extract lightweight text statistics from QASM."""
119
  if not isinstance(qasm_value, str) or not qasm_value.strip():
120
+ return {
121
+ "qasm_length": np.nan,
122
+ "qasm_line_count": np.nan,
123
+ "qasm_gate_keyword_count": np.nan,
124
+ "qasm_measure_count": np.nan,
125
+ "qasm_comment_count": np.nan,
126
+ }
127
  text = qasm_value
128
  lines = [line for line in text.splitlines() if line.strip()]
129
+ gate_keywords = re.findall(
130
+ r"\b(cx|h|x|y|z|rx|ry|rz|u1|u2|u3|u|swap|cz|ccx|rxx|ryy|rzz)\b",
131
+ text,
132
+ flags=re.IGNORECASE,
133
+ )
134
  measure_count = len(re.findall(r"\bmeasure\b", text, flags=re.IGNORECASE))
135
  comment_count = sum(1 for line in lines if line.strip().startswith("//"))
136
  return {
 
143
 
144
 
145
  def enrich_dataframe(df: pd.DataFrame) -> pd.DataFrame:
146
+ """Add derived numeric features for classification."""
147
  df = df.copy()
148
  if "adjacency" in df.columns:
149
  adj_df = df["adjacency"].apply(adjacency_features).apply(pd.Series)
 
156
 
157
 
158
  def load_single_dataset(dataset_key: str) -> pd.DataFrame:
159
+ """Load a dataset shard from Hugging Face and cache it in memory."""
160
  if dataset_key not in _ASSET_CACHE:
161
+ logger.info("Loading dataset: %s", dataset_key)
162
  ds = load_dataset(REPO_CONFIG[dataset_key]["repo"])
163
  df = pd.DataFrame(ds["train"])
164
  df = enrich_dataframe(df)
 
168
 
169
 
170
  def load_combined_dataset() -> pd.DataFrame:
171
+ """Load and merge all four noise-condition datasets."""
172
  global _COMBINED_CACHE
173
  if _COMBINED_CACHE is None:
174
+ frames = [load_single_dataset(key) for key in REPO_CONFIG.keys()]
175
  combined = pd.concat(frames, ignore_index=True)
176
  combined = combined[combined["noise_label"].isin(CLASS_ORDER)].copy()
177
  _COMBINED_CACHE = combined
178
  return _COMBINED_CACHE
179
 
180
 
181
+ def load_guide_content() -> str:
182
+ """Load the markdown guide if it exists."""
183
+ try:
184
+ with open("GUIDE.md", "r", encoding="utf-8") as f:
185
+ return f.read()
186
+ except FileNotFoundError:
187
+ return "# Guide\n\nGuide file not found."
188
+
189
+
190
  def get_available_feature_columns(df: pd.DataFrame) -> List[str]:
191
+ """Return numeric feature columns excluding metadata and target columns."""
192
  numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
193
+ features = []
194
+ for col in numeric_cols:
195
+ if col in NON_FEATURE_COLS:
196
+ continue
197
+ if any(pattern in col for pattern in SOFT_EXCLUDE_PATTERNS):
198
+ continue
199
+ features.append(col)
200
  return sorted(features)
201
 
202
 
203
  def default_feature_selection(features: List[str]) -> List[str]:
204
+ """Select a stable default feature subset."""
205
+ preferred = [
206
+ "gate_entropy",
207
+ "adj_density",
208
+ "adj_degree_mean",
209
+ "adj_degree_std",
210
+ "depth",
211
+ "total_gates",
212
+ "single_qubit_gates",
213
+ "two_qubit_gates",
214
+ "cx_count",
215
+ "qasm_length",
216
+ "qasm_line_count",
217
+ "qasm_gate_keyword_count",
218
+ ]
219
+ selected = [feature for feature in preferred if feature in features]
220
+ return selected[:8] if selected else features[:8]
221
+
222
+
223
+ def make_classification_figure(
224
+ y_true: np.ndarray,
225
+ y_pred: np.ndarray,
226
+ class_names: List[str],
227
+ feature_names: Optional[List[str]] = None,
228
+ importances: Optional[np.ndarray] = None,
229
+ ) -> plt.Figure:
230
+ """Create a compact classification summary figure."""
231
  fig = plt.figure(figsize=(20, 6))
232
  gs = fig.add_gridspec(1, 3)
233
  ax1 = fig.add_subplot(gs[0, 0])
 
235
  ax3 = fig.add_subplot(gs[0, 2])
236
 
237
  cm = confusion_matrix(y_true, y_pred, labels=class_names)
238
+ image = ax1.imshow(cm, interpolation="nearest")
239
  ax1.set_title("Confusion Matrix")
240
  ax1.set_xlabel("Predicted")
241
  ax1.set_ylabel("Actual")
 
246
  for i in range(cm.shape[0]):
247
  for j in range(cm.shape[1]):
248
  ax1.text(j, i, cm[i, j], ha="center", va="center")
249
+ fig.colorbar(image, ax=ax1, fraction=0.046, pad=0.04)
250
 
251
  incorrect = (y_true != y_pred).astype(int)
252
  ax2.hist(incorrect, bins=[-0.5, 0.5, 1.5])
 
260
  ax3.set_title("Top-10 Feature Importances")
261
  ax3.set_xlabel("Importance")
262
  else:
263
+ ax3.text(0.5, 0.5, "Feature importances are unavailable.", ha="center", va="center")
264
  ax3.set_axis_off()
265
 
266
  fig.tight_layout()
267
  return fig
268
 
269
 
270
+ def build_dataset_profile(df: pd.DataFrame) -> str:
271
+ """Build a short dataset summary for the explorer tab."""
272
+ return (
273
+ f"### Dataset profile\n\n"
274
+ f"**Rows:** {len(df):,} \n"
275
+ f"**Columns:** {len(df.columns):,} \n"
276
+ f"**Classes:** {', '.join(CLASS_ORDER)}"
277
+ )
278
+
279
+
280
+ def refresh_explorer(dataset_key: str, split_name: str) -> Tuple[gr.update, pd.DataFrame, str, str, str, str]:
281
+ """Refresh the explorer view for the selected source dataset."""
282
+ df = load_single_dataset(dataset_key)
283
+ splits = df["split"].dropna().unique().tolist() if "split" in df.columns else ["train"]
284
+ if not splits:
285
+ splits = ["train"]
286
+ if split_name not in splits:
287
+ split_name = splits[0]
288
+ filtered = df[df["split"] == split_name] if "split" in df.columns else df
289
+ display_df = filtered.head(12).copy()
290
+ raw_qasm = display_df["qasm_raw"].iloc[0] if "qasm_raw" in display_df.columns and not display_df.empty else "// N/A"
291
+ transpiled_qasm = display_df["qasm_transpiled"].iloc[0] if "qasm_transpiled" in display_df.columns and not display_df.empty else "// N/A"
292
+ profile_box = build_dataset_profile(df)
293
+ summary_box = (
294
+ f"### Split summary\n\n"
295
+ f"**Dataset:** `{dataset_key}` \n"
296
+ f"**Label:** `{REPO_CONFIG[dataset_key]['label']}` \n"
297
+ f"**Available splits:** {', '.join(splits)} \n"
298
+ f"**Preview rows:** {len(display_df)}"
299
+ )
300
+ return (
301
+ gr.update(choices=splits, value=split_name),
302
+ display_df,
303
+ raw_qasm,
304
+ transpiled_qasm,
305
+ profile_box,
306
+ summary_box,
307
+ )
308
+
309
+
310
+ def sync_feature_picker(_dataset_key: str) -> gr.update:
311
+ """Refresh the feature list from the combined dataset."""
312
+ df = load_combined_dataset()
313
+ features = get_available_feature_columns(df)
314
+ defaults = default_feature_selection(features)
315
+ return gr.update(choices=features, value=defaults)
316
+
317
+
318
+ def train_classifier(
319
+ feature_columns: List[str],
320
+ test_size: float,
321
+ n_estimators: int,
322
+ max_depth: float,
323
+ random_state: float,
324
+ ) -> Tuple[Optional[plt.Figure], str]:
325
+ """Train a four-class classifier and return metrics plus a plot."""
326
  if not feature_columns:
327
  return None, "### ❌ Please select at least one feature."
328
+
329
  df = load_combined_dataset()
330
+ required_cols = feature_columns + ["noise_label"]
331
+ train_df = df.dropna(subset=required_cols).copy()
332
+ train_df = train_df[train_df["noise_label"].isin(CLASS_ORDER)]
333
+
334
+ if len(train_df) < 20:
335
+ return None, "### ❌ Not enough clean rows after filtering missing values."
336
+
337
+ X = train_df[feature_columns]
338
+ y = train_df["noise_label"]
339
+
340
+ seed = int(random_state)
341
+ depth = int(max_depth) if max_depth and int(max_depth) > 0 else None
342
+ trees = int(n_estimators)
343
+
344
+ try:
345
+ X_train, X_test, y_train, y_test = train_test_split(
346
+ X,
347
+ y,
348
+ test_size=test_size,
349
+ random_state=seed,
350
+ stratify=y,
351
+ )
352
+ except ValueError:
353
+ X_train, X_test, y_train, y_test = train_test_split(
354
+ X,
355
+ y,
356
+ test_size=test_size,
357
+ random_state=seed,
358
+ )
359
+
360
+ model = Pipeline(
361
+ steps=[
362
+ ("imputer", SimpleImputer(strategy="median")),
363
+ ("scaler", StandardScaler()),
364
+ (
365
+ "classifier",
366
+ HistGradientBoostingClassifier(
367
+ max_iter=trees,
368
+ max_depth=depth,
369
+ random_state=seed,
370
+ min_samples_leaf=1,
371
+ ),
372
+ ),
373
+ ]
374
+ )
375
+
376
  model.fit(X_train, y_train)
377
  y_pred = model.predict(X_test)
378
+
379
+ accuracy = float(accuracy_score(y_test, y_pred))
380
+ macro_f1 = float(f1_score(y_test, y_pred, average="macro", zero_division=0))
381
+ weighted_f1 = float(f1_score(y_test, y_pred, average="weighted", zero_division=0))
382
+
383
  classifier = model.named_steps["classifier"]
384
  importances = getattr(classifier, "feature_importances_", None)
 
 
 
 
385
 
386
+ fig = make_classification_figure(y_test.to_numpy(), y_pred, CLASS_ORDER, list(feature_columns), importances)
387
+
388
+ report = classification_report(
389
+ y_test,
390
+ y_pred,
391
+ labels=CLASS_ORDER,
392
+ zero_division=0,
393
+ )
394
+
395
+ results = (
396
+ "### Classification results\n\n"
397
+ f"**Rows used:** {len(train_df):,} \n"
398
+ f"**Test size:** {test_size:.0%} \n"
399
+ f"**Accuracy:** {accuracy:.4f} \n"
400
+ f"**Macro F1:** {macro_f1:.4f} \n"
401
+ f"**Weighted F1:** {weighted_f1:.4f}\n\n"
402
+ "```text\n"
403
+ f"{report}"
404
+ "```"
405
+ )
406
 
407
+ return fig, results
408
 
409
 
410
+ CUSTOM_CSS = """
411
+ .gradio-container {
412
+ max-width: 1400px !important;
413
+ }
414
+ footer {
415
+ margin-top: 1rem;
416
+ }
417
+ """
418
+
419
  with gr.Blocks(title=APP_TITLE) as demo:
420
  gr.Markdown(f"# 🌌 {APP_TITLE}")
421
  gr.Markdown(APP_SUBTITLE)
422
 
423
+ with gr.Tabs():
424
+ with gr.TabItem("πŸ”Ž Explorer"):
425
+ dataset_dropdown = gr.Dropdown(
426
+ list(REPO_CONFIG.keys()),
427
+ value="clean",
428
+ label="Dataset",
429
+ )
430
+ split_dropdown = gr.Dropdown(
431
+ ["train"],
432
+ value="train",
433
+ label="Split",
434
+ )
435
+ profile_box = gr.Markdown(value="### Loading dataset...")
436
+ summary_box = gr.Markdown(value="### Loading split summary...")
437
+ explorer_df = gr.Dataframe(label="Preview", interactive=False)
438
+
439
+ with gr.Row():
440
+ raw_qasm = gr.Code(label="Raw QASM", language=None)
441
+ transpiled_qasm = gr.Code(label="Transpiled QASM", language=None)
442
+
443
+ with gr.TabItem("🧠 Classification"):
444
+ feature_picker = gr.CheckboxGroup(label="Input features", choices=[])
445
+ test_size = gr.Slider(0.1, 0.4, value=0.2, step=0.05, label="Test split")
446
+ n_estimators = gr.Slider(50, 400, value=200, step=10, label="Trees")
447
+ max_depth = gr.Slider(1, 30, value=12, step=1, label="Max depth")
448
+ seed = gr.Number(value=42, precision=0, label="Random seed")
449
+ run_btn = gr.Button("Train & Evaluate", variant="primary")
450
+ plot = gr.Plot()
451
+ metrics = gr.Markdown()
452
+
453
+ with gr.TabItem("πŸ“– Guide"):
454
+ gr.Markdown(load_guide_content())
455
+
456
+ gr.Markdown("---")
457
+ gr.Markdown(
458
+ "### πŸ”— Links\n"
459
+ "[Website](https://qsbench.github.io) | "
460
+ "[Hugging Face](https://huggingface.co/QSBench) | "
461
+ "[GitHub](https://github.com/QSBench)"
462
+ )
463
+
464
+ dataset_dropdown.change(
465
+ refresh_explorer,
466
+ [dataset_dropdown, split_dropdown],
467
+ [split_dropdown, explorer_df, raw_qasm, transpiled_qasm, profile_box, summary_box],
468
+ )
469
+ split_dropdown.change(
470
+ refresh_explorer,
471
+ [dataset_dropdown, split_dropdown],
472
+ [split_dropdown, explorer_df, raw_qasm, transpiled_qasm, profile_box, summary_box],
473
+ )
474
+
475
+ dataset_dropdown.change(sync_feature_picker, [dataset_dropdown], [feature_picker])
476
+
477
+ run_btn.click(
478
+ train_classifier,
479
+ [feature_picker, test_size, n_estimators, max_depth, seed],
480
+ [plot, metrics],
481
+ )
482
+
483
+ demo.load(
484
+ refresh_explorer,
485
+ [dataset_dropdown, split_dropdown],
486
+ [split_dropdown, explorer_df, raw_qasm, transpiled_qasm, profile_box, summary_box],
487
+ )
488
+ demo.load(sync_feature_picker, [dataset_dropdown], [feature_picker])
489
+
490
 
491
  if __name__ == "__main__":
492
  demo.launch(theme=gr.themes.Soft(), css=CUSTOM_CSS)