QSBench commited on
Commit
eea8163
Β·
verified Β·
1 Parent(s): 3af83b8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -70
app.py CHANGED
@@ -8,7 +8,6 @@ import matplotlib.pyplot as plt
8
  import numpy as np
9
  import pandas as pd
10
  from datasets import load_dataset
11
-
12
  from sklearn.cluster import KMeans
13
  from sklearn.decomposition import PCA
14
  from sklearn.impute import SimpleImputer
@@ -19,7 +18,6 @@ from sklearn.preprocessing import StandardScaler
19
  logging.basicConfig(level=logging.INFO)
20
  logger = logging.getLogger(__name__)
21
 
22
- # ========================= CONFIG =========================
23
  APP_TITLE = "Circuit Complexity Clustering"
24
  APP_SUBTITLE = (
25
  "Unsupervised grouping of quantum circuits by structural complexity "
@@ -34,16 +32,31 @@ REPO_CONFIG = {
34
  }
35
 
36
  NON_FEATURE_COLS = {
37
- "sample_id", "sample_seed", "circuit_hash", "split", "circuit_qasm",
38
- "qasm_raw", "qasm_transpiled", "circuit_type_resolved", "circuit_type_requested",
39
- "noise_type", "noise_prob", "observable_bases", "observable_mode",
40
- "backend_device", "precision_mode", "circuit_signature",
41
- "entanglement", "meyer_wallach", "noise_label",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
 
44
  SOFT_EXCLUDE_PATTERNS = ["ideal_", "noisy_", "error_", "sign_ideal_", "sign_noisy_"]
45
 
46
  _ASSET_CACHE: Dict[str, pd.DataFrame] = {}
 
47
 
48
 
49
  def safe_parse(value):
@@ -140,6 +153,18 @@ def load_dataset_df(dataset_key: str) -> pd.DataFrame:
140
  return _ASSET_CACHE[dataset_key]
141
 
142
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  def load_guide_content() -> str:
144
  """Load the markdown guide if it exists."""
145
  try:
@@ -165,52 +190,130 @@ def get_available_feature_columns(df: pd.DataFrame) -> List[str]:
165
  def default_feature_selection(features: List[str]) -> List[str]:
166
  """Select a stable default feature subset."""
167
  preferred = [
168
- "gate_entropy", "adj_density", "adj_degree_mean", "adj_degree_std",
169
- "depth", "total_gates", "single_qubit_gates", "two_qubit_gates",
170
- "cx_count", "qasm_length", "qasm_line_count", "qasm_gate_keyword_count",
 
 
 
 
 
 
 
 
 
171
  ]
172
  selected = [feature for feature in preferred if feature in features]
173
  return selected[:10] if selected else features[:10]
174
 
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  def run_clustering(
177
- dataset_key: str,
178
  feature_columns: List[str],
179
  n_clusters: int,
180
  random_state: float,
181
  ) -> Tuple[Optional[plt.Figure], str, pd.DataFrame]:
182
- """Run K-Means clustering and return PCA plot + metrics."""
 
 
 
183
  if not feature_columns:
184
- return None, "### ❌ Please select at least one feature.", None
185
 
186
- df = load_dataset_df(dataset_key)
187
  train_df = df.dropna(subset=feature_columns).copy()
188
 
189
  if len(train_df) < 30:
190
- return None, "### ❌ Not enough rows after filtering missing values.", None
191
 
192
  X = train_df[feature_columns]
193
-
194
- pipeline = Pipeline([
195
- ("imputer", SimpleImputer(strategy="median")),
196
- ("scaler", StandardScaler()),
197
- ("pca", PCA(n_components=2, random_state=int(random_state))),
198
- ("kmeans", KMeans(n_clusters=n_clusters, random_state=int(random_state), n_init=10))
199
- ])
 
 
 
200
 
201
  pipeline.fit(X)
202
  labels = pipeline.named_steps["kmeans"].labels_
203
- pca_coords = pipeline.named_steps["pca"].transform(
204
- pipeline.named_steps["scaler"].transform(
205
- pipeline.named_steps["imputer"].transform(X)
206
- )
207
- )
208
 
209
- sil_score = silhouette_score(X, labels)
 
 
 
 
 
 
 
210
 
211
- # Plot
212
  fig, ax = plt.subplots(figsize=(10, 8))
213
- scatter = ax.scatter(pca_coords[:, 0], pca_coords[:, 1], c=labels, cmap="tab10", s=30, alpha=0.8)
 
 
 
 
 
 
 
214
  ax.set_title(f"Circuit Complexity Clusters (K={n_clusters})")
215
  ax.set_xlabel("PCA Component 1")
216
  ax.set_ylabel("PCA Component 2")
@@ -218,21 +321,29 @@ def run_clustering(
218
  plt.colorbar(scatter, ax=ax, label="Cluster")
219
  plt.tight_layout()
220
 
221
- # Cluster summary
222
  summary = train_df.copy()
223
  summary["cluster"] = labels
224
- cluster_summary = summary.groupby("cluster").size().reset_index()
225
- cluster_summary.columns = ["Cluster", "Number of Circuits"]
 
 
 
 
 
226
 
227
  metrics_text = (
228
  f"### Clustering Results\n\n"
229
- f"**Number of circuits clustered:** {len(train_df):,}\n"
230
- f"**Number of clusters:** {n_clusters}\n"
 
231
  f"**Silhouette Score:** {sil_score:.4f} (closer to 1 = better separation)\n\n"
232
  f"**Cluster sizes:**\n"
233
  f"{cluster_summary.to_markdown(index=False)}"
234
  )
235
 
 
 
 
236
  return fig, metrics_text, cluster_summary
237
 
238
 
@@ -270,14 +381,18 @@ with gr.Blocks(title=APP_TITLE) as demo:
270
  transpiled_qasm = gr.Code(label="Transpiled QASM", language=None)
271
 
272
  with gr.TabItem("🧠 Clustering"):
 
 
 
 
 
273
  feature_picker = gr.CheckboxGroup(label="Input features", choices=[])
274
  n_clusters = gr.Slider(2, 10, value=4, step=1, label="Number of Clusters")
275
  seed = gr.Number(value=42, precision=0, label="Random Seed")
276
  run_btn = gr.Button("πŸš€ Run K-Means Clustering", variant="primary")
277
-
278
  plot = gr.Plot()
279
  metrics = gr.Markdown()
280
- cluster_table = gr.Dataframe(label="Cluster Sizes")
281
 
282
  with gr.TabItem("πŸ“– Guide"):
283
  gr.Markdown(load_guide_content())
@@ -290,35 +405,6 @@ with gr.Blocks(title=APP_TITLE) as demo:
290
  "[GitHub](https://github.com/QSBench)"
291
  )
292
 
293
- # Callbacks
294
- def refresh_explorer(dataset_key: str, split_name: str):
295
- df = load_dataset_df(dataset_key)
296
- splits = df["split"].dropna().unique().tolist() if "split" in df.columns else ["train"]
297
- if not splits:
298
- splits = ["train"]
299
- if split_name not in splits:
300
- split_name = splits[0]
301
- filtered = df[df["split"] == split_name] if "split" in df.columns else df
302
- display_df = filtered.head(12).copy()
303
- raw = display_df["qasm_raw"].iloc[0] if not display_df.empty else "// N/A"
304
- transpiled = display_df["qasm_transpiled"].iloc[0] if not display_df.empty else "// N/A"
305
- profile = f"### Dataset profile\n\n**Rows:** {len(df):,}\n**Columns:** {len(df.columns):,}"
306
- summary = f"### Split summary\n\n**Dataset:** `{dataset_key}`\n**Available splits:** {', '.join(splits)}\n**Preview rows:** {len(display_df)}"
307
- return (
308
- gr.update(choices=splits, value=split_name),
309
- display_df,
310
- raw,
311
- transpiled,
312
- profile,
313
- summary,
314
- )
315
-
316
- def sync_feature_picker(dataset_key: str):
317
- df = load_dataset_df(dataset_key)
318
- features = get_available_feature_columns(df)
319
- defaults = default_feature_selection(features)
320
- return gr.update(choices=features, value=defaults)
321
-
322
  dataset_dropdown.change(
323
  refresh_explorer,
324
  [dataset_dropdown, split_dropdown],
@@ -329,11 +415,12 @@ with gr.Blocks(title=APP_TITLE) as demo:
329
  [dataset_dropdown, split_dropdown],
330
  [split_dropdown, explorer_df, raw_qasm, transpiled_qasm, profile_box, summary_box],
331
  )
332
- dataset_dropdown.change(sync_feature_picker, [dataset_dropdown], [feature_picker])
 
333
 
334
  run_btn.click(
335
  run_clustering,
336
- [dataset_dropdown, feature_picker, n_clusters, seed],
337
  [plot, metrics, cluster_table],
338
  )
339
 
@@ -342,8 +429,8 @@ with gr.Blocks(title=APP_TITLE) as demo:
342
  [dataset_dropdown, split_dropdown],
343
  [split_dropdown, explorer_df, raw_qasm, transpiled_qasm, profile_box, summary_box],
344
  )
345
- demo.load(sync_feature_picker, [dataset_dropdown], [feature_picker])
346
 
347
 
348
  if __name__ == "__main__":
349
- demo.launch(theme=gr.themes.Soft(), css=CUSTOM_CSS)
 
8
  import numpy as np
9
  import pandas as pd
10
  from datasets import load_dataset
 
11
  from sklearn.cluster import KMeans
12
  from sklearn.decomposition import PCA
13
  from sklearn.impute import SimpleImputer
 
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
20
 
 
21
  APP_TITLE = "Circuit Complexity Clustering"
22
  APP_SUBTITLE = (
23
  "Unsupervised grouping of quantum circuits by structural complexity "
 
32
  }
33
 
34
  NON_FEATURE_COLS = {
35
+ "sample_id",
36
+ "sample_seed",
37
+ "circuit_hash",
38
+ "split",
39
+ "circuit_qasm",
40
+ "qasm_raw",
41
+ "qasm_transpiled",
42
+ "circuit_type_resolved",
43
+ "circuit_type_requested",
44
+ "noise_type",
45
+ "noise_prob",
46
+ "observable_bases",
47
+ "observable_mode",
48
+ "backend_device",
49
+ "precision_mode",
50
+ "circuit_signature",
51
+ "entanglement",
52
+ "meyer_wallach",
53
+ "noise_label",
54
  }
55
 
56
  SOFT_EXCLUDE_PATTERNS = ["ideal_", "noisy_", "error_", "sign_ideal_", "sign_noisy_"]
57
 
58
  _ASSET_CACHE: Dict[str, pd.DataFrame] = {}
59
+ _COMBINED_CACHE: Optional[pd.DataFrame] = None
60
 
61
 
62
  def safe_parse(value):
 
153
  return _ASSET_CACHE[dataset_key]
154
 
155
 
156
+ def load_combined_dataset(dataset_keys: List[str]) -> pd.DataFrame:
157
+ """Load and merge selected datasets."""
158
+ global _COMBINED_CACHE
159
+ cache_key = "|".join(sorted(dataset_keys))
160
+ if _COMBINED_CACHE is None or getattr(load_combined_dataset, "_cache_key", None) != cache_key:
161
+ frames = [load_dataset_df(key).assign(source_dataset=key) for key in dataset_keys]
162
+ combined = pd.concat(frames, ignore_index=True)
163
+ _COMBINED_CACHE = combined
164
+ load_combined_dataset._cache_key = cache_key # type: ignore[attr-defined]
165
+ return _COMBINED_CACHE
166
+
167
+
168
  def load_guide_content() -> str:
169
  """Load the markdown guide if it exists."""
170
  try:
 
190
  def default_feature_selection(features: List[str]) -> List[str]:
191
  """Select a stable default feature subset."""
192
  preferred = [
193
+ "gate_entropy",
194
+ "adj_density",
195
+ "adj_degree_mean",
196
+ "adj_degree_std",
197
+ "depth",
198
+ "total_gates",
199
+ "single_qubit_gates",
200
+ "two_qubit_gates",
201
+ "cx_count",
202
+ "qasm_length",
203
+ "qasm_line_count",
204
+ "qasm_gate_keyword_count",
205
  ]
206
  selected = [feature for feature in preferred if feature in features]
207
  return selected[:10] if selected else features[:10]
208
 
209
 
210
+ def build_dataset_profile(df: pd.DataFrame) -> str:
211
+ """Build a short dataset summary for the explorer tab."""
212
+ return (
213
+ f"### Dataset profile\n\n"
214
+ f"**Rows:** {len(df):,} \n"
215
+ f"**Columns:** {len(df.columns):,} \n"
216
+ f"**Available datasets:** {len(REPO_CONFIG)}"
217
+ )
218
+
219
+
220
+ def refresh_explorer(dataset_key: str, split_name: str) -> Tuple[gr.update, pd.DataFrame, str, str, str, str]:
221
+ """Refresh the explorer view for the selected source dataset."""
222
+ df = load_dataset_df(dataset_key)
223
+ splits = df["split"].dropna().unique().tolist() if "split" in df.columns else ["train"]
224
+ if not splits:
225
+ splits = ["train"]
226
+
227
+ if split_name not in splits:
228
+ split_name = splits[0]
229
+
230
+ filtered = df[df["split"] == split_name] if "split" in df.columns else df
231
+ display_df = filtered.head(12).copy()
232
+
233
+ raw_qasm = display_df["qasm_raw"].iloc[0] if "qasm_raw" in display_df.columns and not display_df.empty else "// N/A"
234
+ transpiled_qasm = display_df["qasm_transpiled"].iloc[0] if "qasm_transpiled" in display_df.columns and not display_df.empty else "// N/A"
235
+
236
+ profile_box = build_dataset_profile(df)
237
+ summary_box = (
238
+ f"### Split summary\n\n"
239
+ f"**Dataset:** `{dataset_key}` \n"
240
+ f"**Available splits:** {', '.join(splits)} \n"
241
+ f"**Preview rows:** {len(display_df)}"
242
+ )
243
+
244
+ return (
245
+ gr.update(choices=splits, value=split_name),
246
+ display_df,
247
+ raw_qasm,
248
+ transpiled_qasm,
249
+ profile_box,
250
+ summary_box,
251
+ )
252
+
253
+
254
+ def sync_feature_picker(dataset_keys: List[str]) -> gr.update:
255
+ """Refresh the feature list from the selected datasets."""
256
+ if not dataset_keys:
257
+ return gr.update(choices=[], value=[])
258
+
259
+ df = load_combined_dataset(dataset_keys)
260
+ features = get_available_feature_columns(df)
261
+ defaults = default_feature_selection(features)
262
+ return gr.update(choices=features, value=defaults)
263
+
264
+
265
  def run_clustering(
266
+ dataset_keys: List[str],
267
  feature_columns: List[str],
268
  n_clusters: int,
269
  random_state: float,
270
  ) -> Tuple[Optional[plt.Figure], str, pd.DataFrame]:
271
+ """Run K-Means clustering and return a PCA plot plus metrics."""
272
+ if not dataset_keys:
273
+ return None, "### ❌ Please select at least one dataset.", pd.DataFrame()
274
+
275
  if not feature_columns:
276
+ return None, "### ❌ Please select at least one feature.", pd.DataFrame()
277
 
278
+ df = load_combined_dataset(dataset_keys)
279
  train_df = df.dropna(subset=feature_columns).copy()
280
 
281
  if len(train_df) < 30:
282
+ return None, "### ❌ Not enough rows after filtering missing values.", pd.DataFrame()
283
 
284
  X = train_df[feature_columns]
285
+ seed = int(random_state)
286
+
287
+ pipeline = Pipeline(
288
+ steps=[
289
+ ("imputer", SimpleImputer(strategy="median")),
290
+ ("scaler", StandardScaler()),
291
+ ("pca", PCA(n_components=2, random_state=seed)),
292
+ ("kmeans", KMeans(n_clusters=n_clusters, random_state=seed, n_init=10)),
293
+ ]
294
+ )
295
 
296
  pipeline.fit(X)
297
  labels = pipeline.named_steps["kmeans"].labels_
 
 
 
 
 
298
 
299
+ transformed = pipeline.named_steps["imputer"].transform(X)
300
+ transformed = pipeline.named_steps["scaler"].transform(transformed)
301
+ pca_coords = pipeline.named_steps["pca"].transform(transformed)
302
+
303
+ try:
304
+ sil_score = silhouette_score(X, labels)
305
+ except Exception:
306
+ sil_score = float("nan")
307
 
 
308
  fig, ax = plt.subplots(figsize=(10, 8))
309
+ scatter = ax.scatter(
310
+ pca_coords[:, 0],
311
+ pca_coords[:, 1],
312
+ c=labels,
313
+ cmap="tab10",
314
+ s=30,
315
+ alpha=0.8,
316
+ )
317
  ax.set_title(f"Circuit Complexity Clusters (K={n_clusters})")
318
  ax.set_xlabel("PCA Component 1")
319
  ax.set_ylabel("PCA Component 2")
 
321
  plt.colorbar(scatter, ax=ax, label="Cluster")
322
  plt.tight_layout()
323
 
 
324
  summary = train_df.copy()
325
  summary["cluster"] = labels
326
+ cluster_summary = summary.groupby("cluster").size().reset_index(name="Number of Circuits")
327
+
328
+ dataset_counts = (
329
+ summary.groupby(["cluster", "source_dataset"]).size().reset_index(name="Count")
330
+ if "source_dataset" in summary.columns
331
+ else pd.DataFrame()
332
+ )
333
 
334
  metrics_text = (
335
  f"### Clustering Results\n\n"
336
+ f"**Datasets used:** {', '.join(dataset_keys)} \n"
337
+ f"**Number of circuits clustered:** {len(train_df):,} \n"
338
+ f"**Number of clusters:** {n_clusters} \n"
339
  f"**Silhouette Score:** {sil_score:.4f} (closer to 1 = better separation)\n\n"
340
  f"**Cluster sizes:**\n"
341
  f"{cluster_summary.to_markdown(index=False)}"
342
  )
343
 
344
+ if not dataset_counts.empty:
345
+ metrics_text += f"\n\n**Dataset composition per cluster:**\n{dataset_counts.to_markdown(index=False)}"
346
+
347
  return fig, metrics_text, cluster_summary
348
 
349
 
 
381
  transpiled_qasm = gr.Code(label="Transpiled QASM", language=None)
382
 
383
  with gr.TabItem("🧠 Clustering"):
384
+ dataset_picker = gr.CheckboxGroup(
385
+ label="Datasets",
386
+ choices=list(REPO_CONFIG.keys()),
387
+ value=list(REPO_CONFIG.keys()),
388
+ )
389
  feature_picker = gr.CheckboxGroup(label="Input features", choices=[])
390
  n_clusters = gr.Slider(2, 10, value=4, step=1, label="Number of Clusters")
391
  seed = gr.Number(value=42, precision=0, label="Random Seed")
392
  run_btn = gr.Button("πŸš€ Run K-Means Clustering", variant="primary")
 
393
  plot = gr.Plot()
394
  metrics = gr.Markdown()
395
+ cluster_table = gr.Dataframe(label="Cluster Sizes", interactive=False)
396
 
397
  with gr.TabItem("πŸ“– Guide"):
398
  gr.Markdown(load_guide_content())
 
405
  "[GitHub](https://github.com/QSBench)"
406
  )
407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  dataset_dropdown.change(
409
  refresh_explorer,
410
  [dataset_dropdown, split_dropdown],
 
415
  [dataset_dropdown, split_dropdown],
416
  [split_dropdown, explorer_df, raw_qasm, transpiled_qasm, profile_box, summary_box],
417
  )
418
+
419
+ dataset_picker.change(sync_feature_picker, [dataset_picker], [feature_picker])
420
 
421
  run_btn.click(
422
  run_clustering,
423
+ [dataset_picker, feature_picker, n_clusters, seed],
424
  [plot, metrics, cluster_table],
425
  )
426
 
 
429
  [dataset_dropdown, split_dropdown],
430
  [split_dropdown, explorer_df, raw_qasm, transpiled_qasm, profile_box, summary_box],
431
  )
432
+ demo.load(sync_feature_picker, [dataset_picker], [feature_picker])
433
 
434
 
435
  if __name__ == "__main__":
436
+ demo.launch(theme=gr.themes.Soft(), css=CUSTOM_CSS)