harishaseebat92 commited on
Commit
f4fc516
·
1 Parent(s): 610d3d1

QLBM IONQ Upload Window

Browse files
Files changed (1) hide show
  1. qlbm_embedded.py +186 -184
qlbm_embedded.py CHANGED
@@ -314,16 +314,17 @@ def init_state():
314
  "qlbm_qiskit_fig": None, # Stores the Plotly figure for Qiskit results
315
 
316
  # Job upload state (for loading previously saved QPU job results)
317
- "qlbm_job_upload": None, # File upload content
318
  "qlbm_job_upload_filename": "", # Display the uploaded filename
319
  "qlbm_job_upload_error": "", # Error message for upload
320
  "qlbm_job_upload_success": "", # Success message for upload
321
- "qlbm_job_platform": "IBM", # Platform: "IBM" or "IonQ"
 
322
  "qlbm_job_total_time": 3, # Total time T (generates T_list = [1..T])
323
  "qlbm_job_output_resolution": 40, # Grid resolution for density estimation
324
  "qlbm_job_is_processing": False, # True when processing uploaded job
325
  "qlbm_job_flag_qubits": True, # Whether flag qubits were used
326
- "qlbm_job_midcircuit_meas": True, # Whether mid-circuit measurement was used (IBM only)
327
  })
328
  _initialized = True
329
 
@@ -1199,13 +1200,14 @@ def _run_qiskit_simulation(progress_callback=None):
1199
  # --- Job Result Upload Processing ---
1200
  def process_uploaded_job_result():
1201
  """
1202
- Process an uploaded IBM/IonQ job result JSON file and generate the Plotly figure.
1203
 
1204
  This function:
1205
- 1. Decodes the uploaded file (base64 -> JSON)
1206
- 2. Parses the job result based on platform (IBM uses RuntimeDecoder, IonQ uses plain dict)
1207
- 3. Calls load_samples/estimate_density for each timestep
1208
- 4. Generates the slider figure using plot_density_isosurface_slider
 
1209
  """
1210
  global simulation_data_frames, simulation_times, current_grid_object
1211
 
@@ -1218,45 +1220,44 @@ def process_uploaded_job_result():
1218
  log_to_console("Error: visualize_counts module not available")
1219
  return
1220
 
1221
- # Validate file upload
1222
- uploaded = _state.qlbm_job_upload
1223
- if not uploaded:
1224
- _state.qlbm_job_upload_error = "No file uploaded. Please select a JSON file."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1225
  return
1226
 
1227
  # Reset messages
1228
  _state.qlbm_job_upload_error = ""
1229
  _state.qlbm_job_upload_success = ""
1230
  _state.qlbm_job_is_processing = True
1231
- log_to_console("Processing uploaded job result...")
 
1232
 
1233
  try:
1234
- # Handle list or single file
1235
- file_data = uploaded[0] if isinstance(uploaded, list) else uploaded
1236
-
1237
- # Get filename for display
1238
- filename = file_data.get("name", "unknown.json") if isinstance(file_data, dict) else "unknown.json"
1239
- _state.qlbm_job_upload_filename = filename
1240
- log_to_console(f"Processing file: {filename}")
1241
-
1242
- # Decode content - handle both bytes and string (base64 data URI)
1243
- content = file_data.get("content", "") if isinstance(file_data, dict) else ""
1244
-
1245
- # Check if content is bytes or string
1246
- if isinstance(content, bytes):
1247
- # Content is already raw bytes, decode directly
1248
- json_str = content.decode("utf-8")
1249
- log_to_console("Content received as raw bytes")
1250
- else:
1251
- # Content is a string, possibly base64 data URI
1252
- if content.startswith("data:"):
1253
- content = content.split(",", 1)[1] # Remove data URI prefix
1254
- raw_bytes = base64.b64decode(content)
1255
- json_str = raw_bytes.decode("utf-8")
1256
- log_to_console("Content decoded from base64 string")
1257
-
1258
  # Parse timesteps from user input
1259
- # User provides Total Time T, we generate T_list = [1, 2, ..., T]
1260
  try:
1261
  total_time = int(_state.qlbm_job_total_time or 3)
1262
  if total_time < 1:
@@ -1270,117 +1271,119 @@ def process_uploaded_job_result():
1270
  log_to_console(f"Timesteps to process: {T_list}")
1271
 
1272
  # Get processing parameters
1273
- platform = _state.qlbm_job_platform or "IBM"
1274
  output_resolution = int(_state.qlbm_job_output_resolution or 40)
1275
- # Defaulting to True as per user request, and hiding from UI
1276
  flag_qubits = True
1277
- midcircuit_meas = True
1278
 
1279
- log_to_console(f"Platform: {platform}, Resolution: {output_resolution}, Flag qubits: {flag_qubits}")
1280
 
1281
- # Parse JSON based on platform
1282
- if platform == "IBM":
1283
- # IBM uses RuntimeDecoder for proper result parsing
1284
- try:
1285
- from qiskit_ibm_runtime import RuntimeDecoder
1286
- result = json.loads(json_str, cls=RuntimeDecoder)
1287
- log_to_console("Parsed IBM result with RuntimeDecoder")
1288
- except ImportError:
1289
- # Fallback to plain JSON if RuntimeDecoder not available
1290
- result = json.loads(json_str)
1291
- log_to_console("Warning: RuntimeDecoder not available, using plain JSON")
1292
- except Exception as e:
1293
- # Try plain JSON as fallback
1294
- result = json.loads(json_str)
1295
- log_to_console(f"RuntimeDecoder failed ({e}), using plain JSON")
1296
- else:
1297
- # IonQ uses plain JSON
1298
- result = json.loads(json_str)
1299
- log_to_console("Parsed IonQ result as plain JSON")
 
 
1300
 
1301
- # Process each timestep
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1302
  output = []
1303
 
1304
- # Determine how to extract counts based on result structure
1305
- if platform == "IBM":
1306
- # IBM PrimitiveResult structure: result is a list of PubResults
1307
- # Each PubResult has .join_data().get_counts()
1308
- if hasattr(result, '__iter__') and not isinstance(result, dict):
1309
- for i, (T_total, pub) in enumerate(zip(T_list, result)):
1310
- try:
1311
- # Try the PrimitiveResult API
1312
- if hasattr(pub, 'join_data'):
1313
- joined = pub.join_data()
1314
- counts = joined.get_counts()
1315
- elif hasattr(pub, 'data'):
1316
- # Older API
1317
- counts = pub.data.get_counts() if hasattr(pub.data, 'get_counts') else dict(pub.data)
1318
- elif isinstance(pub, dict):
1319
- counts = pub
1320
- else:
1321
- counts = dict(pub)
1322
-
1323
- log_to_console(f"Processing timestep T={T_total}: {len(counts)} unique bitstrings")
1324
- pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1325
- flag_qubits=flag_qubits, midcircuit_meas=midcircuit_meas)
1326
- output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1327
- except Exception as e:
1328
- log_to_console(f"Error processing timestep {i}: {e}")
1329
- elif isinstance(result, dict):
1330
- # Single result or counts dict directly
1331
- if 'counts' in result:
1332
- counts = result['counts']
1333
- else:
1334
- counts = result
1335
- for T_total in T_list:
1336
- log_to_console(f"Processing timestep T={T_total}")
1337
- pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1338
- flag_qubits=flag_qubits, midcircuit_meas=midcircuit_meas)
1339
- output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1340
- else:
1341
- # IonQ result structure: use get_counts(i) or direct counts dict
1342
- if hasattr(result, 'get_counts'):
1343
- for i, T_total in enumerate(T_list):
1344
- try:
1345
- counts = result.get_counts(i)
1346
- log_to_console(f"Processing timestep T={T_total}: {len(counts)} unique bitstrings")
1347
- pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1348
- flag_qubits=flag_qubits, midcircuit_meas=False)
1349
- output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1350
- except Exception as e:
1351
- log_to_console(f"Error processing timestep {i}: {e}")
1352
- elif isinstance(result, list):
1353
- # List of counts dicts
1354
- for i, (T_total, counts) in enumerate(zip(T_list, result)):
1355
- if isinstance(counts, dict):
1356
- log_to_console(f"Processing timestep T={T_total}")
1357
- pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1358
- flag_qubits=flag_qubits, midcircuit_meas=False)
1359
- output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1360
- elif isinstance(result, dict):
1361
- # Single counts dict
1362
- counts = result.get('counts', result)
1363
- for T_total in T_list:
1364
- log_to_console(f"Processing timestep T={T_total}")
1365
- pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1366
- flag_qubits=flag_qubits, midcircuit_meas=False)
1367
- output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1368
 
1369
  if not output:
1370
- _state.qlbm_job_upload_error = "No valid data extracted from job result. Check timesteps and file format."
1371
  _state.qlbm_job_is_processing = False
1372
  return
1373
 
1374
  log_to_console(f"Processed {len(output)} timestep(s) successfully")
1375
 
1376
  # Generate the Plotly figure
1377
- fig = plot_density_isosurface_slider(output, T_list)
1378
 
1379
  # Update state to show results
1380
  _state.qlbm_qiskit_mode = True
1381
  _state.qlbm_qiskit_fig = fig
1382
  _state.qlbm_simulation_has_run = True
1383
- _state.qlbm_job_upload_success = f"✓ Successfully processed {len(output)} timestep(s) from {filename}"
1384
 
1385
  # Update the Plotly figure widget
1386
  if hasattr(_ctrl, "qlbm_qiskit_result_update"):
@@ -1388,11 +1391,8 @@ def process_uploaded_job_result():
1388
 
1389
  log_to_console(f"Results ready! {len(output)} frames generated.")
1390
 
1391
- except json.JSONDecodeError as e:
1392
- _state.qlbm_job_upload_error = f"Invalid JSON file: {e}"
1393
- log_to_console(f"JSON decode error: {e}")
1394
  except Exception as e:
1395
- _state.qlbm_job_upload_error = f"Error processing job result: {e}"
1396
  log_to_console(f"Processing error: {e}")
1397
  import traceback
1398
  log_to_console(traceback.format_exc())
@@ -2393,25 +2393,41 @@ def _build_control_panels(plotter):
2393
 
2394
  # --- Job Result Upload Section ---
2395
  vuetify3.VDivider(classes="my-3")
2396
- html.Div("Upload Job Results", classes="text-subtitle-2 font-weight-bold text-primary mb-2")
2397
- html.Div("Load previously saved IBM/IonQ QPU job results (JSON format)",
2398
  classes="text-caption text-medium-emphasis mb-2")
2399
 
2400
- # Platform selector
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2401
  with vuetify3.VRow(dense=True, classes="mb-2"):
2402
  with vuetify3.VCol(cols=6):
2403
  with vuetify3.VTooltip(location="top"):
2404
  with vuetify3.Template(v_slot_activator="{ props }"):
2405
- vuetify3.VSelect(
2406
  v_bind="props",
2407
- label="Platform",
2408
- v_model=("qlbm_job_platform", "IBM"),
2409
- items=("['IBM', 'IonQ']",),
2410
  density="compact",
2411
  hide_details=True,
2412
  color="primary",
2413
  )
2414
- html.Span("Select the quantum hardware provider")
2415
  with vuetify3.VCol(cols=6):
2416
  with vuetify3.VTooltip(location="top"):
2417
  with vuetify3.Template(v_slot_activator="{ props }"):
@@ -2426,50 +2442,36 @@ def _build_control_panels(plotter):
2426
  )
2427
  html.Span("Resolution for 3D visualization. Should be <= Grid Size (2^n).")
2428
 
2429
- # Timesteps input
2430
- with vuetify3.VTooltip(location="top"):
2431
- with vuetify3.Template(v_slot_activator="{ props }"):
2432
- vuetify3.VTextField(
2433
- v_bind="props",
2434
- label="Total Time",
2435
- v_model=("qlbm_job_total_time", 3),
2436
- type="number",
2437
- density="compact",
2438
- hide_details=True,
2439
- color="primary",
2440
- classes="mb-2",
2441
- hint="Enter the total time T used when running the job",
2442
- )
2443
- html.Span("Total number of time steps in the uploaded job")
2444
-
2445
- # Advanced options (flag qubits, mid-circuit measurement) - HIDDEN
2446
- # Defaulting to True for both as per user request
2447
 
2448
- # File upload and Generate button
2449
- with vuetify3.VRow(dense=True, classes="align-center mt-2"):
2450
- with vuetify3.VCol(cols=8):
2451
- vuetify3.VFileInput(
2452
- label="Upload Job Result (JSON)",
2453
- v_model=("qlbm_job_upload", None),
2454
- accept=".json",
2455
- prepend_icon="mdi-file-upload",
2456
- density="compact",
2457
- hide_details=True,
2458
- color="primary",
2459
- show_size=True,
2460
- clearable=True,
2461
- )
2462
- with vuetify3.VCol(cols=4):
2463
- vuetify3.VBtn(
2464
- text="Generate",
2465
- color="secondary",
2466
- variant="tonal",
2467
- block=True,
2468
- disabled=("!qlbm_job_upload || qlbm_job_is_processing", True),
2469
- loading=("qlbm_job_is_processing", False),
2470
- click=process_uploaded_job_result,
2471
- prepend_icon="mdi-chart-box-outline",
2472
- )
2473
 
2474
  # Success message
2475
  vuetify3.VAlert(
 
314
  "qlbm_qiskit_fig": None, # Stores the Plotly figure for Qiskit results
315
 
316
  # Job upload state (for loading previously saved QPU job results)
317
+ "qlbm_job_upload": None, # File upload content (optional, for extracting job ID from filename)
318
  "qlbm_job_upload_filename": "", # Display the uploaded filename
319
  "qlbm_job_upload_error": "", # Error message for upload
320
  "qlbm_job_upload_success": "", # Success message for upload
321
+ "qlbm_job_platform": "IonQ", # Platform: IonQ only now
322
+ "qlbm_job_id": "", # Job ID text field for direct entry
323
  "qlbm_job_total_time": 3, # Total time T (generates T_list = [1..T])
324
  "qlbm_job_output_resolution": 40, # Grid resolution for density estimation
325
  "qlbm_job_is_processing": False, # True when processing uploaded job
326
  "qlbm_job_flag_qubits": True, # Whether flag qubits were used
327
+ "qlbm_job_midcircuit_meas": False, # IonQ uses midcircuit_meas=False
328
  })
329
  _initialized = True
330
 
 
1200
  # --- Job Result Upload Processing ---
1201
  def process_uploaded_job_result():
1202
  """
1203
+ Process an IonQ job by retrieving it directly using the Job ID.
1204
 
1205
  This function:
1206
+ 1. Takes the Job ID from user input (or extracts from uploaded filename)
1207
+ 2. Connects to IonQ and retrieves the job
1208
+ 3. Calls job.get_counts(i) for each timestep (same as run_sampling_hw_ionq)
1209
+ 4. Calls load_samples/estimate_density for each timestep
1210
+ 5. Generates the slider figure using plot_density_isosurface_slider
1211
  """
1212
  global simulation_data_frames, simulation_times, current_grid_object
1213
 
 
1220
  log_to_console("Error: visualize_counts module not available")
1221
  return
1222
 
1223
+ # Get job ID - either from text field or from uploaded filename
1224
+ job_id = None
1225
+
1226
+ # First try the text field
1227
+ if _state.qlbm_job_id and str(_state.qlbm_job_id).strip():
1228
+ job_id = str(_state.qlbm_job_id).strip()
1229
+ # Remove .json extension if present
1230
+ if job_id.endswith(".json"):
1231
+ job_id = job_id[:-5]
1232
+ log_to_console(f"Using Job ID from text field: {job_id}")
1233
+ else:
1234
+ # Fallback: try to get from uploaded file name
1235
+ uploaded = _state.qlbm_job_upload
1236
+ if uploaded:
1237
+ file_data = uploaded[0] if isinstance(uploaded, list) else uploaded
1238
+ filename = file_data.get("name", "") if isinstance(file_data, dict) else ""
1239
+ if filename:
1240
+ # Extract job ID from filename (remove .json and any extra text)
1241
+ job_id = filename.replace(".json", "").strip()
1242
+ # Handle filenames like "019b368e-6e22-7525-8512-fd16e0503673 (1).json"
1243
+ # Remove any trailing parenthetical like " (1)"
1244
+ import re
1245
+ job_id = re.sub(r'\s*\(\d+\)\s*$', '', job_id)
1246
+ log_to_console(f"Extracted Job ID from filename: {job_id}")
1247
+
1248
+ if not job_id:
1249
+ _state.qlbm_job_upload_error = "No Job ID provided. Please enter a Job ID or upload a JSON file."
1250
  return
1251
 
1252
  # Reset messages
1253
  _state.qlbm_job_upload_error = ""
1254
  _state.qlbm_job_upload_success = ""
1255
  _state.qlbm_job_is_processing = True
1256
+ _state.qlbm_job_upload_filename = job_id
1257
+ log_to_console(f"Processing IonQ Job ID: {job_id}")
1258
 
1259
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1260
  # Parse timesteps from user input
 
1261
  try:
1262
  total_time = int(_state.qlbm_job_total_time or 3)
1263
  if total_time < 1:
 
1271
  log_to_console(f"Timesteps to process: {T_list}")
1272
 
1273
  # Get processing parameters
 
1274
  output_resolution = int(_state.qlbm_job_output_resolution or 40)
1275
+ # Fixed parameters for IonQ (same as run_sampling_hw_ionq)
1276
  flag_qubits = True
1277
+ midcircuit_meas = False # IonQ uses midcircuit_meas=False
1278
 
1279
+ log_to_console(f"Resolution: {output_resolution}, Flag qubits: {flag_qubits}, Midcircuit meas: {midcircuit_meas}")
1280
 
1281
+ # === Connect to IonQ and retrieve the job ===
1282
+ log_to_console("Connecting to IonQ...")
1283
+
1284
+ try:
1285
+ from qiskit_ionq import IonQProvider
1286
+ except ImportError:
1287
+ _state.qlbm_job_upload_error = "qiskit_ionq package not available. Please install it."
1288
+ _state.qlbm_job_is_processing = False
1289
+ log_to_console("Error: qiskit_ionq not installed")
1290
+ return
1291
+
1292
+ # Get API token from environment (same pattern as run_sampling_hw_ionq)
1293
+ ionq_token = os.environ.get("API_KEY_IONQ_QLBM") or os.environ.get("IONQ_API_TOKEN")
1294
+ if not ionq_token:
1295
+ _state.qlbm_job_upload_error = "IonQ API token not found. Set API_KEY_IONQ_QLBM environment variable."
1296
+ _state.qlbm_job_is_processing = False
1297
+ log_to_console("Error: IonQ API token not found in environment")
1298
+ return
1299
+
1300
+ # Set the IONQ_API_TOKEN env var so IonQProvider() can find it (same as run_sampling_hw_ionq)
1301
+ os.environ.setdefault("IONQ_API_TOKEN", ionq_token)
1302
 
1303
+ # Set up the IonQ provider and backend (IonQProvider reads from IONQ_API_TOKEN env var)
1304
+ provider = IonQProvider()
1305
+ backend = provider.get_backend("qpu.forte-enterprise-1")
1306
+ backend_name = backend.name if isinstance(backend.name, str) else backend.name()
1307
+ log_to_console(f"Connected to IonQ backend: {backend_name}")
1308
+
1309
+ # Retrieve the job
1310
+ log_to_console(f"Retrieving job: {job_id}")
1311
+ try:
1312
+ job = backend.retrieve_job(job_id)
1313
+ except Exception as e:
1314
+ _state.qlbm_job_upload_error = f"Failed to retrieve job: {e}"
1315
+ _state.qlbm_job_is_processing = False
1316
+ log_to_console(f"Error retrieving job: {e}")
1317
+ return
1318
+
1319
+ # Check job status
1320
+ try:
1321
+ status = job.status()
1322
+ status_name = status.name if hasattr(status, 'name') else str(status)
1323
+ log_to_console(f"Job status: {status_name}")
1324
+
1325
+ if status_name not in ('DONE', 'COMPLETED'):
1326
+ _state.qlbm_job_upload_error = f"Job is not complete. Current status: {status_name}"
1327
+ _state.qlbm_job_is_processing = False
1328
+ return
1329
+ except Exception as e:
1330
+ log_to_console(f"Warning: Could not check job status: {e}")
1331
+
1332
+ # === Process results (same as run_sampling_hw_ionq) ===
1333
+ log_to_console("Processing job results...")
1334
  output = []
1335
 
1336
+ for i, T_total in enumerate(T_list):
1337
+ try:
1338
+ log_to_console(f"Processing timestep T={T_total} (circuit {i})...")
1339
+
1340
+ # Get counts directly from job (same as run_sampling_hw_ionq)
1341
+ counts = job.get_counts(i)
1342
+ log_to_console(f" Retrieved {len(counts)} unique bitstrings")
1343
+
1344
+ # Debug: show a few sample bitstrings
1345
+ sample_count = 0
1346
+ for bs, cnt in counts.items():
1347
+ if sample_count < 3:
1348
+ log_to_console(f" Sample: {bs} (count={cnt})")
1349
+ sample_count += 1
1350
+
1351
+ # Process samples (same as run_sampling_hw_ionq)
1352
+ pts, processed_counts = load_samples(
1353
+ counts, T_total,
1354
+ logger=log_to_console,
1355
+ flag_qubits=flag_qubits,
1356
+ midcircuit_meas=midcircuit_meas
1357
+ )
1358
+ log_to_console(f" load_samples returned {len(pts)} valid sample points")
1359
+
1360
+ # Estimate density
1361
+ density = estimate_density(pts, processed_counts, bandwidth=0.05, grid_size=output_resolution)
1362
+ output.append(density)
1363
+
1364
+ except IndexError:
1365
+ log_to_console(f"Warning: No data found for timestep T={T_total} (circuit {i})")
1366
+ break
1367
+ except Exception as e:
1368
+ log_to_console(f"Error processing timestep {i}: {e}")
1369
+ import traceback
1370
+ log_to_console(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1371
 
1372
  if not output:
1373
+ _state.qlbm_job_upload_error = "No valid data extracted from job. Check timesteps parameter."
1374
  _state.qlbm_job_is_processing = False
1375
  return
1376
 
1377
  log_to_console(f"Processed {len(output)} timestep(s) successfully")
1378
 
1379
  # Generate the Plotly figure
1380
+ fig = plot_density_isosurface_slider(output, T_list[:len(output)])
1381
 
1382
  # Update state to show results
1383
  _state.qlbm_qiskit_mode = True
1384
  _state.qlbm_qiskit_fig = fig
1385
  _state.qlbm_simulation_has_run = True
1386
+ _state.qlbm_job_upload_success = f"✓ Successfully processed {len(output)} timestep(s) from job {job_id}"
1387
 
1388
  # Update the Plotly figure widget
1389
  if hasattr(_ctrl, "qlbm_qiskit_result_update"):
 
1391
 
1392
  log_to_console(f"Results ready! {len(output)} frames generated.")
1393
 
 
 
 
1394
  except Exception as e:
1395
+ _state.qlbm_job_upload_error = f"Error processing job: {e}"
1396
  log_to_console(f"Processing error: {e}")
1397
  import traceback
1398
  log_to_console(traceback.format_exc())
 
2393
 
2394
  # --- Job Result Upload Section ---
2395
  vuetify3.VDivider(classes="my-3")
2396
+ html.Div("Retrieve IonQ Job Results", classes="text-subtitle-2 font-weight-bold text-primary mb-2")
2397
+ html.Div("Enter the Job ID to retrieve results directly from IonQ",
2398
  classes="text-caption text-medium-emphasis mb-2")
2399
 
2400
+ # Job ID input
2401
+ with vuetify3.VTooltip(location="top"):
2402
+ with vuetify3.Template(v_slot_activator="{ props }"):
2403
+ vuetify3.VTextField(
2404
+ v_bind="props",
2405
+ label="IonQ Job ID",
2406
+ v_model=("qlbm_job_id", ""),
2407
+ density="compact",
2408
+ hide_details=True,
2409
+ color="primary",
2410
+ classes="mb-2",
2411
+ placeholder="e.g., 019b368e-6e22-7525-8512-fd16e0503673",
2412
+ prepend_icon="mdi-identifier",
2413
+ )
2414
+ html.Span("Enter the IonQ Job ID (UUID format)")
2415
+
2416
+ # Output resolution and Total Time in a row
2417
  with vuetify3.VRow(dense=True, classes="mb-2"):
2418
  with vuetify3.VCol(cols=6):
2419
  with vuetify3.VTooltip(location="top"):
2420
  with vuetify3.Template(v_slot_activator="{ props }"):
2421
+ vuetify3.VTextField(
2422
  v_bind="props",
2423
+ label="Total Time",
2424
+ v_model=("qlbm_job_total_time", 3),
2425
+ type="number",
2426
  density="compact",
2427
  hide_details=True,
2428
  color="primary",
2429
  )
2430
+ html.Span("Total number of time steps (T) used when running the job")
2431
  with vuetify3.VCol(cols=6):
2432
  with vuetify3.VTooltip(location="top"):
2433
  with vuetify3.Template(v_slot_activator="{ props }"):
 
2442
  )
2443
  html.Span("Resolution for 3D visualization. Should be <= Grid Size (2^n).")
2444
 
2445
+ # Generate button
2446
+ vuetify3.VBtn(
2447
+ text="Retrieve & Generate Plot",
2448
+ color="secondary",
2449
+ variant="tonal",
2450
+ block=True,
2451
+ disabled=("!qlbm_job_id || qlbm_job_is_processing", True),
2452
+ loading=("qlbm_job_is_processing", False),
2453
+ click=process_uploaded_job_result,
2454
+ prepend_icon="mdi-chart-box-outline",
2455
+ classes="mb-2",
2456
+ )
 
 
 
 
 
 
2457
 
2458
+ # Or use file upload (fallback)
2459
+ with vuetify3.VExpansionPanels(variant="accordion", classes="mt-2"):
2460
+ with vuetify3.VExpansionPanel():
2461
+ vuetify3.VExpansionPanelTitle(children=["Or extract Job ID from JSON filename"])
2462
+ with vuetify3.VExpansionPanelText():
2463
+ vuetify3.VFileInput(
2464
+ label="Upload JSON (for Job ID extraction)",
2465
+ v_model=("qlbm_job_upload", None),
2466
+ accept=".json",
2467
+ prepend_icon="mdi-file-upload",
2468
+ density="compact",
2469
+ hide_details=True,
2470
+ color="primary",
2471
+ show_size=True,
2472
+ clearable=True,
2473
+ hint="The Job ID will be extracted from the filename",
2474
+ )
 
 
 
 
 
 
 
 
2475
 
2476
  # Success message
2477
  vuetify3.VAlert(