maskil commited on
Commit
76c784d
Β·
verified Β·
1 Parent(s): 1f4ffe1

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +613 -262
app.py CHANGED
@@ -1,333 +1,684 @@
1
  """
2
- Human-in-the-Loop Feedback Tool for Rabi Oscillation Classification.
3
- Production-ready Gradio interface with batch processing, AI model integration,
4
- dynamic graphing based on curve overlap, and MongoDB feedback storage.
5
  """
6
  import gradio as gr
7
  import torch
 
8
  import numpy as np
9
  import plotly.graph_objects as go
10
- import json
11
- import os
12
  from datetime import datetime, timezone
13
- from pymongo import MongoClient
 
14
  import certifi
15
 
 
16
  from ai_model import (
17
- RabiEstimator, predict_ai_fit, load_model, rabi_formula, DEVICE
 
18
  )
19
 
20
- # ═══════════════════════════════════════════════════════════════════════════
 
 
 
 
 
21
  # Configuration
22
- # ═══════════════════════════════════════════════════════════════════════════
23
  MONGO_URI = "mongodb+srv://Admin:zeyl3RK8oEu6Qhz3@cluster0.iwlxabj.mongodb.net/?appName=Cluster0"
24
  DB_NAME = "rabi_classifier"
25
- COLLECTION_NAME = "physicist_feedback"
26
- MODEL_WEIGHTS = "rabi_model_best.pt"
 
 
 
27
 
28
- # ═══════════════════════════════════════════════════════════════════════════
29
  # Global state
30
- # ═══════════════════════════════════════════════════════════════════════════
31
- AI_MODEL = None
32
- MONGO_COLLECTION = None
 
 
 
33
 
34
  def init_globals():
35
- """Initialize AI model and MongoDB connection at startup."""
36
- global AI_MODEL, MONGO_COLLECTION
 
 
 
 
 
 
 
 
 
37
 
38
- # Load AI model
39
- if os.path.exists(MODEL_WEIGHTS):
40
  try:
41
- AI_MODEL = load_model(MODEL_WEIGHTS)
42
- print(f"βœ“ AI model loaded from {MODEL_WEIGHTS} (device: {DEVICE})")
 
 
 
 
 
 
43
  except Exception as e:
44
- print(f"⚠ Failed to load AI model: {e}")
 
45
  else:
46
- print(f"⚠ Model weights not found at {MODEL_WEIGHTS}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- # Connect to MongoDB (non-blocking β€” app works without it)
 
 
 
49
  try:
50
- client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=3000,
51
- tlsCAFile=certifi.where())
52
- client.admin.command('ping')
53
- db = client[DB_NAME]
54
- MONGO_COLLECTION = db[COLLECTION_NAME]
55
- print(f"βœ“ MongoDB connected: {DB_NAME}.{COLLECTION_NAME}")
 
 
56
  except Exception as e:
57
- MONGO_COLLECTION = None
58
- print(f"⚠ MongoDB not available (will retry on submit): {e}")
59
-
60
-
61
- # ═══════════════════════════════════════════════════════════════════════════
62
- # JSON helpers
63
- # ═══════════════════════════════════════════════════════════════════════════
64
-
65
- def extract_fit_params(data):
66
- """Extract the 4 fit parameters from JSON fitted_data."""
67
- defaults = {'amplitude': 0.0, 'T': 0.05, 'phase': 0.0, 'offset': 0.0}
68
- fitted = data.get('fitted_data')
69
- if fitted is None:
70
- return defaults
71
- params_list = fitted.get('parameters')
72
- if params_list is None:
73
- return defaults
74
- params = dict(defaults)
75
- for p in params_list:
76
- if p.get('name') in params:
77
- params[p['name']] = float(p.get('value', 0.0))
78
- return params
79
-
80
-
81
- # ═══════════════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
82
  # Plotting
83
- # ═══════════════════════════════════════════════════════════════════════════
84
 
85
- def create_plot(data, filename, regular_params, ai_params, curves_overlap):
86
- """Create Plotly plot with raw data, Regular Fit (blue), AI Fit (green)."""
87
  x = np.array(data['measured_data']['x_values'])
88
  y = np.array(data['measured_data']['y_values'])
89
- x_dense = np.linspace(x.min(), x.max(), 500)
90
 
91
- y_regular = rabi_formula(
92
- x_dense, regular_params['amplitude'], regular_params['T'],
93
- regular_params['phase'], regular_params['offset']
94
- )
95
 
96
  fig = go.Figure()
 
 
 
 
97
 
98
- # Raw data points
99
- fig.add_trace(go.Scatter(
100
- x=x, y=y, mode='markers', name='Raw Data',
101
- marker=dict(color='rgba(52, 152, 219, 0.7)', size=6),
102
- ))
103
-
104
- # Regular Fit β€” Blue
105
- fig.add_trace(go.Scatter(
106
- x=x_dense, y=y_regular, mode='lines', name='Regular Fit (JSON)',
107
- line=dict(color='#2980b9', width=2.5),
108
- ))
109
-
110
- # AI Fit β€” Green
111
  if ai_params is not None:
112
- y_ai = rabi_formula(
113
- x_dense, ai_params['amplitude'], ai_params['T'],
114
- ai_params['phase'], ai_params['offset']
115
- )
116
- if curves_overlap:
117
- fig.add_trace(go.Scatter(
118
- x=x_dense, y=y_ai, mode='lines',
119
- name='AI Fit (overlapping)',
120
- line=dict(color='#27ae60', width=3.5, dash='dot'),
121
- ))
122
  else:
123
- fig.add_trace(go.Scatter(
124
- x=x_dense, y=y_ai, mode='lines', name='AI Fit',
125
- line=dict(color='#27ae60', width=2.5),
126
- ))
127
-
128
- title = f"Rabi Oscillation β€” {filename}"
129
- if curves_overlap:
130
- title += " (OVERLAP)"
131
 
 
132
  fig.update_layout(
133
- title=title,
134
- xaxis_title='Drive Amplitude (a.u.)',
135
- yaxis_title='Signal (a.u.)',
136
- height=550,
137
- margin=dict(l=50, r=30, t=60, b=50),
138
- legend=dict(x=0.01, y=0.99),
139
- font=dict(size=14),
140
  )
141
  return fig
142
 
143
 
144
- # ═══════════════════════════════════════════════════════════════════════════
145
- # Core processing
146
- # ═══════════════════════════════════════════════════════════════════════════
147
 
148
- def process_experiment(data, filename):
149
- x = np.array(data['measured_data']['x_values'])
150
- y = np.array(data['measured_data']['y_values'])
 
 
151
 
152
- regular_params = extract_fit_params(data)
153
- ai_params = None
154
- overlap = True
155
- model_status_msg = "Awaiting model..."
156
-
157
- if AI_MODEL is not None:
158
  try:
159
- _, ai_params = predict_ai_fit(x, y, AI_MODEL)
160
- y_reg = rabi_formula(x, regular_params['amplitude'], regular_params['T'],
161
- regular_params['phase'], regular_params['offset'])
162
- y_ai = rabi_formula(x, ai_params['amplitude'], ai_params['T'],
163
- ai_params['phase'], ai_params['offset'])
164
- overlap = np.allclose(y_reg, y_ai, rtol=1e-3, atol=1e-3)
165
- model_status_msg = "βœ… AI Prediction successful."
166
- except Exception as e:
167
- model_status_msg = f"❌ AI prediction failed: {e}"
168
- ai_params = None
 
 
 
169
  overlap = True
170
- else:
171
- model_status_msg = "⚠ No AI model loaded. Only regular fit shown."
172
-
173
- fig = create_plot(data, filename, regular_params, ai_params, overlap)
174
- return fig, overlap, regular_params, ai_params, model_status_msg
175
-
176
- def display_experiment(batch_data, idx):
177
- if not batch_data or idx < 0 or idx >= len(batch_data):
178
- return (
179
- None, "πŸ“‚ Upload JSON files to begin", "", "Waiting for data...",
180
- gr.update(visible=True), gr.update(visible=False),
181
- None, "", None, None, None, "", "", "",
182
- batch_data, idx, False
183
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- exp = batch_data[idx]
186
- fig, overlap, reg_p, ai_p, mod_stat = process_experiment(exp['data'], exp['filename'])
187
- progress = f"Experiment {idx + 1} / {len(batch_data)} β€” {exp['filename']}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  if overlap:
190
- overlap_info = "πŸ”— Curves overlap (Regular and AI solutions match). Rate once."
191
  else:
192
- overlap_info = "↔️ Curves diverge (Regular = Blue, AI = Green). Rate each separately."
 
 
193
 
194
  return (
195
- fig, progress, overlap_info, mod_stat,
196
- gr.update(visible=overlap),
197
- gr.update(visible=not overlap),
198
- None, "", None, None, None, "", "", "",
199
- batch_data, idx, overlap
200
  )
201
 
202
- def on_upload(files):
203
- if not files: return display_experiment([], 0)
204
- batch_data = []
205
- for f in files:
206
- try:
207
- fpath = f.name if hasattr(f, 'name') else str(f)
208
- with open(fpath, 'r') as fp: data = json.load(fp)
209
- batch_data.append({'data': data, 'filename': os.path.basename(fpath)})
210
- except: pass
211
- return display_experiment(batch_data, 0)
212
-
213
- def on_prev(batch, idx): return display_experiment(batch, max(0, idx - 1))
214
- def on_next(batch, idx): return display_experiment(batch, min(len(batch) - 1, idx + 1) if batch else 0)
215
-
216
- def on_submit(batch, idx, overlap, data_rate, data_comment, fit_sing, fit_reg, fit_ai, c_sing, c_reg, c_ai):
217
- if not batch: return ("⚠ No experiment.",) + display_experiment([], 0)
218
- if data_rate is None: return ("⚠ Rate Data Quality.",) + display_experiment(batch, idx)
219
- if overlap and fit_sing is None: return ("⚠ Rate Fit Quality.",) + display_experiment(batch, idx)
220
- if not overlap and (fit_reg is None or fit_ai is None): return ("⚠ Rate both fits.",) + display_experiment(batch, idx)
221
-
222
- exp = batch[idx]
223
- x = np.array(exp['data']['measured_data']['x_values'])
224
- y = np.array(exp['data']['measured_data']['y_values'])
225
- ai_params = None
226
- if AI_MODEL:
227
- try: _, ai_params = predict_ai_fit(x, y, AI_MODEL)
228
- except: pass
229
-
230
- doc = {
231
- 'filename': exp['filename'],
232
- 'raw_data': exp['data'],
233
- 'ai_prediction': ai_params,
234
- 'curves_overlap': bool(overlap),
235
- 'data_quality_rating': data_rate,
236
  'data_comment': data_comment or '',
237
  'timestamp': datetime.now(timezone.utc).isoformat(),
238
  }
 
239
  if overlap:
240
- doc['fit_quality_rating'] = {'combined': fit_sing}
241
- doc['fit_comments'] = {'combined': c_sing or ''}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  else:
243
- doc['fit_quality_rating'] = {'regular_fit': fit_reg, 'ai_fit': fit_ai}
244
- doc['fit_comments'] = {'regular_fit': c_reg or '', 'ai_fit': c_ai or ''}
245
 
246
- status = "Feedback saved!"
247
- if MONGO_COLLECTION is not None:
248
- try: MONGO_COLLECTION.insert_one(doc)
249
- except Exception as e: status = f"MongoDB error: {e}"
250
- else: status = "⚠ MongoDB disconnected. Not saved."
 
 
 
 
251
 
252
- nxt = idx + 1 if idx < len(batch) - 1 else idx
253
- status += " Loaded next." if idx < len(batch) - 1 else " Last experiment!"
254
- return (status,) + display_experiment(batch, nxt)
255
 
 
 
 
 
 
 
256
 
257
- RATING_CHOICES = ["Bad (0)", "Borderline (1)", "Perfect (2)"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
  def build_ui():
260
  init_globals()
261
- with gr.Blocks(title="Rabi Feedback") as demo:
262
- batch_data = gr.State([])
263
- batch_index = gr.State(0)
264
- overlap_state = gr.State(False)
265
-
266
- gr.Markdown("# βš›οΈ Rabi Oscillation β€” Physicist Feedback Tool")
267
- gr.Markdown("Upload JSON experiments, review the quality of the data and fits, and submit your ratings.")
268
-
269
- with gr.Row():
270
- file_upload = gr.File(label="Upload JSON Files", file_count="multiple", file_types=[".json"])
271
-
272
- with gr.Row():
273
- prev_btn = gr.Button("← Previous")
274
- progress_md = gr.Markdown("πŸ“‚ Upload JSON files to begin")
275
- next_btn = gr.Button("Next β†’")
276
-
277
- plot = gr.Plot()
278
-
279
- with gr.Row():
280
- overlap_info = gr.Markdown("")
281
- model_status = gr.Markdown("Awaiting model...")
282
-
283
- gr.Markdown("---")
284
-
285
- with gr.Row():
286
- with gr.Column():
287
- gr.Markdown("### 1. Data Quality")
288
- data_rating = gr.Radio(choices=RATING_CHOICES, label="Rate raw data visually:")
289
- comment_data = gr.Textbox(label="Data notes", lines=2)
290
-
291
- with gr.Column(visible=True) as fit_single_col:
292
- gr.Markdown("### 2. Fit Quality (Combined)")
293
- fit_single_rating = gr.Radio(choices=RATING_CHOICES, label="Rate the unified fit:")
294
- comment_single = gr.Textbox(label="Fit notes", lines=2)
295
-
296
- with gr.Column(visible=False) as fit_dual_col:
297
- gr.Markdown("### 2. Fit Quality (Diverging)")
298
- with gr.Row():
299
- with gr.Column():
300
- fit_regular_rating = gr.Radio(choices=RATING_CHOICES, label="Regular Fit (Blue):")
301
- comment_regular = gr.Textbox(label="Notes on blue curve", lines=2)
302
- with gr.Column():
303
- fit_ai_rating = gr.Radio(choices=RATING_CHOICES, label="AI Fit (Green):")
304
- comment_ai = gr.Textbox(label="Notes on green curve", lines=2)
305
-
306
- gr.Markdown("---")
307
- submit_btn = gr.Button("Submit Feedback & Next", variant="primary")
308
- status_md = gr.Markdown("")
309
-
310
- outputs = [
311
- plot, progress_md, overlap_info, model_status,
312
- fit_single_col, fit_dual_col,
313
- data_rating, comment_data,
314
- fit_single_rating, fit_regular_rating, fit_ai_rating,
315
- comment_single, comment_regular, comment_ai,
316
- batch_data, batch_index, overlap_state
317
- ]
318
-
319
- file_upload.change(on_upload, [file_upload], outputs)
320
- prev_btn.click(on_prev, [batch_data, batch_index], outputs)
321
- next_btn.click(on_next, [batch_data, batch_index], outputs)
322
- submit_btn.click(on_submit, [
323
- batch_data, batch_index, overlap_state,
324
- data_rating, comment_data,
325
- fit_single_rating, fit_regular_rating, fit_ai_rating,
326
- comment_single, comment_regular, comment_ai
327
- ], [status_md] + outputs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
  return demo
330
 
 
 
 
 
 
331
  if __name__ == '__main__':
332
  demo = build_ui()
333
- demo.launch()
 
1
  """
2
+ Multi-User Data Annotation Platform for Rabi Oscillation Quality Classification.
3
+ 3-Tab Gradio interface: Data Ingestion β†’ Tagging Workspace β†’ Analytics Dashboard.
4
+ MongoDB queue with hash-based deduplication and multi-voter consensus.
5
  """
6
  import gradio as gr
7
  import torch
8
+ import torch.nn.functional as torchF
9
  import numpy as np
10
  import plotly.graph_objects as go
11
+ import json, os, hashlib
 
12
  from datetime import datetime, timezone
13
+ from pymongo import MongoClient, ASCENDING
14
+ from scipy.optimize import curve_fit
15
  import certifi
16
 
17
+ # ── AI Model imports (RabiEstimator for seed prediction) ─────────────────
18
  from ai_model import (
19
+ RabiEstimator, predict_ai_fit, load_model, rabi_formula,
20
+ preprocess_sample as ai_preprocess, target_to_params, DEVICE
21
  )
22
 
23
+ # ── Classifier imports (RabiMultiTaskNet for confidence bars) ────────────
24
+ from model import RabiMultiTaskNet
25
+ from preprocessor import preprocess_sample as clf_preprocess, extract_fit_params
26
+ from config import DEVICE as CLF_DEVICE, MODEL_PATH as CLF_MODEL_PATH
27
+
28
+ # ═════════════════════════════════════════════════════════════════════════
29
  # Configuration
30
+ # ═════════════════════════════════════════════════════════════════════════
31
  MONGO_URI = "mongodb+srv://Admin:zeyl3RK8oEu6Qhz3@cluster0.iwlxabj.mongodb.net/?appName=Cluster0"
32
  DB_NAME = "rabi_classifier"
33
+ COLLECTION_NAME = "experiments"
34
+ ESTIMATOR_WEIGHTS = "rabi_model_best.pt"
35
+ VOTES_REQUIRED = 2
36
+ RATING_CHOICES = ["Bad (0)", "Borderline (1)", "Perfect (2)"]
37
+ RATING_MAP = {"Bad (0)": 0, "Borderline (1)": 1, "Perfect (2)": 2}
38
 
39
+ # ═════════════════════════════════════════════════════════════════════════
40
  # Global state
41
+ # ═════════════════════════════════════════════════════════════════════════
42
+ AI_MODEL = None # RabiEstimator – seed prediction
43
+ CLF_MODEL = None # RabiMultiTaskNet – confidence classification
44
+ MONGO_COL = None
45
+ MONGO_CLIENT = None
46
+
47
 
48
  def init_globals():
49
+ global AI_MODEL, CLF_MODEL, MONGO_COL, MONGO_CLIENT
50
+
51
+ # 1) Load RabiEstimator (seed β†’ curve_fit)
52
+ if os.path.exists(ESTIMATOR_WEIGHTS):
53
+ try:
54
+ AI_MODEL = load_model(ESTIMATOR_WEIGHTS)
55
+ print(f"βœ“ RabiEstimator loaded from {ESTIMATOR_WEIGHTS}")
56
+ except Exception as e:
57
+ print(f"⚠ RabiEstimator load failed: {e}")
58
+ else:
59
+ print(f"⚠ Estimator weights not found: {ESTIMATOR_WEIGHTS}")
60
 
61
+ # 2) Load RabiMultiTaskNet (classifier for confidence bars)
62
+ if os.path.exists(CLF_MODEL_PATH):
63
  try:
64
+ CLF_MODEL = RabiMultiTaskNet().to(CLF_DEVICE)
65
+ ckpt = torch.load(CLF_MODEL_PATH, map_location=CLF_DEVICE, weights_only=False)
66
+ if isinstance(ckpt, dict) and 'model_state_dict' in ckpt:
67
+ CLF_MODEL.load_state_dict(ckpt['model_state_dict'])
68
+ else:
69
+ CLF_MODEL.load_state_dict(ckpt)
70
+ CLF_MODEL.eval()
71
+ print(f"βœ“ Classifier loaded from {CLF_MODEL_PATH}")
72
  except Exception as e:
73
+ CLF_MODEL = None
74
+ print(f"⚠ Classifier load failed: {e}")
75
  else:
76
+ print(f"⚠ Classifier weights not found: {CLF_MODEL_PATH}")
77
+
78
+ # 3) MongoDB
79
+ try:
80
+ MONGO_CLIENT = MongoClient(MONGO_URI, serverSelectionTimeoutMS=3000,
81
+ tlsCAFile=certifi.where())
82
+ MONGO_CLIENT.admin.command('ping')
83
+ except Exception:
84
+ # Fallback: skip cert verification (common on macOS Python)
85
+ try:
86
+ MONGO_CLIENT = MongoClient(MONGO_URI, serverSelectionTimeoutMS=3000,
87
+ tls=True, tlsAllowInvalidCertificates=True)
88
+ MONGO_CLIENT.admin.command('ping')
89
+ except Exception as e:
90
+ MONGO_COL = None
91
+ print(f"⚠ MongoDB unavailable: {e}")
92
+ return
93
+
94
+ db = MONGO_CLIENT[DB_NAME]
95
+ MONGO_COL = db[COLLECTION_NAME]
96
+ MONGO_COL.create_index([("status", ASCENDING), ("num_votes", ASCENDING)])
97
+ print(f"βœ“ MongoDB connected: {DB_NAME}.{COLLECTION_NAME}")
98
+
99
+
100
+ # ═════════════════════════════════════════════════════════════════════════
101
+ # Hashing & Deduplication
102
+ # ═════════════════════════════════════════════════════════════════════════
103
+
104
+ def compute_data_hash(data: dict) -> str:
105
+ """MD5 hash of measured_data arrays for content-based dedup."""
106
+ try:
107
+ md = data.get('measured_data', data)
108
+ x = md.get('x_values', [])
109
+ y = md.get('y_values', [])
110
+ except AttributeError:
111
+ x, y = [], []
112
+ payload = json.dumps({'x': x, 'y': y}, sort_keys=True)
113
+ return hashlib.md5(payload.encode()).hexdigest()
114
+
115
+
116
+ # ═════════════════════════════════════════════════════════════════════════
117
+ # AI Seed β†’ curve_fit Refinement
118
+ # ═════════════════════════════════════════════════════════════════════════
119
+
120
+ def refine_ai_seed(x, y, seed_params):
121
+ """Take AI seed [A,T,phi,C] and refine via scipy curve_fit. Fallback to seed."""
122
+ p0 = [seed_params['amplitude'], seed_params['T'],
123
+ seed_params['phase'], seed_params['offset']]
124
+ try:
125
+ popt, _ = curve_fit(
126
+ rabi_formula, x, y, p0=p0,
127
+ bounds=([0, 0.001, -np.pi*2, -1], [1, 5, np.pi*2, 2]),
128
+ maxfev=5000
129
+ )
130
+ return {'amplitude': popt[0], 'T': popt[1], 'phase': popt[2], 'offset': popt[3]}
131
+ except Exception:
132
+ return seed_params
133
+
134
+
135
+ def get_ai_params(x, y):
136
+ """Full pipeline: AI seed β†’ curve_fit refinement. Returns dict or None."""
137
+ if AI_MODEL is None:
138
+ return None
139
+ try:
140
+ _, seed = predict_ai_fit(x, y, AI_MODEL)
141
+ return refine_ai_seed(x, y, seed)
142
+ except Exception:
143
+ return None
144
+
145
+
146
+ # ═════════════════════════════════════════════════════════════════════════
147
+ # Classifier Confidence
148
+ # ═════════════════════════════════════════════════════════════════════════
149
 
150
+ def get_classifier_probs(data: dict):
151
+ """Run RabiMultiTaskNet and return (data_probs, fit_probs) as numpy arrays of shape (3,)."""
152
+ if CLF_MODEL is None:
153
+ return None, None
154
  try:
155
+ signal_3ch, params_t, _, _ = clf_preprocess(data)
156
+ signal_in = signal_3ch.unsqueeze(0).to(CLF_DEVICE)
157
+ params_in = params_t.unsqueeze(0).to(CLF_DEVICE)
158
+ with torch.no_grad():
159
+ data_logits, fit_logits = CLF_MODEL(signal_in, params_in)
160
+ data_p = torchF.softmax(data_logits, dim=1).cpu().numpy()[0]
161
+ fit_p = torchF.softmax(fit_logits, dim=1).cpu().numpy()[0]
162
+ return data_p, fit_p
163
  except Exception as e:
164
+ print(f"Classifier error: {e}")
165
+ return None, None
166
+
167
+
168
+ def make_confidence_html(data_probs, fit_probs):
169
+ """Build HTML progress bars for classifier confidence."""
170
+ labels = ["Bad", "Borderline", "Perfect"]
171
+ colors = ["#ef4444", "#f59e0b", "#22c55e"]
172
+
173
+ def bar_row(name, probs):
174
+ if probs is None:
175
+ return f"<p style='color:#888;'>{name}: No classifier available</p>"
176
+ best = int(np.argmax(probs))
177
+ rows = f"<p style='font-weight:600; margin:8px 0 4px;'>{name}</p>"
178
+ for i, (lbl, col) in enumerate(zip(labels, colors)):
179
+ pct = probs[i] * 100
180
+ bold = "font-weight:700;" if i == best else ""
181
+ rows += (
182
+ f"<div style='display:flex;align-items:center;margin:2px 0;'>"
183
+ f"<span style='width:80px;font-size:13px;{bold}'>{lbl}</span>"
184
+ f"<div style='flex:1;background:#e5e7eb;border-radius:6px;height:18px;overflow:hidden;'>"
185
+ f"<div style='width:{pct:.1f}%;height:100%;background:{col};border-radius:6px;'></div></div>"
186
+ f"<span style='width:50px;text-align:right;font-size:13px;{bold}'>{pct:.1f}%</span></div>"
187
+ )
188
+ return rows
189
+
190
+ return (
191
+ "<div style='padding:12px;border:1px solid #e5e7eb;border-radius:10px;background:#fafafa;'>"
192
+ "<p style='font-weight:700;font-size:15px;margin:0 0 6px;'>πŸ€– Model Confidence</p>"
193
+ + bar_row("πŸ“Š Data Quality", data_probs)
194
+ + bar_row("πŸ“ Fit Quality", fit_probs)
195
+ + "</div>"
196
+ )
197
+
198
+
199
+ # ═════════════════════════════════════════════════════════════════════════
200
  # Plotting
201
+ # ═════════════════════════════════════════════════════════════════════════
202
 
203
+ def create_plot(data, title_label, regular_params, ai_params, overlap):
 
204
  x = np.array(data['measured_data']['x_values'])
205
  y = np.array(data['measured_data']['y_values'])
206
+ xd = np.linspace(x.min(), x.max(), 500)
207
 
208
+ yr = rabi_formula(xd, regular_params['amplitude'], regular_params['T'],
209
+ regular_params['phase'], regular_params['offset'])
 
 
210
 
211
  fig = go.Figure()
212
+ fig.add_trace(go.Scatter(x=x, y=y, mode='markers', name='Raw Data',
213
+ marker=dict(color='rgba(52,152,219,0.6)', size=5)))
214
+ fig.add_trace(go.Scatter(x=xd, y=yr, mode='lines', name='Regular Fit',
215
+ line=dict(color='#2980b9', width=2.5)))
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  if ai_params is not None:
218
+ ya = rabi_formula(xd, ai_params['amplitude'], ai_params['T'],
219
+ ai_params['phase'], ai_params['offset'])
220
+ if overlap:
221
+ fig.add_trace(go.Scatter(x=xd, y=ya, mode='lines', name='AI Fit (overlap)',
222
+ line=dict(color='#27ae60', width=3, dash='dot')))
 
 
 
 
 
223
  else:
224
+ fig.add_trace(go.Scatter(x=xd, y=ya, mode='lines', name='AI Fit',
225
+ line=dict(color='#27ae60', width=2.5)))
 
 
 
 
 
 
226
 
227
+ tag = " βœ… OVERLAP" if overlap else ""
228
  fig.update_layout(
229
+ title=f"Rabi β€” {title_label}{tag}",
230
+ xaxis_title='Drive Amplitude (a.u.)', yaxis_title='Signal (a.u.)',
231
+ height=500, margin=dict(l=50, r=30, t=50, b=50),
232
+ legend=dict(x=0.01, y=0.99), font=dict(size=13),
 
 
 
233
  )
234
  return fig
235
 
236
 
237
+ # ═════════════════════════════════════════════════════════════════════════
238
+ # Tab 1 β€” Data Ingestion
239
+ # ═════════════════════════════════════════════════════════════════════════
240
 
241
+ def ingest_files(files):
242
+ if MONGO_COL is None:
243
+ return "❌ MongoDB is not connected. Cannot ingest data."
244
+ if not files:
245
+ return "⚠ No files uploaded."
246
 
247
+ added, duped, errors = 0, 0, 0
248
+ for f in files:
 
 
 
 
249
  try:
250
+ fpath = f.name if hasattr(f, 'name') else str(f)
251
+ with open(fpath, 'r') as fp:
252
+ data = json.load(fp)
253
+
254
+ x = np.array(data['measured_data']['x_values'])
255
+ y = np.array(data['measured_data']['y_values'])
256
+ doc_hash = compute_data_hash(data)
257
+
258
+ # Compute fits at ingestion time
259
+ reg_params = extract_fit_params(data)
260
+ ai_params = get_ai_params(x, y)
261
+
262
+ # Overlap detection
263
  overlap = True
264
+ if ai_params is not None:
265
+ yr = rabi_formula(x, reg_params['amplitude'], reg_params['T'],
266
+ reg_params['phase'], reg_params['offset'])
267
+ ya = rabi_formula(x, ai_params['amplitude'], ai_params['T'],
268
+ ai_params['phase'], ai_params['offset'])
269
+ overlap = bool(np.allclose(yr, ya, rtol=1e-3, atol=1e-3))
270
+
271
+ doc = {
272
+ 'raw_data': data,
273
+ 'filename': os.path.basename(fpath),
274
+ 'regular_fit_params': reg_params,
275
+ 'ai_fit_params': ai_params,
276
+ 'curves_overlap': overlap,
277
+ 'status': 'pending',
278
+ 'num_votes': 0,
279
+ 'tagged_by': [],
280
+ 'evaluations': [],
281
+ 'inserted_at': datetime.now(timezone.utc).isoformat(),
282
+ }
283
+
284
+ result = MONGO_COL.update_one(
285
+ {'_id': doc_hash},
286
+ {'$setOnInsert': doc},
287
+ upsert=True
288
+ )
289
+ if result.upserted_id is not None:
290
+ added += 1
291
+ else:
292
+ duped += 1
293
+ except Exception as e:
294
+ errors += 1
295
+ print(f"Ingest error: {e}")
296
+
297
+ parts = []
298
+ if added: parts.append(f"**{added}** new experiments added")
299
+ if duped: parts.append(f"**{duped}** duplicates skipped")
300
+ if errors: parts.append(f"**{errors}** errors")
301
+ return "### Ingestion Complete\n" + " Β· ".join(parts)
302
 
303
+
304
+ # ═════════════════════════════════════════════════════════════════════════
305
+ # Tab 2 β€” Tagging Workspace
306
+ # ═════════════════════════════════════════════════════════════════════════
307
+
308
+ def fetch_next(username):
309
+ """Fetch the next pending experiment that this user hasn't tagged yet."""
310
+ if not username or not username.strip():
311
+ return _empty_workspace("⚠ Please enter your username first.")
312
+ username = username.strip()
313
+
314
+ if MONGO_COL is None:
315
+ return _empty_workspace("❌ MongoDB not connected.")
316
+
317
+ doc = MONGO_COL.find_one(
318
+ {'status': 'pending', 'tagged_by': {'$nin': [username]}},
319
+ sort=[('num_votes', ASCENDING)]
320
+ )
321
+
322
+ if doc is None:
323
+ return _empty_workspace("πŸŽ‰ **Queue empty!** No pending experiments for you.")
324
+
325
+ data = doc['raw_data']
326
+ reg_p = doc.get('regular_fit_params') or extract_fit_params(data)
327
+ ai_p = doc.get('ai_fit_params')
328
+ overlap = doc.get('curves_overlap', True)
329
+ doc_id = doc['_id']
330
+ fname = doc.get('filename', doc_id[:12])
331
+
332
+ fig = create_plot(data, fname, reg_p, ai_p, overlap)
333
+ data_probs, fit_probs = get_classifier_probs(data)
334
+ conf_html = make_confidence_html(data_probs, fit_probs)
335
 
336
  if overlap:
337
+ overlap_msg = "πŸ”— **Curves overlap** β€” fits converged to same solution. Rate once."
338
  else:
339
+ overlap_msg = "↔️ **Curves diverge** β€” Regular (blue) vs AI (green). Rate each."
340
+
341
+ info = f"**{fname}** Β· votes: {doc['num_votes']} Β· id: `{doc_id[:12]}…`"
342
 
343
  return (
344
+ fig, info, overlap_msg, conf_html, "",
345
+ gr.update(visible=overlap), gr.update(visible=not overlap),
346
+ "Perfect (2)", "Perfect (2)", "Perfect (2)", "Perfect (2)",
347
+ "", "", "",
348
+ doc_id, overlap
349
  )
350
 
351
+
352
+ def _empty_workspace(msg):
353
+ return (
354
+ None, msg, "", "<p style='color:#888'>No data loaded.</p>", "",
355
+ gr.update(visible=True), gr.update(visible=False),
356
+ "Perfect (2)", "Perfect (2)", "Perfect (2)", "Perfect (2)",
357
+ "", "", "",
358
+ None, False
359
+ )
360
+
361
+
362
+ def submit_feedback(username, doc_id, overlap,
363
+ data_rate, data_comment,
364
+ fit_single, fit_reg, fit_ai,
365
+ comment_single, comment_reg, comment_ai):
366
+ """Validate, write evaluation to MongoDB, fetch next."""
367
+ if not username or not username.strip():
368
+ return ("⚠ Enter your username.",) + _empty_workspace("")
369
+ username = username.strip()
370
+
371
+ if doc_id is None:
372
+ return ("⚠ No experiment loaded. Fetch one first.",) + _empty_workspace("")
373
+
374
+ if MONGO_COL is None:
375
+ return ("❌ MongoDB not connected.",) + _empty_workspace("")
376
+
377
+ # Map ratings
378
+ dr = RATING_MAP.get(data_rate)
379
+ if dr is None:
380
+ return ("⚠ Please rate **Data Quality**.",) + _empty_workspace("")
381
+
382
+ evaluation = {
383
+ 'username': username,
384
+ 'data_quality': dr,
385
  'data_comment': data_comment or '',
386
  'timestamp': datetime.now(timezone.utc).isoformat(),
387
  }
388
+
389
  if overlap:
390
+ fr = RATING_MAP.get(fit_single)
391
+ if fr is None:
392
+ return ("⚠ Please rate **Fit Quality**.",) + _empty_workspace("")
393
+ evaluation['fit_quality'] = {'combined': fr}
394
+ evaluation['fit_comment'] = {'combined': comment_single or ''}
395
+ else:
396
+ fr_reg = RATING_MAP.get(fit_reg)
397
+ fr_ai = RATING_MAP.get(fit_ai)
398
+ if fr_reg is None or fr_ai is None:
399
+ return ("⚠ Please rate **both** fits.",) + _empty_workspace("")
400
+ evaluation['fit_quality'] = {'regular_fit': fr_reg, 'ai_fit': fr_ai}
401
+ evaluation['fit_comment'] = {'regular_fit': comment_reg or '', 'ai_fit': comment_ai or ''}
402
+
403
+ try:
404
+ MONGO_COL.update_one(
405
+ {'_id': doc_id},
406
+ {
407
+ '$push': {'evaluations': evaluation},
408
+ '$inc': {'num_votes': 1},
409
+ '$addToSet': {'tagged_by': username},
410
+ }
411
+ )
412
+ # Check if we reached the vote threshold
413
+ updated = MONGO_COL.find_one({'_id': doc_id})
414
+ if updated and updated.get('num_votes', 0) >= VOTES_REQUIRED:
415
+ MONGO_COL.update_one({'_id': doc_id}, {'$set': {'status': 'completed'}})
416
+
417
+ status = "βœ… Feedback saved!"
418
+ except Exception as e:
419
+ status = f"❌ Error: {e}"
420
+
421
+ # Fetch next
422
+ nxt = fetch_next(username)
423
+ return (status,) + nxt
424
+
425
+
426
+ def mark_perfect_and_next(username, doc_id, overlap):
427
+ """Shortcut: submit all ratings as Perfect(2) and fetch next."""
428
+ return submit_feedback(
429
+ username, doc_id, overlap,
430
+ "Perfect (2)", "",
431
+ "Perfect (2)", "Perfect (2)", "Perfect (2)",
432
+ "", "", ""
433
+ )
434
+
435
+
436
+ # ═════════════════════════════════════════════════════════════════════════
437
+ # Tab 3 β€” Analytics Dashboard
438
+ # ═════════════════════════════════════════════════════════════════════════
439
+
440
+ def refresh_analytics():
441
+ if MONGO_COL is None:
442
+ return "❌ MongoDB not connected."
443
+
444
+ total = MONGO_COL.count_documents({})
445
+ pending = MONGO_COL.count_documents({'status': 'pending'})
446
+ completed = MONGO_COL.count_documents({'status': 'completed'})
447
+
448
+ # Model success rate: % of evaluations with data_quality == 2
449
+ pipeline = [
450
+ {'$unwind': '$evaluations'},
451
+ {'$group': {
452
+ '_id': None,
453
+ 'total_evals': {'$sum': 1},
454
+ 'perfect_data': {'$sum': {'$cond': [{'$eq': ['$evaluations.data_quality', 2]}, 1, 0]}},
455
+ }}
456
+ ]
457
+ agg = list(MONGO_COL.aggregate(pipeline))
458
+
459
+ if agg:
460
+ total_evals = agg[0]['total_evals']
461
+ perfect_data = agg[0]['perfect_data']
462
+ success_pct = (perfect_data / total_evals * 100) if total_evals > 0 else 0
463
  else:
464
+ total_evals, perfect_data, success_pct = 0, 0, 0
 
465
 
466
+ # Top taggers
467
+ tagger_pipeline = [
468
+ {'$unwind': '$evaluations'},
469
+ {'$group': {'_id': '$evaluations.username', 'count': {'$sum': 1}}},
470
+ {'$sort': {'count': -1}},
471
+ {'$limit': 10}
472
+ ]
473
+ taggers = list(MONGO_COL.aggregate(tagger_pipeline))
474
+ tagger_rows = "\n".join([f"| {t['_id']} | {t['count']} |" for t in taggers]) if taggers else "| β€” | β€” |"
475
 
476
+ md = f"""### πŸ“ˆ Platform Statistics
 
 
477
 
478
+ | Metric | Count |
479
+ |--------|-------|
480
+ | Total Experiments | **{total}** |
481
+ | Pending | **{pending}** |
482
+ | Completed (β‰₯{VOTES_REQUIRED} votes) | **{completed}** |
483
+ | Total Evaluations | **{total_evals}** |
484
 
485
+ ---
486
+
487
+ ### 🎯 Model Success Rate
488
+ **{success_pct:.1f}%** of evaluations rated Data Quality as Perfect (Class 2)
489
+ ({perfect_data} / {total_evals})
490
+
491
+ ---
492
+
493
+ ### πŸ† Top Annotators
494
+
495
+ | Username | Evaluations |
496
+ |----------|-------------|
497
+ {tagger_rows}
498
+ """
499
+ return md
500
+
501
+
502
+ # ═════════════════════════════════════════════════════════════════════════
503
+ # Custom CSS
504
+ # ═════════════════════════════════════════════════════════════════════════
505
+
506
+ CUSTOM_CSS = """
507
+ .gradio-container { max-width: 1100px !important; margin: 0 auto !important; }
508
+
509
+ /* Rating radio colors */
510
+ .rating-radio label { padding: 10px 20px !important; border-radius: 10px !important;
511
+ margin: 3px 6px !important; font-weight: 600 !important; font-size: 14px !important;
512
+ transition: all 0.2s !important; cursor: pointer !important; }
513
+
514
+ /* Bad = Red */
515
+ .rating-radio label:nth-child(1) {
516
+ border: 2px solid #ef4444 !important; color: #dc2626 !important;
517
+ background: rgba(239,68,68,0.06) !important; }
518
+ .rating-radio label:nth-child(1).selected,
519
+ .rating-radio label:nth-child(1):has(input:checked) {
520
+ background: #ef4444 !important; color: #fff !important; }
521
+
522
+ /* Borderline = Orange */
523
+ .rating-radio label:nth-child(2) {
524
+ border: 2px solid #f59e0b !important; color: #d97706 !important;
525
+ background: rgba(245,158,11,0.06) !important; }
526
+ .rating-radio label:nth-child(2).selected,
527
+ .rating-radio label:nth-child(2):has(input:checked) {
528
+ background: #f59e0b !important; color: #fff !important; }
529
+
530
+ /* Perfect = Green */
531
+ .rating-radio label:nth-child(3) {
532
+ border: 2px solid #22c55e !important; color: #16a34a !important;
533
+ background: rgba(34,197,94,0.06) !important; }
534
+ .rating-radio label:nth-child(3).selected,
535
+ .rating-radio label:nth-child(3):has(input:checked) {
536
+ background: #22c55e !important; color: #fff !important; }
537
+
538
+ /* Submit button */
539
+ .submit-btn { font-size: 16px !important; padding: 14px 28px !important;
540
+ border-radius: 12px !important; }
541
+ .perfect-btn { font-size: 14px !important; padding: 14px 28px !important;
542
+ border-radius: 12px !important; background: #22c55e !important;
543
+ color: white !important; border: none !important; }
544
+ """
545
+
546
+
547
+ # ═════════════════════════════════════════════════════════════════════════
548
+ # Build UI
549
+ # ═════════════════════════════════════════════════════════════════════════
550
 
551
  def build_ui():
552
  init_globals()
553
+
554
+ with gr.Blocks(title="Rabi Annotation Platform") as demo:
555
+
556
+ gr.Markdown("# βš›οΈ Rabi Oscillation β€” Data Annotation Platform")
557
+
558
+ with gr.Tabs():
559
+
560
+ # ─────────────────────────────────────────────────────
561
+ # TAB 1: Data Ingestion
562
+ # ─────────────────────────────────────────────────────
563
+ with gr.Tab("πŸ“₯ Data Ingestion"):
564
+ gr.Markdown("Upload JSON experiment files. Duplicates (identical measured data) are automatically skipped.")
565
+ file_upload = gr.File(
566
+ label="Upload JSON Experiments",
567
+ file_count="multiple", file_types=[".json"]
568
+ )
569
+ ingest_btn = gr.Button("Process & Upload to Database", variant="primary")
570
+ ingest_result = gr.Markdown("")
571
+
572
+ ingest_btn.click(ingest_files, [file_upload], [ingest_result])
573
+
574
+ # ─────────────────────────────────────────────────────
575
+ # TAB 2: Tagging Workspace
576
+ # ─────────────────────────────────────────────────────
577
+ with gr.Tab("🏷️ Tagging Workspace"):
578
+
579
+ # State
580
+ current_doc_id = gr.State(None)
581
+ overlap_state = gr.State(False)
582
+
583
+ # User ID + Fetch
584
+ with gr.Row():
585
+ username_box = gr.Textbox(label="Your Username", placeholder="e.g. alice",
586
+ scale=2, max_lines=1)
587
+ fetch_btn = gr.Button("πŸ”„ Fetch Next Experiment", variant="primary", scale=1)
588
+
589
+ status_md = gr.Markdown("")
590
+
591
+ # Plot + Confidence side by side
592
+ with gr.Row():
593
+ plot = gr.Plot(label="Signal & Fit Curves", scale=3)
594
+ with gr.Column(scale=1):
595
+ exp_info = gr.Markdown("")
596
+ overlap_info = gr.Markdown("")
597
+ confidence_html = gr.HTML("<p style='color:#888'>Fetch an experiment to see predictions.</p>")
598
+
599
+ gr.Markdown("---")
600
+
601
+ # Ratings
602
+ with gr.Row():
603
+ # Data Quality β€” always visible
604
+ with gr.Column():
605
+ gr.Markdown("### πŸ“Š Data Quality")
606
+ data_rating = gr.Radio(choices=RATING_CHOICES, value="Perfect (2)",
607
+ label="Rate raw data:", elem_classes=["rating-radio"])
608
+ comment_data = gr.Textbox(label="Data notes", lines=2)
609
+
610
+ # Fit Quality β€” SINGLE (overlap)
611
+ with gr.Column(visible=True) as fit_single_col:
612
+ gr.Markdown("### πŸ“ Fit Quality (Combined)")
613
+ fit_single_rating = gr.Radio(choices=RATING_CHOICES, value="Perfect (2)",
614
+ label="Rate the fit:", elem_classes=["rating-radio"])
615
+ comment_single = gr.Textbox(label="Fit notes", lines=2)
616
+
617
+ # Fit Quality β€” DUAL (diverge)
618
+ with gr.Column(visible=False) as fit_dual_col:
619
+ gr.Markdown("### πŸ“ Fit Quality (Diverging)")
620
+ with gr.Row():
621
+ with gr.Column():
622
+ gr.Markdown("#### 🟦 Regular Fit (Blue)")
623
+ fit_regular_rating = gr.Radio(choices=RATING_CHOICES, value="Perfect (2)",
624
+ label="Regular Fit:", elem_classes=["rating-radio"])
625
+ comment_regular = gr.Textbox(label="Regular fit notes", lines=2)
626
+ with gr.Column():
627
+ gr.Markdown("#### 🟩 AI Fit (Green)")
628
+ fit_ai_rating = gr.Radio(choices=RATING_CHOICES, value="Perfect (2)",
629
+ label="AI Fit:", elem_classes=["rating-radio"])
630
+ comment_ai = gr.Textbox(label="AI fit notes", lines=2)
631
+
632
+ gr.Markdown("---")
633
+
634
+ with gr.Row():
635
+ submit_btn = gr.Button("πŸš€ Submit Feedback & Next", variant="primary",
636
+ elem_classes=["submit-btn"], scale=2)
637
+ perfect_btn = gr.Button("βœ… Mark as Perfect & Next",
638
+ elem_classes=["perfect-btn"], scale=1)
639
+
640
+ # ── Wire Tab 2 ───────────────────────────────────
641
+ fetch_outputs = [
642
+ plot, exp_info, overlap_info, confidence_html, status_md,
643
+ fit_single_col, fit_dual_col,
644
+ data_rating, fit_single_rating, fit_regular_rating, fit_ai_rating,
645
+ comment_data, comment_single, comment_ai,
646
+ current_doc_id, overlap_state
647
+ ]
648
+
649
+ fetch_btn.click(fetch_next, [username_box], fetch_outputs)
650
+
651
+ submit_btn.click(
652
+ submit_feedback,
653
+ [username_box, current_doc_id, overlap_state,
654
+ data_rating, comment_data,
655
+ fit_single_rating, fit_regular_rating, fit_ai_rating,
656
+ comment_single, comment_regular, comment_ai],
657
+ [status_md] + fetch_outputs
658
+ )
659
+
660
+ perfect_btn.click(
661
+ mark_perfect_and_next,
662
+ [username_box, current_doc_id, overlap_state],
663
+ [status_md] + fetch_outputs
664
+ )
665
+
666
+ # ─────────────────────────────────────────────────────
667
+ # TAB 3: Analytics Dashboard
668
+ # ─────────────────────────────────────────────────────
669
+ with gr.Tab("πŸ“Š Analytics"):
670
+ gr.Markdown("View aggregate statistics from all annotator submissions.")
671
+ refresh_btn = gr.Button("πŸ”„ Refresh Statistics", variant="primary")
672
+ analytics_md = gr.Markdown("Click **Refresh** to load stats.")
673
+ refresh_btn.click(refresh_analytics, [], [analytics_md])
674
 
675
  return demo
676
 
677
+
678
+ # ═════════════════════════════════════════════════════════════════════════
679
+ # Launch
680
+ # ═════════════════════════════════════════════════════════════════════════
681
+
682
  if __name__ == '__main__':
683
  demo = build_ui()
684
+ demo.launch(css=CUSTOM_CSS)