harishaseebat92 commited on
Commit
841a0ef
·
1 Parent(s): 33cec4b

Runtime Version and QLBM Upload Option

Browse files
Files changed (3) hide show
  1. em/simulation.py +203 -51
  2. em/utils.py +0 -3
  3. qlbm_embedded.py +728 -146
em/simulation.py CHANGED
@@ -5,6 +5,9 @@ Contains simulation logic including run_simulation_only, reset_to_defaults,
5
  and stop handlers.
6
  """
7
  import re
 
 
 
8
  import numpy as np
9
 
10
  from .state import state, ctrl, _apply_workflow_highlights, is_statevector_estimator_selected, is_ibm_qpu_selected
@@ -36,6 +39,77 @@ except ModuleNotFoundError:
36
  )
37
  import utils.delta_impulse_generator as qutils
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  __all__ = [
40
  "run_simulation_only",
41
  "reset_to_defaults",
@@ -239,11 +313,42 @@ def redraw_surface_plot():
239
  ctrl.view_update()
240
 
241
 
 
 
 
 
242
  def run_simulation_only():
243
- """Run the simulation based on current state settings."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  from . import globals as g
245
  from .excitation import nearest_node_index
246
  from .qpu import build_qpu_timeseries_plotly_multi
 
 
 
 
 
 
 
 
247
 
248
  # Require selections before running
249
  if not state.geometry_selection:
@@ -255,6 +360,7 @@ def run_simulation_only():
255
  state.show_progress = False
256
  state.is_running = False
257
  state.run_button_text = "RUN!"
 
258
  return
259
 
260
  if not state.dist_type:
@@ -266,6 +372,7 @@ def run_simulation_only():
266
  state.show_progress = False
267
  state.is_running = False
268
  state.run_button_text = "RUN!"
 
269
  return
270
 
271
  # Show status: Starting simulation
@@ -275,13 +382,20 @@ def run_simulation_only():
275
  state.status_type = "info"
276
  state.show_progress = True
277
  state.simulation_progress = 0
 
 
 
 
278
 
 
 
279
  last_logged_percent = [0]
280
  def _progress_callback(percent):
281
  state.simulation_progress = percent
282
  if percent - last_logged_percent[0] >= 10:
283
  log_to_console(f"Simulation progress: {int(percent)}%")
284
  last_logged_percent[0] = percent
 
285
 
286
  # Reset stop flag and enable Stop button at start
287
  set_stop_simulation(False)
@@ -293,10 +407,9 @@ def run_simulation_only():
293
  state.is_running = True
294
  state.simulation_has_run = False
295
  state.run_button_text = "Running"
296
- try:
297
- ctrl.view_update()
298
- except Exception:
299
- pass
300
 
301
  nx, T = int(state.nx), float(state.T)
302
  na, R = 1, 4
@@ -304,6 +417,8 @@ def run_simulation_only():
304
  try:
305
  state.status_message = "Creating initial state..."
306
  state.simulation_progress = 10
 
 
307
  if state.dist_type == "Delta":
308
  initial_state = create_impulse_state_from_pos(
309
  (nx, nx),
@@ -325,6 +440,9 @@ def run_simulation_only():
325
  state.is_running = False
326
  state.run_button_text = "RUN!"
327
  state.stop_button_disabled = True
 
 
 
328
  return
329
 
330
  sve_selected = is_statevector_estimator_selected()
@@ -335,6 +453,8 @@ def run_simulation_only():
335
  log_to_console("Running Statevector Estimator...")
336
  state.status_message = "Running Statevector Estimator simulation..."
337
  state.simulation_progress = 20
 
 
338
  state.qpu_ts_ready = False
339
  state.qpu_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
340
  state.qpu_ts_other_ready = False
@@ -361,6 +481,7 @@ def run_simulation_only():
361
 
362
  state.status_message = "Building Statevector Estimator time series..."
363
  state.simulation_progress = 60
 
364
 
365
  def _sve_series_runner(field_type, positions, total_time, snapshot_dt, nx, impulse_pos, progress_callback=None, print_callback=None):
366
  return qutils.run_sve(
@@ -377,13 +498,16 @@ def run_simulation_only():
377
  print_callback=print_callback,
378
  )
379
 
380
- # Build and render Plotly chart
381
- fig = build_qpu_timeseries_plotly_multi(
382
- configs, nx, T, snapshot_dt, impulse_pos,
383
- series_runner=_sve_series_runner,
384
- progress_callback=_progress_callback,
385
- print_callback=log_to_console
386
- )
 
 
 
387
  qpu_ts_cache["fig"] = fig
388
 
389
  try:
@@ -425,10 +549,9 @@ def run_simulation_only():
425
  finally:
426
  state.is_running = False
427
  state.stop_button_disabled = True
428
- try:
429
- ctrl.view_update()
430
- except Exception:
431
- pass
432
  return
433
 
434
  # IBM QPU branch
@@ -438,10 +561,7 @@ def run_simulation_only():
438
  log_to_console("Running IBM QPU simulation...")
439
  state.status_message = "Running IBM QPU simulation..."
440
  state.simulation_progress = 5
441
- state.qpu_ts_ready = False
442
- state.qpu_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
443
- state.qpu_ts_other_ready = False
444
- state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
445
 
446
  # Import IBM QPU backend
447
  try:
@@ -476,34 +596,38 @@ def run_simulation_only():
476
 
477
  state.status_message = "Step 1: Circuit Construction & Optimization (0-40%)..."
478
  state.simulation_progress = 10
 
479
 
480
  def _ibm_progress_callback(pct):
481
  state.simulation_progress = int(pct)
482
- # Update status message based on progress stage
483
  if pct < 40:
484
  state.status_message = f"Step 1: Circuit Construction & Optimization ({int(pct)}%)"
485
  elif pct < 90:
486
  state.status_message = f"Step 2: Circuit Execution ({int(pct)}%)"
487
  else:
488
  state.status_message = f"Step 3: Result Processing ({int(pct)}%)"
 
489
 
490
- # Call the IBM QPU get_field_values function
491
- field_values = ibm_get_field_values(
492
- field=field_type,
493
- x=monitor_x,
494
- y=monitor_y,
495
- T=float(T),
496
- snapshot_time=snapshot_dt,
497
- nx=nx,
498
- impulse_pos=impulse_pos,
499
- shots=10000,
500
- pm_optimization_level=2,
501
- simulation="False",
502
- optimization="True",
503
- platform="IBM",
504
- progress_callback=_ibm_progress_callback,
505
- print_callback=log_to_console,
506
- )
 
 
 
507
 
508
  # Build time frames to match the output
509
  times = ibm_create_time_frames(float(T), snapshot_dt)
@@ -578,6 +702,7 @@ def run_simulation_only():
578
  log_to_console("IBM QPU run completed")
579
  state.status_type = "success"
580
  state.show_progress = False
 
581
 
582
  ready = bool(field_values) and len(field_values) > 0
583
  state.qpu_ts_ready = ready
@@ -613,10 +738,9 @@ def run_simulation_only():
613
  finally:
614
  state.is_running = False
615
  state.stop_button_disabled = True
616
- try:
617
- ctrl.view_update()
618
- except Exception:
619
- pass
620
  return
621
 
622
  # IonQ QPU placeholder branch (not yet implemented)
@@ -629,16 +753,18 @@ def run_simulation_only():
629
  state.run_button_text = "RUN!"
630
  state.stop_button_disabled = True
631
  log_to_console("IonQ QPU backend not connected. Use IBM QPU or Statevector Estimator instead.")
 
632
  try:
633
  ctrl.view_update()
634
  except Exception:
635
  pass
636
  return
637
 
638
- # Simulator path
639
  log_to_console("Running simulation...")
640
  state.status_message = "Running simulation... This may take a while."
641
  state.simulation_progress = 30
 
642
 
643
  snapshot_dt = float(state.dt_user)
644
 
@@ -646,19 +772,40 @@ def run_simulation_only():
646
  return g.stop_simulation
647
 
648
  state.simulation_progress = 50
649
- sim_data, times = run_sim(
650
- nx, na, R, initial_state, T,
651
- snapshot_dt=snapshot_dt,
652
- stop_check=_stop_check,
653
- progress_callback=_progress_callback,
654
- print_callback=log_to_console
655
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  g.simulation_data = sim_data
657
  g.snapshot_times = times
658
  log_to_console("Simulation complete.")
659
 
660
  state.simulation_progress = 80
661
  state.status_message = "Processing simulation results..."
 
662
 
663
  if sim_data.size > 0:
664
  setup_surface_plot_data(sim_data, nx)
@@ -678,6 +825,11 @@ def run_simulation_only():
678
 
679
  state.is_running = False
680
  state.stop_button_disabled = True
 
 
 
 
 
681
 
682
 
683
  def reset_to_defaults():
@@ -1059,4 +1211,4 @@ def update_value_display(point):
1059
  text = f"Index: ({ix}, {iy}) | Position: ({x_code:.3f}, {y_code:.3f})\nValue: {value:.3e}"
1060
 
1061
  plotter.add_text(text, name="value_text", position="lower_left", color="black", font_size=10)
1062
- ctrl.view_update()
 
5
  and stop handlers.
6
  """
7
  import re
8
+ import asyncio
9
+ import threading
10
+ import time
11
  import numpy as np
12
 
13
  from .state import state, ctrl, _apply_workflow_highlights, is_statevector_estimator_selected, is_ibm_qpu_selected
 
39
  )
40
  import utils.delta_impulse_generator as qutils
41
 
42
+ # --- Module-level async infrastructure ---
43
+ _heartbeat_thread = None
44
+ _heartbeat_on = False
45
+ _sim_start_time = None
46
+ _simulation_executor = None # Thread pool for async execution
47
+ _main_loop = None # Reference to main event loop for thread-safe callbacks
48
+
49
+ def _get_server():
50
+ """Get the trame server from state module."""
51
+ from .state import get_server
52
+ return get_server()
53
+
54
+ def _flush_state():
55
+ """Force state flush to browser (synchronous, for main thread use)."""
56
+ try:
57
+ server = _get_server()
58
+ if server:
59
+ server.state.flush()
60
+ except Exception:
61
+ pass
62
+
63
+ def _flush_state_threadsafe():
64
+ """
65
+ Thread-safe state flush - schedules flush on the main event loop.
66
+ Use this from background threads (e.g., inside executor callbacks).
67
+ """
68
+ global _main_loop
69
+ try:
70
+ server = _get_server()
71
+ if server and _main_loop is not None and _main_loop.is_running():
72
+ # Schedule the flush on the main event loop
73
+ _main_loop.call_soon_threadsafe(server.state.flush)
74
+ elif server:
75
+ # Fallback: direct flush (may not work from threads)
76
+ server.state.flush()
77
+ except Exception:
78
+ pass
79
+
80
+ async def _flush_async():
81
+ """Async helper to flush state and yield to event loop."""
82
+ _flush_state()
83
+ await asyncio.sleep(0) # Yield control to event loop
84
+
85
+ def _start_progress_heartbeat():
86
+ """Start background thread for continuous progress updates."""
87
+ global _heartbeat_thread, _heartbeat_on, _sim_start_time
88
+
89
+ if _heartbeat_thread and _heartbeat_thread.is_alive():
90
+ return
91
+
92
+ _sim_start_time = time.time()
93
+
94
+ def loop_fn():
95
+ global _heartbeat_on
96
+ while _heartbeat_on:
97
+ if state.is_running and _sim_start_time is not None:
98
+ elapsed = time.time() - _sim_start_time
99
+ state.simulation_elapsed = elapsed
100
+ _flush_state_threadsafe() # Use thread-safe version
101
+ time.sleep(0.1) # Update every 100ms
102
+
103
+ _heartbeat_on = True
104
+ _heartbeat_thread = threading.Thread(target=loop_fn, daemon=True)
105
+ _heartbeat_thread.start()
106
+
107
+ def _stop_progress_heartbeat():
108
+ """Stop the background heartbeat thread."""
109
+ global _heartbeat_on, _heartbeat_thread
110
+ _heartbeat_on = False
111
+ _heartbeat_thread = None
112
+
113
  __all__ = [
114
  "run_simulation_only",
115
  "reset_to_defaults",
 
313
  ctrl.view_update()
314
 
315
 
316
+ # ---------------------------------------------------------------------------
317
+ # Async Simulation Runner with Full Async Pattern
318
+ # ---------------------------------------------------------------------------
319
+
320
  def run_simulation_only():
321
+ """
322
+ Entry point for simulation - launches the async worker.
323
+ This is called by the UI button click and schedules the async task.
324
+ """
325
+ server = _get_server()
326
+ if server is None:
327
+ log_to_console("Error: Server not available")
328
+ return
329
+
330
+ # Schedule the async simulation
331
+ asyncio.ensure_future(_run_simulation_async())
332
+
333
+
334
+ async def _run_simulation_async():
335
+ """
336
+ Async simulation runner that uses thread pool for blocking work.
337
+ This allows the UI to update in real-time during simulation.
338
+ """
339
+ global _main_loop
340
+
341
  from . import globals as g
342
  from .excitation import nearest_node_index
343
  from .qpu import build_qpu_timeseries_plotly_multi
344
+ from concurrent.futures import ThreadPoolExecutor
345
+
346
+ # Capture the main event loop for thread-safe callbacks
347
+ _main_loop = asyncio.get_event_loop()
348
+
349
+ # Create executor for blocking operations
350
+ executor = ThreadPoolExecutor(max_workers=1)
351
+ loop = _main_loop
352
 
353
  # Require selections before running
354
  if not state.geometry_selection:
 
360
  state.show_progress = False
361
  state.is_running = False
362
  state.run_button_text = "RUN!"
363
+ await _flush_async()
364
  return
365
 
366
  if not state.dist_type:
 
372
  state.show_progress = False
373
  state.is_running = False
374
  state.run_button_text = "RUN!"
375
+ await _flush_async()
376
  return
377
 
378
  # Show status: Starting simulation
 
382
  state.status_type = "info"
383
  state.show_progress = True
384
  state.simulation_progress = 0
385
+ await _flush_async()
386
+
387
+ # Start heartbeat for continuous elapsed time updates
388
+ _start_progress_heartbeat()
389
 
390
+ # Progress callback that updates state (called from worker thread)
391
+ # Uses thread-safe flush to push updates to browser
392
  last_logged_percent = [0]
393
  def _progress_callback(percent):
394
  state.simulation_progress = percent
395
  if percent - last_logged_percent[0] >= 10:
396
  log_to_console(f"Simulation progress: {int(percent)}%")
397
  last_logged_percent[0] = percent
398
+ _flush_state_threadsafe() # Thread-safe flush!
399
 
400
  # Reset stop flag and enable Stop button at start
401
  set_stop_simulation(False)
 
407
  state.is_running = True
408
  state.simulation_has_run = False
409
  state.run_button_text = "Running"
410
+
411
+ # Initial flush to show "Running" state
412
+ _flush_state()
 
413
 
414
  nx, T = int(state.nx), float(state.T)
415
  na, R = 1, 4
 
417
  try:
418
  state.status_message = "Creating initial state..."
419
  state.simulation_progress = 10
420
+ _flush_state()
421
+
422
  if state.dist_type == "Delta":
423
  initial_state = create_impulse_state_from_pos(
424
  (nx, nx),
 
440
  state.is_running = False
441
  state.run_button_text = "RUN!"
442
  state.stop_button_disabled = True
443
+ _stop_progress_heartbeat()
444
+ await _flush_async()
445
+ executor.shutdown(wait=False)
446
  return
447
 
448
  sve_selected = is_statevector_estimator_selected()
 
453
  log_to_console("Running Statevector Estimator...")
454
  state.status_message = "Running Statevector Estimator simulation..."
455
  state.simulation_progress = 20
456
+ await _flush_async()
457
+
458
  state.qpu_ts_ready = False
459
  state.qpu_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
460
  state.qpu_ts_other_ready = False
 
481
 
482
  state.status_message = "Building Statevector Estimator time series..."
483
  state.simulation_progress = 60
484
+ await _flush_async()
485
 
486
  def _sve_series_runner(field_type, positions, total_time, snapshot_dt, nx, impulse_pos, progress_callback=None, print_callback=None):
487
  return qutils.run_sve(
 
498
  print_callback=print_callback,
499
  )
500
 
501
+ # Run SVE in executor to keep UI responsive
502
+ def _run_sve_blocking():
503
+ return build_qpu_timeseries_plotly_multi(
504
+ configs, nx, T, snapshot_dt, impulse_pos,
505
+ series_runner=_sve_series_runner,
506
+ progress_callback=_progress_callback,
507
+ print_callback=log_to_console
508
+ )
509
+
510
+ fig = await loop.run_in_executor(executor, _run_sve_blocking)
511
  qpu_ts_cache["fig"] = fig
512
 
513
  try:
 
549
  finally:
550
  state.is_running = False
551
  state.stop_button_disabled = True
552
+ _stop_progress_heartbeat()
553
+ await _flush_async()
554
+ executor.shutdown(wait=False)
 
555
  return
556
 
557
  # IBM QPU branch
 
561
  log_to_console("Running IBM QPU simulation...")
562
  state.status_message = "Running IBM QPU simulation..."
563
  state.simulation_progress = 5
564
+ await _flush_async()
 
 
 
565
 
566
  # Import IBM QPU backend
567
  try:
 
596
 
597
  state.status_message = "Step 1: Circuit Construction & Optimization (0-40%)..."
598
  state.simulation_progress = 10
599
+ await _flush_async()
600
 
601
  def _ibm_progress_callback(pct):
602
  state.simulation_progress = int(pct)
 
603
  if pct < 40:
604
  state.status_message = f"Step 1: Circuit Construction & Optimization ({int(pct)}%)"
605
  elif pct < 90:
606
  state.status_message = f"Step 2: Circuit Execution ({int(pct)}%)"
607
  else:
608
  state.status_message = f"Step 3: Result Processing ({int(pct)}%)"
609
+ _flush_state_threadsafe() # Thread-safe flush from callback thread
610
 
611
+ # Call the IBM QPU get_field_values function in executor to keep UI responsive
612
+ def _run_ibm_qpu():
613
+ return ibm_get_field_values(
614
+ field=field_type,
615
+ x=monitor_x,
616
+ y=monitor_y,
617
+ T=float(T),
618
+ snapshot_time=snapshot_dt,
619
+ nx=nx,
620
+ impulse_pos=impulse_pos,
621
+ shots=10000,
622
+ pm_optimization_level=2,
623
+ simulation="True",
624
+ optimization="True",
625
+ platform="IBM",
626
+ progress_callback=_ibm_progress_callback,
627
+ print_callback=log_to_console,
628
+ )
629
+
630
+ field_values = await loop.run_in_executor(executor, _run_ibm_qpu)
631
 
632
  # Build time frames to match the output
633
  times = ibm_create_time_frames(float(T), snapshot_dt)
 
702
  log_to_console("IBM QPU run completed")
703
  state.status_type = "success"
704
  state.show_progress = False
705
+ await _flush_async() # Update UI with completion status
706
 
707
  ready = bool(field_values) and len(field_values) > 0
708
  state.qpu_ts_ready = ready
 
738
  finally:
739
  state.is_running = False
740
  state.stop_button_disabled = True
741
+ _stop_progress_heartbeat()
742
+ executor.shutdown(wait=False)
743
+ await _flush_async()
 
744
  return
745
 
746
  # IonQ QPU placeholder branch (not yet implemented)
 
753
  state.run_button_text = "RUN!"
754
  state.stop_button_disabled = True
755
  log_to_console("IonQ QPU backend not connected. Use IBM QPU or Statevector Estimator instead.")
756
+ await _flush_async()
757
  try:
758
  ctrl.view_update()
759
  except Exception:
760
  pass
761
  return
762
 
763
+ # Simulator path - run blocking simulation in executor
764
  log_to_console("Running simulation...")
765
  state.status_message = "Running simulation... This may take a while."
766
  state.simulation_progress = 30
767
+ await _flush_async()
768
 
769
  snapshot_dt = float(state.dt_user)
770
 
 
772
  return g.stop_simulation
773
 
774
  state.simulation_progress = 50
775
+ await _flush_async()
776
+
777
+ # Run the blocking simulation in a thread pool to keep UI responsive
778
+ def _run_blocking_sim():
779
+ return run_sim(
780
+ nx, na, R, initial_state, T,
781
+ snapshot_dt=snapshot_dt,
782
+ stop_check=_stop_check,
783
+ progress_callback=_progress_callback,
784
+ print_callback=log_to_console
785
+ )
786
+
787
+ try:
788
+ sim_data, times = await loop.run_in_executor(executor, _run_blocking_sim)
789
+ except Exception as e:
790
+ state.error_message = f"Simulation error: {e}"
791
+ state.status_message = f"Error: {e}"
792
+ state.status_type = "error"
793
+ state.show_progress = False
794
+ state.is_running = False
795
+ state.run_button_text = "RUN!"
796
+ state.stop_button_disabled = True
797
+ _stop_progress_heartbeat()
798
+ await _flush_async()
799
+ executor.shutdown(wait=False)
800
+ return
801
+
802
  g.simulation_data = sim_data
803
  g.snapshot_times = times
804
  log_to_console("Simulation complete.")
805
 
806
  state.simulation_progress = 80
807
  state.status_message = "Processing simulation results..."
808
+ await _flush_async()
809
 
810
  if sim_data.size > 0:
811
  setup_surface_plot_data(sim_data, nx)
 
825
 
826
  state.is_running = False
827
  state.stop_button_disabled = True
828
+ _stop_progress_heartbeat()
829
+ await _flush_async()
830
+
831
+ # Cleanup executor
832
+ executor.shutdown(wait=False)
833
 
834
 
835
  def reset_to_defaults():
 
1211
  text = f"Index: ({ix}, {iy}) | Position: ({x_code:.3f}, {y_code:.3f})\nValue: {value:.3e}"
1212
 
1213
  plotter.add_text(text, name="value_text", position="lower_left", color="black", font_size=10)
1214
+ ctrl.view_update()
em/utils.py CHANGED
@@ -120,9 +120,6 @@ def load_logo_data_uri() -> str:
120
  base_dir = os.path.dirname(os.path.dirname(__file__)) # quantum_embedded folder
121
  candidates = [
122
  os.path.join(base_dir, "ansys-part-of-synopsys-logo.svg"),
123
- os.path.join(base_dir, "synopsys-logo-color-rgb.svg"),
124
- os.path.join(base_dir, "synopsys-logo-color-rgb.png"),
125
- os.path.join(base_dir, "synopsys-logo-color-rgb.jpg"),
126
  # Also check in parent quantum folder
127
  os.path.join(os.path.dirname(base_dir), "quantum", "ansys-part-of-synopsys-logo.svg"),
128
  ]
 
120
  base_dir = os.path.dirname(os.path.dirname(__file__)) # quantum_embedded folder
121
  candidates = [
122
  os.path.join(base_dir, "ansys-part-of-synopsys-logo.svg"),
 
 
 
123
  # Also check in parent quantum folder
124
  os.path.join(os.path.dirname(base_dir), "quantum", "ansys-part-of-synopsys-logo.svg"),
125
  ]
qlbm_embedded.py CHANGED
@@ -12,6 +12,11 @@ import math
12
  import pyvista as pv
13
  import plotly.graph_objects as go
14
  import tempfile
 
 
 
 
 
15
  from pathlib import Path
16
  from datetime import datetime
17
  from trame_vuetify.widgets import vuetify3
@@ -25,6 +30,7 @@ pv.OFF_SCREEN = True
25
  # --- Qiskit Backend Detection ---
26
  _QISKIT_BACKEND_AVAILABLE = False
27
  _QISKIT_IMPORT_ERROR = None
 
28
 
29
  try:
30
  from qlbm.qlbm_sample_app import (
@@ -41,6 +47,20 @@ except ImportError as e:
41
  _QISKIT_IMPORT_ERROR = str(e)
42
  print(f"Qiskit backend not available: {e}")
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  # --- CUDA-Q Backend Detection ---
45
  def _env_flag(name: str) -> bool:
46
  return os.environ.get(name, "").strip().lower() in ("1", "true", "yes")
@@ -90,6 +110,69 @@ simulation_data_frames = []
90
  simulation_times = []
91
  current_grid_object = None
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  GRID_SIZES = [8, 16, 32, 64, 128, 256]
94
  _WORKFLOW_BASE_STYLE = "font-size: 0.8rem; border: 1px solid transparent; transition: box-shadow 0.2s ease;"
95
  _WORKFLOW_HIGHLIGHT_STYLE = "font-size: 0.8rem; box-shadow: 0 0 0 2px #6200ea;"
@@ -213,6 +296,18 @@ def init_state():
213
  "qlbm_qiskit_mode": False, # True when using Qiskit backend (shows Plotly slider)
214
  "qlbm_qiskit_backend_available": _QISKIT_BACKEND_AVAILABLE,
215
  "qlbm_qiskit_fig": None, # Stores the Plotly figure for Qiskit results
 
 
 
 
 
 
 
 
 
 
 
 
216
  })
217
  _initialized = True
218
 
@@ -1077,10 +1172,231 @@ def _run_qiskit_simulation(progress_callback=None):
1077
  return output, fig, params["T_list"]
1078
 
1079
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1080
  # --- Main Simulation ---
1081
  def run_simulation():
1082
- """Run the QLBM simulation."""
1083
- global simulation_data_frames, simulation_times, current_grid_object, _plotter
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1084
 
1085
  if not _SIMULATION_CAN_RUN:
1086
  msg = _SIMULATION_DISABLED_REASON or "Simulation backend is not available on this platform."
@@ -1088,6 +1404,8 @@ def run_simulation():
1088
  log_to_console(f"Error: {msg}")
1089
  _state.qlbm_status_message = "Error: Backend unavailable"
1090
  _state.qlbm_status_type = "error"
 
 
1091
  return
1092
 
1093
  _state.qlbm_is_running = True
@@ -1096,8 +1414,12 @@ def run_simulation():
1096
  _state.qlbm_qiskit_mode = False # Reset Qiskit mode
1097
  _state.qlbm_show_progress = True
1098
  _state.qlbm_simulation_progress = 0
1099
- _state.qlbm_status_message = "Running simulation..."
1100
  _state.qlbm_status_type = "info"
 
 
 
 
1101
 
1102
  # Determine if using Qiskit backend
1103
  use_qiskit = (
@@ -1126,21 +1448,27 @@ def run_simulation():
1126
  log_to_console("Job Initiated")
1127
  log_to_console(f"Grid Size: {_state.qlbm_grid_size}×{_state.qlbm_grid_size}×{_state.qlbm_grid_size}, Time Steps: {_state.qlbm_time_steps}, Distribution: {_state.qlbm_dist_type}, Boundary: {_state.qlbm_boundary_condition}, Backend: {backend_info}, Velocity: vx={_state.qlbm_vx_expr}, vy={_state.qlbm_vy_expr}, vz={_state.qlbm_vz_expr}")
1128
 
1129
- last_logged_percent = 0
 
1130
  def _progress_callback(percent):
1131
- nonlocal last_logged_percent
1132
  _state.qlbm_simulation_progress = percent
1133
- if percent - last_logged_percent >= 10:
1134
  log_to_console(f"Simulation progress: {int(percent)}%")
1135
- last_logged_percent = percent
 
1136
 
1137
  try:
1138
  # === Qiskit Backend (IBM Qiskit Simulator) ===
1139
  if use_qiskit:
1140
  log_to_console("Using IBM Qiskit Simulator backend...")
 
 
 
 
 
 
1141
 
1142
- # Run Qiskit simulation
1143
- output, plotly_fig, T_list = _run_qiskit_simulation(progress_callback=_progress_callback)
1144
 
1145
  # Store results
1146
  simulation_data_frames = output
@@ -1156,14 +1484,17 @@ def run_simulation():
1156
  _state.qlbm_simulation_has_run = True
1157
  _state.qlbm_qiskit_mode = True # Use Plotly display instead of PyVista
1158
 
1159
- _progress_callback(100)
1160
  log_to_console("Qiskit simulation completed successfully.")
1161
  _state.qlbm_status_message = "Simulation completed successfully."
1162
  _state.qlbm_status_type = "success"
 
1163
 
1164
  # === IBM QPU Backend ===
1165
  elif use_ibm_qpu:
1166
  log_to_console("Using IBM QPU backend...")
 
 
1167
 
1168
  params = _map_state_to_qiskit_params()
1169
  if params is None:
@@ -1171,6 +1502,9 @@ def run_simulation():
1171
 
1172
  # Create initial state circuit
1173
  log_to_console("Creating initial state circuit...")
 
 
 
1174
  init_state_prep_circ = get_named_init_state_circuit(
1175
  n=params["n"],
1176
  init_state_name=params["init_state_name"],
@@ -1187,24 +1521,37 @@ def run_simulation():
1187
  )
1188
 
1189
  log_to_console("Submitting job to IBM Quantum...")
 
 
 
1190
 
1191
- # Run HW simulation
1192
- job, get_result = run_sampling_hw_ibm(
1193
- n=params["n"],
1194
- ux=params["vx_expr"],
1195
- uy=params["vy_expr"],
1196
- uz=params["vz_expr"],
1197
- init_state_prep_circ=init_state_prep_circ,
1198
- T_list=params["T_list"],
1199
- # shots=2**14, # Reduced shots for responsiveness/quota
1200
- shots=2**14,
1201
- vel_resolution=min(params['grid_size'], 32),
1202
- output_resolution=min(2*params['grid_size'], 40),
1203
- logger=log_to_console
1204
- )
 
 
 
 
1205
 
1206
- log_to_console("Waiting for job results (this may take time)...")
1207
- output, plotly_fig = get_result(job)
 
 
 
 
 
 
1208
 
1209
  # Generate T=0 initial snapshot and prepend to output
1210
  log_to_console("Generating T=0 initial distribution snapshot...")
@@ -1240,6 +1587,10 @@ def run_simulation():
1240
  log_to_console(f"Warning: Could not generate T=0 snapshot: {exc}")
1241
  extended_T_list = params["T_list"]
1242
 
 
 
 
 
1243
  # Update UI
1244
  if hasattr(_ctrl, "qlbm_qiskit_result_update"):
1245
  _ctrl.qlbm_qiskit_result_update(plotly_fig)
@@ -1250,14 +1601,17 @@ def run_simulation():
1250
  _state.qlbm_simulation_has_run = True
1251
  _state.qlbm_qiskit_mode = True
1252
 
1253
- _progress_callback(100)
1254
  log_to_console("IBM QPU simulation completed successfully.")
1255
  _state.qlbm_status_message = "Simulation completed successfully."
1256
  _state.qlbm_status_type = "success"
 
1257
 
1258
  # === IonQ QPU Backend ===
1259
  elif use_ionq_qpu:
1260
  log_to_console("Using IonQ QPU backend...")
 
 
1261
 
1262
  params = _map_state_to_qiskit_params()
1263
  if params is None:
@@ -1265,6 +1619,9 @@ def run_simulation():
1265
 
1266
  # Create initial state circuit
1267
  log_to_console("Creating initial state circuit...")
 
 
 
1268
  init_state_prep_circ = get_named_init_state_circuit(
1269
  n=params["n"],
1270
  init_state_name=params["init_state_name"],
@@ -1281,23 +1638,37 @@ def run_simulation():
1281
  )
1282
 
1283
  log_to_console("Submitting job to IonQ Quantum...")
 
 
 
1284
 
1285
- # Run IonQ HW simulation
1286
- job, get_result = run_sampling_hw_ionq(
1287
- n=params["n"],
1288
- ux=params["vx_expr"],
1289
- uy=params["vy_expr"],
1290
- uz=params["vz_expr"],
1291
- init_state_prep_circ=init_state_prep_circ,
1292
- T_list=params["T_list"],
1293
- shots=2**14,
1294
- vel_resolution=min(params['grid_size'], 32),
1295
- output_resolution=min(2*params['grid_size'], 40),
1296
- logger=log_to_console
1297
- )
 
 
 
 
 
 
 
 
 
1298
 
1299
- log_to_console("Waiting for IonQ job results (this may take time)...")
1300
- output, plotly_fig = get_result(job)
 
 
1301
 
1302
  # Generate T=0 initial snapshot and prepend to output
1303
  log_to_console("Generating T=0 initial distribution snapshot...")
@@ -1333,6 +1704,10 @@ def run_simulation():
1333
  log_to_console(f"Warning: Could not generate T=0 snapshot: {exc}")
1334
  extended_T_list = params["T_list"]
1335
 
 
 
 
 
1336
  # Update UI
1337
  if hasattr(_ctrl, "qlbm_qiskit_result_update"):
1338
  _ctrl.qlbm_qiskit_result_update(plotly_fig)
@@ -1343,14 +1718,17 @@ def run_simulation():
1343
  _state.qlbm_simulation_has_run = True
1344
  _state.qlbm_qiskit_mode = True
1345
 
1346
- _progress_callback(100)
1347
  log_to_console("IonQ QPU simulation completed successfully.")
1348
  _state.qlbm_status_message = "Simulation completed successfully."
1349
  _state.qlbm_status_type = "success"
 
1350
 
1351
  # === CUDA-Q Backend ===
1352
  elif _state.qlbm_backend_type == "Simulator" and _state.qlbm_selected_simulator == "CUDA-Q simulator":
1353
  _state.qlbm_qiskit_mode = False # Use PyVista display
 
 
1354
 
1355
  grid_size = int(_state.qlbm_grid_size)
1356
  num_reg_qubits = int(math.log2(grid_size)) if grid_size > 0 else 3
@@ -1362,37 +1740,49 @@ def run_simulation():
1362
  vy_func = make_velocity_func(_state.qlbm_vy_expr)
1363
  vz_func = make_velocity_func(_state.qlbm_vz_expr)
1364
 
1365
- _progress_callback(0)
 
1366
 
1367
  if simulate_qlbm_3D_and_animate is not None:
1368
  log_to_console("Running CUDA-Q Simulation...")
1369
- _plotter.clear()
1370
- _, frames, times, grid_obj = simulate_qlbm_3D_and_animate(
1371
- num_reg_qubits=num_reg_qubits,
1372
- T=T,
1373
- distribution_type=distribution_type,
1374
- vx_input=vx_func,
1375
- vy_input=vy_func,
1376
- vz_input=vz_func,
1377
- boundary_condition=boundary_condition,
1378
- plotter=_plotter,
1379
- add_slider=False,
1380
- progress_callback=_progress_callback
1381
- )
 
 
 
 
 
 
1382
  else:
1383
  # Fallback to CPU demo if CUDA-Q not available
1384
  log_to_console("CUDA-Q not available, falling back to CPU Demo...")
1385
- frames, times, grid_obj = _run_cpu_demo_simulation(
1386
- grid_size=grid_size,
1387
- T=T,
1388
- distribution_type=distribution_type or "Sinusoidal",
1389
- vx_func=vx_func,
1390
- vy_func=vy_func,
1391
- vz_func=vz_func,
1392
- progress_callback=_progress_callback
1393
- )
 
 
 
 
1394
 
1395
- _progress_callback(100)
 
1396
 
1397
  # Update plotter with results
1398
  if grid_obj:
@@ -1421,15 +1811,19 @@ def run_simulation():
1421
  _state.qlbm_status_message = "Simulation completed successfully."
1422
  _state.qlbm_status_type = "success"
1423
  _state.qlbm_simulation_progress = 100
 
1424
  else:
1425
  _state.qlbm_run_error = "Simulation produced no data."
1426
  log_to_console("Error: Simulation produced no data.")
1427
  _state.qlbm_status_message = "Error: No data produced"
1428
  _state.qlbm_status_type = "error"
 
1429
 
1430
  # === CPU Demo Backend (for QPU or fallback) ===
1431
  else:
1432
  _state.qlbm_qiskit_mode = False # Use PyVista display
 
 
1433
 
1434
  grid_size = int(_state.qlbm_grid_size)
1435
  num_reg_qubits = int(math.log2(grid_size)) if grid_size > 0 else 3
@@ -1443,19 +1837,27 @@ def run_simulation():
1443
 
1444
  _progress_callback(0)
1445
 
1446
- # CPU Demo Simulation
 
 
 
1447
  log_to_console("Running CPU Demo Simulation...")
1448
- frames, times, grid_obj = _run_cpu_demo_simulation(
1449
- grid_size=grid_size,
1450
- T=T,
1451
- distribution_type=distribution_type or "Sinusoidal",
1452
- vx_func=vx_func,
1453
- vy_func=vy_func,
1454
- vz_func=vz_func,
1455
- progress_callback=_progress_callback
1456
- )
1457
 
1458
- _progress_callback(100)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1459
 
1460
  # Update plotter with results
1461
  if grid_obj:
@@ -1484,11 +1886,13 @@ def run_simulation():
1484
  _state.qlbm_status_message = "Simulation completed successfully."
1485
  _state.qlbm_status_type = "success"
1486
  _state.qlbm_simulation_progress = 100
 
1487
  else:
1488
  _state.qlbm_run_error = "Simulation produced no data."
1489
  log_to_console("Error: Simulation produced no data.")
1490
  _state.qlbm_status_message = "Error: No data produced"
1491
  _state.qlbm_status_type = "error"
 
1492
 
1493
  except Exception as e:
1494
  _state.qlbm_run_error = f"Simulation failed: {str(e)}"
@@ -1498,10 +1902,14 @@ def run_simulation():
1498
  traceback.print_exc()
1499
  _state.qlbm_status_message = "Simulation failed"
1500
  _state.qlbm_status_type = "error"
 
1501
  finally:
1502
  _state.qlbm_is_running = False
 
 
1503
  if _state.qlbm_status_type != "success":
1504
  _state.qlbm_show_progress = False
 
1505
 
1506
 
1507
  def stop_simulation():
@@ -1659,23 +2067,27 @@ def _build_control_panels(plotter):
1659
  with vuetify3.VCardText():
1660
  vuetify3.VDivider(classes="my-2")
1661
  vuetify3.VCardSubtitle("Problems", classes="text-caption font-weight-bold mt-2")
1662
- vuetify3.VSelect(
1663
- key="qlbm_overview_problems",
1664
- label="Select a problem",
1665
- v_model=("qlbm_problems_selection", None),
1666
- items=(
1667
- "qlbm_qlbm_problems",
1668
- [
1669
- "Scalar advection-diffusion in a box",
1670
- "Laminar flow & heat transfer for a heated body in water.",
1671
- ],
1672
- ),
1673
- placeholder="Select",
1674
- density="compact",
1675
- hide_details=True,
1676
- color="primary",
1677
- classes="mb-2"
1678
- )
 
 
 
 
1679
 
1680
  # Geometry card
1681
  with vuetify3.VCard(classes="mb-2"):
@@ -1703,11 +2115,20 @@ def _build_control_panels(plotter):
1703
  vuetify3.VCardSubtitle("Domain dimensions", classes="text-caption font-weight-bold mb-2")
1704
  with vuetify3.VRow(dense=True):
1705
  with vuetify3.VCol():
1706
- vuetify3.VTextField(label="Length (L)", v_model=("qlbm_domain_L", 1.0), type="number", step="0.1", density="compact", hide_details=True, color="primary")
 
 
 
1707
  with vuetify3.VCol():
1708
- vuetify3.VTextField(label="Width (W)", v_model=("qlbm_domain_W", 1.0), type="number", step="0.1", density="compact", hide_details=True, color="primary")
 
 
 
1709
  with vuetify3.VCol():
1710
- vuetify3.VTextField(label="Height (H)", v_model=("qlbm_domain_H", 1.0), type="number", step="0.1", density="compact", hide_details=True, color="primary")
 
 
 
1711
 
1712
  # Initial Distribution card
1713
  with vuetify3.VCard(classes="mb-2", style=("qlbm_distribution_card_style", _WORKFLOW_BASE_STYLE)):
@@ -1715,13 +2136,17 @@ def _build_control_panels(plotter):
1715
  with vuetify3.VCardText():
1716
  with vuetify3.VRow(classes="d-flex align-center mb-2", no_gutters=True):
1717
  with vuetify3.VCol(cols="auto", classes="flex-grow-1"):
1718
- vuetify3.VSelect(
1719
- label="Initial Distribution",
1720
- v_model=("qlbm_dist_type", None),
1721
- items=("qlbm_dist_modes",),
1722
- density="compact",
1723
- hide_details=True
1724
- )
 
 
 
 
1725
  with vuetify3.VCol(cols="auto", classes="ml-2"):
1726
  with vuetify3.VBtn(
1727
  icon=True, density="compact", variant="text",
@@ -1734,30 +2159,42 @@ def _build_control_panels(plotter):
1734
  vuetify3.VCardTitle("Sinusoidal Frequencies")
1735
  with vuetify3.VCardText():
1736
  for axis in ['x', 'y', 'z']:
1737
- vuetify3.VSlider(
1738
- label=f"Freq {axis.upper()}",
1739
- v_model=(f"qlbm_sine_k_{axis}", 1.0),
1740
- min=1, max=5, step=1,
1741
- thumb_label="always", density="compact"
1742
- )
 
 
 
 
1743
 
1744
  # Gaussian controls
1745
  with vuetify3.VCard(classes="mb-2", v_if="qlbm_custom_dist_params && qlbm_dist_type === 'Gaussian'"):
1746
  vuetify3.VCardTitle("Gaussian Parameters")
1747
  with vuetify3.VCardText():
1748
  for axis in ['x', 'y', 'z']:
1749
- vuetify3.VSlider(
1750
- label=f"Center {axis.upper()}",
1751
- v_model=(f"qlbm_gauss_c{axis}", 16),
1752
- min=0, max=("qlbm_nx", 32), step=1,
1753
- thumb_label="always", density="compact"
1754
- )
1755
- vuetify3.VSlider(
1756
- label="Width (Sigma)",
1757
- v_model=("qlbm_gauss_sigma", 6.0),
1758
- min=1.0, max=20.0, step=0.5,
1759
- thumb_label="always", density="compact"
1760
- )
 
 
 
 
 
 
 
 
1761
 
1762
  # Multi-Dirac-Delta controls
1763
  with vuetify3.VCard(classes="mb-2", v_if="qlbm_custom_dist_params && qlbm_dist_type === 'Multi-Dirac-Delta'"):
@@ -1765,19 +2202,26 @@ def _build_control_panels(plotter):
1765
  with vuetify3.VCardText():
1766
  vuetify3.VCardSubtitle("Number of delta peaks per axis = 2^k", classes="text-caption mb-2")
1767
  for axis in ['x', 'y', 'z']:
1768
- vuetify3.VSlider(
1769
- label=f"k_{axis.upper()} (log₂)",
1770
- v_model=(f"qlbm_mdd_k{axis}_log2", 1),
1771
- min=1, max=4, step=1,
1772
- thumb_label="always", density="compact"
1773
- )
 
 
 
 
1774
 
1775
  # Boundary Conditions
1776
  with vuetify3.VCard(classes="mb-2"):
1777
  vuetify3.VCardTitle("Boundary Conditions", classes="text-subtitle-2 font-weight-bold text-primary")
1778
  with vuetify3.VCardText():
1779
- vuetify3.VSelect(label="Boundary Condition", v_model=("qlbm_boundary_condition", "Periodic"),
1780
- items=("['Periodic']",), density="compact", hide_details=True, color="primary")
 
 
 
1781
 
1782
  # Advecting Fields
1783
  with vuetify3.VCard(classes="mb-2", style=("qlbm_advect_card_style", _WORKFLOW_BASE_STYLE)):
@@ -1785,15 +2229,19 @@ def _build_control_panels(plotter):
1785
  with vuetify3.VCardText():
1786
  with vuetify3.VRow(classes="d-flex align-center mb-2", no_gutters=True):
1787
  with vuetify3.VCol(cols="auto", classes="flex-grow-1"):
1788
- vuetify3.VSelect(
1789
- label="Select Advecting field",
1790
- v_model=("qlbm_advecting_field", None),
1791
- items=("['Uniform', 'Swirl', 'Shear', 'TGV']",),
1792
- density="compact",
1793
- hide_details=True,
1794
- color="primary",
1795
- placeholder="Select",
1796
- )
 
 
 
 
1797
  with vuetify3.VCol(cols="auto", classes="ml-2"):
1798
  with vuetify3.VBtn(
1799
  icon=True, density="compact", variant="text",
@@ -1802,9 +2250,18 @@ def _build_control_panels(plotter):
1802
  vuetify3.VIcon("mdi-cog", color=("qlbm_show_advect_params ? 'primary' : 'grey'",))
1803
  with vuetify3.VContainer(v_if="qlbm_show_advect_params", classes="pa-0 mt-2"):
1804
  html.Div("Velocity Components", classes="text-caption mb-1")
1805
- vuetify3.VTextField(label="Velocity vx", v_model=("qlbm_vx_expr", "0.2"), density="compact", hide_details=True, color="primary", classes="mb-1")
1806
- vuetify3.VTextField(label="Velocity vy", v_model=("qlbm_vy_expr", "-0.15"), density="compact", hide_details=True, color="primary", classes="mb-1")
1807
- vuetify3.VTextField(label="Velocity vz", v_model=("qlbm_vz_expr", "0.3"), density="compact", hide_details=True, color="primary")
 
 
 
 
 
 
 
 
 
1808
 
1809
  # Meshing
1810
  with vuetify3.VCard(classes="mb-2", style=("qlbm_meshing_card_style", _WORKFLOW_BASE_STYLE)):
@@ -1837,8 +2294,11 @@ def _build_control_panels(plotter):
1837
  with vuetify3.VCard(classes="mb-2"):
1838
  vuetify3.VCardTitle("Time", classes="text-subtitle-2 font-weight-bold text-primary")
1839
  with vuetify3.VCardText():
1840
- vuetify3.VSlider(label="Total Time", v_model=("qlbm_time_steps", 10), min=0, max=30, step=1,
1841
- thumb_label="always", show_ticks="always", color="primary", density="compact", hide_details=True)
 
 
 
1842
  vuetify3.VAlert(v_if="qlbm_time_steps > 100", type="warning", variant="tonal", density="compact",
1843
  children=["Warning: High time steps may increase runtime."], classes="mt-2")
1844
 
@@ -1886,13 +2346,33 @@ def _build_control_panels(plotter):
1886
  children=["⚠️ Grid size > 16 may exceed IBM QPU capacity!"],
1887
  classes="mt-2"
1888
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1889
 
1890
  vuetify3.VDivider(classes="my-3")
1891
  vuetify3.VBtn(
1892
  text="Run",
1893
  color="primary",
1894
  block=True,
1895
- disabled=("qlbm_is_running || !qlbm_backend_type", True),
1896
  click=run_simulation,
1897
  style=("qlbm_is_running ? '' : 'background-color:#87CEFA;'", ""),
1898
  )
@@ -1925,6 +2405,108 @@ def _build_control_panels(plotter):
1925
  click=stop_simulation,
1926
  disabled=("!qlbm_is_running", True),
1927
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1928
 
1929
 
1930
  def _build_visualization_panel(plotter):
 
12
  import pyvista as pv
13
  import plotly.graph_objects as go
14
  import tempfile
15
+ import base64
16
+ import json
17
+ import asyncio
18
+ import threading
19
+ import time as time_module
20
  from pathlib import Path
21
  from datetime import datetime
22
  from trame_vuetify.widgets import vuetify3
 
30
  # --- Qiskit Backend Detection ---
31
  _QISKIT_BACKEND_AVAILABLE = False
32
  _QISKIT_IMPORT_ERROR = None
33
+ _VISUALIZE_COUNTS_AVAILABLE = False
34
 
35
  try:
36
  from qlbm.qlbm_sample_app import (
 
47
  _QISKIT_IMPORT_ERROR = str(e)
48
  print(f"Qiskit backend not available: {e}")
49
 
50
+ # Import visualize_counts for job result processing
51
+ try:
52
+ from qlbm.visualize_counts import (
53
+ load_samples,
54
+ estimate_density,
55
+ plot_density_isosurface_slider,
56
+ )
57
+ _VISUALIZE_COUNTS_AVAILABLE = True
58
+ except ImportError as e:
59
+ print(f"visualize_counts not available: {e}")
60
+ load_samples = None
61
+ estimate_density = None
62
+ plot_density_isosurface_slider = None
63
+
64
  # --- CUDA-Q Backend Detection ---
65
  def _env_flag(name: str) -> bool:
66
  return os.environ.get(name, "").strip().lower() in ("1", "true", "yes")
 
110
  simulation_times = []
111
  current_grid_object = None
112
 
113
+ # --- Async infrastructure for real-time progress updates ---
114
+ _qlbm_main_loop = None # Reference to main event loop for thread-safe callbacks
115
+ _qlbm_heartbeat_thread = None
116
+ _qlbm_heartbeat_on = False
117
+ _qlbm_sim_start_time = None
118
+
119
+ def _qlbm_flush_state():
120
+ """Force state flush to browser (synchronous, for main thread use)."""
121
+ try:
122
+ if _server:
123
+ _server.state.flush()
124
+ except Exception:
125
+ pass
126
+
127
+ def _qlbm_flush_state_threadsafe():
128
+ """
129
+ Thread-safe state flush - schedules flush on the main event loop.
130
+ Use this from background threads (e.g., inside executor callbacks).
131
+ """
132
+ global _qlbm_main_loop
133
+ try:
134
+ if _server and _qlbm_main_loop is not None and _qlbm_main_loop.is_running():
135
+ # Schedule the flush on the main event loop
136
+ _qlbm_main_loop.call_soon_threadsafe(_server.state.flush)
137
+ elif _server:
138
+ # Fallback: direct flush (may not work from threads)
139
+ _server.state.flush()
140
+ except Exception:
141
+ pass
142
+
143
+ async def _qlbm_flush_async():
144
+ """Async helper to flush state and yield to event loop."""
145
+ _qlbm_flush_state()
146
+ await asyncio.sleep(0) # Yield control to event loop
147
+
148
+ def _qlbm_start_progress_heartbeat():
149
+ """Start background thread for continuous progress updates."""
150
+ global _qlbm_heartbeat_thread, _qlbm_heartbeat_on, _qlbm_sim_start_time
151
+
152
+ if _qlbm_heartbeat_thread and _qlbm_heartbeat_thread.is_alive():
153
+ return
154
+
155
+ _qlbm_sim_start_time = time_module.time()
156
+
157
+ def loop_fn():
158
+ global _qlbm_heartbeat_on
159
+ while _qlbm_heartbeat_on:
160
+ if _state is not None and _state.qlbm_is_running and _qlbm_sim_start_time is not None:
161
+ elapsed = time_module.time() - _qlbm_sim_start_time
162
+ # Optionally update elapsed time state here if needed
163
+ _qlbm_flush_state_threadsafe()
164
+ time_module.sleep(0.1) # Update every 100ms
165
+
166
+ _qlbm_heartbeat_on = True
167
+ _qlbm_heartbeat_thread = threading.Thread(target=loop_fn, daemon=True)
168
+ _qlbm_heartbeat_thread.start()
169
+
170
+ def _qlbm_stop_progress_heartbeat():
171
+ """Stop the background heartbeat thread."""
172
+ global _qlbm_heartbeat_on, _qlbm_heartbeat_thread
173
+ _qlbm_heartbeat_on = False
174
+ _qlbm_heartbeat_thread = None
175
+
176
  GRID_SIZES = [8, 16, 32, 64, 128, 256]
177
  _WORKFLOW_BASE_STYLE = "font-size: 0.8rem; border: 1px solid transparent; transition: box-shadow 0.2s ease;"
178
  _WORKFLOW_HIGHLIGHT_STYLE = "font-size: 0.8rem; box-shadow: 0 0 0 2px #6200ea;"
 
296
  "qlbm_qiskit_mode": False, # True when using Qiskit backend (shows Plotly slider)
297
  "qlbm_qiskit_backend_available": _QISKIT_BACKEND_AVAILABLE,
298
  "qlbm_qiskit_fig": None, # Stores the Plotly figure for Qiskit results
299
+
300
+ # Job upload state (for loading previously saved QPU job results)
301
+ "qlbm_job_upload": None, # File upload content
302
+ "qlbm_job_upload_filename": "", # Display the uploaded filename
303
+ "qlbm_job_upload_error": "", # Error message for upload
304
+ "qlbm_job_upload_success": "", # Success message for upload
305
+ "qlbm_job_platform": "IBM", # Platform: "IBM" or "IonQ"
306
+ "qlbm_job_total_time": 3, # Total time T (generates T_list = [1..T])
307
+ "qlbm_job_output_resolution": 40, # Grid resolution for density estimation
308
+ "qlbm_job_is_processing": False, # True when processing uploaded job
309
+ "qlbm_job_flag_qubits": True, # Whether flag qubits were used
310
+ "qlbm_job_midcircuit_meas": True, # Whether mid-circuit measurement was used (IBM only)
311
  })
312
  _initialized = True
313
 
 
1172
  return output, fig, params["T_list"]
1173
 
1174
 
1175
+ # --- Job Result Upload Processing ---
1176
+ def process_uploaded_job_result():
1177
+ """
1178
+ Process an uploaded IBM/IonQ job result JSON file and generate the Plotly figure.
1179
+
1180
+ This function:
1181
+ 1. Decodes the uploaded file (base64 -> JSON)
1182
+ 2. Parses the job result based on platform (IBM uses RuntimeDecoder, IonQ uses plain dict)
1183
+ 3. Calls load_samples/estimate_density for each timestep
1184
+ 4. Generates the slider figure using plot_density_isosurface_slider
1185
+ """
1186
+ global simulation_data_frames, simulation_times, current_grid_object
1187
+
1188
+ if _state is None:
1189
+ return
1190
+
1191
+ # Validate required imports
1192
+ if not _VISUALIZE_COUNTS_AVAILABLE:
1193
+ _state.qlbm_job_upload_error = "visualize_counts module not available. Cannot process job results."
1194
+ log_to_console("Error: visualize_counts module not available")
1195
+ return
1196
+
1197
+ # Validate file upload
1198
+ uploaded = _state.qlbm_job_upload
1199
+ if not uploaded:
1200
+ _state.qlbm_job_upload_error = "No file uploaded. Please select a JSON file."
1201
+ return
1202
+
1203
+ # Reset messages
1204
+ _state.qlbm_job_upload_error = ""
1205
+ _state.qlbm_job_upload_success = ""
1206
+ _state.qlbm_job_is_processing = True
1207
+ log_to_console("Processing uploaded job result...")
1208
+
1209
+ try:
1210
+ # Handle list or single file
1211
+ file_data = uploaded[0] if isinstance(uploaded, list) else uploaded
1212
+
1213
+ # Get filename for display
1214
+ filename = file_data.get("name", "unknown.json") if isinstance(file_data, dict) else "unknown.json"
1215
+ _state.qlbm_job_upload_filename = filename
1216
+ log_to_console(f"Processing file: {filename}")
1217
+
1218
+ # Decode base64 content
1219
+ content = file_data.get("content", "") if isinstance(file_data, dict) else ""
1220
+ if content.startswith("data:"):
1221
+ content = content.split(",", 1)[1] # Remove data URI prefix
1222
+
1223
+ raw_bytes = base64.b64decode(content)
1224
+ json_str = raw_bytes.decode("utf-8")
1225
+
1226
+ # Parse timesteps from user input
1227
+ # User provides Total Time T, we generate T_list = [1, 2, ..., T]
1228
+ try:
1229
+ total_time = int(_state.qlbm_job_total_time or 3)
1230
+ if total_time < 1:
1231
+ total_time = 1
1232
+ T_list = list(range(1, total_time + 1))
1233
+ except ValueError:
1234
+ _state.qlbm_job_upload_error = "Invalid Total Time. Please enter a positive integer."
1235
+ _state.qlbm_job_is_processing = False
1236
+ return
1237
+
1238
+ log_to_console(f"Timesteps to process: {T_list}")
1239
+
1240
+ # Get processing parameters
1241
+ platform = _state.qlbm_job_platform or "IBM"
1242
+ output_resolution = int(_state.qlbm_job_output_resolution or 40)
1243
+ # Defaulting to True as per user request, and hiding from UI
1244
+ flag_qubits = True
1245
+ midcircuit_meas = True
1246
+
1247
+ log_to_console(f"Platform: {platform}, Resolution: {output_resolution}, Flag qubits: {flag_qubits}")
1248
+
1249
+ # Parse JSON based on platform
1250
+ if platform == "IBM":
1251
+ # IBM uses RuntimeDecoder for proper result parsing
1252
+ try:
1253
+ from qiskit_ibm_runtime import RuntimeDecoder
1254
+ result = json.loads(json_str, cls=RuntimeDecoder)
1255
+ log_to_console("Parsed IBM result with RuntimeDecoder")
1256
+ except ImportError:
1257
+ # Fallback to plain JSON if RuntimeDecoder not available
1258
+ result = json.loads(json_str)
1259
+ log_to_console("Warning: RuntimeDecoder not available, using plain JSON")
1260
+ except Exception as e:
1261
+ # Try plain JSON as fallback
1262
+ result = json.loads(json_str)
1263
+ log_to_console(f"RuntimeDecoder failed ({e}), using plain JSON")
1264
+ else:
1265
+ # IonQ uses plain JSON
1266
+ result = json.loads(json_str)
1267
+ log_to_console("Parsed IonQ result as plain JSON")
1268
+
1269
+ # Process each timestep
1270
+ output = []
1271
+
1272
+ # Determine how to extract counts based on result structure
1273
+ if platform == "IBM":
1274
+ # IBM PrimitiveResult structure: result is a list of PubResults
1275
+ # Each PubResult has .join_data().get_counts()
1276
+ if hasattr(result, '__iter__') and not isinstance(result, dict):
1277
+ for i, (T_total, pub) in enumerate(zip(T_list, result)):
1278
+ try:
1279
+ # Try the PrimitiveResult API
1280
+ if hasattr(pub, 'join_data'):
1281
+ joined = pub.join_data()
1282
+ counts = joined.get_counts()
1283
+ elif hasattr(pub, 'data'):
1284
+ # Older API
1285
+ counts = pub.data.get_counts() if hasattr(pub.data, 'get_counts') else dict(pub.data)
1286
+ elif isinstance(pub, dict):
1287
+ counts = pub
1288
+ else:
1289
+ counts = dict(pub)
1290
+
1291
+ log_to_console(f"Processing timestep T={T_total}: {len(counts)} unique bitstrings")
1292
+ pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1293
+ flag_qubits=flag_qubits, midcircuit_meas=midcircuit_meas)
1294
+ output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1295
+ except Exception as e:
1296
+ log_to_console(f"Error processing timestep {i}: {e}")
1297
+ elif isinstance(result, dict):
1298
+ # Single result or counts dict directly
1299
+ if 'counts' in result:
1300
+ counts = result['counts']
1301
+ else:
1302
+ counts = result
1303
+ for T_total in T_list:
1304
+ log_to_console(f"Processing timestep T={T_total}")
1305
+ pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1306
+ flag_qubits=flag_qubits, midcircuit_meas=midcircuit_meas)
1307
+ output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1308
+ else:
1309
+ # IonQ result structure: use get_counts(i) or direct counts dict
1310
+ if hasattr(result, 'get_counts'):
1311
+ for i, T_total in enumerate(T_list):
1312
+ try:
1313
+ counts = result.get_counts(i)
1314
+ log_to_console(f"Processing timestep T={T_total}: {len(counts)} unique bitstrings")
1315
+ pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1316
+ flag_qubits=flag_qubits, midcircuit_meas=False)
1317
+ output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1318
+ except Exception as e:
1319
+ log_to_console(f"Error processing timestep {i}: {e}")
1320
+ elif isinstance(result, list):
1321
+ # List of counts dicts
1322
+ for i, (T_total, counts) in enumerate(zip(T_list, result)):
1323
+ if isinstance(counts, dict):
1324
+ log_to_console(f"Processing timestep T={T_total}")
1325
+ pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1326
+ flag_qubits=flag_qubits, midcircuit_meas=False)
1327
+ output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1328
+ elif isinstance(result, dict):
1329
+ # Single counts dict
1330
+ counts = result.get('counts', result)
1331
+ for T_total in T_list:
1332
+ log_to_console(f"Processing timestep T={T_total}")
1333
+ pts, cnts = load_samples(counts, T_total, logger=log_to_console,
1334
+ flag_qubits=flag_qubits, midcircuit_meas=False)
1335
+ output.append(estimate_density(pts, cnts, bandwidth=0.05, grid_size=output_resolution))
1336
+
1337
+ if not output:
1338
+ _state.qlbm_job_upload_error = "No valid data extracted from job result. Check timesteps and file format."
1339
+ _state.qlbm_job_is_processing = False
1340
+ return
1341
+
1342
+ log_to_console(f"Processed {len(output)} timestep(s) successfully")
1343
+
1344
+ # Generate the Plotly figure
1345
+ fig = plot_density_isosurface_slider(output, T_list)
1346
+
1347
+ # Update state to show results
1348
+ _state.qlbm_qiskit_mode = True
1349
+ _state.qlbm_qiskit_fig = fig
1350
+ _state.qlbm_simulation_has_run = True
1351
+ _state.qlbm_job_upload_success = f"✓ Successfully processed {len(output)} timestep(s) from {filename}"
1352
+
1353
+ # Update the Plotly figure widget
1354
+ if hasattr(_ctrl, "qlbm_qiskit_plot_update"):
1355
+ _ctrl.qlbm_qiskit_plot_update(fig)
1356
+
1357
+ log_to_console(f"Results ready! {len(output)} frames generated.")
1358
+
1359
+ except json.JSONDecodeError as e:
1360
+ _state.qlbm_job_upload_error = f"Invalid JSON file: {e}"
1361
+ log_to_console(f"JSON decode error: {e}")
1362
+ except Exception as e:
1363
+ _state.qlbm_job_upload_error = f"Error processing job result: {e}"
1364
+ log_to_console(f"Processing error: {e}")
1365
+ import traceback
1366
+ log_to_console(traceback.format_exc())
1367
+ finally:
1368
+ _state.qlbm_job_is_processing = False
1369
+
1370
+
1371
  # --- Main Simulation ---
1372
  def run_simulation():
1373
+ """
1374
+ Entry point for simulation - launches the async worker.
1375
+ This is called by the UI button click and schedules the async task.
1376
+ """
1377
+ if _server is None:
1378
+ log_to_console("Error: Server not available")
1379
+ return
1380
+
1381
+ # Schedule the async simulation
1382
+ asyncio.ensure_future(_run_simulation_async())
1383
+
1384
+
1385
+ async def _run_simulation_async():
1386
+ """
1387
+ Async simulation runner that uses thread pool for blocking work.
1388
+ This allows the UI to update in real-time during simulation.
1389
+ """
1390
+ global simulation_data_frames, simulation_times, current_grid_object, _plotter, _qlbm_main_loop
1391
+
1392
+ from concurrent.futures import ThreadPoolExecutor
1393
+
1394
+ # Capture the main event loop for thread-safe callbacks
1395
+ _qlbm_main_loop = asyncio.get_event_loop()
1396
+
1397
+ # Create executor for blocking operations
1398
+ executor = ThreadPoolExecutor(max_workers=1)
1399
+ loop = _qlbm_main_loop
1400
 
1401
  if not _SIMULATION_CAN_RUN:
1402
  msg = _SIMULATION_DISABLED_REASON or "Simulation backend is not available on this platform."
 
1404
  log_to_console(f"Error: {msg}")
1405
  _state.qlbm_status_message = "Error: Backend unavailable"
1406
  _state.qlbm_status_type = "error"
1407
+ await _qlbm_flush_async()
1408
+ executor.shutdown(wait=False)
1409
  return
1410
 
1411
  _state.qlbm_is_running = True
 
1414
  _state.qlbm_qiskit_mode = False # Reset Qiskit mode
1415
  _state.qlbm_show_progress = True
1416
  _state.qlbm_simulation_progress = 0
1417
+ _state.qlbm_status_message = "Initializing simulation..."
1418
  _state.qlbm_status_type = "info"
1419
+ await _qlbm_flush_async()
1420
+
1421
+ # Start heartbeat for continuous progress updates
1422
+ _qlbm_start_progress_heartbeat()
1423
 
1424
  # Determine if using Qiskit backend
1425
  use_qiskit = (
 
1448
  log_to_console("Job Initiated")
1449
  log_to_console(f"Grid Size: {_state.qlbm_grid_size}×{_state.qlbm_grid_size}×{_state.qlbm_grid_size}, Time Steps: {_state.qlbm_time_steps}, Distribution: {_state.qlbm_dist_type}, Boundary: {_state.qlbm_boundary_condition}, Backend: {backend_info}, Velocity: vx={_state.qlbm_vx_expr}, vy={_state.qlbm_vy_expr}, vz={_state.qlbm_vz_expr}")
1450
 
1451
+ # Progress callback that uses thread-safe flush for real-time updates
1452
+ last_logged_percent = [0] # Use list for nonlocal in nested function
1453
  def _progress_callback(percent):
 
1454
  _state.qlbm_simulation_progress = percent
1455
+ if percent - last_logged_percent[0] >= 10:
1456
  log_to_console(f"Simulation progress: {int(percent)}%")
1457
+ last_logged_percent[0] = percent
1458
+ _qlbm_flush_state_threadsafe() # Thread-safe flush!
1459
 
1460
  try:
1461
  # === Qiskit Backend (IBM Qiskit Simulator) ===
1462
  if use_qiskit:
1463
  log_to_console("Using IBM Qiskit Simulator backend...")
1464
+ _state.qlbm_status_message = "Running Qiskit Aer simulation..."
1465
+ await _qlbm_flush_async()
1466
+
1467
+ # Run Qiskit simulation in executor to keep UI responsive
1468
+ def _run_qiskit_blocking():
1469
+ return _run_qiskit_simulation(progress_callback=_progress_callback)
1470
 
1471
+ output, plotly_fig, T_list = await loop.run_in_executor(executor, _run_qiskit_blocking)
 
1472
 
1473
  # Store results
1474
  simulation_data_frames = output
 
1484
  _state.qlbm_simulation_has_run = True
1485
  _state.qlbm_qiskit_mode = True # Use Plotly display instead of PyVista
1486
 
1487
+ _state.qlbm_simulation_progress = 100
1488
  log_to_console("Qiskit simulation completed successfully.")
1489
  _state.qlbm_status_message = "Simulation completed successfully."
1490
  _state.qlbm_status_type = "success"
1491
+ await _qlbm_flush_async()
1492
 
1493
  # === IBM QPU Backend ===
1494
  elif use_ibm_qpu:
1495
  log_to_console("Using IBM QPU backend...")
1496
+ _state.qlbm_status_message = "Preparing IBM QPU job..."
1497
+ await _qlbm_flush_async()
1498
 
1499
  params = _map_state_to_qiskit_params()
1500
  if params is None:
 
1502
 
1503
  # Create initial state circuit
1504
  log_to_console("Creating initial state circuit...")
1505
+ _state.qlbm_simulation_progress = 10
1506
+ await _qlbm_flush_async()
1507
+
1508
  init_state_prep_circ = get_named_init_state_circuit(
1509
  n=params["n"],
1510
  init_state_name=params["init_state_name"],
 
1521
  )
1522
 
1523
  log_to_console("Submitting job to IBM Quantum...")
1524
+ _state.qlbm_status_message = "Submitting job to IBM Quantum..."
1525
+ _state.qlbm_simulation_progress = 20
1526
+ await _qlbm_flush_async()
1527
 
1528
+ # Run HW simulation in executor
1529
+ def _run_ibm_qpu_blocking():
1530
+ log_to_console("Submitting and running IBM QPU job...")
1531
+ job, get_result = run_sampling_hw_ibm(
1532
+ n=params["n"],
1533
+ ux=params["vx_expr"],
1534
+ uy=params["vy_expr"],
1535
+ uz=params["vz_expr"],
1536
+ init_state_prep_circ=init_state_prep_circ,
1537
+ T_list=params["T_list"],
1538
+ shots=2**14,
1539
+ vel_resolution=min(params['grid_size'], 32),
1540
+ output_resolution=min(2*params['grid_size'], 40),
1541
+ logger=log_to_console
1542
+ )
1543
+ log_to_console("Waiting for job results (this may take time)...")
1544
+ output, plotly_fig = get_result(job)
1545
+ return output, plotly_fig, init_state_prep_circ
1546
 
1547
+ _state.qlbm_status_message = "Waiting for IBM QPU results..."
1548
+ _state.qlbm_simulation_progress = 40
1549
+ await _qlbm_flush_async()
1550
+
1551
+ output, plotly_fig, init_state_prep_circ = await loop.run_in_executor(executor, _run_ibm_qpu_blocking)
1552
+
1553
+ _state.qlbm_simulation_progress = 80
1554
+ await _qlbm_flush_async()
1555
 
1556
  # Generate T=0 initial snapshot and prepend to output
1557
  log_to_console("Generating T=0 initial distribution snapshot...")
 
1587
  log_to_console(f"Warning: Could not generate T=0 snapshot: {exc}")
1588
  extended_T_list = params["T_list"]
1589
 
1590
+ # Store results
1591
+ simulation_data_frames = output
1592
+ simulation_times = [float(t) for t in extended_T_list]
1593
+
1594
  # Update UI
1595
  if hasattr(_ctrl, "qlbm_qiskit_result_update"):
1596
  _ctrl.qlbm_qiskit_result_update(plotly_fig)
 
1601
  _state.qlbm_simulation_has_run = True
1602
  _state.qlbm_qiskit_mode = True
1603
 
1604
+ _state.qlbm_simulation_progress = 100
1605
  log_to_console("IBM QPU simulation completed successfully.")
1606
  _state.qlbm_status_message = "Simulation completed successfully."
1607
  _state.qlbm_status_type = "success"
1608
+ await _qlbm_flush_async()
1609
 
1610
  # === IonQ QPU Backend ===
1611
  elif use_ionq_qpu:
1612
  log_to_console("Using IonQ QPU backend...")
1613
+ _state.qlbm_status_message = "Preparing IonQ QPU job..."
1614
+ await _qlbm_flush_async()
1615
 
1616
  params = _map_state_to_qiskit_params()
1617
  if params is None:
 
1619
 
1620
  # Create initial state circuit
1621
  log_to_console("Creating initial state circuit...")
1622
+ _state.qlbm_simulation_progress = 10
1623
+ await _qlbm_flush_async()
1624
+
1625
  init_state_prep_circ = get_named_init_state_circuit(
1626
  n=params["n"],
1627
  init_state_name=params["init_state_name"],
 
1638
  )
1639
 
1640
  log_to_console("Submitting job to IonQ Quantum...")
1641
+ _state.qlbm_status_message = "Submitting job to IonQ Quantum..."
1642
+ _state.qlbm_simulation_progress = 20
1643
+ await _qlbm_flush_async()
1644
 
1645
+ # Run IonQ HW simulation in executor
1646
+ def _run_ionq_qpu_blocking():
1647
+ log_to_console("Submitting and running IonQ QPU job...")
1648
+ job, get_result = run_sampling_hw_ionq(
1649
+ n=params["n"],
1650
+ ux=params["vx_expr"],
1651
+ uy=params["vy_expr"],
1652
+ uz=params["vz_expr"],
1653
+ init_state_prep_circ=init_state_prep_circ,
1654
+ T_list=params["T_list"],
1655
+ shots=2**14,
1656
+ vel_resolution=min(params['grid_size'], 32),
1657
+ output_resolution=min(2*params['grid_size'], 40),
1658
+ logger=log_to_console
1659
+ )
1660
+ log_to_console("Waiting for IonQ job results (this may take time)...")
1661
+ output, plotly_fig = get_result(job)
1662
+ return output, plotly_fig, init_state_prep_circ
1663
+
1664
+ _state.qlbm_status_message = "Waiting for IonQ QPU results..."
1665
+ _state.qlbm_simulation_progress = 40
1666
+ await _qlbm_flush_async()
1667
 
1668
+ output, plotly_fig, init_state_prep_circ = await loop.run_in_executor(executor, _run_ionq_qpu_blocking)
1669
+
1670
+ _state.qlbm_simulation_progress = 80
1671
+ await _qlbm_flush_async()
1672
 
1673
  # Generate T=0 initial snapshot and prepend to output
1674
  log_to_console("Generating T=0 initial distribution snapshot...")
 
1704
  log_to_console(f"Warning: Could not generate T=0 snapshot: {exc}")
1705
  extended_T_list = params["T_list"]
1706
 
1707
+ # Store results
1708
+ simulation_data_frames = output
1709
+ simulation_times = [float(t) for t in extended_T_list]
1710
+
1711
  # Update UI
1712
  if hasattr(_ctrl, "qlbm_qiskit_result_update"):
1713
  _ctrl.qlbm_qiskit_result_update(plotly_fig)
 
1718
  _state.qlbm_simulation_has_run = True
1719
  _state.qlbm_qiskit_mode = True
1720
 
1721
+ _state.qlbm_simulation_progress = 100
1722
  log_to_console("IonQ QPU simulation completed successfully.")
1723
  _state.qlbm_status_message = "Simulation completed successfully."
1724
  _state.qlbm_status_type = "success"
1725
+ await _qlbm_flush_async()
1726
 
1727
  # === CUDA-Q Backend ===
1728
  elif _state.qlbm_backend_type == "Simulator" and _state.qlbm_selected_simulator == "CUDA-Q simulator":
1729
  _state.qlbm_qiskit_mode = False # Use PyVista display
1730
+ _state.qlbm_status_message = "Running CUDA-Q simulation..."
1731
+ await _qlbm_flush_async()
1732
 
1733
  grid_size = int(_state.qlbm_grid_size)
1734
  num_reg_qubits = int(math.log2(grid_size)) if grid_size > 0 else 3
 
1740
  vy_func = make_velocity_func(_state.qlbm_vy_expr)
1741
  vz_func = make_velocity_func(_state.qlbm_vz_expr)
1742
 
1743
+ _state.qlbm_simulation_progress = 5
1744
+ await _qlbm_flush_async()
1745
 
1746
  if simulate_qlbm_3D_and_animate is not None:
1747
  log_to_console("Running CUDA-Q Simulation...")
1748
+
1749
+ # Run CUDA-Q simulation in executor
1750
+ def _run_cudaq_blocking():
1751
+ _plotter.clear()
1752
+ return simulate_qlbm_3D_and_animate(
1753
+ num_reg_qubits=num_reg_qubits,
1754
+ T=T,
1755
+ distribution_type=distribution_type,
1756
+ vx_input=vx_func,
1757
+ vy_input=vy_func,
1758
+ vz_input=vz_func,
1759
+ boundary_condition=boundary_condition,
1760
+ plotter=_plotter,
1761
+ add_slider=False,
1762
+ progress_callback=_progress_callback
1763
+ )
1764
+
1765
+ result = await loop.run_in_executor(executor, _run_cudaq_blocking)
1766
+ _, frames, times, grid_obj = result
1767
  else:
1768
  # Fallback to CPU demo if CUDA-Q not available
1769
  log_to_console("CUDA-Q not available, falling back to CPU Demo...")
1770
+
1771
+ def _run_cpu_demo_blocking():
1772
+ return _run_cpu_demo_simulation(
1773
+ grid_size=grid_size,
1774
+ T=T,
1775
+ distribution_type=distribution_type or "Sinusoidal",
1776
+ vx_func=vx_func,
1777
+ vy_func=vy_func,
1778
+ vz_func=vz_func,
1779
+ progress_callback=_progress_callback
1780
+ )
1781
+
1782
+ frames, times, grid_obj = await loop.run_in_executor(executor, _run_cpu_demo_blocking)
1783
 
1784
+ _state.qlbm_simulation_progress = 95
1785
+ await _qlbm_flush_async()
1786
 
1787
  # Update plotter with results
1788
  if grid_obj:
 
1811
  _state.qlbm_status_message = "Simulation completed successfully."
1812
  _state.qlbm_status_type = "success"
1813
  _state.qlbm_simulation_progress = 100
1814
+ await _qlbm_flush_async()
1815
  else:
1816
  _state.qlbm_run_error = "Simulation produced no data."
1817
  log_to_console("Error: Simulation produced no data.")
1818
  _state.qlbm_status_message = "Error: No data produced"
1819
  _state.qlbm_status_type = "error"
1820
+ await _qlbm_flush_async()
1821
 
1822
  # === CPU Demo Backend (for QPU or fallback) ===
1823
  else:
1824
  _state.qlbm_qiskit_mode = False # Use PyVista display
1825
+ _state.qlbm_status_message = "Running CPU Demo simulation..."
1826
+ await _qlbm_flush_async()
1827
 
1828
  grid_size = int(_state.qlbm_grid_size)
1829
  num_reg_qubits = int(math.log2(grid_size)) if grid_size > 0 else 3
 
1837
 
1838
  _progress_callback(0)
1839
 
1840
+ _state.qlbm_simulation_progress = 5
1841
+ await _qlbm_flush_async()
1842
+
1843
+ # CPU Demo Simulation in executor
1844
  log_to_console("Running CPU Demo Simulation...")
 
 
 
 
 
 
 
 
 
1845
 
1846
+ def _run_cpu_demo_fallback():
1847
+ return _run_cpu_demo_simulation(
1848
+ grid_size=grid_size,
1849
+ T=T,
1850
+ distribution_type=distribution_type or "Sinusoidal",
1851
+ vx_func=vx_func,
1852
+ vy_func=vy_func,
1853
+ vz_func=vz_func,
1854
+ progress_callback=_progress_callback
1855
+ )
1856
+
1857
+ frames, times, grid_obj = await loop.run_in_executor(executor, _run_cpu_demo_fallback)
1858
+
1859
+ _state.qlbm_simulation_progress = 95
1860
+ await _qlbm_flush_async()
1861
 
1862
  # Update plotter with results
1863
  if grid_obj:
 
1886
  _state.qlbm_status_message = "Simulation completed successfully."
1887
  _state.qlbm_status_type = "success"
1888
  _state.qlbm_simulation_progress = 100
1889
+ await _qlbm_flush_async()
1890
  else:
1891
  _state.qlbm_run_error = "Simulation produced no data."
1892
  log_to_console("Error: Simulation produced no data.")
1893
  _state.qlbm_status_message = "Error: No data produced"
1894
  _state.qlbm_status_type = "error"
1895
+ await _qlbm_flush_async()
1896
 
1897
  except Exception as e:
1898
  _state.qlbm_run_error = f"Simulation failed: {str(e)}"
 
1902
  traceback.print_exc()
1903
  _state.qlbm_status_message = "Simulation failed"
1904
  _state.qlbm_status_type = "error"
1905
+ await _qlbm_flush_async()
1906
  finally:
1907
  _state.qlbm_is_running = False
1908
+ _qlbm_stop_progress_heartbeat()
1909
+ executor.shutdown(wait=False)
1910
  if _state.qlbm_status_type != "success":
1911
  _state.qlbm_show_progress = False
1912
+ await _qlbm_flush_async()
1913
 
1914
 
1915
  def stop_simulation():
 
2067
  with vuetify3.VCardText():
2068
  vuetify3.VDivider(classes="my-2")
2069
  vuetify3.VCardSubtitle("Problems", classes="text-caption font-weight-bold mt-2")
2070
+ with vuetify3.VTooltip(location="top"):
2071
+ with vuetify3.Template(v_slot_activator="{ props }"):
2072
+ vuetify3.VSelect(
2073
+ v_bind="props",
2074
+ key="qlbm_overview_problems",
2075
+ label="Select a problem",
2076
+ v_model=("qlbm_problems_selection", None),
2077
+ items=(
2078
+ "qlbm_qlbm_problems",
2079
+ [
2080
+ "Scalar advection-diffusion in a box",
2081
+ "Laminar flow & heat transfer for a heated body in water.",
2082
+ ],
2083
+ ),
2084
+ placeholder="Select",
2085
+ density="compact",
2086
+ hide_details=True,
2087
+ color="primary",
2088
+ classes="mb-2"
2089
+ )
2090
+ html.Span("Select a predefined fluid dynamics problem to solve")
2091
 
2092
  # Geometry card
2093
  with vuetify3.VCard(classes="mb-2"):
 
2115
  vuetify3.VCardSubtitle("Domain dimensions", classes="text-caption font-weight-bold mb-2")
2116
  with vuetify3.VRow(dense=True):
2117
  with vuetify3.VCol():
2118
+ with vuetify3.VTooltip(location="top"):
2119
+ with vuetify3.Template(v_slot_activator="{ props }"):
2120
+ vuetify3.VTextField(v_bind="props", label="Length (L)", v_model=("qlbm_domain_L", 1.0), type="number", step="0.1", density="compact", hide_details=True, color="primary")
2121
+ html.Span("Length of the domain along X axis")
2122
  with vuetify3.VCol():
2123
+ with vuetify3.VTooltip(location="top"):
2124
+ with vuetify3.Template(v_slot_activator="{ props }"):
2125
+ vuetify3.VTextField(v_bind="props", label="Width (W)", v_model=("qlbm_domain_W", 1.0), type="number", step="0.1", density="compact", hide_details=True, color="primary")
2126
+ html.Span("Width of the domain along Y axis")
2127
  with vuetify3.VCol():
2128
+ with vuetify3.VTooltip(location="top"):
2129
+ with vuetify3.Template(v_slot_activator="{ props }"):
2130
+ vuetify3.VTextField(v_bind="props", label="Height (H)", v_model=("qlbm_domain_H", 1.0), type="number", step="0.1", density="compact", hide_details=True, color="primary")
2131
+ html.Span("Height of the domain along Z axis")
2132
 
2133
  # Initial Distribution card
2134
  with vuetify3.VCard(classes="mb-2", style=("qlbm_distribution_card_style", _WORKFLOW_BASE_STYLE)):
 
2136
  with vuetify3.VCardText():
2137
  with vuetify3.VRow(classes="d-flex align-center mb-2", no_gutters=True):
2138
  with vuetify3.VCol(cols="auto", classes="flex-grow-1"):
2139
+ with vuetify3.VTooltip(location="top"):
2140
+ with vuetify3.Template(v_slot_activator="{ props }"):
2141
+ vuetify3.VSelect(
2142
+ v_bind="props",
2143
+ label="Initial Distribution",
2144
+ v_model=("qlbm_dist_type", None),
2145
+ items=("qlbm_dist_modes",),
2146
+ density="compact",
2147
+ hide_details=True
2148
+ )
2149
+ html.Span("Select the initial density distribution function")
2150
  with vuetify3.VCol(cols="auto", classes="ml-2"):
2151
  with vuetify3.VBtn(
2152
  icon=True, density="compact", variant="text",
 
2159
  vuetify3.VCardTitle("Sinusoidal Frequencies")
2160
  with vuetify3.VCardText():
2161
  for axis in ['x', 'y', 'z']:
2162
+ with vuetify3.VTooltip(location="top"):
2163
+ with vuetify3.Template(v_slot_activator="{ props }"):
2164
+ vuetify3.VSlider(
2165
+ v_bind="props",
2166
+ label=f"Freq {axis.upper()}",
2167
+ v_model=(f"qlbm_sine_k_{axis}", 1.0),
2168
+ min=1, max=5, step=1,
2169
+ thumb_label="always", density="compact"
2170
+ )
2171
+ html.Span(f"Frequency multiplier for {axis.upper()} axis")
2172
 
2173
  # Gaussian controls
2174
  with vuetify3.VCard(classes="mb-2", v_if="qlbm_custom_dist_params && qlbm_dist_type === 'Gaussian'"):
2175
  vuetify3.VCardTitle("Gaussian Parameters")
2176
  with vuetify3.VCardText():
2177
  for axis in ['x', 'y', 'z']:
2178
+ with vuetify3.VTooltip(location="top"):
2179
+ with vuetify3.Template(v_slot_activator="{ props }"):
2180
+ vuetify3.VSlider(
2181
+ v_bind="props",
2182
+ label=f"Center {axis.upper()}",
2183
+ v_model=(f"qlbm_gauss_c{axis}", 16),
2184
+ min=0, max=("qlbm_nx", 32), step=1,
2185
+ thumb_label="always", density="compact"
2186
+ )
2187
+ html.Span(f"Center position along {axis.upper()} axis")
2188
+ with vuetify3.VTooltip(location="top"):
2189
+ with vuetify3.Template(v_slot_activator="{ props }"):
2190
+ vuetify3.VSlider(
2191
+ v_bind="props",
2192
+ label="Width (Sigma)",
2193
+ v_model=("qlbm_gauss_sigma", 6.0),
2194
+ min=1.0, max=20.0, step=0.5,
2195
+ thumb_label="always", density="compact"
2196
+ )
2197
+ html.Span("Standard deviation (spread) of the Gaussian")
2198
 
2199
  # Multi-Dirac-Delta controls
2200
  with vuetify3.VCard(classes="mb-2", v_if="qlbm_custom_dist_params && qlbm_dist_type === 'Multi-Dirac-Delta'"):
 
2202
  with vuetify3.VCardText():
2203
  vuetify3.VCardSubtitle("Number of delta peaks per axis = 2^k", classes="text-caption mb-2")
2204
  for axis in ['x', 'y', 'z']:
2205
+ with vuetify3.VTooltip(location="top"):
2206
+ with vuetify3.Template(v_slot_activator="{ props }"):
2207
+ vuetify3.VSlider(
2208
+ v_bind="props",
2209
+ label=f"k_{axis.upper()} (log₂)",
2210
+ v_model=(f"qlbm_mdd_k{axis}_log2", 1),
2211
+ min=1, max=4, step=1,
2212
+ thumb_label="always", density="compact"
2213
+ )
2214
+ html.Span(f"Log2 of number of peaks along {axis.upper()}")
2215
 
2216
  # Boundary Conditions
2217
  with vuetify3.VCard(classes="mb-2"):
2218
  vuetify3.VCardTitle("Boundary Conditions", classes="text-subtitle-2 font-weight-bold text-primary")
2219
  with vuetify3.VCardText():
2220
+ with vuetify3.VTooltip(location="top"):
2221
+ with vuetify3.Template(v_slot_activator="{ props }"):
2222
+ vuetify3.VSelect(v_bind="props", label="Boundary Condition", v_model=("qlbm_boundary_condition", "Periodic"),
2223
+ items=("['Periodic']",), density="compact", hide_details=True, color="primary")
2224
+ html.Span("Select boundary conditions for the simulation domain")
2225
 
2226
  # Advecting Fields
2227
  with vuetify3.VCard(classes="mb-2", style=("qlbm_advect_card_style", _WORKFLOW_BASE_STYLE)):
 
2229
  with vuetify3.VCardText():
2230
  with vuetify3.VRow(classes="d-flex align-center mb-2", no_gutters=True):
2231
  with vuetify3.VCol(cols="auto", classes="flex-grow-1"):
2232
+ with vuetify3.VTooltip(location="top"):
2233
+ with vuetify3.Template(v_slot_activator="{ props }"):
2234
+ vuetify3.VSelect(
2235
+ v_bind="props",
2236
+ label="Select Advecting field",
2237
+ v_model=("qlbm_advecting_field", None),
2238
+ items=("['Uniform', 'Swirl', 'Shear', 'TGV']",),
2239
+ density="compact",
2240
+ hide_details=True,
2241
+ color="primary",
2242
+ placeholder="Select",
2243
+ )
2244
+ html.Span("Select the velocity field that transports the fluid")
2245
  with vuetify3.VCol(cols="auto", classes="ml-2"):
2246
  with vuetify3.VBtn(
2247
  icon=True, density="compact", variant="text",
 
2250
  vuetify3.VIcon("mdi-cog", color=("qlbm_show_advect_params ? 'primary' : 'grey'",))
2251
  with vuetify3.VContainer(v_if="qlbm_show_advect_params", classes="pa-0 mt-2"):
2252
  html.Div("Velocity Components", classes="text-caption mb-1")
2253
+ with vuetify3.VTooltip(location="top"):
2254
+ with vuetify3.Template(v_slot_activator="{ props }"):
2255
+ vuetify3.VTextField(v_bind="props", label="Velocity vx", v_model=("qlbm_vx_expr", "0.2"), density="compact", hide_details=True, color="primary", classes="mb-1")
2256
+ html.Span("X-component of velocity field")
2257
+ with vuetify3.VTooltip(location="top"):
2258
+ with vuetify3.Template(v_slot_activator="{ props }"):
2259
+ vuetify3.VTextField(v_bind="props", label="Velocity vy", v_model=("qlbm_vy_expr", "-0.15"), density="compact", hide_details=True, color="primary", classes="mb-1")
2260
+ html.Span("Y-component of velocity field")
2261
+ with vuetify3.VTooltip(location="top"):
2262
+ with vuetify3.Template(v_slot_activator="{ props }"):
2263
+ vuetify3.VTextField(v_bind="props", label="Velocity vz", v_model=("qlbm_vz_expr", "0.3"), density="compact", hide_details=True, color="primary")
2264
+ html.Span("Z-component of velocity field")
2265
 
2266
  # Meshing
2267
  with vuetify3.VCard(classes="mb-2", style=("qlbm_meshing_card_style", _WORKFLOW_BASE_STYLE)):
 
2294
  with vuetify3.VCard(classes="mb-2"):
2295
  vuetify3.VCardTitle("Time", classes="text-subtitle-2 font-weight-bold text-primary")
2296
  with vuetify3.VCardText():
2297
+ with vuetify3.VTooltip(location="top"):
2298
+ with vuetify3.Template(v_slot_activator="{ props }"):
2299
+ vuetify3.VSlider(v_bind="props", label="Total Time", v_model=("qlbm_time_steps", 10), min=0, max=30, step=1,
2300
+ thumb_label="always", show_ticks="always", color="primary", density="compact", hide_details=True)
2301
+ html.Span("Number of time steps to simulate")
2302
  vuetify3.VAlert(v_if="qlbm_time_steps > 100", type="warning", variant="tonal", density="compact",
2303
  children=["Warning: High time steps may increase runtime."], classes="mt-2")
2304
 
 
2346
  children=["⚠️ Grid size > 16 may exceed IBM QPU capacity!"],
2347
  classes="mt-2"
2348
  )
2349
+
2350
+ # Sinusoidal Warning for IBM QPU
2351
+ vuetify3.VAlert(
2352
+ v_if="qlbm_backend_type === 'QPU' && qlbm_selected_qpu === 'IBM QPU' && qlbm_dist_type === 'Sinusoidal'",
2353
+ type="warning",
2354
+ variant="tonal",
2355
+ density="compact",
2356
+ children=["⚠️ Sinusoidal distribution results in very high circuit depth on IBM QPU!"],
2357
+ classes="mt-2"
2358
+ )
2359
+
2360
+ # IonQ Restriction Warning
2361
+ vuetify3.VAlert(
2362
+ v_if="qlbm_backend_type === 'QPU' && qlbm_selected_qpu === 'IonQ QPU' && qlbm_dist_type !== 'Multi-Dirac-Delta'",
2363
+ type="error",
2364
+ variant="tonal",
2365
+ density="compact",
2366
+ children=["⚠️ IonQ QPU only supports Multi-Dirac-Delta distribution."],
2367
+ classes="mt-2"
2368
+ )
2369
 
2370
  vuetify3.VDivider(classes="my-3")
2371
  vuetify3.VBtn(
2372
  text="Run",
2373
  color="primary",
2374
  block=True,
2375
+ disabled=("qlbm_is_running || !qlbm_backend_type || (qlbm_backend_type === 'QPU' && qlbm_selected_qpu === 'IonQ QPU' && qlbm_dist_type !== 'Multi-Dirac-Delta')", True),
2376
  click=run_simulation,
2377
  style=("qlbm_is_running ? '' : 'background-color:#87CEFA;'", ""),
2378
  )
 
2405
  click=stop_simulation,
2406
  disabled=("!qlbm_is_running", True),
2407
  )
2408
+
2409
+ # --- Job Result Upload Section ---
2410
+ vuetify3.VDivider(classes="my-3")
2411
+ html.Div("Upload Job Results", classes="text-subtitle-2 font-weight-bold text-primary mb-2")
2412
+ html.Div("Load previously saved IBM/IonQ QPU job results (JSON format)",
2413
+ classes="text-caption text-medium-emphasis mb-2")
2414
+
2415
+ # Platform selector
2416
+ with vuetify3.VRow(dense=True, classes="mb-2"):
2417
+ with vuetify3.VCol(cols=6):
2418
+ with vuetify3.VTooltip(location="top"):
2419
+ with vuetify3.Template(v_slot_activator="{ props }"):
2420
+ vuetify3.VSelect(
2421
+ v_bind="props",
2422
+ label="Platform",
2423
+ v_model=("qlbm_job_platform", "IBM"),
2424
+ items=("['IBM', 'IonQ']",),
2425
+ density="compact",
2426
+ hide_details=True,
2427
+ color="primary",
2428
+ )
2429
+ html.Span("Select the quantum hardware provider")
2430
+ with vuetify3.VCol(cols=6):
2431
+ with vuetify3.VTooltip(location="top"):
2432
+ with vuetify3.Template(v_slot_activator="{ props }"):
2433
+ vuetify3.VTextField(
2434
+ v_bind="props",
2435
+ label="Output Resolution",
2436
+ v_model=("qlbm_job_output_resolution", 40),
2437
+ type="number",
2438
+ density="compact",
2439
+ hide_details=True,
2440
+ color="primary",
2441
+ )
2442
+ html.Span("Resolution for 3D visualization. Should be <= Grid Size (2^n).")
2443
+
2444
+ # Timesteps input
2445
+ with vuetify3.VTooltip(location="top"):
2446
+ with vuetify3.Template(v_slot_activator="{ props }"):
2447
+ vuetify3.VTextField(
2448
+ v_bind="props",
2449
+ label="Total Time",
2450
+ v_model=("qlbm_job_total_time", 3),
2451
+ type="number",
2452
+ density="compact",
2453
+ hide_details=True,
2454
+ color="primary",
2455
+ classes="mb-2",
2456
+ hint="Enter the total time T used when running the job",
2457
+ )
2458
+ html.Span("Total number of time steps in the uploaded job")
2459
+
2460
+ # Advanced options (flag qubits, mid-circuit measurement) - HIDDEN
2461
+ # Defaulting to True for both as per user request
2462
+
2463
+ # File upload and Generate button
2464
+ with vuetify3.VRow(dense=True, classes="align-center mt-2"):
2465
+ with vuetify3.VCol(cols=8):
2466
+ vuetify3.VFileInput(
2467
+ label="Upload Job Result (JSON)",
2468
+ v_model=("qlbm_job_upload", None),
2469
+ accept=".json",
2470
+ prepend_icon="mdi-file-upload",
2471
+ density="compact",
2472
+ hide_details=True,
2473
+ color="primary",
2474
+ show_size=True,
2475
+ clearable=True,
2476
+ )
2477
+ with vuetify3.VCol(cols=4):
2478
+ vuetify3.VBtn(
2479
+ text="Generate",
2480
+ color="secondary",
2481
+ variant="tonal",
2482
+ block=True,
2483
+ disabled=("!qlbm_job_upload || qlbm_job_is_processing", True),
2484
+ loading=("qlbm_job_is_processing", False),
2485
+ click=process_uploaded_job_result,
2486
+ prepend_icon="mdi-chart-box-outline",
2487
+ )
2488
+
2489
+ # Success message
2490
+ vuetify3.VAlert(
2491
+ v_if="qlbm_job_upload_success",
2492
+ type="success",
2493
+ variant="tonal",
2494
+ density="compact",
2495
+ closable=True,
2496
+ children=["{{ qlbm_job_upload_success }}"],
2497
+ classes="mt-2",
2498
+ )
2499
+
2500
+ # Error message
2501
+ vuetify3.VAlert(
2502
+ v_if="qlbm_job_upload_error",
2503
+ type="error",
2504
+ variant="tonal",
2505
+ density="compact",
2506
+ closable=True,
2507
+ children=["{{ qlbm_job_upload_error }}"],
2508
+ classes="mt-2",
2509
+ )
2510
 
2511
 
2512
  def _build_visualization_panel(plotter):