harishaseebat92 commited on
Commit
10c45a9
·
1 Parent(s): c27d6ee

Added Console Window

Browse files
Files changed (3) hide show
  1. README.md +9 -7
  2. em_trame.py +67 -27
  3. qlbm.py +130 -21
README.md CHANGED
@@ -36,15 +36,17 @@ individual services so the browser never tries to contact `127.0.0.1` directly.
36
  ## CUDA-Q backend on CPU-only hosts
37
 
38
  The 3D lattice Boltzmann solver relies on CUDA-Q, which expects a CUDA-capable GPU.
39
- CPU-only runtimes such as Hugging Face Spaces will now automatically skip loading the
40
- quantum backend so the UI can still boot. Use the following environment variables to
41
- override the default:
 
42
 
43
  | Variable | Effect |
44
  | --- | --- |
45
- | `DISABLE_CUDAQ=1` | Force-disable the CUDA-Q backend regardless of GPU availability. |
46
  | `FORCE_ENABLE_CUDAQ=1` | Attempt to load the CUDA-Q backend even if the platform was auto-detected as CPU-only. |
47
 
48
- When the backend is disabled the "Run Simulation" button is greyed out and a warning
49
- appears in the UI explaining why. Run the app locally on a CUDA-enabled machine and
50
- set `FORCE_ENABLE_CUDAQ=1` if you still want to attempt a run on a constrained host.
 
 
36
  ## CUDA-Q backend on CPU-only hosts
37
 
38
  The 3D lattice Boltzmann solver relies on CUDA-Q, which expects a CUDA-capable GPU.
39
+ CPU-only runtimes such as Hugging Face Spaces automatically fall back to a lightweight
40
+ "CPU demo" mode so the UI and preview still run. The plots update with an approximate
41
+ synthetic evolution and clearly indicate the active backend. Use these environment
42
+ variables to control the behavior:
43
 
44
  | Variable | Effect |
45
  | --- | --- |
46
+ | `DISABLE_CUDAQ=1` | Skip loading the CUDA-Q backend (the CPU demo remains available). |
47
  | `FORCE_ENABLE_CUDAQ=1` | Attempt to load the CUDA-Q backend even if the platform was auto-detected as CPU-only. |
48
 
49
+ On GPU-enabled machines, leave both variables unset to run the full CUDA-Q solver. On
50
+ CPU-only hosts you can still explore the workflow in demo mode, or deploy to a GPU-
51
+ backed Space/local workstation and set `FORCE_ENABLE_CUDAQ=1` to activate the quantum
52
+ backend once hardware is available.
em_trame.py CHANGED
@@ -98,6 +98,7 @@ state.update({
98
  "qpu_monitor_gridpoints": "", # Derived gridpoints for QPU monitors
99
  "qpu_monitor_samples": "(0.5, 0.5)", # User-facing normalized sample input
100
  "qpu_monitor_sample_info": "",
 
101
  # Square aspect for initial preview
102
  "pyvista_view_style": "aspect-ratio: 1 / 1; width: 100%;",
103
  "qpu_ts_fig": None,
@@ -139,6 +140,7 @@ state.update({
139
  "status_type": "info", # info, success, warning, error
140
  "simulation_progress": 0,
141
  "show_progress": False,
 
142
  })
143
 
144
  # Ensure hole snap state exists
@@ -724,6 +726,7 @@ def run_simulation_only():
724
  # Require selections before running
725
  if not state.geometry_selection:
726
  state.error_message = "Please select a geometry before running the simulation."
 
727
  state.status_visible = True
728
  state.status_message = "Error: Please select a geometry before running."
729
  state.status_type = "error"
@@ -733,6 +736,7 @@ def run_simulation_only():
733
  return
734
  if not state.dist_type:
735
  state.error_message = "Please select an initial state before running the simulation."
 
736
  state.status_visible = True
737
  state.status_message = "Error: Please select an initial state before running."
738
  state.status_type = "error"
@@ -744,6 +748,7 @@ def run_simulation_only():
744
  # Show status: Starting simulation
745
  state.status_visible = True
746
  state.status_message = "Initializing simulation..."
 
747
  state.status_type = "info"
748
  state.show_progress = True
749
  state.simulation_progress = 0
@@ -1166,13 +1171,13 @@ def _build_sim_timeseries_plotly(field_type: str, positions, nx: int, times, sim
1166
  )
1167
  fig.update_xaxes(
1168
  title_text="Time (s)", title_font=dict(size=22), tickfont=dict(size=16),
1169
- showgrid=True, gridcolor="rgba(95,37,159,.08)", zeroline=False,
1170
  showline=True, linewidth=1, linecolor="rgba(0,0,0,.2)",
1171
  showspikes=True, spikemode='across', spikesnap='cursor'
1172
  )
1173
  fig.update_yaxes(
1174
  title_text="Field Amplitude", title_font=dict(size=22), tickfont=dict(size=16),
1175
- showgrid=True, gridcolor="rgba(95,37,159,.08)", zeroline=True, zerolinecolor="rgba(0,0,0,.25)",
1176
  showline=True, linewidth=1, linecolor="rgba(0,0,0,.2)"
1177
  )
1178
  if max_abs > 0:
@@ -1755,6 +1760,20 @@ def handle_file_upload(uploaded_file_info, **kwargs):
1755
  state.upload_status_message = f"File '{file_name}' uploaded."
1756
  state.show_upload_status = True
1757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1758
  def update_excitation_info_message():
1759
  """Calculates and displays the coordinate snapping message."""
1760
  if state.nx is None or state.dist_type is None:
@@ -1852,14 +1871,28 @@ def on_time_change(time_val, **kwargs):
1852
  idx = int(np.argmin(np.abs(times - float(time_val))))
1853
  max_idx = len(data_frames[field]) - 1
1854
  idx = max(0, min(idx, max_idx))
 
1855
  z_data = data_frames[field][idx]
1856
- if current_mesh.n_points == z_data.size:
1857
- current_mesh.points[:, 2] = z_data.ravel() * z_scale
1858
- current_mesh['scalars'] = z_data.ravel()
1859
- ctrl.view_update()
1860
- else:
1861
- redraw_surface_plot()
1862
-
 
 
 
 
 
 
 
 
 
 
 
 
 
1863
  def update_value_display(point):
1864
  if current_mesh is None:
1865
  return
@@ -1943,7 +1976,7 @@ def export_vtk_all_frames():
1943
  if snapshot_times is None:
1944
  raise ValueError("Snapshot times are unavailable.")
1945
 
1946
- dl_dir = os.path.join(os.path.expanduser("~"), "Downloads")
1947
  suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
1948
  nx = int(state.nx)
1949
  out_dir = os.path.join(dl_dir, f"vtk_sequence_{field}_nx{nx}_{suffix}")
@@ -2151,8 +2184,6 @@ def qpu_set_monitor_field(index, value):
2151
  try:
2152
  i = int(index)
2153
  v = str(value).strip() if value is not None else "Ez"
2154
- if v not in ("Ez", "Hx", "Hy"):
2155
- v = "Ez"
2156
  cfgs = list(state.qpu_monitor_configs or [])
2157
  if 0 <= i < len(cfgs):
2158
  cfgs[i]["field"] = v
@@ -2281,8 +2312,8 @@ def _rebuild_qpu_fig_others(selected_field: str, position_filter: str = "All pos
2281
  color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
2282
  fig.add_trace(go.Scatter(
2283
  x=times, y=ys, mode='lines+markers', name=f"({px}, {py})",
2284
- line=dict(color=color_hex, width=2.5, dash=dashes[i % len(dashes)]),
2285
- marker=dict(size=7, symbol=markers[i % len(markers)], color=color_hex),
2286
  hovertemplate=f"{sel} | t=%{{x:.3f}}s<br>Value=%{{y:.6g}}<extra>({px}, {py})</extra>",
2287
  ))
2288
  fig.update_layout(title=f"IBM QPU Time Series (Other components: {sel})", height=660, width=900, margin=dict(l=50, r=30, t=50, b=50), hovermode="x unified", legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1, title_text=""))
@@ -2424,8 +2455,7 @@ with SinglePageLayout(server) as layout:
2424
  children=["{{ hole_error_message }}"],
2425
  classes="mt-1",
2426
  )
2427
- # Restoring all subsequent input cards
2428
- # Cell 4: Excitation
2429
  with vuetify3.VCard(classes="mb-1", style=("excitation_card_style", "font-size: 0.8rem;")):
2430
  with vuetify3.VCardTitle("Excitation: Initial State", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
2431
  pass
@@ -2469,25 +2499,20 @@ with SinglePageLayout(server) as layout:
2469
  style="white-space: pre-line;",
2470
  )
2471
 
2472
- # Cell 5: Medium
2473
  with vuetify3.VCard(classes="mb-1", style="font-size: 0.8rem;"):
2474
  with vuetify3.VCardTitle("Material Properties (Medium)", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
2475
  pass
2476
  with vuetify3.VCardText(classes="py-1 px-2"):
2477
  with vuetify3.VRow(dense=True):
2478
- with vuetify3.VCol():
2479
  with vuetify3.VTooltip("Relative permittivity. ε_r = ε / ε₀. Default 1.0 (free space).", location="bottom", color="primary"):
2480
  with vuetify3.Template(v_slot_activator="{ props }"):
2481
- vuetify3.VTextField(v_bind="props", v_model=("coeff_permittivity", 1.0), label="Relative permittivity _r)", type="number", density="compact", color="primary")
2482
- with vuetify3.VCol():
2483
- with vuetify3.VTooltip("Relative permeability. μ_r = μ / μ₀. Typically 1.0 for non-magnetic materials.", location="bottom", color="primary"):
2484
  with vuetify3.Template(v_slot_activator="{ props }"):
2485
- vuetify3.VTextField(v_bind="props", v_model=("coeff_permeability", 1.0), label="Relative permeability _r)", type="number", density="compact", color="primary")
2486
-
2487
- # New Cell: Temporal Settings (moved here)
2488
- with vuetify3.VCard(classes="mb-1", style="font-size: 0.8rem;"):
2489
- with vuetify3.VCardTitle("Temporal Settings", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
2490
- pass
2491
  with vuetify3.VCardText(classes="py-1 px-2"):
2492
  with vuetify3.VTooltip("Sets the total duration for the simulation to run.", location="bottom", color="primary"):
2493
  with vuetify3.Template(v_slot_activator="{ props }"):
@@ -2766,6 +2791,21 @@ with SinglePageLayout(server) as layout:
2766
  with vuetify3.VContainer(v_if="!geometry_selection", fluid=True, classes="flex-grow-1 d-flex align-center justify-center text-medium-emphasis"):
2767
  vuetify3.VCardText("Select a geometry to display the preview and results.")
2768
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2769
  # Status Window - Fixed at bottom right
2770
  with vuetify3.VCard(
2771
  v_if="status_visible",
 
98
  "qpu_monitor_gridpoints": "", # Derived gridpoints for QPU monitors
99
  "qpu_monitor_samples": "(0.5, 0.5)", # User-facing normalized sample input
100
  "qpu_monitor_sample_info": "",
101
+ "console_output": "Console initialized.\n",
102
  # Square aspect for initial preview
103
  "pyvista_view_style": "aspect-ratio: 1 / 1; width: 100%;",
104
  "qpu_ts_fig": None,
 
140
  "status_type": "info", # info, success, warning, error
141
  "simulation_progress": 0,
142
  "show_progress": False,
143
+ "console_logs": "Console initialized...\n",
144
  })
145
 
146
  # Ensure hole snap state exists
 
726
  # Require selections before running
727
  if not state.geometry_selection:
728
  state.error_message = "Please select a geometry before running the simulation."
729
+ log_to_console("Error: Please select a geometry before running.")
730
  state.status_visible = True
731
  state.status_message = "Error: Please select a geometry before running."
732
  state.status_type = "error"
 
736
  return
737
  if not state.dist_type:
738
  state.error_message = "Please select an initial state before running the simulation."
739
+ log_to_console("Error: Please select an initial state before running.")
740
  state.status_visible = True
741
  state.status_message = "Error: Please select an initial state before running."
742
  state.status_type = "error"
 
748
  # Show status: Starting simulation
749
  state.status_visible = True
750
  state.status_message = "Initializing simulation..."
751
+ log_to_console("Initializing simulation...")
752
  state.status_type = "info"
753
  state.show_progress = True
754
  state.simulation_progress = 0
 
1171
  )
1172
  fig.update_xaxes(
1173
  title_text="Time (s)", title_font=dict(size=22), tickfont=dict(size=16),
1174
+ showgrid=True, gridcolor="rgba(95,37,159,0.08)", zeroline=False,
1175
  showline=True, linewidth=1, linecolor="rgba(0,0,0,.2)",
1176
  showspikes=True, spikemode='across', spikesnap='cursor'
1177
  )
1178
  fig.update_yaxes(
1179
  title_text="Field Amplitude", title_font=dict(size=22), tickfont=dict(size=16),
1180
+ showgrid=True, gridcolor="rgba(95,37,159,0.08)", zeroline=True, zerolinecolor="rgba(0,0,0,.25)",
1181
  showline=True, linewidth=1, linecolor="rgba(0,0,0,.2)"
1182
  )
1183
  if max_abs > 0:
 
1760
  state.upload_status_message = f"File '{file_name}' uploaded."
1761
  state.show_upload_status = True
1762
 
1763
+ def log_message(message, level="INFO"):
1764
+ """Append a message to the console log with a timestamp and level."""
1765
+ timestamp = datetime.now().strftime("%H:%M:%S")
1766
+ new_entry = f"[{timestamp}] [{level}] {message}\n"
1767
+ state.console_logs += new_entry
1768
+ # Optional: Limit log size to prevent performance issues
1769
+ if len(state.console_logs) > 50000:
1770
+ state.console_logs = state.console_logs[-50000:]
1771
+
1772
+ def log_to_console(message):
1773
+ timestamp = datetime.now().strftime("%H:%M:%S")
1774
+ new_line = f"[{timestamp}] {message}\n"
1775
+ state.console_output = (state.console_output or "") + new_line
1776
+
1777
  def update_excitation_info_message():
1778
  """Calculates and displays the coordinate snapping message."""
1779
  if state.nx is None or state.dist_type is None:
 
1871
  idx = int(np.argmin(np.abs(times - float(time_val))))
1872
  max_idx = len(data_frames[field]) - 1
1873
  idx = max(0, min(idx, max_idx))
1874
+
1875
  z_data = data_frames[field][idx]
1876
+ points = np.c_[X_grids[field].ravel(), Y_grids[field].ravel(), z_data.ravel() * z_scale]
1877
+ poly = pv.PolyData(points)
1878
+ mesh = poly.delaunay_2d()
1879
+ mesh['scalars'] = z_data.ravel()
1880
+ current_mesh = mesh
1881
+ plotter.add_mesh(mesh, scalars='scalars', clim=surface_clims[field], cmap="RdBu", show_scalar_bar=False, show_edges=True, edge_color='grey', line_width=0.5)
1882
+ plotter.add_scalar_bar(title=f"{field} Amplitude")
1883
+ try:
1884
+ plotter.disable_picking()
1885
+ except Exception:
1886
+ pass
1887
+ plotter.enable_point_picking(callback=update_value_display, show_message=False)
1888
+ plotter.add_axes()
1889
+ plotter.view_isometric()
1890
+ try:
1891
+ plotter.camera.parallel_projection = True
1892
+ except Exception:
1893
+ pass
1894
+ ctrl.view_update()
1895
+
1896
  def update_value_display(point):
1897
  if current_mesh is None:
1898
  return
 
1976
  if snapshot_times is None:
1977
  raise ValueError("Snapshot times are unavailable.")
1978
 
1979
+ dl_dir = os.path.expanduser("~/Downloads")
1980
  suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
1981
  nx = int(state.nx)
1982
  out_dir = os.path.join(dl_dir, f"vtk_sequence_{field}_nx{nx}_{suffix}")
 
2184
  try:
2185
  i = int(index)
2186
  v = str(value).strip() if value is not None else "Ez"
 
 
2187
  cfgs = list(state.qpu_monitor_configs or [])
2188
  if 0 <= i < len(cfgs):
2189
  cfgs[i]["field"] = v
 
2312
  color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
2313
  fig.add_trace(go.Scatter(
2314
  x=times, y=ys, mode='lines+markers', name=f"({px}, {py})",
2315
+ line=dict(color=color_hex, width=2.5, dash=dash_styles[i % len(dash_styles)]),
2316
+ marker=dict(size=7, symbol=marker_symbols[i % len(marker_symbols)], color=color_hex),
2317
  hovertemplate=f"{sel} | t=%{{x:.3f}}s<br>Value=%{{y:.6g}}<extra>({px}, {py})</extra>",
2318
  ))
2319
  fig.update_layout(title=f"IBM QPU Time Series (Other components: {sel})", height=660, width=900, margin=dict(l=50, r=30, t=50, b=50), hovermode="x unified", legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1, title_text=""))
 
2455
  children=["{{ hole_error_message }}"],
2456
  classes="mt-1",
2457
  )
2458
+ # Cell 3: Excitation
 
2459
  with vuetify3.VCard(classes="mb-1", style=("excitation_card_style", "font-size: 0.8rem;")):
2460
  with vuetify3.VCardTitle("Excitation: Initial State", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
2461
  pass
 
2499
  style="white-space: pre-line;",
2500
  )
2501
 
2502
+ # Cell 4: Medium
2503
  with vuetify3.VCard(classes="mb-1", style="font-size: 0.8rem;"):
2504
  with vuetify3.VCardTitle("Material Properties (Medium)", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
2505
  pass
2506
  with vuetify3.VCardText(classes="py-1 px-2"):
2507
  with vuetify3.VRow(dense=True):
2508
+ with vuetify3.VCol(cols="6"):
2509
  with vuetify3.VTooltip("Relative permittivity. ε_r = ε / ε₀. Default 1.0 (free space).", location="bottom", color="primary"):
2510
  with vuetify3.Template(v_slot_activator="{ props }"):
2511
+ vuetify3.VTextField(v_bind="props", v_model=("coeff_permittivity", 1.0), label="Permittivityr)", type="number", step="0.1", density="compact", color="primary")
2512
+ with vuetify3.VCol(cols="6"):
2513
+ with vuetify3.VTooltip("Relative permeability. μ_r = μ / μ₀. Default 1.0 (non-magnetic).", location="bottom", color="primary"):
2514
  with vuetify3.Template(v_slot_activator="{ props }"):
2515
+ vuetify3.VTextField(v_bind="props", v_model=("coeff_permeability", 1.0), label="Permeabilityr)", type="number", step="0.1", density="compact", color="primary")
 
 
 
 
 
2516
  with vuetify3.VCardText(classes="py-1 px-2"):
2517
  with vuetify3.VTooltip("Sets the total duration for the simulation to run.", location="bottom", color="primary"):
2518
  with vuetify3.Template(v_slot_activator="{ props }"):
 
2791
  with vuetify3.VContainer(v_if="!geometry_selection", fluid=True, classes="flex-grow-1 d-flex align-center justify-center text-medium-emphasis"):
2792
  vuetify3.VCardText("Select a geometry to display the preview and results.")
2793
 
2794
+ # Console Window
2795
+ with vuetify3.VCard(classes="mt-2", style="font-size: 0.8rem;"):
2796
+ with vuetify3.VCardTitle("Console Output", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
2797
+ pass
2798
+ with vuetify3.VCardText(classes="py-1 px-2", style="height: 150px; overflow-y: auto; background-color: #f5f5f5; font-family: monospace;"):
2799
+ vuetify3.VTextarea(
2800
+ v_model=("console_output", ""),
2801
+ readonly=True,
2802
+ auto_grow=False,
2803
+ rows=6,
2804
+ variant="plain",
2805
+ hide_details=True,
2806
+ style="font-family: monospace; width: 100%; height: 100%;"
2807
+ )
2808
+
2809
  # Status Window - Fixed at bottom right
2810
  with vuetify3.VCard(
2811
  v_if="status_visible",
qlbm.py CHANGED
@@ -48,6 +48,8 @@ def _should_disable_quantum_backend():
48
 
49
  simulate_qlbm_3D_and_animate = None
50
  _SIMULATION_BACKEND_DISABLED, _SIMULATION_DISABLED_REASON = _should_disable_quantum_backend()
 
 
51
 
52
  if not _SIMULATION_BACKEND_DISABLED:
53
  try:
@@ -69,6 +71,23 @@ _SIMULATION_BACKEND_READY = simulate_qlbm_3D_and_animate is not None
69
  if not _SIMULATION_BACKEND_READY and _SIMULATION_DISABLED_REASON:
70
  print(f"Warning: {_SIMULATION_DISABLED_REASON}")
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  # --- Server Setup ---
73
  server = get_server()
74
  state, ctrl = server.state, server.controller
@@ -121,8 +140,9 @@ state.update({
121
  "simulation_has_run": False,
122
  "time_val": 0,
123
  "max_time_step": 0,
124
- "simulation_backend_ready": _SIMULATION_BACKEND_READY,
125
- "simulation_disabled_reason": _SIMULATION_DISABLED_REASON,
 
126
  # Workflow guidance styles
127
  "workflow_step": 0,
128
  "overview_card_style": _WORKFLOW_BASE_STYLE,
@@ -422,6 +442,84 @@ def make_velocity_func(expr):
422
  return np.zeros_like(x) if isinstance(x, np.ndarray) else 0.0
423
  return func
424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  def get_geometry_figure():
426
  """Generates a 3D Plotly figure for the selected geometry."""
427
  geom = state.geometry_selection
@@ -516,8 +614,8 @@ def update_geometry_view():
516
  def run_simulation():
517
  global simulation_data_frames, simulation_times, current_grid_object
518
 
519
- if simulate_qlbm_3D_and_animate is None:
520
- state.run_error = _SIMULATION_DISABLED_REASON or "Simulation module is not available on this platform."
521
  return
522
 
523
  state.is_running = True
@@ -537,20 +635,30 @@ def run_simulation():
537
  vy_func = make_velocity_func(state.vy_expr)
538
  vz_func = make_velocity_func(state.vz_expr)
539
 
540
- # 2. Run Simulation
541
- # Note: simulate_qlbm_3D_and_animate updates the passed plotter with the first frame
542
- plotter.clear()
543
- _, frames, times, grid_obj = simulate_qlbm_3D_and_animate(
544
- num_reg_qubits=num_reg_qubits,
545
- T=T,
546
- distribution_type=distribution_type,
547
- vx_input=vx_func,
548
- vy_input=vy_func,
549
- vz_input=vz_func,
550
- boundary_condition=boundary_condition,
551
- plotter=plotter,
552
- add_slider=False
553
- )
 
 
 
 
 
 
 
 
 
 
554
 
555
  # Force Blues colormap
556
  if grid_obj:
@@ -1011,12 +1119,13 @@ with SinglePageLayout(server) as layout:
1011
  click=run_simulation,
1012
  style=("is_running ? '' : 'background-color:#87CEFA;'", ""),
1013
  )
 
1014
  vuetify3.VAlert(
1015
- v_if="simulation_disabled_reason",
1016
- type="warning",
1017
  variant="tonal",
1018
  density="compact",
1019
- children=["{{ simulation_disabled_reason }}"],
1020
  classes="mt-2",
1021
  )
1022
  with vuetify3.VRow(dense=True, classes="mt-2"):
 
48
 
49
  simulate_qlbm_3D_and_animate = None
50
  _SIMULATION_BACKEND_DISABLED, _SIMULATION_DISABLED_REASON = _should_disable_quantum_backend()
51
+ _CPU_DEMO_AVAILABLE = True
52
+ _CPU_DEMO_MAX_GRID = 48
53
 
54
  if not _SIMULATION_BACKEND_DISABLED:
55
  try:
 
71
  if not _SIMULATION_BACKEND_READY and _SIMULATION_DISABLED_REASON:
72
  print(f"Warning: {_SIMULATION_DISABLED_REASON}")
73
 
74
+ if _SIMULATION_BACKEND_READY:
75
+ _SIMULATION_BACKEND_NOTE = ""
76
+ _SIMULATION_MODE_LABEL = "Quantum CUDA-Q backend"
77
+ else:
78
+ if _CPU_DEMO_AVAILABLE:
79
+ reason = _SIMULATION_DISABLED_REASON or "Quantum backend is unavailable in this environment."
80
+ _SIMULATION_BACKEND_NOTE = (
81
+ f"CPU demo mode active ({reason}). Results are approximate. "
82
+ "Set FORCE_ENABLE_CUDAQ=1 on a GPU host to use the quantum backend."
83
+ )
84
+ _SIMULATION_MODE_LABEL = "CPU demo backend"
85
+ else:
86
+ _SIMULATION_BACKEND_NOTE = _SIMULATION_DISABLED_REASON or "Simulation backend unavailable."
87
+ _SIMULATION_MODE_LABEL = "Unavailable"
88
+
89
+ _SIMULATION_CAN_RUN = _SIMULATION_BACKEND_READY or _CPU_DEMO_AVAILABLE
90
+
91
  # --- Server Setup ---
92
  server = get_server()
93
  state, ctrl = server.state, server.controller
 
140
  "simulation_has_run": False,
141
  "time_val": 0,
142
  "max_time_step": 0,
143
+ "simulation_backend_ready": _SIMULATION_CAN_RUN,
144
+ "simulation_backend_note": _SIMULATION_BACKEND_NOTE,
145
+ "simulation_backend_mode": _SIMULATION_MODE_LABEL,
146
  # Workflow guidance styles
147
  "workflow_step": 0,
148
  "overview_card_style": _WORKFLOW_BASE_STYLE,
 
442
  return np.zeros_like(x) if isinstance(x, np.ndarray) else 0.0
443
  return func
444
 
445
+
446
+ def _safe_velocity_sample(func) -> float:
447
+ try:
448
+ val = func(0.5, 0.5, 0.5)
449
+ if isinstance(val, np.ndarray):
450
+ val = float(np.mean(val))
451
+ return float(val)
452
+ except Exception:
453
+ return 0.0
454
+
455
+
456
+ def _cpu_distribution_field(distribution_type: str, Xi, Yi, Zi, grid_size: int, drift, phase_fraction: float):
457
+ if distribution_type == "Sinusoidal":
458
+ kx = max(1.0, round(float(state.sine_k_x))) if hasattr(state, "sine_k_x") else 1.0
459
+ ky = max(1.0, round(float(state.sine_k_y))) if hasattr(state, "sine_k_y") else 1.0
460
+ kz = max(1.0, round(float(state.sine_k_z))) if hasattr(state, "sine_k_z") else 1.0
461
+ x_term = np.sin((np.mod(Xi + drift[0], grid_size)) * 2 * np.pi * kx / grid_size)
462
+ y_term = np.sin((np.mod(Yi + drift[1], grid_size)) * 2 * np.pi * ky / grid_size)
463
+ z_term = np.sin((np.mod(Zi + drift[2], grid_size)) * 2 * np.pi * kz / grid_size)
464
+ field = x_term * y_term * z_term + 1.0
465
+ else:
466
+ # Gaussian (default fallback)
467
+ nx_val = max(1.0, float(state.nx)) if hasattr(state, "nx") else float(grid_size)
468
+ cx = float(state.gauss_cx) if hasattr(state, "gauss_cx") else nx_val / 2
469
+ cy = float(state.gauss_cy) if hasattr(state, "gauss_cy") else nx_val / 2
470
+ cz = float(state.gauss_cz) if hasattr(state, "gauss_cz") else nx_val / 2
471
+ sigma = float(state.gauss_sigma) if hasattr(state, "gauss_sigma") else nx_val / 6
472
+ scale = (grid_size - 1) / nx_val if nx_val else 1.0
473
+ cx = cx * scale + drift[0]
474
+ cy = cy * scale + drift[1]
475
+ cz = cz * scale + drift[2]
476
+ sigma = max(1.0, sigma * scale)
477
+ field = np.exp(-(((Xi - cx) ** 2 + (Yi - cy) ** 2 + (Zi - cz) ** 2) / (2 * sigma ** 2))) * 1.8 + 0.2
478
+
479
+ modulation = 0.15 * np.sin(2 * np.pi * phase_fraction + (Xi + Yi + Zi) * np.pi / max(1, grid_size))
480
+ return field + modulation
481
+
482
+
483
+ def _run_cpu_demo_simulation(grid_size: int, T: int, distribution_type: str, vx_func, vy_func, vz_func):
484
+ grid_size = int(max(8, min(grid_size, _CPU_DEMO_MAX_GRID)))
485
+ idx_coords = np.linspace(0, grid_size - 1, grid_size, dtype=np.float32)
486
+ Xi, Yi, Zi = np.meshgrid(idx_coords, idx_coords, idx_coords, indexing='ij')
487
+ geom_coords = np.linspace(0, 1, grid_size, dtype=np.float32)
488
+ Xg, Yg, Zg = np.meshgrid(geom_coords, geom_coords, geom_coords, indexing='ij')
489
+
490
+ if T <= 0:
491
+ target = 1.0
492
+ else:
493
+ target = float(T)
494
+ num_frames = min(30, max(2, int(min(target, 20)) + 1))
495
+ timeline = list(np.linspace(0.0, target, num_frames))
496
+ if len(timeline) < 2:
497
+ timeline.append(target)
498
+
499
+ vx = _safe_velocity_sample(vx_func)
500
+ vy = _safe_velocity_sample(vy_func)
501
+ vz = _safe_velocity_sample(vz_func)
502
+ drift_scale = 0.25 * grid_size
503
+
504
+ frames = []
505
+ for idx, t_val in enumerate(timeline):
506
+ phase_fraction = idx / (len(timeline) - 1) if len(timeline) > 1 else 0.0
507
+ drift = (
508
+ vx * phase_fraction * drift_scale,
509
+ vy * phase_fraction * drift_scale,
510
+ vz * phase_fraction * drift_scale,
511
+ )
512
+ field = _cpu_distribution_field(distribution_type, Xi, Yi, Zi, grid_size, drift, phase_fraction)
513
+ frames.append(field.astype(np.float32))
514
+
515
+ grid = pv.StructuredGrid()
516
+ grid.points = np.column_stack((Xg.ravel(), Yg.ravel(), Zg.ravel()))
517
+ grid.dimensions = [grid_size, grid_size, grid_size]
518
+ grid["scalars"] = frames[0].ravel()
519
+
520
+ times = [float(t) for t in timeline]
521
+ return frames, times, grid
522
+
523
  def get_geometry_figure():
524
  """Generates a 3D Plotly figure for the selected geometry."""
525
  geom = state.geometry_selection
 
614
  def run_simulation():
615
  global simulation_data_frames, simulation_times, current_grid_object
616
 
617
+ if not _SIMULATION_CAN_RUN:
618
+ state.run_error = _SIMULATION_DISABLED_REASON or "Simulation backend is not available on this platform."
619
  return
620
 
621
  state.is_running = True
 
635
  vy_func = make_velocity_func(state.vy_expr)
636
  vz_func = make_velocity_func(state.vz_expr)
637
 
638
+ if simulate_qlbm_3D_and_animate is not None:
639
+ # 2a. Run CUDA-Q Simulation
640
+ plotter.clear()
641
+ _, frames, times, grid_obj = simulate_qlbm_3D_and_animate(
642
+ num_reg_qubits=num_reg_qubits,
643
+ T=T,
644
+ distribution_type=distribution_type,
645
+ vx_input=vx_func,
646
+ vy_input=vy_func,
647
+ vz_input=vz_func,
648
+ boundary_condition=boundary_condition,
649
+ plotter=plotter,
650
+ add_slider=False
651
+ )
652
+ else:
653
+ # 2b. CPU Demo Simulation (approximate)
654
+ frames, times, grid_obj = _run_cpu_demo_simulation(
655
+ grid_size=grid_size,
656
+ T=T,
657
+ distribution_type=distribution_type or "Sinusoidal",
658
+ vx_func=vx_func,
659
+ vy_func=vy_func,
660
+ vz_func=vz_func,
661
+ )
662
 
663
  # Force Blues colormap
664
  if grid_obj:
 
1119
  click=run_simulation,
1120
  style=("is_running ? '' : 'background-color:#87CEFA;'", ""),
1121
  )
1122
+ html.Div("Backend: {{ simulation_backend_mode }}", classes="text-caption text-medium-emphasis mt-2")
1123
  vuetify3.VAlert(
1124
+ v_if="simulation_backend_note",
1125
+ type="info",
1126
  variant="tonal",
1127
  density="compact",
1128
+ children=["{{ simulation_backend_note }}"],
1129
  classes="mt-2",
1130
  )
1131
  with vuetify3.VRow(dense=True, classes="mt-2"):