harishaseebat92 commited on
Commit
75a1323
·
1 Parent(s): f7e81bd

Merged QLBM Application and Added features in EM_Trame

Browse files
.dockerignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .git/
12
+ .gitignore
13
+ .gitattributes
14
+ venv/
15
+ env/
16
+ ENV/
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+ *~
22
+ .DS_Store
23
+ Thumbs.db
24
+ *.md
25
+ README.md
26
+ current_requirements.txt
27
+ tempCodeRunnerFile.py
28
+ em_trame1.py
29
+ em_tramefinal.py
30
+ trial.py
.gitattributes CHANGED
@@ -33,4 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- synopsys-logo-color-rgb.jpg filter=lfs diff=lfs merge=lfs -text
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.jpg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .venv/
2
+ synopsys-logo-color-rgb.jpg
ansys-part-of-synopsys-logo.svg ADDED
app.py CHANGED
@@ -1,86 +1,27 @@
1
- import numpy as np
2
- import re
3
- import pyvista as pv
4
- import webbrowser
5
- import threading
6
- import base64
7
-
8
  from trame.app import get_server
9
  from trame_vuetify.ui.vuetify3 import SinglePageLayout
10
  from trame_vuetify.widgets import vuetify3
11
- from pyvista.trame.ui import plotter_ui
12
- import plotly.graph_objects as go
13
- from trame_plotly.widgets import plotly as plotly_widgets
14
- import os
15
- from datetime import datetime
16
  from trame.widgets import html as trame_html
 
17
 
18
- from qiskit.circuit import QuantumCircuit, QuantumRegister
19
- from qiskit.circuit.library import StatePreparation, QFTGate, RZGate
20
- from qiskit.quantum_info import Statevector
21
- from delta_impulse_generator import *
22
-
23
- # Set PyVista to use off-screen rendering for Trame
24
- pv.OFF_SCREEN = True
25
 
26
- # --- Server and State Initialization ---
27
  server = get_server()
28
  state, ctrl = server.state, server.controller
29
 
30
- # --- Application State ---
31
- state.update({
32
- "dist_type": None, "impulse_x": 0.5, "impulse_y": 0.5,
33
- "peak_pair": "(0.5, 0.5)",
34
- "mu_x": 0.5, "mu_y": 0.5, "sigma_x": 0.25, "sigma_y": 0.15,
35
- "mu_pair": "(0.5, 0.5)", # Gaussian Mu as normalized pair string
36
- "sigma_pair": "(0.25, 0.15)", # Gaussian Sigma as normalized pair string
37
- "nx": None, "T": 10.0, "time_val": 0.0,
38
- "L": 1.0, # Side length used for coordinate conversion
39
- "output_type": "Surface Plot",
40
- "surface_field": "Ez",
41
- "timeseries_field": "Ez",
42
- "timeseries_points": "(8, 8), (10, 8)",
43
- "error_message": "",
44
- "is_running": False,
45
- "simulation_has_run": False,
46
- "geometry_selection": None,
47
- "show_upload_dialog": False,
48
- "uploaded_file_info": None,
49
- "show_upload_status": False,
50
- "upload_status_message": "",
51
- "coeff_permittivity": 1.0, # Relative permittivity (ε_r)
52
- "coeff_permeability": 1.0, # Relative permeability (μ_r)
53
- "run_button_text": "Run Simulation",
54
- "backend_type": "Simulator",
55
- "selected_simulator": "IBM Qiskit simulator",
56
- "selected_qpu": "IBM QPU",
57
- "stop_button_disabled": True, # Stop button is initially disabled
58
- "export_format": "vtk", # Dummy export format for Surface Plot
59
- "nx_slider_index": None, # No selection until user chooses on the slider
60
- "show_export_status": False,
61
- "export_status_message": "",
62
- "logo_src": None,
63
- # Dummy geometry-hole controls (UI only)
64
- "hole_size_edge": 0.2, # edge length in [0, 1]
65
- "hole_center_x": 0.5, # center X in (0, 1)
66
- "hole_center_y": 0.5, # center Y in (0, 1)
67
- "hole_center_pair": "(0.5, 0.5)", # bracket-format center for dropdown
68
- "hole_error_message": "", # validation message
69
- "excitation_error_message": "", # parse/validation for Gaussian pair inputs
70
- "excitation_info_message": "", # Snapping info for excitation coordinates
71
- "dt_user": 0.1, # Dummy Δt input from user (seconds)
72
- "temporal_warning": "", # Warning message for invalid Δt
73
- # Square aspect for initial preview
74
- "pyvista_view_style": "aspect-ratio: 1 / 1; width: 100%;",
75
- })
76
 
77
- # Ensure hole snap state exists
78
- state.hole_snap = True
79
 
80
- # --- Load Synopsys logo (from quantum folder) as data URI ---
81
- def load_logo_data_uri():
82
  base_dir = os.path.dirname(__file__)
83
  candidates = [
 
84
  os.path.join(base_dir, "synopsys-logo-color-rgb.svg"),
85
  os.path.join(base_dir, "synopsys-logo-color-rgb.png"),
86
  os.path.join(base_dir, "synopsys-logo-color-rgb.jpg"),
@@ -94,1454 +35,41 @@ def load_logo_data_uri():
94
  return f"data:{mime};base64,{b64}"
95
  return None
96
 
97
- state.logo_src = load_logo_data_uri()
98
-
99
- # --- Global PyVista and Data Variables ---
100
- plotter = pv.Plotter()
101
- simulation_data = None
102
- current_mesh = None
103
- data_frames = None
104
- z_scale = 1.0
105
- X_grids, Y_grids = {}, {}
106
- surface_clims = {}
107
- stop_simulation = False # Flag to stop running simulation
108
- snapshot_times = None # Times corresponding to saved frames (user snapshots)
109
-
110
- # --- Constants ---
111
- GRID_SIZES = ["16", "32", "64", "128", "256", "512"]
112
-
113
- # --- Plotting and Simulation Logic ---
114
- def get_time_series_chart(simulation_data, field_type, positions, nx, times):
115
- # times: 1D array of snapshot times aligning with simulation_data frames
116
- n_frames = int(simulation_data.shape[0]) if simulation_data is not None else 0
117
- time_axis = np.asarray(times) if times is not None else np.arange(n_frames)
118
- chart = pv.Chart2D()
119
- if (field_type == 'Ez'):
120
- grid_width, grid_height = nx, nx
121
- elif (field_type == 'Hx'):
122
- grid_width, grid_height = nx, nx - 1
123
- else:
124
- grid_width, grid_height = nx - 1, nx
125
- colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
126
- for i, (pos_x, pos_y) in enumerate(positions):
127
- if not (0 <= pos_x < grid_width and 0 <= pos_y < grid_height):
128
- print(f"Warning: Skipping invalid position {(pos_x, pos_y)} for {field_type}")
129
- continue
130
- if field_type == 'Ez': values = simulation_data[:, pos_y * grid_width + pos_x]
131
- elif field_type == 'Hx': values = simulation_data[:, 2*nx*nx : 3*nx*nx-nx].reshape(n_frames, grid_height, grid_width)[:, pos_y, pos_x]
132
- else:
133
- mask = np.arange(1, nx * nx + 1) % nx != 0
134
- raw_block = simulation_data[:, -nx*nx:]
135
- values = [raw_block[t, mask].reshape(nx, nx - 1)[pos_y, pos_x] for t in range(n_frames)]
136
- chart.line(time_axis, values, label=f'{field_type} at ({pos_x}, {pos_y})', color=colors[i % len(colors)])
137
- chart.x_label = "Time (s)"; chart.y_label = "Field Amplitude"; chart.title = f"Time Evolution of {field_type}"
138
- return chart
139
-
140
- def setup_surface_plot_data(simulation_data, nx):
141
- global data_frames, z_scale, X_grids, Y_grids, surface_clims
142
- mask = np.arange(1, nx * nx + 1) % nx != 0
143
- data_frames = {'Ez': [], 'Hx': [], 'Hy': []}
144
- surface_clims = {'Ez': [np.inf, -np.inf], 'Hx': [np.inf, -np.inf], 'Hy': [np.inf, -np.inf]}
145
- for u in simulation_data:
146
- ez, hx, hy = u[:nx*nx].reshape(nx,nx), u[2*nx*nx:3*nx*nx-nx].reshape(nx-1,nx), u[-nx*nx:][mask].reshape(nx,nx-1)
147
- data_frames['Ez'].append(ez); data_frames['Hx'].append(hx); data_frames['Hy'].append(hy)
148
- if ez.size > 0: surface_clims['Ez'][0], surface_clims['Ez'][1] = min(surface_clims['Ez'][0], ez.min()), max(surface_clims['Ez'][1], ez.max())
149
- if hx.size > 0: surface_clims['Hx'][0], surface_clims['Hx'][1] = min(surface_clims['Hx'][0], hx.min()), max(surface_clims['Hx'][1], hx.max())
150
- if hy.size > 0: surface_clims['Hy'][0], surface_clims['Hy'][1] = min(surface_clims['Hy'][0], hy.min()), max(surface_clims['Hy'][1], hy.max())
151
- for key in surface_clims:
152
- if surface_clims[key][0] == surface_clims[key][1]: surface_clims[key][0] -= 1e-9; surface_clims[key][1] += 1e-9
153
- # Revert to integer grid coordinates (like app.py) to keep output plots in grids
154
- x, y, x_m1, y_m1 = np.arange(nx), np.arange(nx), np.arange(nx-1), np.arange(nx-1)
155
- X_grids['Ez'], Y_grids['Ez'] = np.meshgrid(x, y)
156
- X_grids['Hx'], Y_grids['Hx'] = np.meshgrid(x, y_m1)
157
- X_grids['Hy'], Y_grids['Hy'] = np.meshgrid(x_m1, y)
158
- max_abs = max(abs(v) for pair in surface_clims.values() for v in pair if v is not np.inf and v is not -np.inf)
159
- z_scale = (nx / 2) / max(max_abs, 1e-9)
160
-
161
- def run_simulation_only():
162
- global simulation_data, current_mesh, snapshot_times, stop_simulation
163
- # Require selections before running
164
- if not state.geometry_selection:
165
- state.error_message = "Please select a geometry before running the simulation."
166
- state.is_running = False
167
- state.run_button_text = "Run Simulation"
168
- return
169
- if not state.dist_type:
170
- state.error_message = "Please select an initial state before running the simulation."
171
- state.is_running = False
172
- state.run_button_text = "Run Simulation"
173
- return
174
-
175
- # Reset stop flag and enable Stop button at start
176
- stop_simulation = False
177
- state.stop_button_disabled = False
178
-
179
- plotter.clear()
180
- current_mesh = None
181
- state.error_message = ""
182
- state.is_running = True
183
- state.simulation_has_run = False
184
- state.run_button_text = "Running"
185
- ctrl.view_update()
186
-
187
- nx, T = int(state.nx), float(state.T)
188
- na, R = 1, 4
189
-
190
- try:
191
- if state.dist_type == "Delta":
192
- initial_state = create_impulse_state_from_pos((nx, nx), (float(state.impulse_x), float(state.impulse_y)))
193
- else:
194
- initial_state = create_gaussian_state_from_pos((nx, nx), (float(state.mu_x), float(state.mu_y)), (float(state.sigma_x), float(state.sigma_y)))
195
- except ValueError as e:
196
- state.error_message = f"Initial State Error: {e}"
197
- state.is_running = False
198
- state.run_button_text = "Run Simulation"
199
- state.stop_button_disabled = True
200
- return
201
-
202
- print("Running simulation...")
203
- # Pass user-defined snapshot Δt; keep solver dt=0.1 inside run_sim
204
- snapshot_dt = float(state.dt_user)
205
- def _stop_check():
206
- return stop_simulation
207
- simulation_data, snapshot_times = run_sim(nx, na, R, initial_state, T, snapshot_dt=snapshot_dt, stop_check=_stop_check)
208
- print("Simulation complete.")
209
-
210
- if simulation_data.size > 0:
211
- setup_surface_plot_data(simulation_data, nx)
212
- state.simulation_has_run = True
213
- state.run_button_text = "Successful!"
214
- # Allow the view to use full area after run (remove strict square)
215
- state.pyvista_view_style = ""
216
- generate_plot()
217
- else:
218
- state.error_message = "Simulation produced no data. Check parameters (e.g., T > 0)."
219
- state.run_button_text = "Run Simulation"
220
-
221
- state.is_running = False
222
- state.stop_button_disabled = True
223
-
224
- def reset_to_defaults():
225
- """Reset all parameters to their default values"""
226
- global simulation_data, current_mesh, data_frames, stop_simulation, snapshot_times
227
-
228
- # Stop any running simulation
229
- stop_simulation = True
230
-
231
- # Reset global variables
232
- simulation_data = None
233
- current_mesh = None
234
- data_frames = None
235
- snapshot_times = None
236
-
237
- # Reset state to default values
238
- state.update({
239
- "dist_type": None,
240
- "impulse_x": 0.5,
241
- "impulse_y": 0.5,
242
- "peak_pair": "(0.5, 0.5)",
243
- "mu_x": 0.5,
244
- "mu_y": 0.5,
245
- "sigma_x": 0.25,
246
- "sigma_y": 0.15,
247
- "mu_pair": "(0.5, 0.5)",
248
- "sigma_pair": "(0.25, 0.15)",
249
- "nx": None,
250
- "T": 10.0,
251
- "time_val": 0.0,
252
- "output_type": "Surface Plot",
253
- "surface_field": "Ez",
254
- "timeseries_field": "Ez",
255
- "timeseries_points": "(8, 8), (10, 8)",
256
- "error_message": "",
257
- "excitation_info_message": "",
258
- "is_running": False,
259
- "simulation_has_run": False,
260
- "geometry_selection": None,
261
- "coeff_permittivity": 1.0,
262
- "coeff_permeability": 1.0,
263
- "run_button_text": "Run Simulation",
264
- "backend_type": "Simulator",
265
- "selected_simulator": "IBM Qiskit simulator",
266
- "selected_qpu": "IBM QPU",
267
- "stop_button_disabled": True,
268
- "export_format": "vtk",
269
- "nx_slider_index": None,
270
- "dt_user": 0.1,
271
- "temporal_warning": "",
272
- # Restore square aspect for initial preview
273
- "pyvista_view_style": "aspect-ratio: 1 / 1; width: 100%;",
274
- })
275
-
276
- # Ensure stop flag is cleared for next run
277
- stop_simulation = False
278
-
279
- # Update the preview with default values
280
- update_initial_state_preview()
281
- print("Reset to default settings")
282
-
283
- @state.change("peak_pair")
284
- def sync_peak_pair(peak_pair, **kwargs):
285
- """Parse normalized pair (x, y) in [0,1] for Peak and update impulse_x/impulse_y."""
286
- try:
287
- m = re.match(r"\(\s*([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*\)", str(peak_pair))
288
- if not m:
289
- raise ValueError("Invalid format")
290
- x = max(0.0, min(1.0, float(m.group(1))))
291
- y = max(0.0, min(1.0, float(m.group(2))))
292
- state.impulse_x = x
293
- state.impulse_y = y
294
- state.excitation_error_message = ""
295
- except Exception:
296
- state.excitation_error_message = "Invalid Peak. Use format (x, y) in [0,1]."
297
- finally:
298
- update_excitation_info_message()
299
-
300
- @state.change("mu_pair")
301
- def sync_mu_pair(mu_pair, **kwargs):
302
- """Parse normalized pair (x, y) in [0,1] for Mu and update mu_x/mu_y."""
303
- try:
304
- m = re.match(r"\(\s*([-+]?[0-9]*\.?[0-9]+)\s*,\s*([-+]?[0-9]*\.?[0-9]+)\s*\)", str(mu_pair))
305
- if not m:
306
- raise ValueError("Invalid format")
307
- x = max(0.0, min(1.0, float(m.group(1))))
308
- y = max(0.0, min(1.0, float(m.group(2))))
309
- state.mu_x = x
310
- state.mu_y = y
311
- state.excitation_error_message = ""
312
- except Exception:
313
- state.excitation_error_message = "Invalid Mu. Use format (x, y) in [0,1]."
314
- finally:
315
- update_excitation_info_message()
316
-
317
- def stop_simulation_handler():
318
- """Stop the currently running simulation"""
319
- global stop_simulation
320
- stop_simulation = True
321
- state.stop_button_disabled = True
322
- print("Stopping simulation...")
323
-
324
- def generate_plot():
325
- global current_mesh
326
- if not state.simulation_has_run: return
327
-
328
- plotter.clear()
329
- try: plotter.disable_picking()
330
- except: pass
331
-
332
- nx, T = int(state.nx), float(state.T)
333
-
334
- if state.output_type == "Surface Plot":
335
- redraw_surface_plot()
336
- else: # Time Series
337
- try:
338
- points_str = state.timeseries_points
339
- positions = [tuple(map(int, match)) for match in re.findall(r'\((\d+)\s*,\s*(\d+)\)', points_str)]
340
- if not positions and points_str.strip(): raise ValueError("No valid points found.")
341
- chart = get_time_series_chart(simulation_data, state.timeseries_field, positions, nx, snapshot_times)
342
- plotter.add_chart(chart)
343
- plotter.view_xy() # Set a 2D view for the chart
344
- except Exception as e:
345
- state.error_message = f"Plotting Error: {e}"
346
-
347
- ctrl.view_update()
348
-
349
- def redraw_surface_plot():
350
- global current_mesh
351
- plotter.clear()
352
- field = state.surface_field
353
- if data_frames is None or not data_frames.get(field): return
354
- if snapshot_times is None or len(snapshot_times) == 0: return
355
-
356
- # Find nearest snapshot index to requested time and clamp to available frames
357
- req_t = float(state.time_val)
358
- times = np.asarray(snapshot_times)
359
- idx = int(np.argmin(np.abs(times - req_t)))
360
- max_idx = len(data_frames[field]) - 1
361
- idx = max(0, min(idx, max_idx))
362
-
363
- z_data = data_frames[field][idx]
364
- points = np.c_[X_grids[field].ravel(), Y_grids[field].ravel(), z_data.ravel() * z_scale]
365
- poly = pv.PolyData(points)
366
- mesh = poly.delaunay_2d()
367
- mesh['scalars'] = z_data.ravel()
368
- current_mesh = mesh
369
- 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)
370
- plotter.add_scalar_bar(title=f"{field} Amplitude")
371
- try:
372
- plotter.disable_picking()
373
- except Exception:
374
- pass
375
- plotter.enable_point_picking(callback=update_value_display, show_message=False)
376
- plotter.add_axes()
377
- plotter.view_isometric()
378
- try:
379
- plotter.camera.parallel_projection = True
380
- except Exception:
381
- pass
382
- ctrl.view_update()
383
-
384
- # Helper: add a dotted unit grid (0..1) overlay in light Synopsys purple
385
- def _add_dotted_unit_grid(plotter, ticks=(0.0, 0.25, 0.5, 0.75, 1.0), segments=48, gap_ratio=0.4, color="#AE8BD8", line_width=0.2):
386
- try:
387
- step = 1.0 / float(max(segments, 1))
388
- seg_len = step * float(max(0.0, min(1.0, 1.0 - gap_ratio)))
389
- pts = []
390
- lines = []
391
- # Horizontal dotted lines at given y=tick
392
- for y in ticks:
393
- pos = 0.0
394
- while pos < 1.0 - 1e-9:
395
- y0, y1 = pos, min(pos + seg_len, 1.0)
396
- pts.extend([(0.0, y, 0.0), (1.0, y, 0.0)]) # end points along x (we'll segment via multiple x positions)
397
- # Replace with segmented along X
398
- pts[-2] = (pos, y, 0.0)
399
- pts[-1] = (y1 if seg_len > 0 else pos, y, 0.0)
400
- i0 = len(pts) - 2
401
- lines.extend([2, i0, i0 + 1])
402
- pos += step
403
- # Vertical dotted lines at given x=tick
404
- for x in ticks:
405
- pos = 0.0
406
- while pos < 1.0 - 1e-9:
407
- y0, y1 = pos, min(pos + seg_len, 1.0)
408
- pts.extend([(x, pos, 0.0), (x, y1 if seg_len > 0 else pos, 0.0)])
409
- i0 = len(pts) - 2
410
- lines.extend([2, i0, i0 + 1])
411
- pos += step
412
- if pts and lines:
413
- poly = pv.PolyData(np.array(pts))
414
- poly.lines = np.array(lines)
415
- plotter.add_mesh(poly, color=color, line_width=line_width, name="dotted_unit_grid", pickable=False)
416
- except Exception:
417
- pass
418
-
419
- # Scaled dotted unit grid overlay for integer-coordinate previews (Delta/Gaussian)
420
- def _add_dotted_unit_grid_scaled(plotter, denom, ticks=(0.0, 0.25, 0.5, 0.75, 1.0), segments=48, gap_ratio=0.6, color="#AE8BD8", line_width=1.0, name="dotted_unit_grid_preview"):
421
- """Overlay a 0–1 dotted grid scaled to [0, denom] on the XY plane without changing mesh coordinates."""
422
- try:
423
- step = 1.0 / float(max(segments, 1))
424
- seg_len = step * float(max(0.0, min(1.0, 1.0 - gap_ratio)))
425
- # Set a z slightly below mesh to avoid z-fighting
426
- try:
427
- z0 = float(current_mesh.points[:, 2].min()) - 1e-6 if current_mesh is not None else 0.0
428
- except Exception:
429
- z0 = 0.0
430
- pts, lines = [], []
431
- # Vertical lines at x = t * denom
432
- for t in ticks:
433
- x = float(t) * float(denom)
434
- pos = 0.0
435
- while pos < 1.0 - 1e-9:
436
- y0 = pos * denom
437
- y1 = min(pos + seg_len, 1.0) * denom
438
- pts.extend([(x, y0, z0), (x, y1, z0)])
439
- i0 = len(pts) - 2
440
- lines.extend([2, i0, i0 + 1])
441
- pos += step
442
- # Horizontal lines at y = t * denom
443
- for t in ticks:
444
- y = float(t) * float(denom)
445
- pos = 0.0
446
- while pos < 1.0 - 1e-9:
447
- x0 = pos * denom
448
- x1 = min(pos + seg_len, 1.0) * denom
449
- pts.extend([(x0, y, z0), (x1, y, z0)])
450
- i0 = len(pts) - 2
451
- lines.extend([2, i0, i0 + 1])
452
- pos += step
453
- try:
454
- plotter.remove_actor(name)
455
- except Exception:
456
- pass
457
- if pts and lines:
458
- poly = pv.PolyData(np.array(pts))
459
- poly.lines = np.array(lines)
460
- plotter.add_mesh(poly, color=color, line_width=line_width, name=name, pickable=False)
461
- except Exception:
462
- pass
463
-
464
- # --- Plain Square Domain Preview ---
465
- def update_geometry_preview():
466
- """Render a flat square mesh (Z=0) with edges for 'Square Domain'."""
467
- global current_mesh
468
- if state.is_running or state.simulation_has_run:
469
- return
470
- plotter.clear()
471
- nx = int(state.nx) if state.nx is not None else 32
472
- # Build normalized coordinates in [0, 1]
473
- denom = max(nx - 1, 1)
474
- x, y = np.arange(nx) / denom, np.arange(nx) / denom
475
- X, Y = np.meshgrid(x, y)
476
- Z = np.zeros_like(X, dtype=float)
477
- points = np.c_[X.ravel(), Y.ravel(), Z.ravel()]
478
- poly = pv.PolyData(points)
479
- mesh = poly.delaunay_2d()
480
- mesh['scalars'] = Z.ravel()
481
- current_mesh = mesh
482
- plotter.add_mesh(
483
- mesh,
484
- color="#FFDAB9", # Peach Puff to match Square Metallic Body domain
485
- show_scalar_bar=False,
486
- show_edges=False,
487
- edge_color='grey',
488
- line_width=0.5,
489
- )
490
- try:
491
- plotter.disable_picking()
492
- except Exception:
493
- pass
494
- plotter.enable_point_picking(callback=update_value_display, show_message=False)
495
- plotter.add_axes()
496
- # Axes scaled 0..1
497
- plotter.show_grid(bounds=(0.0, 1.0, 0.0, 1.0, 0.0, 0.0), xtitle="x (0–1)", ytitle="y (0–1)", ztitle=" ", color="#AE8BD8")
498
- _add_dotted_unit_grid(plotter, ticks=(0.0, 0.25, 0.5, 0.75, 1.0), segments=48, gap_ratio=0.6, color="#AE8BD8", line_width=1)
499
- plotter.view_isometric()
500
- try:
501
- plotter.camera.parallel_projection = True
502
- except Exception:
503
- pass
504
- ctrl.view_update()
505
-
506
- # --- Plain Square Domain (Hole) Preview ---
507
- def update_geometry_hole_preview():
508
- """Render a flat square mesh with a square hole defined by center (cx, cy) and size a."""
509
- global current_mesh
510
- if state.is_running or state.simulation_has_run:
511
- return
512
- plotter.clear()
513
- nx = int(state.nx) if state.nx is not None else 32
514
- denom = max(nx - 1, 1)
515
- # Normalized grid in [0,1]
516
- x, y = np.arange(nx) / denom, np.arange(nx) / denom
517
- X, Y = np.meshgrid(x, y)
518
- Z = np.zeros_like(X, dtype=float)
519
- points = np.c_[X.ravel(), Y.ravel(), Z.ravel()]
520
- poly = pv.PolyData(points)
521
- mesh = poly.delaunay_2d()
522
-
523
- # Read user inputs
524
- try:
525
- a = float(state.hole_size_edge)
526
- cx = float(state.hole_center_x)
527
- cy = float(state.hole_center_y)
528
- except Exception:
529
- a, cx, cy = 0.2, 0.5, 0.5 # fallback
530
-
531
- mode_snap = bool(state.hole_snap)
532
- edges = _compute_hole_edges(nx, cx, cy, a, snap=mode_snap)
533
- if edges is not None:
534
- xL, xR, yB, yT = edges
535
- # Open rectangle: remove cells with centers strictly inside (boundaries excluded)
536
- centers = mesh.cell_centers().points
537
- in_x = (centers[:, 0] > xL) & (centers[:, 0] < xR)
538
- in_y = (centers[:, 1] > yB) & (centers[:, 1] < yT)
539
- hole_mask = in_x & in_y
540
- if hole_mask.any():
541
- mesh = mesh.remove_cells(np.where(hole_mask)[0])
542
- mesh.clean(inplace=True)
543
-
544
- # Color domain peach and overlay a black filled rectangle for the hole
545
- current_mesh = mesh
546
- try:
547
- plotter.remove_actor("hole_overlay")
548
- except Exception:
549
- pass
550
-
551
- # Draw domain in peach color so it doesn't blend with background
552
- plotter.add_mesh(
553
- mesh,
554
- color="#FFDAB9", # Peach Puff
555
- show_scalar_bar=False,
556
- show_edges=False,
557
- edge_color='grey',
558
- line_width=0.5,
559
- )
560
-
561
- # If edges are valid, add a black filled rectangle for the hole area
562
- if edges is not None:
563
- xL, xR, yB, yT = edges
564
- z0 = -1e-6 # Slightly below to avoid z-fighting at boundaries
565
- rect_pts = np.array([
566
- [xL, yB, z0],
567
- [xR, yB, z0],
568
- [xR, yT, z0],
569
- [xL, yT, z0],
570
- ], dtype=float)
571
- rect_faces = np.hstack([[4, 0, 1, 2, 3]])
572
- rect = pv.PolyData(rect_pts, rect_faces)
573
- plotter.add_mesh(rect, color="black", name="hole_overlay", pickable=False)
574
-
575
- # Grid and view setup
576
- plotter.show_grid(bounds=(0.0, 1.0, 0.0, 1.0, 0.0, 0.0), xtitle="x (0–1)", ytitle="y (0–1)", ztitle=" ", color="#AE8BD8")
577
- _add_dotted_unit_grid(plotter, ticks=(0.0, 0.25, 0.5, 0.75, 1.0), segments=48, gap_ratio=0.6, color="#AE8BD8", line_width=1)
578
- try:
579
- plotter.disable_picking()
580
- except Exception:
581
- pass
582
- plotter.enable_point_picking(callback=update_value_display, show_message=False)
583
- plotter.add_axes()
584
- plotter.view_isometric()
585
- try:
586
- plotter.camera.parallel_projection = True
587
- except Exception:
588
- pass
589
- ctrl.view_update()
590
-
591
- # Helper: map normalized [0,1] to nearest node index on an nx×ny grid
592
- def _nearest_node_index(x: float, y: float, nx: int, ny: int | None = None):
593
- ny = ny or nx
594
- i = int(round(float(x) * (nx - 1)))
595
- j = int(round(float(y) * (ny - 1)))
596
- i = max(0, min(nx - 1, i))
597
- j = max(0, min(ny - 1, j))
598
- return i, j
599
-
600
- def update_initial_state_preview():
601
- global current_mesh
602
- # Don't render any preview while running
603
- if state.is_running:
604
- plotter.clear(); ctrl.view_update(); return
605
- # If no geometry selected, clear and stop
606
- if not state.geometry_selection:
607
- plotter.clear(); ctrl.view_update(); return
608
- # Geometry-only previews before initial state selection
609
- if not state.simulation_has_run and not state.dist_type:
610
- if state.geometry_selection == "Square Domain":
611
- update_geometry_preview(); return
612
- if state.geometry_selection == "Square Metallic Body":
613
- update_geometry_hole_preview(); return
614
-
615
- plotter.clear()
616
- state.error_message = ""
617
- # Default to a high-resolution 128x128 preview grid
618
- preview_n = 128
619
- nx_sel = state.nx
620
- # Show grid edges only when a mesh size is selected
621
- show_grid_edges = nx_sel is not None
622
-
623
- try:
624
- grid_n = int(nx_sel) if nx_sel is not None else preview_n
625
-
626
- if state.dist_type == "Delta":
627
- ix, iy = _nearest_node_index(float(state.impulse_x), float(state.impulse_y), grid_n)
628
- full_state = create_impulse_state((grid_n, grid_n), (ix, iy))
629
- elif state.dist_type == "Gaussian":
630
- ix, iy = _nearest_node_index(float(state.mu_x), float(state.mu_y), grid_n)
631
- sx = max(float(state.sigma_x) * (grid_n - 1), 1e-9)
632
- sy = max(float(state.sigma_y) * (grid_n - 1), 1e-9)
633
- full_state = create_gaussian_state((grid_n, grid_n), (ix, iy), (sx, sy))
634
- else:
635
- return
636
-
637
- # Build preview mesh using the correct grid size
638
- initial_grid = full_state[: grid_n * grid_n].reshape(grid_n, grid_n)
639
- denom = float(max(grid_n - 1, 1))
640
- x, y = np.arange(grid_n) / denom, np.arange(grid_n) / denom
641
- X, Y = np.meshgrid(x, y)
642
- max_abs = float(np.max(np.abs(initial_grid))) if initial_grid.size else 1.0
643
- if max_abs < 1e-12:
644
- max_abs = 1.0
645
- height_scale = 0.15
646
- Z = (initial_grid / max_abs) * height_scale
647
- mesh = pv.StructuredGrid()
648
- mesh.points = np.c_[X.ravel(), Y.ravel(), Z.ravel()]
649
- mesh.dimensions = (grid_n, grid_n, 1)
650
- mesh['scalars'] = initial_grid.ravel()
651
- current_mesh = mesh
652
-
653
- plotter.add_mesh(
654
- mesh,
655
- scalars='scalars',
656
- cmap="Blues",
657
- show_scalar_bar=False,
658
- show_edges=show_grid_edges,
659
- edge_color='grey',
660
- line_width=0.5,
661
- )
662
- # No scalar bar, axes, or grid overlays in excitation preview; picking only
663
- try:
664
- plotter.disable_picking()
665
- except Exception:
666
- pass
667
- plotter.enable_point_picking(callback=update_value_display, show_message=False)
668
- plotter.view_isometric()
669
- try:
670
- plotter.camera.parallel_projection = True
671
- except Exception:
672
- pass
673
- ctrl.view_update()
674
-
675
- except ValueError as e:
676
- state.error_message = f"Parameter Error: {e}"
677
- except Exception as e:
678
- state.error_message = f"An unexpected error occurred: {e}"
679
-
680
- @state.change("geometry_selection")
681
- def handle_geometry_add(geometry_selection, **kwargs):
682
- # Normalize unselect options to None
683
- if geometry_selection in (None, "", "None"):
684
- state.geometry_selection = None
685
- update_initial_state_preview()
686
- return
687
- if (geometry_selection == "Add"):
688
- state.show_upload_dialog = True
689
- state.geometry_selection = None
690
- return
691
- # Update preview on any geometry change (e.g., show Square Domain flat mesh)
692
- update_initial_state_preview()
693
-
694
- @state.change("uploaded_file_info")
695
- def handle_file_upload(uploaded_file_info, **kwargs):
696
- if uploaded_file_info:
697
- file_name = uploaded_file_info.get("name", "unknown file")
698
- print(f"File selected (dummy upload): {file_name}")
699
- state.show_upload_dialog = False
700
- state.upload_status_message = f"File '{file_name}' uploaded."
701
- state.show_upload_status = True
702
-
703
- def update_excitation_info_message():
704
- """Calculates and displays the coordinate snapping message."""
705
- if state.nx is None or state.dist_type is None:
706
- state.excitation_info_message = ""
707
- return
708
-
709
- try:
710
- nx = int(state.nx)
711
- denom = float(max(nx - 1, 1))
712
-
713
- if state.dist_type == "Delta":
714
- x_in, y_in = float(state.impulse_x), float(state.impulse_y)
715
- elif state.dist_type == "Gaussian":
716
- x_in, y_in = float(state.mu_x), float(state.mu_y)
717
- else:
718
- state.excitation_info_message = ""
719
- return
720
-
721
- ix, iy = _nearest_node_index(x_in, y_in, nx)
722
- x_snapped, y_snapped = ix / denom, iy / denom
723
-
724
- if abs(x_in - x_snapped) > 1e-9 or abs(y_in - y_snapped) > 1e-9:
725
- state.excitation_info_message = f"Input ({x_in:.3f}, {y_in:.3f}) adjusted to nearest grid point ({x_snapped:.3f}, {y_snapped:.3f})."
726
- else:
727
- state.excitation_info_message = ""
728
- except Exception:
729
- state.excitation_info_message = ""
730
-
731
- @state.change("nx_slider_index")
732
- def on_slider_index_change(nx_slider_index, **kwargs):
733
- if nx_slider_index is None:
734
- state.nx = None
735
- else:
736
- try:
737
- state.nx = int(GRID_SIZES[int(nx_slider_index)])
738
- except Exception:
739
- state.nx = None
740
- update_excitation_info_message()
741
-
742
- @state.change("nx", "T", "dist_type", "impulse_x", "impulse_y", "mu_x", "mu_y", "sigma_x", "sigma_y", "coeff_permittivity", "coeff_permeability")
743
- def on_input_parameter_change(**kwargs):
744
- # Do nothing while running
745
- if state.is_running:
746
- return
747
-
748
- update_excitation_info_message()
749
-
750
- changed_keys = set(kwargs.keys())
751
-
752
- # If a simulation has already run, keep current results and only indicate re-run is needed
753
- if state.simulation_has_run:
754
- state.run_button_text = "Re-run Simulation"
755
- return
756
-
757
- # Before a run, update the initial preview only when relevant preview params changed
758
- preview_params = {"nx", "dist_type", "impulse_x", "impulse_y", "mu_x", "mu_y", "sigma_x", "sigma_y"}
759
- if changed_keys & preview_params:
760
- update_initial_state_preview()
761
-
762
- @state.change("output_type", "timeseries_field", "timeseries_points")
763
- def on_output_config_change(**kwargs):
764
- if state.simulation_has_run:
765
- generate_plot()
766
-
767
- @state.change("surface_field")
768
- def on_surface_field_change(surface_field, **kwargs):
769
- if state.simulation_has_run and state.output_type == "Surface Plot":
770
- redraw_surface_plot()
771
-
772
- @state.change("time_val")
773
- def on_time_change(time_val, **kwargs):
774
- if not state.simulation_has_run or state.output_type != "Surface Plot" or current_mesh is None or data_frames is None:
775
- return
776
- if snapshot_times is None or len(snapshot_times) == 0:
777
- return
778
- field = state.surface_field
779
- # Find nearest snapshot index to requested time and clamp to available frames
780
- times = np.asarray(snapshot_times)
781
- idx = int(np.argmin(np.abs(times - float(time_val))))
782
- max_idx = len(data_frames[field]) - 1
783
- idx = max(0, min(idx, max_idx))
784
- z_data = data_frames[field][idx]
785
- if current_mesh.n_points == z_data.size:
786
- current_mesh.points[:, 2] = z_data.ravel() * z_scale
787
- current_mesh['scalars'] = z_data.ravel()
788
- ctrl.view_update()
789
- else:
790
- redraw_surface_plot()
791
-
792
- def update_value_display(point):
793
- if current_mesh is None:
794
- return
795
- try:
796
- plotter.remove_actor("value_text")
797
- except Exception:
798
- pass
799
-
800
- closest_id = current_mesh.find_closest_point(point)
801
- if closest_id == -1:
802
- return
803
-
804
- # Sample value and coordinates at closest vertex
805
- value = current_mesh['scalars'][closest_id] if 'scalars' in current_mesh.array_names else 0.0
806
- px, py, pz = current_mesh.points[closest_id]
807
- px = float(px); py = float(py)
808
-
809
- # Determine if current mesh is on unit square [0,1] (initial preview/geometry) or integer grid (output plots)
810
- xmin, xmax, ymin, ymax, _, _ = current_mesh.bounds
811
- is_unit_square = (xmax <= 1.00001 and ymax <= 1.00001)
812
-
813
- if not state.simulation_has_run and is_unit_square:
814
- # Disable updating inputs based on point picking
815
- text = f"Position: ({px:.3f}, {py:.3f})\nValue: {value:.3e}"
816
- else:
817
- # Output configuration or integer-grid context: keep grid indices visible like app.py
818
- nx_val = int(state.nx)
819
- denom = max(float(nx_val - 1), 1.0)
820
- if is_unit_square:
821
- ix = int(round(px * denom)); iy = int(round(py * denom))
822
- x_code = max(0.0, min(1.0, px)); y_code = max(0.0, min(1.0, py))
823
- else:
824
- ix = int(round(px)); iy = int(round(py))
825
- x_code = max(0.0, min(1.0, px / denom)); y_code = max(0.0, min(1.0, py / denom))
826
- ix = max(0, min(ix, nx_val - 1)); iy = max(0, min(iy, nx_val - 1))
827
- if state.simulation_has_run:
828
- time = float(state.time_val)
829
- text = f"Index: ({ix}, {iy}) | Position: ({x_code:.3f}, {y_code:.3f})\nTime: {time:.2f}s\nValue: {value:.3e}"
830
- else:
831
- text = f"Index: ({ix}, {iy}) | Position: ({x_code:.3f}, {y_code:.3f})\nValue: {value:.3e}"
832
-
833
- plotter.add_text(text, name="value_text", position="lower_left", color="black", font_size=10)
834
- ctrl.view_update()
835
-
836
  try:
837
- plotter.disable_picking()
 
838
  except Exception:
839
- pass
840
- plotter.enable_point_picking(callback=update_value_display, show_message=False)
841
-
842
- def export_vtk():
843
- """Export current surface mesh to user's Downloads as .vtp and notify via snackbar."""
844
- global current_mesh
845
- if current_mesh is None:
846
- state.export_status_message = "No mesh to export."
847
- state.show_export_status = True
848
- return
849
- try:
850
- dl_dir = os.path.join(os.path.expanduser("~"), "Downloads")
851
- os.makedirs(dl_dir, exist_ok=True)
852
- field = state.surface_field or "Ez"
853
- nx = int(state.nx)
854
- suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
855
- path = os.path.join(dl_dir, f"surface_{field}_nx{nx}_{suffix}.vtp")
856
- current_mesh.save(path)
857
- state.export_status_message = f"Exported VTK to {path}"
858
- except Exception as e:
859
- state.export_status_message = f"Export failed: {e}"
860
- state.show_export_status = True
861
-
862
- def export_vtk_all_frames():
863
- """Export a .vtp file for each time frame of the selected component into a timestamped folder in Downloads."""
864
- global data_frames, X_grids, z_scale, snapshot_times
865
- try:
866
- if not state.simulation_has_run:
867
- raise ValueError("Run a simulation before exporting all frames.")
868
- field = state.surface_field or "Ez"
869
- frames = data_frames.get(field)
870
- if not frames:
871
- raise ValueError(f"No frames available for {field}.")
872
- if snapshot_times is None:
873
- raise ValueError("Snapshot times are unavailable.")
874
-
875
- dl_dir = os.path.join(os.path.expanduser("~"), "Downloads")
876
- suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
877
- nx = int(state.nx)
878
- out_dir = os.path.join(dl_dir, f"vtk_sequence_{field}_nx{nx}_{suffix}")
879
- os.makedirs(out_dir, exist_ok=True)
880
-
881
- times = np.asarray(snapshot_times)
882
- for i, (z_data, t) in enumerate(zip(frames, times)):
883
- points = np.c_[X_grids[field].ravel(), Y_grids[field].ravel(), z_data.ravel() * z_scale]
884
- poly = pv.PolyData(points)
885
- mesh = poly.delaunay_2d()
886
- mesh["scalars"] = z_data.ravel()
887
- fname = f"{field}_frame_{i:04d}_t{t:.3f}s.vtp"
888
- mesh.save(os.path.join(out_dir, fname))
889
-
890
- state.export_status_message = f"Exported {len(frames)} frames to {out_dir}"
891
- except Exception as e:
892
- state.export_status_message = f"Export failed: {e}"
893
- finally:
894
- state.show_export_status = True
895
-
896
- def export_mp4():
897
- """Export the surface plot time slider animation to MP4 using a dedicated off-screen plotter."""
898
- global data_frames
899
- try:
900
- if not state.simulation_has_run:
901
- raise ValueError("Run a simulation before exporting MP4.")
902
- field = state.surface_field or "Ez"
903
- frames = data_frames.get(field)
904
- if not frames:
905
- raise ValueError(f"No frames available for {field}.")
906
- if len(frames) < 2:
907
- raise ValueError("Only one frame available; increase T or simulation steps.")
908
-
909
- # Output path
910
- dl_dir = os.path.join(os.path.expanduser("~"), "Downloads")
911
- os.makedirs(dl_dir, exist_ok=True)
912
- nx = int(state.nx)
913
- suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
914
- path = os.path.join(dl_dir, f"surface_anim_{field}_nx{nx}_{suffix}.mp4")
915
-
916
- # Build with a dedicated off-screen plotter at a macro-block friendly size
917
- movie_plotter = pv.Plotter(off_screen=True, window_size=(1280, 720))
918
-
919
- # Initial mesh from first frame
920
- X = X_grids[field]
921
- Y = Y_grids[field]
922
- first = frames[0]
923
- points = np.c_[X.ravel(), Y.ravel(), first.ravel() * z_scale]
924
- poly = pv.PolyData(points)
925
- mesh = poly.delaunay_2d()
926
- mesh['scalars'] = first.ravel()
927
- actor = movie_plotter.add_mesh(
928
- mesh,
929
- scalars='scalars',
930
- clim=surface_clims[field],
931
- cmap="RdBu",
932
- show_scalar_bar=False,
933
- show_edges=True,
934
- edge_color='grey',
935
- line_width=0.5,
936
- )
937
- movie_plotter.add_axes()
938
- # Use similar camera if available, else default
939
- try:
940
- if hasattr(plotter, 'camera_position') and plotter.camera_position:
941
- movie_plotter.camera_position = plotter.camera_position
942
- else:
943
- movie_plotter.view_isometric()
944
- except Exception:
945
- movie_plotter.view_isometric()
946
-
947
- movie_plotter.open_movie(path, framerate=20)
948
- n_frames = len(frames)
949
- for z_data in frames:
950
- if mesh.n_points != z_data.size:
951
- # Rebuild mesh if topology changes (unlikely here)
952
- points = np.c_[X.ravel(), Y.ravel(), z_data.ravel() * z_scale]
953
- poly = pv.PolyData(points)
954
- mesh = poly.delaunay_2d()
955
- mesh['scalars'] = z_data.ravel()
956
- movie_plotter.clear()
957
- actor = movie_plotter.add_mesh(
958
- mesh,
959
- scalars='scalars',
960
- clim=surface_clims[field],
961
- cmap="RdBu",
962
- show_scalar_bar=False,
963
- show_edges=True,
964
- edge_color='grey',
965
- line_width=0.5,
966
- )
967
- else:
968
- mesh.points[:, 2] = z_data.ravel() * z_scale
969
- mesh['scalars'] = z_data.ravel()
970
- movie_plotter.render()
971
- movie_plotter.write_frame()
972
- movie_plotter.close()
973
 
974
- state.export_status_message = f"Exported MP4 to {path}"
975
- except Exception as e:
976
- state.export_status_message = f"Export failed: {e}"
977
- finally:
978
- state.show_export_status = True
979
-
980
- # --- Small Plot under Meshing: Qubit requirement vs Grid Size ---
981
- def build_qubit_plot(grid_size: int):
982
- x_sizes = np.array([16, 32, 64, 128, 256, 512])
983
- y_qubits = 2 * np.ceil(np.log2(x_sizes)).astype(int) + 3
984
- current_nq = int(2 * np.ceil(np.log2(max(1, int(grid_size)))) + 3)
985
-
986
- fig = go.Figure()
987
- # Match app.py: x = grid size, y = total qubits
988
- fig.add_trace(go.Scatter(x=x_sizes, y=y_qubits, mode='lines', name='Total Qubits', line=dict(color='#7A3DB5', width=3)))
989
- fig.add_trace(go.Scatter(x=[grid_size], y=[current_nq], mode='markers', marker=dict(size=10, color='#5F259F'), name='Current Selection'))
990
-
991
- x_min = int(x_sizes.min()); x_max = int(x_sizes.max())
992
- y_min = int(y_qubits.min()); y_max = int(max(y_qubits.max(), current_nq))
993
- fig.update_xaxes(range=[x_min - 8, x_max + 8], tickmode='array', tickvals=x_sizes, ticktext=[str(v) for v in x_sizes], title_text="Grid Size (nx)", gridcolor='rgba(95,37,159,0.1)', zerolinecolor='rgba(95,37,159,0.3)')
994
- fig.update_yaxes(range=[y_min - 1, y_max + 1], dtick=1, title_text="Total Qubits (nq)", gridcolor='rgba(95,37,159,0.1)', zerolinecolor='rgba(95,37,159,0.3)')
995
- fig.update_layout(
996
- margin=dict(l=30, r=10, t=10, b=30),
997
- autosize=True,
998
- legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1),
999
- font=dict(color='#1A1A1A'),
1000
- paper_bgcolor='#FFFFFF',
1001
- plot_bgcolor='#FFFFFF',
1002
- colorway=['#5F259F', '#7A3DB5', '#AE8BD8', '#5F259F'],
1003
- )
1004
- return fig
1005
-
1006
- # --- UI Layout ---
1007
  with SinglePageLayout(server) as layout:
1008
- layout.title.set_text("ELECTROMAGNETIC SCATTERING")
1009
- # Synopsys branding: primary purple + shades, white surface, dark text
1010
- layout.title.style = "color: #5f259f; font-weight: 600;"
1011
  layout.toolbar.classes = "pl-2 pr-1 py-1 elevation-0"
1012
  layout.toolbar.style = "background-color: #ffffff; border-bottom: 3px solid #5f259f;"
1013
- trame_html.Style(
1014
- """
1015
- :root{
1016
- --v-theme-primary: 95, 37, 159; /* #5F259F */
1017
- --v-theme-secondary: 122, 61, 181; /* #7A3DB5 */
1018
- --v-theme-accent: 174, 139, 216; /* #AE8BD8 */
1019
- --v-theme-surface: 255, 255, 255; /* #FFFFFF */
1020
- --v-theme-background: 255, 255, 255; /* #FFFFFF */
1021
- --v-theme-on-primary: 255, 255, 255; /* #FFFFFF */
1022
- --v-theme-on-surface: 26, 26, 26; /* #1A1A1A */
1023
- }
1024
- .syn-title{ color:#5f259f !important; }
1025
- .syn-border-bottom{ border-bottom:3px solid #5f259f !important; }
1026
- .syn-bg-white{ background:#ffffff !important; }
1027
- /* Synopsys UI refinements */
1028
- a, .syn-link { color: #5f259f; text-decoration: none; }
1029
- a:hover, .syn-link:hover { color: #7A3DB5; text-decoration: underline; }
1030
- .v-list .v-list-item:hover { background-color: rgba(95,37,159,.08) !important; }
1031
- .v-list-item--active { background-color: rgba(95,37,159,.16) !important; color: #5f259f !important; }
1032
- """
1033
- )
1034
  with layout.toolbar:
 
 
 
 
 
1035
  vuetify3.VSpacer()
1036
  vuetify3.VImg(
1037
  v_if="logo_src",
1038
  src=("logo_src", None),
1039
- style="height: 56px; width: auto; margin-right: 0px;",
1040
- classes="mr-0",
1041
  )
1042
- with layout.content:
1043
- with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
1044
- with vuetify3.VDialog(v_model=("show_upload_dialog", False), max_width="500px"):
1045
- with vuetify3.VCard():
1046
- vuetify3.VCardTitle("Upload Geometry")
1047
- with vuetify3.VCardText():
1048
- vuetify3.VFileInput(
1049
- show_size=True,
1050
- label="Select geometry file",
1051
- accept=".vtp,.vtk,.glb,.stl",
1052
- update_binary=("uploaded_file_info", 1),
1053
- )
1054
- with vuetify3.VCardActions():
1055
- vuetify3.VSpacer()
1056
- vuetify3.VBtn("Cancel", click="show_upload_dialog = false")
1057
-
1058
- vuetify3.VSnackbar(
1059
- v_model=("show_upload_status", False),
1060
- children=["{{ upload_status_message }}"],
1061
- timeout=4000,
1062
- location="bottom right",
1063
- color="primary",
1064
- variant="tonal",
1065
- )
1066
- vuetify3.VSnackbar(
1067
- v_model=("show_export_status", False),
1068
- children=["{{ export_status_message }}"],
1069
- timeout=4000,
1070
- location="bottom right",
1071
- color="primary",
1072
- variant="tonal",
1073
- )
1074
-
1075
- with vuetify3.VRow(no_gutters=True, classes="fill-height"):
1076
- with vuetify3.VCol(cols=5, classes="pa-4 d-flex flex-column"):
1077
- # Cell 1: Introduction
1078
- with vuetify3.VCard(classes="mb-4"):
1079
- with vuetify3.VCardTitle("Overview", classes="text-h5 font-weight-bold text-primary"):
1080
- pass
1081
- with vuetify3.VCardText():
1082
- # Removed subtitle and restyled sections
1083
- vuetify3.VDivider(classes="my-2")
1084
- vuetify3.VCardSubtitle("Problem", classes="text-subtitle-1 font-weight-bold mt-2")
1085
- vuetify3.VDivider(classes="mb-1")
1086
- vuetify3.VList(
1087
- density="compact",
1088
- lines="one",
1089
- items=(
1090
- "intro_items_problem",
1091
- [
1092
- {"title": "1. Propagation in a given medium (no bodies)"},
1093
- {"title": "2. Scattering from a perfectly conducting body"},
1094
- ],
1095
- ),
1096
- )
1097
- vuetify3.VCardSubtitle("Governing Equations", classes="text-subtitle-1 font-weight-bold mt-2")
1098
- vuetify3.VDivider(classes="mb-1")
1099
- vuetify3.VListItemTitle("Maxwell’s time-domain, 2D, TEz polarized.", classes="text-body-2")
1100
- vuetify3.VCardSubtitle("Inputs", classes="text-subtitle-1 font-weight-bold mt-2")
1101
- vuetify3.VDivider(classes="mb-1")
1102
- vuetify3.VListItemTitle("Geometry, excitation, medium, output visualization preferences.", classes="text-body-2")
1103
- vuetify3.VCardSubtitle("Outputs", classes="text-subtitle-1 font-weight-bold mt-2")
1104
- vuetify3.VDivider(classes="mb-1")
1105
- vuetify3.VListItemTitle("Surface plots of field components OR time evolution of field components at specified points.", classes="text-body-2")
1106
- # Cell 2: Geometry
1107
- with vuetify3.VCard(classes="mb-4"):
1108
- with vuetify3.VCardTitle("Geometry", classes="text-primary"):
1109
- pass
1110
- with vuetify3.VCardText():
1111
- vuetify3.VSelect(
1112
- label="Select",
1113
- v_model=("geometry_selection", None),
1114
- items=("geometry_options", ["None", "Square Metallic Body", "Square Domain", "Geometry 2", "Add"]),
1115
- placeholder="Select",
1116
- density="compact",
1117
- color="primary",
1118
- )
1119
- with vuetify3.VContainer(v_if="geometry_selection === 'Square Metallic Body'", classes="pa-0 mt-2"):
1120
- with vuetify3.VRow(dense=True):
1121
- with vuetify3.VCol():
1122
- with vuetify3.VTooltip("Square hole edge length s in domain units [0,1]. Must be ≤ 1. UI-only.", location="bottom", color="primary"):
1123
- with vuetify3.Template(v_slot_activator="{ props }"):
1124
- vuetify3.VTextField(
1125
- v_bind="props",
1126
- v_model=("hole_size_edge", 0.2),
1127
- label="Hole Edge Length [0 - 1]",
1128
- type="number",
1129
- step=0.05,
1130
- min=0,
1131
- density="compact",
1132
- color="primary",
1133
- )
1134
- with vuetify3.VCol():
1135
- with vuetify3.VTooltip("Hole center as (x, y). Both x and y must be strictly within (0,1). Example: (0.5, 0.5). UI-only.", location="bottom", color="primary"):
1136
- with vuetify3.Template(v_slot_activator="{ props }"):
1137
- vuetify3.VTextField(
1138
- v_bind="props",
1139
- v_model=("hole_center_pair", "(0.5, 0.5)"),
1140
- label="Hole Center (X, Y)",
1141
- density="compact",
1142
- color="primary",
1143
- )
1144
- with vuetify3.VRow(dense=True, classes="mt-1"):
1145
- with vuetify3.VCol(cols=12):
1146
- vuetify3.VSwitch(
1147
- v_model=("hole_snap", True),
1148
- label="Snap edges to nearest grid lines",
1149
- color="primary",
1150
- inset=True,
1151
- density="compact",
1152
- )
1153
- vuetify3.VAlert(
1154
- v_if="hole_error_message",
1155
- type="error",
1156
- variant="tonal",
1157
- density="compact",
1158
- children=["{{ hole_error_message }}"],
1159
- classes="mt-2",
1160
- )
1161
- # Restoring all subsequent input cards
1162
- # Cell 4: Excitation
1163
- with vuetify3.VCard(classes="mb-4"):
1164
- with vuetify3.VCardTitle("Excitation: Initial State", classes="text-primary"):
1165
- pass
1166
- with vuetify3.VCardText():
1167
- vuetify3.VSelect(
1168
- label="Select",
1169
- v_model=("dist_type", None),
1170
- items=("dist_type_options", ["None", "Delta", "Gaussian"]),
1171
- placeholder="Select",
1172
- density="compact",
1173
- color="primary",
1174
- )
1175
- with vuetify3.VContainer(v_if="dist_type === 'Delta'", classes="pa-0"):
1176
- with vuetify3.VTooltip("Impulse position (x, y) in [0,1]. Example: (0.6, 0.6).", location="bottom", color="primary"):
1177
- with vuetify3.Template(v_slot_activator="{ props }"):
1178
- vuetify3.VTextField(v_bind="props", v_model=("peak_pair", "(0.5, 0.5)"), label="Peak (x, y) in [0,1]", density="compact", color="primary")
1179
- with vuetify3.VContainer(v_if="dist_type === 'Gaussian'", classes="pa-0"):
1180
- with vuetify3.VRow(dense=True):
1181
- with vuetify3.VCol():
1182
- with vuetify3.VTooltip("Gaussian center μ (x, y) in [0,1]. Example: (0.5, 0.5).", location="bottom", color="primary"):
1183
- with vuetify3.Template(v_slot_activator="{ props }"):
1184
- vuetify3.VTextField(v_bind="props", v_model=("mu_pair", "(0.5, 0.5)"), label="Mu (x, y) in [0,1]", density="compact", color="primary")
1185
- # Separate Sigma inputs (normalized)
1186
- with vuetify3.VRow(dense=True, classes="mt-1"):
1187
- with vuetify3.VCol():
1188
- with vuetify3.VTooltip("Gaussian spread σx in [0,1] of domain length.", location="bottom", color="primary"):
1189
- with vuetify3.Template(v_slot_activator="{ props }"):
1190
- vuetify3.VTextField(v_bind="props", v_model=("sigma_x", 0.25), label="Sigma X (0–1)", type="number", step="0.01", density="compact", color="primary")
1191
- with vuetify3.VCol():
1192
- with vuetify3.VTooltip("Gaussian spread σy in [0,1] of domain length.", location="bottom", color="primary"):
1193
- with vuetify3.Template(v_slot_activator="{ props }"):
1194
- vuetify3.VTextField(v_bind="props", v_model=("sigma_y", 0.15), label="Sigma Y (0–1)", type="number", step="0.01", density="compact", color="primary")
1195
- vuetify3.VAlert(v_if="excitation_error_message", type="error", variant="tonal", density="compact", children=["{{ excitation_error_message }}"], classes="mt-2")
1196
- vuetify3.VAlert(
1197
- v_if="excitation_info_message",
1198
- type="info",
1199
- variant="tonal",
1200
- density="compact",
1201
- children=["{{ excitation_info_message }}"],
1202
- classes="mt-2",
1203
- )
1204
-
1205
- # Cell 5: Medium
1206
- with vuetify3.VCard(classes="mb-4"):
1207
- with vuetify3.VCardTitle("Material Properties (Medium)", classes="text-primary"):
1208
- pass
1209
- with vuetify3.VCardText():
1210
- with vuetify3.VRow(dense=True):
1211
- with vuetify3.VCol():
1212
- with vuetify3.VTooltip("Relative permittivity. ε_r = ε / ε₀. Default 1.0 (free space).", location="bottom", color="primary"):
1213
- with vuetify3.Template(v_slot_activator="{ props }"):
1214
- vuetify3.VTextField(v_bind="props", v_model=("coeff_permittivity", 1.0), label="Relative permittivity (ε_r)", type="number", density="compact", color="primary")
1215
- with vuetify3.VCol():
1216
- with vuetify3.VTooltip("Relative permeability. μ_r = μ / μ₀. Typically 1.0 for non-magnetic materials.", location="bottom", color="primary"):
1217
- with vuetify3.Template(v_slot_activator="{ props }"):
1218
- vuetify3.VTextField(v_bind="props", v_model=("coeff_permeability", 1.0), label="Relative permeability (μ_r)", type="number", density="compact", color="primary")
1219
-
1220
- # New Cell: Temporal Settings (moved here)
1221
- with vuetify3.VCard(classes="mb-4"):
1222
- with vuetify3.VCardTitle("Temporal Settings", classes="text-primary"):
1223
- pass
1224
- with vuetify3.VCardText():
1225
- with vuetify3.VTooltip("Sets the total duration for the simulation to run.", location="bottom", color="primary"):
1226
- with vuetify3.Template(v_slot_activator="{ props }"):
1227
- vuetify3.VTextField(v_bind="props", v_model=("T", 1.0), label="Total Time (T)", type="number", step="0.1", density="compact", color="primary")
1228
- # Small bold heading before Δt input
1229
- vuetify3.VCardSubtitle("Select Δt intervals for plotting output snapshots", classes="text-subtitle-2 font-weight-bold mt-2")
1230
- with vuetify3.VTooltip("Snapshot interval (Δt). Solver runs at fixed 0.1 s; frames are saved every Δt. Values < 0.1 or not multiples of 0.1 are unsupported.", location="bottom", color="primary"):
1231
- with vuetify3.Template(v_slot_activator="{ props }"):
1232
- vuetify3.VTextField(v_bind="props", v_model=("dt_user", 0.1), label="Δt", type="number", step="0.1", density="compact", color="primary", classes="mt-2")
1233
- vuetify3.VAlert(v_if="temporal_warning", type="warning", variant="tonal", density="compact", children=["{{ temporal_warning }}"], classes="mt-2")
1234
- # Moved Meshing card: now under Temporal Settings and before Backends
1235
- with vuetify3.VCard(classes="mb-4"):
1236
- with vuetify3.VCardTitle("Meshing", classes="text-primary"):
1237
- pass
1238
- with vuetify3.VCardText():
1239
- # Show the qubit graph only while hovering over the slider (like app.py)
1240
- with vuetify3.VMenu(open_on_hover=True, close_on_content_click=False, location="end", offset=8):
1241
- with vuetify3.Template(v_slot_activator="{ props }"):
1242
- with vuetify3.VSlider(
1243
- v_bind="props",
1244
- v_model=("nx_slider_index", None),
1245
- label="No. of points per direction:",
1246
- min=0,
1247
- max=5,
1248
- step=1,
1249
- show_ticks="always",
1250
- thumb_label="always",
1251
- density="compact",
1252
- color="primary",
1253
- ):
1254
- vuetify3.Template(v_slot_thumb_label="{ modelValue }", children=["{{ modelValue === null ? 'Select' : [16, 32, 64, 128, 256, 512][modelValue] }}"])
1255
- # Hover content: enlarged Plotly graph with app.py axes (x=nx, y=nq)
1256
- with vuetify3.VSheet(classes="pa-2", elevation=6, rounded=True, style="width: 644px;"):
1257
- qubit_fig_widget = plotly_widgets.Figure(
1258
- figure=build_qubit_plot(int(state.nx or 16)),
1259
- responsive=True,
1260
- style="width: 616px; height: 364px; min-height: 364px;",
1261
- )
1262
- # Cell: Backends (from app.py)
1263
- with vuetify3.VCard(classes="mb-4"):
1264
- with vuetify3.VCardTitle("Backends", classes="text-primary"):
1265
- pass
1266
- with vuetify3.VCardText():
1267
- with vuetify3.VRow(dense=True, classes="mb-2"):
1268
- with vuetify3.VCol():
1269
- vuetify3.VAlert(
1270
- type="info",
1271
- color="primary",
1272
- variant="tonal",
1273
- density="compact",
1274
- children=[
1275
- "Selected: ",
1276
- "{{ backend_type || '—' }}",
1277
- " - ",
1278
- "{{ backend_type === 'Simulator' ? selected_simulator : (backend_type === 'QPU' ? selected_qpu : '—') }}",
1279
- ],
1280
- )
1281
- with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"):
1282
- with vuetify3.Template(v_slot_activator="{ props }"):
1283
- vuetify3.VBtn(v_bind="props", text="Choose Backend", color="primary", variant="tonal", block=True)
1284
- with vuetify3.VList(density="compact"):
1285
- with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8):
1286
- with vuetify3.Template(v_slot_activator="{ props }"):
1287
- vuetify3.VListItem(v_bind="props", title="Simulator", prepend_icon="mdi-robot-outline", append_icon="mdi-chevron-right")
1288
- with vuetify3.VList(density="compact"):
1289
- vuetify3.VListItem(title="IBM Qiskit simulator", click="backend_type = 'Simulator'; selected_simulator = 'IBM Qiskit simulator'")
1290
- vuetify3.VListItem(title="IonQ simulator", click="backend_type = 'Simulator'; selected_simulator = 'IonQ simulator'")
1291
- with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8):
1292
- with vuetify3.Template(v_slot_activator="{ props }"):
1293
- vuetify3.VListItem(v_bind="props", title="QPU", prepend_icon="mdi-chip", append_icon="mdi-chevron-right")
1294
- with vuetify3.VList(density="compact"):
1295
- vuetify3.VListItem(title="IBM QPU", click="backend_type = 'QPU'; selected_qpu = 'IBM QPU'")
1296
- vuetify3.VListItem(title="IonQ QPU", click="backend_type = 'QPU'; selected_qpu = 'IonQ QPU'")
1297
-
1298
- # Run Simulation and Stop Buttons Row
1299
- with vuetify3.VRow(dense=True, classes="mb-2"):
1300
- with vuetify3.VCol(cols=9):
1301
- with vuetify3.VTooltip("Starts the quantum simulation with the specified parameters. This may take some time.", location="bottom", color="primary"):
1302
- with vuetify3.Template(v_slot_activator="{ props }"):
1303
- vuetify3.VBtn(
1304
- v_bind="props",
1305
- text=("run_button_text", "Run Simulation"),
1306
- click=run_simulation_only,
1307
- color="primary",
1308
- block=True,
1309
- disabled=("is_running || run_button_text === 'Successful!' || !geometry_selection || !dist_type || !!temporal_warning || nx === null", False),
1310
- )
1311
- with vuetify3.VCol(cols=3):
1312
- with vuetify3.VTooltip("Stop the running simulation", location="bottom", color="primary"):
1313
- with vuetify3.Template(v_slot_activator="{ props }"):
1314
- vuetify3.VBtn(
1315
- v_bind="props",
1316
- text="Stop",
1317
- click=stop_simulation_handler,
1318
- color="error",
1319
- block=True,
1320
- disabled=("stop_button_disabled", True),
1321
- )
1322
-
1323
- # Reset Button
1324
- with vuetify3.VTooltip("Reset all parameters to their default values", location="bottom", color="primary"):
1325
- with vuetify3.Template(v_slot_activator="{ props }"):
1326
- vuetify3.VBtn(
1327
- v_bind="props",
1328
- text="Reset",
1329
- click=reset_to_defaults,
1330
- color="secondary",
1331
- block=True,
1332
- classes="mt-auto"
1333
- )
1334
-
1335
- # Main graph column
1336
- with vuetify3.VCol(cols=7, classes="pa-4 d-flex flex-column"):
1337
- # Output Configuration (appears after simulation)
1338
- with vuetify3.VCard(v_if="simulation_has_run", classes="mb-4"):
1339
- with vuetify3.VCardSubtitle("Output Configuration", classes="text-primary"):
1340
- with vuetify3.VCardText():
1341
- with vuetify3.VRadioGroup(v_model=("output_type", "Surface Plot"), row=True, density="compact", color="primary"):
1342
- vuetify3.VRadio(label="Surface", value="Surface Plot")
1343
- vuetify3.VRadio(label="Time Series", value="Time Series Plot")
1344
- with vuetify3.VContainer(v_if="output_type === 'Surface Plot'", classes="pa-0"):
1345
- vuetify3.VSelect(v_model=("surface_field", "Ez"), items=("surface_field_options", ["Ez", "Hx", "Hy"]), label="Field Component", density="compact", color="primary")
1346
- # Replace export format dropdown and individual buttons with a single Export menu
1347
- with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"):
1348
- with vuetify3.Template(v_slot_activator="{ props }"):
1349
- vuetify3.VBtn(v_bind="props", text="Export", color="primary", variant="tonal", block=True, classes="mt-2")
1350
- with vuetify3.VList(density="compact"):
1351
- vuetify3.VListSubheader("VTK")
1352
- vuetify3.VListItem(title="Current frame (VTK)", prepend_icon="mdi-download", click=export_vtk)
1353
- vuetify3.VListItem(title="All frames (VTK sequence)", prepend_icon="mdi-download-multiple", click=export_vtk_all_frames)
1354
- vuetify3.VDivider()
1355
- vuetify3.VListItem(title="Animation (MP4)", prepend_icon="mdi-movie", click=export_mp4)
1356
- with vuetify3.VContainer(v_if="output_type === 'Time Series Plot'", classes="pa-0"):
1357
- vuetify3.VSelect(v_model=("timeseries_field", "Ez"), items=("timeseries_field_options", ["Ez", "Hx", "Hy"]), label="Field Component", density="compact", color="primary")
1358
- vuetify3.VTextarea(v_model=("timeseries_points", "(8, 8), (10, 8)"), label="Monitor Points", hint="e.g., (8, 8), (10, 8)", rows=2, auto_grow=True, color="primary")
1359
-
1360
- # Main plot area (hidden until geometry selected)
1361
- with vuetify3.VCard(v_if="geometry_selection", classes="flex-grow-1", style="min-height: 0;"):
1362
- with vuetify3.VContainer(v_if="is_running", fluid=True, classes="fill-height d-flex flex-column align-center justify-center"):
1363
- vuetify3.VProgressCircular(indeterminate=True, size=64, color="primary")
1364
- vuetify3.VCardSubtitle("Running simulation...", classes="mt-4")
1365
- with vuetify3.VContainer(v_if="!is_running", fluid=True, classes="fill-height pa-0", style=("pyvista_view_style", "aspect-ratio: 1 / 1; width: 100%;")):
1366
- view = plotter_ui(plotter)
1367
- ctrl.view_update = view.update
1368
-
1369
- # Placeholder when no geometry selected
1370
- with vuetify3.VContainer(v_if="!geometry_selection", fluid=True, classes="flex-grow-1 d-flex align-center justify-center text-medium-emphasis"):
1371
- vuetify3.VCardText("Select a geometry to display the preview and results.")
1372
-
1373
- # Time slider for surface plot (restored to right panel)
1374
- with vuetify3.VContainer(v_if="simulation_has_run && output_type === 'Surface Plot'", fluid=True, classes="pa-0 mt-4"):
1375
- vuetify3.VSlider(v_model=("time_val", 0.0), label="Time", min=0, max=("T", 10.0), step=("dt_user", 0.1), thumb_label="always", density="compact", color="primary")
1376
-
1377
- # Store the widget's update method in the controller for later updates
1378
- ctrl.qubit_plot_update = qubit_fig_widget.update
1379
-
1380
- @state.change("nx")
1381
- def update_qubit_plot(nx, **kwargs):
1382
- try:
1383
- ctrl.qubit_plot_update(build_qubit_plot(int(nx)))
1384
- except Exception:
1385
- pass
1386
-
1387
- @state.change("hole_size_edge", "hole_center_x", "hole_center_y", "geometry_selection", "hole_snap")
1388
- def validate_hole_inputs(**kwargs):
1389
- # Only validate when Square Metallic Body is selected
1390
- if state.geometry_selection != "Square Metallic Body":
1391
- state.hole_error_message = ""
1392
- return
1393
- try:
1394
- s = float(state.hole_size_edge)
1395
- cx = float(state.hole_center_x)
1396
- cy = float(state.hole_center_y)
1397
- except Exception:
1398
- state.hole_error_message = "Hole size and center must be numeric."
1399
- return
1400
-
1401
- # Use selected nx, fall back to a safe default when not selected yet
1402
- try:
1403
- nx = int(state.nx or 32)
1404
- except Exception:
1405
- nx = 32
1406
-
1407
- if s > 1.0:
1408
- state.hole_error_message = "Hole edge length must be <= 1."
1409
- return
1410
- if not (0.0 < cx < 1.0) or not (0.0 < cy < 1.0):
1411
- state.hole_error_message = "Hole center must be strictly within (0, 1) for both X and Y."
1412
- return
1413
-
1414
- # Alignment check (strict vs snap)
1415
- mode_snap = bool(state.hole_snap)
1416
- edges = _compute_hole_edges(nx, cx, cy, s, snap=mode_snap)
1417
- if edges is None and not mode_snap:
1418
- h = _grid_spacing(nx)
1419
- state.hole_error_message = f"Strict alignment failed: edges must lie on grid lines k·h, h=1/(nx-1) ≈ {h:.4f}."
1420
- return
1421
-
1422
- state.hole_error_message = ""
1423
-
1424
- # Refresh preview if applicable
1425
- if state.geometry_selection == "Square Metallic Body" and not state.is_running and not state.simulation_has_run:
1426
- update_geometry_hole_preview()
1427
-
1428
- def _grid_spacing(nx: int) -> float:
1429
- return 1.0 / float(max(int(nx) - 1, 1))
1430
-
1431
-
1432
- def _is_on_grid(val: float, h: float, atol: float = 1e-9) -> bool:
1433
- r = val / h
1434
- return abs(r - round(r)) <= atol
1435
-
1436
-
1437
- def _snap_to_grid(val: float, h: float) -> float:
1438
- return round(val / h) * h
1439
-
1440
-
1441
- def _compute_hole_edges(nx: int, cx: float, cy: float, a: float, snap: bool, atol: float = 1e-9):
1442
- """
1443
- Build requested open rectangle edges from center (cx, cy) and size a, then either
1444
- validate (strict) or quantize (snap) to grid P = {k*h} with h = 1/(nx-1).
1445
- Returns (xL, xR, yB, yT) or None if strict alignment fails.
1446
- """
1447
- h = _grid_spacing(nx)
1448
- # Requested edges
1449
- xL_req, xR_req = cx - a / 2.0, cx + a / 2.0
1450
- yB_req, yT_req = cy - a / 2.0, cy + a / 2.0
1451
-
1452
- # Keep edges within (0,1) for numerical stability; open set semantics unchanged
1453
- eps = 1e-12
1454
- xL_req = max(eps, min(1.0 - eps, xL_req))
1455
- xR_req = max(eps, min(1.0 - eps, xR_req))
1456
- yB_req = max(eps, min(1.0 - eps, yB_req))
1457
- yT_req = max(eps, min(1.0 - eps, yT_req))
1458
-
1459
- if not snap:
1460
- ok = all(_is_on_grid(v, h, atol) for v in (xL_req, xR_req, yB_req, yT_req))
1461
- if not ok:
1462
- return None
1463
- return xL_req, xR_req, yB_req, yT_req
1464
-
1465
- # Snap each edge independently
1466
- xL, xR = _snap_to_grid(xL_req, h), _snap_to_grid(xR_req, h)
1467
- yB, yT = _snap_to_grid(yB_req, h), _snap_to_grid(yT_req, h)
1468
-
1469
- # Ensure proper ordering and minimum width/height (at least one cell)
1470
- if xL >= xR:
1471
- cx_idx = round(cx / h)
1472
- xL = max(h, (cx_idx - 1) * h)
1473
- xR = min(1.0 - h, (cx_idx + 1) * h)
1474
- if yB >= yT:
1475
- cy_idx = round(cy / h)
1476
- yB = max(h, (cy_idx - 1) * h)
1477
- yT = min(1.0 - h, (cy_idx + 1) * h)
1478
-
1479
- # Clamp into (0,1)
1480
- xL = max(eps, min(1.0 - eps, xL))
1481
- xR = max(eps, min(1.0 - eps, xR))
1482
- yB = max(eps, min(1.0 - eps, yB))
1483
- yT = max(eps, min(1.0 - eps, yT))
1484
-
1485
- if xL < xR and yB < yT:
1486
- return xL, xR, yB, yT
1487
- return None
1488
-
1489
- @state.change("hole_center_pair")
1490
- def sync_hole_center_pair(hole_center_pair, **kwargs):
1491
- """Parse bracket-format pair (x, y) from dropdown into numeric center fields."""
1492
- try:
1493
- m = re.match(r"\(\s*([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*\)", str(hole_center_pair))
1494
- if not m:
1495
- raise ValueError("Invalid format")
1496
- state.hole_center_x = float(m.group(1))
1497
- state.hole_center_y = float(m.group(2))
1498
- # Defer range validation to validate_hole_inputs
1499
- except Exception:
1500
- state.hole_error_message = "Invalid hole center. Use format (x, y)."
1501
-
1502
- @state.change("sigma_pair")
1503
- def sync_sigma_pair(sigma_pair, **kwargs):
1504
- """Parse bracket-format pair (x, y) for Sigma and update sigma_x/sigma_y."""
1505
- try:
1506
- m = re.match(r"\(\s*([-+]?[0-9]*\.?[0-9]+)\s*,\s*([-+]?[0-9]*\.?[0-9]+)\s*\)", str(sigma_pair))
1507
- if not m:
1508
- raise ValueError("Invalid format")
1509
- x = max(0.0, min(1.0, float(m.group(1))))
1510
- y = max(0.0, min(1.0, float(m.group(2))))
1511
- state.sigma_x = x
1512
- state.sigma_y = y
1513
- state.excitation_error_message = ""
1514
- except Exception:
1515
- state.excitation_error_message = "Invalid Sigma. Use format (x, y) in [0,1]."
1516
-
1517
- @state.change("dist_type")
1518
- def normalize_dist_type(dist_type, **kwargs):
1519
- # Allow unselecting via 'None'
1520
- if dist_type in (None, "", "None"):
1521
- state.dist_type = None
1522
- update_initial_state_preview()
1523
- update_excitation_info_message()
1524
-
1525
- @state.change("dt_user")
1526
- def validate_dt_user(dt_user, **kwargs):
1527
- """Validate snapshot Δt: must be >= 0.1 (solver dt) and a multiple of 0.1."""
1528
- try:
1529
- dt_val = float(dt_user)
1530
- except Exception:
1531
- state.temporal_warning = "Δt must be numeric. Frames are captured every Δt."
1532
- return
1533
- tol = 1e-9
1534
- if dt_val < 0.1 - tol:
1535
- state.temporal_warning = "Δt < 0.1 is unsupported (solver dt = 0.1 s)."
1536
- elif abs((dt_val / 0.1) - round(dt_val / 0.1)) > 1e-9:
1537
- state.temporal_warning = "Δt must be a multiple of 0.1 s."
1538
- else:
1539
- state.temporal_warning = ""
1540
-
1541
- # --- Initial Setup Call ---
1542
- update_initial_state_preview()
1543
-
1544
- server.start()
1545
 
 
 
 
 
 
 
 
 
 
 
1546
 
1547
 
 
 
 
 
 
 
 
 
1
  from trame.app import get_server
2
  from trame_vuetify.ui.vuetify3 import SinglePageLayout
3
  from trame_vuetify.widgets import vuetify3
 
 
 
 
 
4
  from trame.widgets import html as trame_html
5
+ import os
6
 
7
+ # Import pages
8
+ from pages import em_page
9
+ import qlbm as qlbm_page
 
 
 
 
10
 
11
+ # Create a single server for the multipage app
12
  server = get_server()
13
  state, ctrl = server.state, server.controller
14
 
15
+ # App state: track which page is active
16
+ state.current_page = "EM" # or "QLBM"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ # Load Synopsys/Ansys logo as data URI for main toolbar
19
+ import base64
20
 
21
+ def _load_logo_data_uri():
 
22
  base_dir = os.path.dirname(__file__)
23
  candidates = [
24
+ os.path.join(base_dir, "ansys-part-of-synopsys-logo.svg"),
25
  os.path.join(base_dir, "synopsys-logo-color-rgb.svg"),
26
  os.path.join(base_dir, "synopsys-logo-color-rgb.png"),
27
  os.path.join(base_dir, "synopsys-logo-color-rgb.jpg"),
 
35
  return f"data:{mime};base64,{b64}"
36
  return None
37
 
38
+ # Safely initialize logo in state (trame state isn't a dict; avoid .get())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  try:
40
+ if not hasattr(state, "logo_src") or state.logo_src in (None, ""):
41
+ state.logo_src = _load_logo_data_uri()
42
  except Exception:
43
+ state.logo_src = _load_logo_data_uri()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  with SinglePageLayout(server) as layout:
46
+ layout.title.set_text("Quantum Applications")
 
 
47
  layout.toolbar.classes = "pl-2 pr-1 py-1 elevation-0"
48
  layout.toolbar.style = "background-color: #ffffff; border-bottom: 3px solid #5f259f;"
49
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  with layout.toolbar:
51
+ # Simple navigation with tabs
52
+ with vuetify3.VTabs(v_model=("current_page", "EM"), grow=True, density="compact", color="primary"):
53
+ vuetify3.VTab(value="EM", children=["EM Scattering"])
54
+ vuetify3.VTab(value="QLBM", children=["Fluids"]) # renamed label only
55
+ # Right-aligned logo
56
  vuetify3.VSpacer()
57
  vuetify3.VImg(
58
  v_if="logo_src",
59
  src=("logo_src", None),
60
+ style="height: 40px; width: auto;", # slightly smaller than app bar height
61
+ classes="ml-2",
62
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ with layout.content:
65
+ # Mount EM page
66
+ with vuetify3.VContainer(v_if="current_page === 'EM'", fluid=True, classes="pa-0 fill-height"):
67
+ em_page.build(server)
68
+ # Mount QLBM page
69
+ with vuetify3.VContainer(v_if="current_page === 'QLBM'", fluid=True, classes="pa-0 fill-height"):
70
+ qlbm_page.build(server)
71
+
72
+ port = int(os.environ.get("PORT", "8080"))
73
+ server.start(address="0.0.0.0", port=port, open_browser=True)
74
 
75
 
em_trame.py ADDED
The diff for this file is too large to render. See raw diff
 
pages/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Make pages a package and re-export builders for convenience
2
+ from . import em_page, qlbm_page
3
+
4
+ __all__ = ["em_page", "qlbm_page"]
pages/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (311 Bytes). View file
 
pages/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (284 Bytes). View file
 
pages/__pycache__/em_page.cpython-311.pyc ADDED
Binary file (3.43 kB). View file
 
pages/__pycache__/em_page.cpython-313.pyc ADDED
Binary file (1.56 kB). View file
 
pages/__pycache__/qlbm_page.cpython-311.pyc ADDED
Binary file (18.3 kB). View file
 
pages/__pycache__/qlbm_page.cpython-313.pyc ADDED
Binary file (14.6 kB). View file
 
pages/em_page.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from trame_vuetify.widgets import vuetify3
2
+ from trame.widgets import html as trame_html
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import atexit
7
+
8
+ # Keep a single child process for the EM app
9
+ _em_proc = None
10
+
11
+
12
+ def _kill_em_process():
13
+ """Ensure any running EM process is terminated."""
14
+ global _em_proc
15
+ if _em_proc and _em_proc.poll() is None:
16
+ try:
17
+ _em_proc.terminate()
18
+ _em_proc.wait(timeout=2)
19
+ except Exception:
20
+ try:
21
+ _em_proc.kill()
22
+ except Exception:
23
+ pass
24
+ _em_proc = None
25
+
26
+
27
+ def _ensure_em_process_started():
28
+ global _em_proc
29
+ # Check if process is still running
30
+ if (_em_proc and _em_proc.poll() is None):
31
+ return
32
+
33
+ # Kill any stale process first
34
+ _kill_em_process()
35
+
36
+ base_dir = os.path.dirname(os.path.dirname(__file__))
37
+ em_path = os.path.join(base_dir, "em_trame.py")
38
+ env = os.environ.copy()
39
+ # Port used by iframe
40
+ env.setdefault("EM_APP_PORT", env.get("PORT_EM", "8701"))
41
+ # Start em_trame.py in a separate process
42
+ python_exe = sys.executable or "python"
43
+ _em_proc = subprocess.Popen([python_exe, em_path], cwd=base_dir, env=env)
44
+
45
+
46
+ # Register cleanup on exit
47
+ atexit.register(_kill_em_process)
48
+
49
+
50
+ def build(server):
51
+ """Render the EM app via iframe and ensure its process is running."""
52
+ _ensure_em_process_started()
53
+ port = os.environ.get("EM_APP_PORT", os.environ.get("PORT_EM", "8701"))
54
+ with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
55
+ trame_html.Iframe(
56
+ src=("em_iframe_src", f"http://localhost:{port}/"),
57
+ style="border:0; width:100%; height: calc(100vh - 64px);",
58
+ )
59
+ trame_html.Div(
60
+ "If the EM page is blank, wait a few seconds for the subprocess to start.",
61
+ style="color: rgba(0,0,0,.6); padding: 6px;",
62
+ )
pages/qlbm_page.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from trame_vuetify.widgets import vuetify3
2
+ from trame.widgets import html as trame_html
3
+ from trame_plotly.widgets import plotly as plotly_widgets
4
+ import plotly.graph_objects as go
5
+ import os
6
+
7
+ GRID_SIZES = [16, 32, 64, 128, 256, 512]
8
+
9
+
10
+ def _ensure_state_defaults(state):
11
+ try:
12
+ state.update({
13
+ "qlbm_initialized": True,
14
+ "logo_src": getattr(state, "logo_src", None),
15
+ "is_running": False,
16
+ "simulation_has_run": False,
17
+ "error_message": "",
18
+ "geometry_selection": getattr(state, "geometry_selection", None),
19
+ "lbm_dim": getattr(state, "lbm_dim", "2D"),
20
+ "domain_L": getattr(state, "domain_L", 1.0),
21
+ "domain_W": getattr(state, "domain_W", 1.0),
22
+ "domain_H": getattr(state, "domain_H", 1.0),
23
+ "dist_type": getattr(state, "dist_type", None),
24
+ "advecting_field": getattr(state, "advecting_field", None),
25
+ "inlet_velocity": getattr(state, "inlet_velocity", 1.0),
26
+ "inlet_temperature": getattr(state, "inlet_temperature", 300.0),
27
+ "nx_slider_index": getattr(state, "nx_slider_index", None),
28
+ "nx": getattr(state, "nx", None),
29
+ "output_type": getattr(state, "output_type", "Surface Plot"),
30
+ })
31
+ except Exception:
32
+ pass
33
+
34
+
35
+ def _build_placeholder_figure(state):
36
+ try:
37
+ if state.geometry_selection == "Rectangular domain with a heated box (2D/3D)" and str(state.lbm_dim) == "3D":
38
+ fig = go.Figure(data=[go.Scatter3d(x=[0, 1], y=[0, 1], z=[0, 1], mode="markers")])
39
+ fig.update_layout(height=560, margin=dict(l=10, r=10, t=30, b=10))
40
+ return fig
41
+ fig = go.Figure(data=go.Heatmap(z=[[0, 1], [1, 0]], colorscale="RdBu"))
42
+ fig.update_layout(height=560, margin=dict(l=10, r=10, t=30, b=10))
43
+ return fig
44
+ except Exception:
45
+ return go.Figure()
46
+
47
+
48
+ def build(server):
49
+ state, ctrl = server.state, server.controller
50
+ _ensure_state_defaults(state)
51
+
52
+ def run_simulation():
53
+ state.is_running = True
54
+ state.error_message = ""
55
+ try:
56
+ fig = _build_placeholder_figure(state)
57
+ try:
58
+ ctrl.qlbm_plot_update(fig)
59
+ except Exception:
60
+ pass
61
+ state.simulation_has_run = True
62
+ except Exception as e:
63
+ state.error_message = f"Run failed: {e}"
64
+ finally:
65
+ state.is_running = False
66
+
67
+ def reset_all():
68
+ try:
69
+ state.update({
70
+ "geometry_selection": None,
71
+ "lbm_dim": "2D",
72
+ "domain_L": 1.0,
73
+ "domain_W": 1.0,
74
+ "domain_H": 1.0,
75
+ "dist_type": None,
76
+ "advecting_field": None,
77
+ "inlet_velocity": 1.0,
78
+ "inlet_temperature": 300.0,
79
+ "nx_slider_index": None,
80
+ "nx": None,
81
+ "output_type": "Surface Plot",
82
+ "is_running": False,
83
+ "simulation_has_run": False,
84
+ "error_message": "",
85
+ })
86
+ try:
87
+ ctrl.qlbm_plot_update(go.Figure())
88
+ except Exception:
89
+ pass
90
+ except Exception:
91
+ pass
92
+
93
+ @state.change("nx_slider_index")
94
+ def _on_nx_index_change(nx_slider_index, **_):
95
+ try:
96
+ state.nx = GRID_SIZES[int(nx_slider_index)] if nx_slider_index is not None else None
97
+ except Exception:
98
+ state.nx = None
99
+
100
+ with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
101
+ with vuetify3.VRow(no_gutters=True, classes="fill-height"):
102
+ with vuetify3.VCol(cols=5, classes="pa-4 d-flex flex-column"):
103
+ with vuetify3.VCard(classes="mb-4"):
104
+ vuetify3.VCardTitle("Overview", classes="text-h5 font-weight-bold text-primary")
105
+ with vuetify3.VCardText():
106
+ vuetify3.VDivider(classes="my-2")
107
+ vuetify3.VCardSubtitle("Problems", classes="text-subtitle-1 font-weight-bold mt-2")
108
+ vuetify3.VList(density="compact", lines="one", items=(
109
+ "qlbm_problems",
110
+ [
111
+ {"title": "1. Scalar advection-diffusion in a box"},
112
+ {"title": "2. Laminar flow & heat transfer for a heated body in water."},
113
+ ],
114
+ ))
115
+ vuetify3.VCardSubtitle("Governing Equations", classes="text-subtitle-1 font-weight-bold mt-2")
116
+ vuetify3.VListItemTitle("Laminar Navier-Stokes including energy", classes="text-body-2")
117
+ vuetify3.VCardSubtitle("Inputs", classes="text-subtitle-1 font-weight-bold mt-2")
118
+ vuetify3.VListItemTitle("Geometry, Boundary conditions - temperature and flow", classes="text-body-2")
119
+ vuetify3.VCardSubtitle("Outputs", classes="text-subtitle-1 font-weight-bold mt-2")
120
+ vuetify3.VListItemTitle("Surface plots on sections OR sampling through a line in 3D domain", classes="text-body-2")
121
+
122
+ with vuetify3.VCard(classes="mb-4"):
123
+ vuetify3.VCardTitle("Geometry", classes="text-primary")
124
+ with vuetify3.VCardText():
125
+ vuetify3.VSelect(
126
+ label="Select",
127
+ v_model=("geometry_selection", None),
128
+ items=(
129
+ "geometry_options",
130
+ [
131
+ "None",
132
+ "Free space",
133
+ "Rectangular domain with a heated box (2D/3D)",
134
+ ],
135
+ ),
136
+ placeholder="Select",
137
+ density="compact",
138
+ color="primary",
139
+ )
140
+ with vuetify3.VContainer(v_if="geometry_selection === 'Rectangular domain with a heated box (2D/3D)'", classes="pa-0 mt-2"):
141
+ vuetify3.VRadioGroup(v_model=("lbm_dim", "2D"), row=True, density="compact", color="primary", children=[
142
+ vuetify3.VRadio(label="2D", value="2D"),
143
+ vuetify3.VRadio(label="3D", value="3D"),
144
+ ])
145
+ with vuetify3.VRow(dense=True):
146
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Length (L)", v_model=("domain_L", 1.0), type="number", step="0.1", density="compact", color="primary")])
147
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Width (W)", v_model=("domain_W", 1.0), type="number", step="0.1", density="compact", color="primary")])
148
+ vuetify3.VTextField(v_if="lbm_dim === '3D'", label="Height (H)", v_model=("domain_H", 1.0), type="number", step="0.1", density="compact", color="primary")
149
+
150
+ with vuetify3.VCard(v_if="geometry_selection === 'Rectangular domain with a heated box (2D/3D)'", classes="mb-4"):
151
+ vuetify3.VCardTitle("Initial & Boundary Conditions", classes="text-primary")
152
+ with vuetify3.VCardText():
153
+ vuetify3.VSelect(label="Initial Condition", v_model=("dist_type", None), items=("dist_type_opts", ["None", "Delta", "Gaussian"]), density="compact", color="primary")
154
+ vuetify3.VSelect(label="Advecting field", v_model=("advecting_field", None), items=("advect_fields", ["Uniform", "Swirl", "Shear", "TGV"]), density="compact", color="primary", classes="mt-2")
155
+ with vuetify3.VRow(dense=True, classes="mt-2"):
156
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Inlet flow velocity", v_model=("inlet_velocity", 1.0), type="number", step="0.1", density="compact", color="primary")])
157
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Inlet temperature (K)", v_model=("inlet_temperature", 300.0), type="number", step="1", density="compact", color="primary")])
158
+
159
+ with vuetify3.VCard(classes="mb-4"):
160
+ vuetify3.VCardTitle("Meshing", classes="text-primary")
161
+ with vuetify3.VCardText():
162
+ with vuetify3.VSlider(
163
+ v_model=("nx_slider_index", None),
164
+ label="No. of points per direction:",
165
+ min=0,
166
+ max=len(GRID_SIZES) - 1,
167
+ step=1,
168
+ show_ticks="always",
169
+ thumb_label="always",
170
+ density="compact",
171
+ color="primary",
172
+ ):
173
+ vuetify3.Template(v_slot_thumb_label="{ modelValue }", children=["{{ modelValue === null ? 'Select' : [16, 32, 64, 128, 256, 512][modelValue] }}"])
174
+
175
+ with vuetify3.VCard(classes="mb-4"):
176
+ vuetify3.VCardTitle("Backends", classes="text-primary")
177
+ with vuetify3.VCardText():
178
+ vuetify3.VAlert(type="info", color="primary", variant="tonal", density="compact", children=["Simulator only (placeholder)"])
179
+
180
+ with vuetify3.VRow(dense=True, classes="mb-2"):
181
+ vuetify3.VCol(children=[
182
+ vuetify3.VBtn(text=("Run Simulation"), color="primary", block=True, disabled=("is_running || !geometry_selection || (geometry_selection === 'Rectangular domain with a heated box (2D/3D)' && nx === null)", False), click=run_simulation)
183
+ ])
184
+ vuetify3.VCol(children=[
185
+ vuetify3.VBtn(text=("Reset"), color="secondary", variant="tonal", block=True, click=reset_all)
186
+ ])
187
+
188
+ with vuetify3.VCol(cols=7, classes="pa-4 d-flex flex-column"):
189
+ with vuetify3.VCard(v_if="simulation_has_run", classes="mb-4"):
190
+ vuetify3.VCardSubtitle("Output Configuration", classes="text-primary")
191
+ with vuetify3.VCardText():
192
+ with vuetify3.VRadioGroup(v_model=("output_type", "Surface Plot"), row=True, density="compact", color="primary"):
193
+ vuetify3.VRadio(label="Surface", value="Surface Plot")
194
+ vuetify3.VRadio(label="Line Sampling", value="Line Sampling")
195
+
196
+ with vuetify3.VCard(classes="flex-grow-1", style="min-height: 0;"):
197
+ with vuetify3.VContainer(v_if="is_running", fluid=True, classes="fill-height d-flex flex-column align-center justify-center"):
198
+ vuetify3.VProgressCircular(indeterminate=True, size=64, color="primary")
199
+ vuetify3.VCardSubtitle("Running simulation...", classes="mt-4")
200
+ with vuetify3.VContainer(v_if="!is_running", fluid=True, classes="fill-height pa-2"):
201
+ fig = plotly_widgets.Figure(figure=go.Figure(), responsive=True, style="width: 100%; min-height: 560px;")
202
+ ctrl.qlbm_plot_update = fig.update
203
+ vuetify3.VContainer(v_if="!simulation_has_run", classes="d-flex align-center justify-center", style="height: 360px; color: rgba(0,0,0,.6);", children=["Configure inputs and run to display results."])
qlbm.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import math
4
+ import re # Added missing import for regex usage
5
+ from trame.app import get_server
6
+ from trame_vuetify.ui.vuetify3 import SinglePageLayout
7
+ from trame_vuetify.widgets import vuetify3
8
+ from trame.widgets import html as trame_html
9
+ from trame_plotly.widgets import plotly as plotly_widgets
10
+ import plotly.graph_objects as go
11
+
12
+ GRID_SIZES = [16, 32, 64, 128, 256, 512, 1024, 2048]
13
+
14
+ def load_logo_data_uri():
15
+ try:
16
+ base_dir = os.path.dirname(__file__)
17
+ for name in [
18
+ "ansys-part-of-synopsys-logo.svg",
19
+ "synopsys-logo-color-rgb.svg",
20
+ "synopsys-logo-color-rgb.png",
21
+ "synopsys-logo-color-rgb.jpg",
22
+ ]:
23
+ p = os.path.join(base_dir, name)
24
+ if os.path.exists(p):
25
+ ext = os.path.splitext(p)[1].lower()
26
+ mime = "image/svg+xml" if ext == ".svg" else ("image/png" if ext == ".png" else "image/jpeg")
27
+ import base64
28
+ with open(p, "rb") as f:
29
+ b64 = base64.b64encode(f.read()).decode("ascii")
30
+ return f"data:{mime};base64,{b64}"
31
+ except Exception:
32
+ pass
33
+ return None
34
+
35
+ def update_qubit_3D_info(grid_size: int):
36
+ try:
37
+ num_reg_qubits = int(math.log2(grid_size)) if grid_size > 0 else 3
38
+ total_qubits = 3 * num_reg_qubits + 3
39
+ grid_display = f"Grid Size: {grid_size} × {grid_size} × {grid_size}"
40
+ x = np.array([16, 32, 64, 128, 256])
41
+ y = np.log2(x).astype(int)
42
+ fig = go.Figure()
43
+ fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='Qubits/Direction', line=dict(color='#7A3DB5', width=3)))
44
+ fig.add_trace(go.Scatter(x=[grid_size], y=[num_reg_qubits], mode='markers', marker=dict(size=12, color='red'), name='Current Selection'))
45
+ fig.update_layout(xaxis_title="Grid Size (Points/Direction)", yaxis_title="Qubits/Direction", width=616, height=320, margin=dict(l=40, r=20, t=20, b=40))
46
+ warning = "⚠️ Warning: Grid sizes > 64 may exceed simulator/memory limits!" if grid_size > 64 else ""
47
+ return fig, grid_display, total_qubits, warning
48
+ except Exception:
49
+ return go.Figure(), "Grid Size: N/A", 0, ""
50
+
51
+ # Controllers (created per server instance)
52
+
53
+ def _make_controllers(server):
54
+ state, ctrl = server.state, server.controller
55
+
56
+ def run_simulation():
57
+ state.is_running = True
58
+ state.error_message = ""
59
+ try:
60
+ state.simulation_has_run = True
61
+ except Exception as e:
62
+ state.error_message = f"Run failed: {e}"
63
+ finally:
64
+ state.is_running = False
65
+
66
+ def reset_all():
67
+ state.update({
68
+ "problems_selection": None,
69
+ "geometry_selection": None,
70
+ "domain_L": 1.0,
71
+ "domain_W": 1.0,
72
+ "domain_H": 1.0,
73
+ "dist_type": None,
74
+ "impulse_x": 0.5,
75
+ "impulse_y": 0.5,
76
+ "peak_pair": "(0.5, 0.5)",
77
+ "mu_x": 0.5,
78
+ "mu_y": 0.5,
79
+ "sigma_x": 0.25,
80
+ "sigma_y": 0.15,
81
+ "mu_pair": "(0.5, 0.5)",
82
+ "excitation_error_message": "",
83
+ "advecting_field": None,
84
+ "inlet_velocity": 1.0,
85
+ "inlet_temperature": 300.0,
86
+ "nx_slider_index": None,
87
+ "nx": None,
88
+ "output_type": "Surface Plot",
89
+ "is_running": False,
90
+ "simulation_has_run": False,
91
+ "error_message": "",
92
+ "backend_type": "Simulator",
93
+ "selected_simulator": "IBM Qiskit simulator",
94
+ "selected_qpu": "IBM QPU",
95
+ "qubit_grid_info": "",
96
+ "qubit_warning": "",
97
+ })
98
+
99
+ ctrl.run_qlbm_sim = run_simulation
100
+ ctrl.reset_qlbm = reset_all
101
+
102
+ def on_nx_index_change(nx_slider_index, **_):
103
+ try:
104
+ state.nx = GRID_SIZES[int(nx_slider_index)] if nx_slider_index is not None else None
105
+ except Exception:
106
+ state.nx = None
107
+
108
+ def update_qubit_plot(nx, **_):
109
+ try:
110
+ if nx is None:
111
+ state.qubit_grid_info = ""
112
+ state.qubit_warning = ""
113
+ return
114
+ fig, grid_info, _, warning = update_qubit_3D_info(nx)
115
+ state.qubit_grid_info = grid_info
116
+ state.qubit_warning = warning
117
+ if hasattr(ctrl, 'qlbm_qubit_update'):
118
+ ctrl.qlbm_qubit_update(fig)
119
+ except Exception:
120
+ pass
121
+
122
+ def sync_peak_pair(peak_pair, **_):
123
+ try:
124
+ m = re.match(r"\(\s*([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*\)", str(peak_pair))
125
+ if not m:
126
+ raise ValueError
127
+ state.impulse_x = max(0.0, min(1.0, float(m.group(1))))
128
+ state.impulse_y = max(0.0, min(1.0, float(m.group(2))))
129
+ state.excitation_error_message = ""
130
+ except Exception:
131
+ state.excitation_error_message = "Invalid Peak. Use (x, y) in [0,1]."
132
+
133
+ def sync_mu_pair(mu_pair, **_):
134
+ try:
135
+ m = re.match(r"\(\s*([-+]?[0-9]*\.?[0-9]+)\s*,\s*([-+]?[0-9]*\.?[0-9]+)\s*\)", str(mu_pair))
136
+ if not m:
137
+ raise ValueError
138
+ state.mu_x = max(0.0, min(1.0, float(m.group(1))))
139
+ state.mu_y = max(0.0, min(1.0, float(m.group(2))))
140
+ state.excitation_error_message = ""
141
+ except Exception:
142
+ state.excitation_error_message = "Invalid Mu. Use (x, y) in [0,1]."
143
+
144
+ # Register watchers
145
+ server.state.change("nx_slider_index")(on_nx_index_change)
146
+ server.state.change("nx")(update_qubit_plot)
147
+ server.state.change("peak_pair")(sync_peak_pair)
148
+ server.state.change("mu_pair")(sync_mu_pair)
149
+
150
+ # Initialize defaults once per server
151
+
152
+ def _init_state(server):
153
+ state = server.state
154
+ if getattr(state, "qlbm_initialized", False):
155
+ return
156
+ state.update({
157
+ "logo_src": load_logo_data_uri(),
158
+ "is_running": False,
159
+ "simulation_has_run": False,
160
+ "error_message": "",
161
+ "problems_selection": None,
162
+ "geometry_selection": None,
163
+ "domain_L": 1.0,
164
+ "domain_W": 1.0,
165
+ "domain_H": 1.0,
166
+ "dist_type": None,
167
+ "impulse_x": 0.5,
168
+ "impulse_y": 0.5,
169
+ "peak_pair": "(0.5, 0.5)",
170
+ "mu_x": 0.5,
171
+ "mu_y": 0.5,
172
+ "sigma_x": 0.25,
173
+ "sigma_y": 0.15,
174
+ "mu_pair": "(0.5, 0.5)",
175
+ "excitation_error_message": "",
176
+ "advecting_field": None,
177
+ "inlet_velocity": 1.0,
178
+ "inlet_temperature": 300.0,
179
+ "nx_slider_index": None,
180
+ "nx": None,
181
+ "backend_type": "Simulator",
182
+ "selected_simulator": "IBM Qiskit simulator",
183
+ "selected_qpu": "IBM QPU",
184
+ "output_type": "Surface Plot",
185
+ "qubit_grid_info": "",
186
+ "qubit_warning": "",
187
+ "qlbm_initialized": True,
188
+ })
189
+
190
+ # Public build(server) used by multipage app
191
+
192
+ def build(server):
193
+ _init_state(server)
194
+ _make_controllers(server)
195
+ state, ctrl = server.state, server.controller
196
+ with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
197
+ with vuetify3.VRow(no_gutters=True, classes="fill-height"):
198
+ # Left column
199
+ with vuetify3.VCol(cols=5, classes="pa-4 d-flex flex-column"):
200
+ with vuetify3.VCard(classes="mb-4"):
201
+ vuetify3.VCardTitle("Overview", classes="text-h5 font-weight-bold text-primary")
202
+ with vuetify3.VCardText():
203
+ vuetify3.VDivider(classes="my-2")
204
+ vuetify3.VCardSubtitle("Problems!", classes="text-subtitle-1 font-weight-bold mt-2")
205
+ vuetify3.VSelect(label="Select a problem", v_model=("problems_selection", None), items=("qlbm_problems", ["1. Scalar advection-diffusion in a box", "2. Laminar flow & heat transfer for a heated body in water."]), placeholder="Select", density="compact", color="primary")
206
+ vuetify3.VCardSubtitle("Governing Equations", classes="text-subtitle-1 font-weight-bold mt-2")
207
+ vuetify3.VListItemTitle("Laminar Navier-Stokes including energy", classes="text-body-2")
208
+ vuetify3.VCardSubtitle("Inputs", classes="text-subtitle-1 font-weight-bold mt-2")
209
+ vuetify3.VListItemTitle("Geometry, Boundary conditions - temperature and flow", classes="text-body-2")
210
+ vuetify3.VCardSubtitle("Outputs", classes="text-subtitle-1 font-weight-bold mt-2")
211
+ vuetify3.VListItemTitle("Surface plots on sections OR sampling through a line in 3D domain", classes="text-body-2")
212
+ with vuetify3.VCard(classes="mb-4"):
213
+ vuetify3.VCardTitle("Geometry", classes="text-primary")
214
+ with vuetify3.VCardText():
215
+ vuetify3.VSelect(label="Select", v_model=("geometry_selection", None), items=("geometry_options", ["Free space", "Rectangular domain with a heated box (3D)"]), placeholder="Select", density="compact", color="primary")
216
+ with vuetify3.VContainer(v_if="geometry_selection === 'Rectangular domain with a heated box (3D)'", classes="pa-0 mt-2"):
217
+ with vuetify3.VRow(dense=True):
218
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Length (L)", v_model=("domain_L", 1.0), type="number", step="0.1", density="compact", color="primary")])
219
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Breadth (B)", v_model=("domain_W", 1.0), type="number", step="0.1", density="compact", color="primary")])
220
+ vuetify3.VTextField(label="Height (H)", v_model=("domain_H", 1.0), type="number", step="0.1", density="compact", color="primary")
221
+ with vuetify3.VCard(v_if="geometry_selection === 'Rectangular domain with a heated box (3D)'", classes="mb-4"):
222
+ vuetify3.VCardTitle("Initial & Boundary Conditions", classes="text-primary")
223
+ with vuetify3.VCardText():
224
+ vuetify3.VTextField(v_model=("mu_pair", "(0.5, 0.5)"), label="Gaussian μ (x, y) in [0,1]", density="compact", color="primary")
225
+ with vuetify3.VRow(dense=True, classes="mt-1"):
226
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Sigma X (0–1)", v_model=("sigma_x", 0.25), type="number", step="0.01", density="compact", color="primary")])
227
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Sigma Y (0–1)", v_model=("sigma_y", 0.15), type="number", step="0.01", density="compact", color="primary")])
228
+ vuetify3.VAlert(v_if="excitation_error_message", type="error", variant="tonal", density="compact", children=["{{ excitation_error_message }}"], classes="mt-2")
229
+ vuetify3.VSelect(label="Advecting field", v_model=("advecting_field", None), items=("advect_fields", ["Uniform", "Swirl", "Shear", "TGV"]), density="compact", color="primary", classes="mt-2")
230
+ with vuetify3.VRow(dense=True, classes="mt-2"):
231
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Inlet flow velocity", v_model=("inlet_velocity", 1.0), type="number", step="0.1", density="compact", color="primary")])
232
+ vuetify3.VCol(children=[vuetify3.VTextField(label="Inlet temperature (K)", v_model=("inlet_temperature", 300.0), type="number", step="1", density="compact", color="primary")])
233
+ with vuetify3.VCard(classes="mb-4"):
234
+ vuetify3.VCardTitle("Meshing", classes="text-primary")
235
+ with vuetify3.VCardText():
236
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=False, location="end"):
237
+ with vuetify3.Template(v_slot_activator="{ props }"):
238
+ with vuetify3.VSlider(v_bind="props", v_model=("nx_slider_index", None), label="No. of points per direction:", min=0, max=len(GRID_SIZES)-1, step=1, show_ticks="always", thumb_label="always", density="compact", color="primary"):
239
+ vuetify3.Template(v_slot_thumb_label="{ modelValue }", children=["{{ modelValue === null ? 'Select' : [16, 32, 64, 128, 256, 512, 1024, 2048][modelValue] }}"])
240
+ with vuetify3.VSheet(classes="pa-2", elevation=6, rounded=True, style="width: 700px;"):
241
+ qubit_fig_widget = plotly_widgets.Figure(figure=go.Figure(), responsive=True, style="width: 616px; height: 320px; min-height: 320px;")
242
+ ctrl.qlbm_qubit_update = qubit_fig_widget.update
243
+ vuetify3.VCardText(children=["{{ qubit_grid_info }}", "{{ qubit_warning }}"], classes="text-caption mt-2")
244
+ with vuetify3.VCard(classes="mb-4"):
245
+ vuetify3.VCardTitle("Backends", classes="text-primary")
246
+ with vuetify3.VCardText():
247
+ with vuetify3.VRow(dense=True, classes="mb-2"):
248
+ with vuetify3.VCol():
249
+ vuetify3.VAlert(type="info", color="primary", variant="tonal", density="compact", children=["Selected: ", "{{ backend_type || '—' }}", " - ", "{{ backend_type === 'Simulator' ? selected_simulator : (backend_type === 'QPU' ? selected_qpu : '—') }}"])
250
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"):
251
+ with vuetify3.Template(v_slot_activator="{ props }"):
252
+ vuetify3.VBtn(v_bind="props", text="Choose Backend", color="primary", variant="tonal", block=True)
253
+ with vuetify3.VList(density="compact"):
254
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8):
255
+ with vuetify3.Template(v_slot_activator="{ props }"):
256
+ vuetify3.VListItem(v_bind="props", title="Simulator", prepend_icon="mdi-robot-outline", append_icon="mdi-chevron-right")
257
+ with vuetify3.VList(density="compact"):
258
+ vuetify3.VListItem(title="IBM Qiskit simulator", click="backend_type = 'Simulator'; selected_simulator = 'IBM Qiskit simulator'")
259
+ vuetify3.VListItem(title="IonQ simulator", click="backend_type = 'Simulator'; selected_simulator = 'IonQ simulator'")
260
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8):
261
+ with vuetify3.Template(v_slot_activator="{ props }"):
262
+ vuetify3.VListItem(v_bind="props", title="QPU", prepend_icon="mdi-chip", append_icon="mdi-chevron-right")
263
+ with vuetify3.VList(density="compact"):
264
+ vuetify3.VListItem(title="IBM QPU", click="backend_type = 'QPU'; selected_qpu = 'IBM QPU'")
265
+ vuetify3.VListItem(title="IonQ QPU", click="backend_type = 'QPU'; selected_qpu = 'IonQ QPU'")
266
+ with vuetify3.VRow(dense=True, classes="gap-2 mt-4"):
267
+ with vuetify3.VCol():
268
+ vuetify3.VBtn(text="Run Simulation", color="primary", block=True, disabled=("is_running || !geometry_selection || (geometry_selection === 'Rectangular domain with a heated box (3D)' && nx === null)", False), click=ctrl.run_qlbm_sim)
269
+ with vuetify3.VCol():
270
+ vuetify3.VBtn(text="Reset", color="secondary", variant="tonal", block=True, click=ctrl.reset_qlbm)
271
+ with vuetify3.VCol(cols=7, classes="pa-4 d-flex flex-column"):
272
+ with vuetify3.VContainer(fluid=True, classes="fill-height d-flex align-center justify-center"):
273
+ vuetify3.VCardText("Select a geometry and configure inputs to display results.", classes="text-center text-medium-emphasis")
274
+
275
+ # Standalone execution
276
+ if __name__ == "__main__":
277
+ from trame.app import get_server
278
+ from trame_vuetify.ui.vuetify3 import SinglePageLayout
279
+ _server = get_server()
280
+ # NOTE: build must run inside a layout/content context; do not call before layout
281
+ with SinglePageLayout(_server) as _layout:
282
+ _layout.title.set_text("QLBM: Lattice Boltzmann")
283
+ with _layout.content:
284
+ build(_server)
285
+ _server.start(open_browser=True)
synopsys-logo-color-rgb.png DELETED
Binary file (22.1 kB)
 
synopsys-logo-color-rgb.svg DELETED
utils/__pycache__/base_functions.cpython-311.pyc ADDED
Binary file (22.3 kB). View file
 
utils/__pycache__/base_functions.cpython-313.pyc ADDED
Binary file (18.6 kB). View file
 
utils/__pycache__/delta_impulse_generator.cpython-311.pyc ADDED
Binary file (29.3 kB). View file
 
utils/__pycache__/delta_impulse_generator.cpython-313.pyc ADDED
Binary file (25.1 kB). View file
 
utils/base_functions.py ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import scipy.sparse as sp
3
+ import math
4
+ import random
5
+ import matplotlib.pyplot as plt
6
+ from scipy.special import jn
7
+ from scipy.sparse import identity, csr_matrix, kron, diags, eye
8
+ from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
9
+ from qiskit.circuit.library import MCXGate, MCPhaseGate, RXGate, CRXGate, QFTGate, StatePreparation, PauliEvolutionGate, RZGate
10
+ from qiskit.quantum_info import SparsePauliOp, Statevector, Operator, Pauli
11
+ from scipy.linalg import expm
12
+ # from tools import *
13
+ from qiskit.qasm3 import dumps # QASM 3 exporter
14
+ from qiskit.qasm3 import loads
15
+ from qiskit.circuit.library import QFT
16
+ from qiskit.primitives import StatevectorEstimator
17
+ from qiskit import transpile
18
+ from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit, apply_circuit_to_state, compute_overlap
19
+ from qiskit_aer import AerSimulator
20
+
21
+
22
+ simulator_settings = AerSimulator(
23
+ method="matrix_product_state",
24
+ matrix_product_state_max_bond_dimension=100,
25
+ )
26
+
27
+ def Wj(j, theta, lam, name='Wj', xgate=False):
28
+ if not xgate:
29
+ name = f' $W_{j}$ '
30
+ qc=QuantumCircuit(j, name=name)
31
+
32
+ if j > 1:
33
+ qc.cx(j-1, range(j-1))
34
+ if lam != 0:
35
+ qc.p(lam, j-1)
36
+ qc.h(j-1)
37
+ if xgate:
38
+ qc.x(range(j-1))
39
+
40
+ # the multicontrolled rz gate
41
+ # it will be decomposed in qiskit
42
+ if j > 1:
43
+ qc.mcrz(theta, range(j-1), j-1)
44
+ else:
45
+ qc.rz(theta, j-1)
46
+
47
+ if xgate:
48
+ qc.x(range(j-1))
49
+ qc.h(j-1)
50
+ if lam != 0:
51
+ qc.p(-lam, j-1)
52
+ if j > 1:
53
+ qc.cx(j-1, range(j-1))
54
+
55
+ return qc
56
+
57
+ def Wj_block(j, n, ctrl_state, theta, lam, name='Wj_block', xgate=False):
58
+ if not xgate:
59
+ name = f' $W_{j}_block$ '
60
+ qc=QuantumCircuit(n + j, name=name)
61
+
62
+ if j > 1:
63
+ qc.cx(n + j-1, range(n, n+j-1))
64
+ if lam != 0:
65
+ qc.p(lam, n + j -1)
66
+ qc.h(n + j -1)
67
+
68
+ if xgate and j>1:
69
+ if isinstance(xgate, (list, tuple)): # selective application
70
+ for idx, flag in enumerate(xgate):
71
+ if flag: # only apply where flag == 1
72
+ qc.x(n + idx)
73
+ elif xgate is True: # apply to all
74
+ qc.x(range(n, n+j-1))
75
+
76
+ # the multicontrolled rz gate
77
+ # it will be decomposed in qiskit
78
+ if j > 1:
79
+ mcrz = RZGate(theta).control(len(ctrl_state) + j-1, ctrl_state = "1"*(j-1)+ctrl_state)
80
+ qc.append(mcrz, range(0, n + j))
81
+ else:
82
+ mcrz = RZGate(theta).control(len(ctrl_state), ctrl_state = ctrl_state)
83
+ qc.append(mcrz, range(0, n+j))
84
+
85
+ if xgate and j>1:
86
+ if isinstance(xgate, (list, tuple)): # selective application
87
+ for idx, flag in enumerate(xgate):
88
+ if flag: # only apply where flag == 1
89
+ qc.x(n + idx)
90
+ elif xgate is True: # apply to all
91
+ qc.x(range(n, n+j-1))
92
+
93
+ qc.h(n+ j-1)
94
+ if lam != 0:
95
+ qc.p(-lam, n + j-1)
96
+ if j > 1:
97
+ qc.cx(n + j-1, range(n, n +j-1))
98
+
99
+ return qc.to_gate(label=name)
100
+
101
+ def V1(nx, dt, name = "V1"):
102
+ n = int(np.ceil(np.log2(nx)))
103
+
104
+ derivatives = QuantumRegister(2*n)
105
+ blocks = QuantumRegister(2)
106
+
107
+ qc = QuantumCircuit(derivatives, blocks)
108
+
109
+ W1 = Wj_block(2, n, "0"*n, -dt , 0, xgate=True)
110
+ qc.append(W1, list(derivatives[0:n])+list(blocks[:]))
111
+
112
+ # qc.barrier()
113
+
114
+ W2 = Wj_block(3, n-1, "1"*(n-1), dt , 0, xgate=[0,1])
115
+ qc.append(W2, list(derivatives[1:n])+[derivatives[0]]+list(blocks[:]))
116
+
117
+ # qc.barrier()
118
+
119
+ W3 = Wj_block(1, n+1, "0"*(n+1), dt , 0, xgate=False)
120
+ qc.append(W3, list(derivatives[n:2*n])+list(blocks[:]))
121
+
122
+ # qc.barrier()
123
+
124
+ W4 = Wj_block(2, n, "0"+"1"*(n-1), -dt , 0, xgate=False)
125
+ qc.append(W4, list(derivatives[n+1:2*n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
126
+
127
+ return qc
128
+
129
+ def V2(nx, dt, name = "V2"):
130
+ n = int(np.ceil(np.log2(nx)))
131
+
132
+ derivatives = QuantumRegister(2*n)
133
+ blocks = QuantumRegister(2)
134
+
135
+ qc = QuantumCircuit(derivatives, blocks)
136
+
137
+ W1 = Wj_block(2, 0, "", -2*dt , -np.pi/2, xgate=True)
138
+ qc.append(W1, list(blocks[:]))
139
+
140
+ # qc.barrier()
141
+
142
+ for j in range(1, n+1):
143
+ W2 = Wj_block(2+j, 0, "", 2*dt , -np.pi/2, xgate=[1]*(j-1)+[0,1])
144
+ qc.append(W2, list(derivatives[0:j])+list(blocks[:]))
145
+
146
+ # qc.barrier()
147
+
148
+ W3 = Wj_block(2, n, "0"*n, -dt , -np.pi/2, xgate=True)
149
+ qc.append(W3, list(derivatives[0:n])+list(blocks[:]))
150
+
151
+ # qc.barrier()
152
+
153
+ W4 = Wj_block(2, n, "1"*n, 2*dt , -np.pi/2, xgate=True)
154
+ qc.append(W4, list(derivatives[0:n])+list(blocks[:]))
155
+
156
+ # qc.barrier()
157
+
158
+ W5 = Wj_block(3, n-1, "1"*(n-1), dt , -np.pi/2, xgate=[0,1])
159
+ qc.append(W5, list(derivatives[1:n])+[derivatives[0]]+list(blocks[:]))
160
+
161
+ # qc.barrier()
162
+
163
+ W6 = Wj_block(1, 1, "0", 2*dt , -np.pi/2, xgate=False)
164
+ qc.append(W6, list(blocks[:]))
165
+
166
+ # qc.barrier()
167
+
168
+ for j in range(1, n+1):
169
+ W7 = Wj_block(1+j, 1, "0", -2*dt , -np.pi/2, xgate=[1]*(j-1))
170
+ qc.append(W7, [blocks[0]]+list(derivatives[n:n+j])+[blocks[1]])
171
+
172
+ # qc.barrier()
173
+
174
+ W8 = Wj_block(1, n+1, "0"*(n+1), dt , -np.pi/2, xgate=False)
175
+ qc.append(W8, list(derivatives[n:2*n])+list(blocks[:]))
176
+
177
+ # qc.barrier()
178
+
179
+ W9 = Wj_block(1, n+1, "0"+"1"*(n), -2*dt , -np.pi/2, xgate=False)
180
+ qc.append(W9, list(derivatives[n:2*n])+list(blocks[:]))
181
+
182
+ # qc.barrier()
183
+
184
+ W10 = Wj_block(2, n, "0"+"1"*(n-1), -dt , -np.pi/2, xgate=False)
185
+ qc.append(W10, list(derivatives[n+1:2*n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
186
+
187
+ # qc.barrier()
188
+
189
+ return qc
190
+
191
+ def schro(nx, na, R, dt, initial_state, steps):
192
+
193
+ nq = int(np.ceil(np.log2(nx)))
194
+
195
+ # warped phase transformation
196
+ dp = 2 * R * np.pi / 2**na
197
+ p = np.arange(- R * np.pi, R * np.pi, step=dp)
198
+ fp = np.exp(-np.abs(p))
199
+ norm1 = np.linalg.norm(fp[2**(na-1):]) # norm of p>=0
200
+
201
+ # construct quantum circuit
202
+ system = QuantumRegister(2*nq+2, name='system')
203
+ ancilla = QuantumRegister(na, name='ancilla')
204
+ qc = QuantumCircuit(system, ancilla)
205
+
206
+ # initialization
207
+ prep = StatePreparation(initial_state)
208
+ anc_prep = StatePreparation(fp / np.linalg.norm(fp))
209
+
210
+ qc.append(prep, system)
211
+ # qc.append(anc_prep, ancilla)
212
+ qc.initialize(fp / np.linalg.norm(fp), ancilla)
213
+
214
+
215
+ # QFT
216
+ qc.append(QFTGate(na), ancilla)
217
+ qc.x(ancilla[-1])
218
+
219
+ A1 = V1(nx, dt, name = "V1").to_gate()
220
+ A2 = V2(nx, dt, name = "V2")
221
+
222
+
223
+ # Hamiltonian simulation for Nt steps
224
+ for i in range(steps):
225
+ # circuit for one step
226
+ for j in range(na):
227
+ # repeat controlled H1 for 2**j times
228
+ qc.append(A1.control().repeat(2**j), [ancilla[j]] + system[:])
229
+
230
+ # qc.append(A1.inverse().control(ctrl_state = "0").repeat(2**(na-1)), [ancilla[na-1]] + system[:])
231
+ qc.append(A1.inverse().repeat(2**(na-1)), system[:])
232
+ qc.append(A2, system[:])
233
+
234
+ # rearrange eta
235
+ qc.x(ancilla[-1])
236
+ qc.append(QFTGate(na).inverse(), ancilla)
237
+
238
+ return qc
239
+
240
+
241
+
242
+ def circ_for_magnitude(field, x, y, nx, na, R, dt, initial_state, steps):
243
+
244
+ qc = schro(nx, na, R, dt, initial_state, steps)
245
+ naimark = QuantumRegister(1, name='Naimark')
246
+ qc.add_register(naimark)
247
+
248
+ if field == 'Ez':
249
+ index = nx * y + x
250
+ elif field == 'Hx':
251
+ index = 2*nx*nx + nx * y + x
252
+ else:
253
+ index = 3*nx*nx + nx * y + x
254
+
255
+ index_bin = format(index, f'0{qc.num_qubits-2}b')
256
+ ctrl_state = '1' + index_bin
257
+ ctrl_qubits = qc.qubits[:-1]
258
+ qc.mcx(ctrl_qubits, naimark[0], ctrl_state=ctrl_state)
259
+
260
+ return qc
261
+
262
+ def circuits_for_sign(field, x, y, nx, na, dt, R, initial_state, steps, xref, yref, field_ref = 'Ez'):
263
+ qc = schro(nx, na, R, dt, initial_state, steps)
264
+
265
+ naimark = QuantumRegister(1, name='Naimark')
266
+ qc.add_register(naimark)
267
+
268
+ if field == 'Ez':
269
+ index = nx * y + x
270
+ elif field == 'Hx':
271
+ index = 2*nx*nx + nx * y + x
272
+ else:
273
+ index = 3*nx*nx + nx * y + x
274
+
275
+ if field_ref == 'Ez':
276
+ index_ref = nx * yref + xref
277
+ elif field_ref == 'Hx':
278
+ index_ref = 2*nx*nx + nx * yref + xref
279
+ else:
280
+ index_ref = 3*nx*nx + nx * yref + xref
281
+
282
+ index_bin = [(index >> i) & 1 for i in range(qc.num_qubits-2)]
283
+ index_ref_bin = [(index_ref >> i) & 1 for i in range(qc.num_qubits-2)]
284
+ index_bin.append(1)
285
+ index_ref_bin.append(1)
286
+
287
+ #Convert reference bitstring to 00000
288
+ for i, bit in enumerate(index_ref_bin):
289
+ if bit == 1:
290
+ qc.x(i)
291
+
292
+ d_bits = [b ^ r for b, r in zip(index_ref_bin, index_bin)]
293
+ control = d_bits.index(1)
294
+
295
+ #Convert the other bitstring to 0001000
296
+ for target, bit in enumerate(d_bits):
297
+ if bit == 1 and target != control:
298
+ qc.cx(control, target)
299
+ qc.h(control)
300
+
301
+ ctrl_state_sum = '0'*(qc.num_qubits-1)
302
+ ctrl_state_diff = '0'*(qc.num_qubits-1-control-1)+'1'+'0'*(control)
303
+
304
+ qcdiff = qc.copy()
305
+
306
+ ctrl_qubits = qc.qubits[:-1]
307
+
308
+ qc.mcx(ctrl_qubits, naimark[0], ctrl_state=ctrl_state_sum)
309
+ qcdiff.mcx(ctrl_qubits, naimark[0], ctrl_state=ctrl_state_diff)
310
+
311
+ return qc, qcdiff
312
+
313
+ def get_absolute_field_value(qc, nq, na, offset, norm):
314
+
315
+ pauli_label = 'Z'+'I'*(2*nq+2+na)
316
+ observable = SparsePauliOp(Pauli(pauli_label))
317
+ ########################################################################################
318
+ estimator = StatevectorEstimator()
319
+
320
+ # === Run Estimator (no parameters needed) ===
321
+ pub = (qc, observable)
322
+ job = estimator.run([pub])
323
+ result = job.result()[0]
324
+ z_exp = result.data.evs.item()
325
+ #########################################################################################
326
+ # === Compute projector expectation ===
327
+ pi_expect = (1 - z_exp) / 2
328
+
329
+ Absolute_value = norm*np.sqrt(pi_expect)-offset
330
+
331
+ return Absolute_value
332
+
333
+ def get_relative_sign(qc, qcdiff, nq, na):
334
+
335
+ pauli_label = 'Z'+'I'*(2*nq+2+na)
336
+ observable = SparsePauliOp(Pauli(pauli_label))
337
+ ########################################################################################
338
+ estimator = StatevectorEstimator()
339
+
340
+ # === Run Estimator ===
341
+ pub = (qc, observable)
342
+ job = estimator.run([pub])
343
+ result = job.result()[0]
344
+ z_exp = result.data.evs.item()
345
+
346
+ pub_diff = (qcdiff, observable)
347
+ job_diff = estimator.run([pub_diff])
348
+ result_diff = job_diff.result()[0]
349
+ z_exp_diff = result_diff.data.evs.item()
350
+ #########################################################################################
351
+ # === Compute projector expectation ===
352
+ pi_expect_sum = (1 - z_exp) / 2
353
+ pi_expect_diff = (1 - z_exp_diff) / 2
354
+
355
+ relative_sign = 'same' if pi_expect_sum >= pi_expect_diff else 'different'
356
+
357
+ return relative_sign
358
+
359
+ def Eref_value(nx, nq, R, dt, na, steps, xref, yref, field_ref = 'Ez'):
360
+ if steps < 31:
361
+ offset = 1
362
+ else :
363
+ offset = 0.15
364
+ deltastate = np.zeros(4*nx*nx)
365
+ # deltastate[nx*nx//2+nx//2:nx*nx//2+nx//2+1] = 1
366
+ deltastate[nx*yref+xref] = 1
367
+ deltastate[0:nx*nx] = deltastate[0:nx*nx] + offset
368
+ norm1 = np.linalg.norm(deltastate)
369
+ initial_state = deltastate/norm1
370
+
371
+ dp = 2 * R * np.pi / 2**na
372
+ p = np.arange(- R * np.pi, R * np.pi, step=dp)
373
+ fp = np.exp(-np.abs(p))
374
+ norm2 = np.linalg.norm(fp)
375
+ norm = norm1 * norm2
376
+
377
+ qc = circ_for_magnitude(field_ref, xref, yref, nx, na, R, dt, initial_state, steps)
378
+
379
+ Ezref = get_absolute_field_value(qc, nq, na, offset, norm)
380
+
381
+ return Ezref
382
+
383
+
384
+ def transpile_circ(circ, basis_gates=None):
385
+ """
386
+ Transpile the circuit to the specified basis gates.
387
+ """
388
+ if basis_gates is None:
389
+ basis_gates = ['z', 'y', 'x', 'sdg', 's', 'h', 'rz', 'ry', 'rx', 'ecr', 'cz', 'cx']
390
+
391
+ transpiled_circ = transpile(circ, basis_gates=basis_gates)
392
+ return transpiled_circ
393
+
394
+ def compute_fidelity(circ1, circ2):
395
+
396
+ circ_1 = tensornetwork_from_circuit(transpile_circ(circ1), simulator_settings)
397
+ circ_2 = tensornetwork_from_circuit(transpile_circ(circ2), simulator_settings)
398
+ fidelity = abs(compute_overlap(circ_1, circ_2))**2
399
+
400
+ return fidelity
401
+
402
+ # def create_impulse_state(grid_dims, impulse_pos):
403
+ # """
404
+ # Creates an initial state vector with a single delta impulse at a specified grid position.
405
+
406
+ # The 2D grid is flattened into a 1D vector in row-major order, and this
407
+ # vector is then padded to match the full simulation state space size (4x).
408
+
409
+ # Args:
410
+ # grid_dims (tuple): A tuple (width, height) defining the simulation grid dimensions.
411
+ # For your original code, this would be (nx, nx).
412
+ # impulse_pos (tuple): A tuple (x, y) for the position of the impulse.
413
+ # Coordinates are 0-indexed.
414
+
415
+ # Returns:
416
+ # numpy.ndarray: The full, padded initial state vector with a single 1.
417
+
418
+ # Raises:
419
+ # ValueError: If the impulse position is outside the grid dimensions.
420
+ # """
421
+ # grid_width, grid_height = grid_dims
422
+ # impulse_x, impulse_y = impulse_pos
423
+
424
+ # # --- Input Validation ---
425
+ # # Ensure the requested impulse position is actually on the grid.
426
+ # if not (0 <= impulse_x < grid_width and 0 <= impulse_y < grid_height):
427
+ # raise ValueError(f"Impulse position ({impulse_x}, {impulse_y}) is outside the "
428
+ # f"grid dimensions ({grid_width}x{grid_height}).")
429
+
430
+ # # --- 1. Calculate the 1D Array Index ---
431
+ # # Convert the (x, y) coordinate to a single index in a flattened 1D array.
432
+ # # The formula for row-major order is: index = y_coord * width + x_coord
433
+ # flat_index = impulse_y * grid_width + impulse_x
434
+
435
+ # # --- 2. Create the Full, Padded State Vector ---
436
+ # grid_size = grid_width * grid_height
437
+ # total_size = 4 * grid_size # The simulation space is 4x the grid size.
438
+ # initial_state = np.zeros(total_size)
439
+
440
+ # # --- 3. Set the Delta Impulse ---
441
+ # initial_state[flat_index] = 1
442
+
443
+ # return initial_state
utils/delta_impulse_generator.py ADDED
@@ -0,0 +1,468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import math
3
+ from qiskit.circuit import QuantumCircuit, QuantumRegister
4
+ from qiskit.circuit.library import StatePreparation, QFTGate, RZGate
5
+ from qiskit.quantum_info import Statevector
6
+ import pyvista as pv
7
+
8
+ def create_impulse_state(grid_dims, impulse_pos):
9
+ """
10
+ Creates an initial state vector with a single delta impulse at a specified grid position.
11
+
12
+ The 2D grid is flattened into a 1D vector in row-major order, and this
13
+ vector is then padded to match the full simulation state space size (4x).
14
+
15
+ Args:
16
+ grid_dims (tuple): A tuple (width, height) defining the simulation grid dimensions.
17
+ For your original code, this would be (nx, nx).
18
+ impulse_pos (tuple): A tuple (x, y) for the position of the impulse.
19
+ Coordinates are 0-indexed.
20
+
21
+ Returns:
22
+ numpy.ndarray: The full, padded initial state vector with a single 1.
23
+
24
+ Raises:
25
+ ValueError: If the impulse position is outside the grid dimensions.
26
+ """
27
+ grid_width, grid_height = grid_dims
28
+ impulse_x, impulse_y = impulse_pos
29
+
30
+ # --- Input Validation ---
31
+ # Ensure the requested impulse position is actually on the grid.
32
+ if not (0 <= impulse_x < grid_width and 0 <= impulse_y < grid_height):
33
+ raise ValueError(f"Impulse position ({impulse_x}, {impulse_y}) is outside the "
34
+ f"grid dimensions ({grid_width}x{grid_height}).")
35
+
36
+ # --- 1. Calculate the 1D Array Index ---
37
+ # Convert the (x, y) coordinate to a single index in a flattened 1D array.
38
+ # The formula for row-major order is: index = y_coord * width + x_coord
39
+ flat_index = impulse_y * grid_width + impulse_x
40
+
41
+ # --- 2. Create the Full, Padded State Vector ---
42
+ grid_size = grid_width * grid_height
43
+ total_size = 4 * grid_size # The simulation space is 4x the grid size.
44
+ initial_state = np.zeros(total_size)
45
+
46
+ # --- 3. Set the Delta Impulse ---
47
+ initial_state[flat_index] = 1
48
+
49
+ return initial_state
50
+
51
+ def create_gaussian_state(grid_dims, mu, sigma):
52
+ """
53
+ Creates an initial state vector with a 2D Gaussian distribution.
54
+
55
+ The state is normalized and padded to match the full simulation state space size (4x).
56
+
57
+ Args:
58
+ grid_dims (tuple): A tuple (width, height) defining the grid dimensions.
59
+ mu (tuple): A tuple (mu_x, mu_y) for the center (mean) of the Gaussian.
60
+ sigma (tuple): A tuple (sigma_x, sigma_y) for the standard deviation (spread).
61
+
62
+ Returns:
63
+ numpy.ndarray: The full, padded initial state vector for the Gaussian state.
64
+
65
+ Raises:
66
+ ValueError: If sigma values are not positive.
67
+ """
68
+ grid_width, grid_height = grid_dims
69
+ mu_x, mu_y = mu
70
+ sigma_x, sigma_y = sigma
71
+
72
+ if sigma_x <= 0 or sigma_y <= 0:
73
+ raise ValueError("Sigma values (spread) must be positive.")
74
+
75
+ # --- 1. Create a Coordinate Grid ---
76
+ x = np.arange(0, grid_width)
77
+ y = np.arange(0, grid_height)
78
+ X, Y = np.meshgrid(x, y)
79
+
80
+ # --- 2. Calculate the 2D Gaussian Function ---
81
+ gaussian_2d = np.exp(-((X - mu_x)**2 / (2 * sigma_x**2)) -
82
+ ((Y - mu_y)**2 / (2 * sigma_y**2)))
83
+
84
+ # --- 3. Normalize the State Vector ---
85
+ # For a valid quantum state, the L2 norm (sum of squares of amplitudes) must be 1.
86
+ norm = np.linalg.norm(gaussian_2d)
87
+ if norm > 0:
88
+ gaussian_2d = gaussian_2d / norm
89
+
90
+ # --- 4. Flatten and Pad the Vector ---
91
+ gaussian_flat = gaussian_2d.flatten()
92
+ grid_size = grid_width * grid_height
93
+ total_size = 4 * grid_size
94
+ initial_state = np.pad(gaussian_flat, (0, total_size - grid_size), mode='constant')
95
+
96
+ return initial_state
97
+
98
+
99
+
100
+
101
+
102
+ # --- New: Continuous-position helpers for excitation before meshing ---
103
+ def _normalize_to_unit(vec: np.ndarray) -> np.ndarray:
104
+ n = np.linalg.norm(vec)
105
+ return vec / n if n > 0 else vec
106
+
107
+
108
+
109
+
110
+ def create_impulse_state_from_pos(grid_dims, pos01):
111
+ """
112
+ Create a delta-like initial state from continuous position pos01=(x,y) in [0,1].
113
+
114
+ Why grid_dims?
115
+ - Simulation runs on a discrete nx×ny lattice; the continuous position must be
116
+ discretized onto that grid to produce the state vector fed into the solver.
117
+ - grid_dims provides (nx, ny) so we can map (x,y)∈[0,1]→grid coordinates via
118
+ gx = x*(nx-1), gy = y*(ny-1), then distribute amplitude bilinearly to the 4
119
+ neighboring nodes. This is required only for the simulation state, not the preview.
120
+
121
+ The preview uses create_impulse_preview_state(), which renders a smooth bump on a
122
+ fixed unit-square grid independent of nx for visualization.
123
+ """
124
+ grid_width, grid_height = grid_dims
125
+ px, py = pos01
126
+ px = float(max(0.0, min(1.0, px)))
127
+ py = float(max(0.0, min(1.0, py)))
128
+
129
+ gx = px * (grid_width - 1)
130
+ gy = py * (grid_height - 1)
131
+ i0, j0 = int(np.floor(gx)), int(np.floor(gy))
132
+ i1, j1 = min(i0 + 1, grid_width - 1), min(j0 + 1, grid_height - 1)
133
+ dx, dy = gx - i0, gy - j0
134
+
135
+ w00 = (1 - dx) * (1 - dy)
136
+ w10 = dx * (1 - dy)
137
+ w01 = (1 - dx) * dy
138
+ w11 = dx * dy
139
+
140
+ grid_size = grid_width * grid_height
141
+ total_size = 4 * grid_size
142
+ field = np.zeros(grid_size)
143
+ field[j0 * grid_width + i0] += w00
144
+ field[j0 * grid_width + i1] += w10
145
+ field[j1 * grid_width + i0] += w01
146
+ field[j1 * grid_width + i1] += w11
147
+ field = _normalize_to_unit(field)
148
+
149
+ initial_state = np.zeros(total_size)
150
+ initial_state[:grid_size] = field
151
+ return initial_state
152
+
153
+
154
+ def create_gaussian_state_from_pos(grid_dims, mu01, sigma01):
155
+ """
156
+ Create a Gaussian initial state with center mu01=(x,y) and spreads sigma01=(sx,sy)
157
+ in [0,1] of the domain, then discretize to the solver grid given by grid_dims.
158
+
159
+ Why grid_dims?
160
+ - The quantum solver expects a vector aligned to the chosen nx×ny simulation grid.
161
+ We convert normalized μ and σ (fractions of the domain) into grid units using
162
+ (nx-1) and (ny-1). This step is necessary for the simulation, not for the preview.
163
+
164
+ For preview-only rendering, use create_impulse_preview_state() to keep the visuals
165
+ continuous and independent of nx.
166
+ """
167
+ grid_width, grid_height = grid_dims
168
+ mu_x01, mu_y01 = mu01
169
+ sig_x01, sig_y01 = sigma01
170
+
171
+ mu_x01 = float(max(0.0, min(1.0, mu_x01)))
172
+ mu_y01 = float(max(0.0, min(1.0, mu_y01)))
173
+ sig_x01 = float(sig_x01)
174
+ sig_y01 = float(sig_y01)
175
+ if sig_x01 <= 0 or sig_y01 <= 0:
176
+ raise ValueError("Sigma values (spread) must be positive.")
177
+
178
+ mu_x = mu_x01 * (grid_width - 1)
179
+ mu_y = mu_y01 * (grid_height - 1)
180
+ sigma_x = sig_x01 * (grid_width - 1)
181
+ sigma_y = sig_y01 * (grid_height - 1)
182
+
183
+ x = np.arange(0, grid_width)
184
+ y = np.arange(0, grid_height)
185
+ X, Y = np.meshgrid(x, y)
186
+ gaussian_2d = np.exp(-((X - mu_x) ** 2) / (2 * sigma_x ** 2) - ((Y - mu_y) ** 2) / (2 * sigma_y ** 2))
187
+
188
+ field = _normalize_to_unit(gaussian_2d.ravel())
189
+ grid_size = grid_width * grid_height
190
+ total_size = 4 * grid_size
191
+ initial_state = np.zeros(total_size)
192
+ initial_state[:grid_size] = field
193
+ return initial_state
194
+
195
+ # --- Simulation Code (from previous context) ---
196
+ def Wj_block(j, n, ctrl_state, theta, lam, name='Wj_block', xgate=False):
197
+ qc = QuantumCircuit(n + j, name=name)
198
+ if j > 1: qc.cx(n + j - 1, range(n, n + j - 1))
199
+ if lam != 0: qc.p(lam, n + j - 1)
200
+ qc.h(n + j - 1)
201
+ if xgate and j > 1:
202
+ if isinstance(xgate, (list, tuple)):
203
+ for idx, flag in enumerate(xgate):
204
+ if flag: qc.x(n + idx)
205
+ elif xgate is True: qc.x(range(n, n + j - 1))
206
+ if j > 1:
207
+ mcrz = RZGate(theta).control(len(ctrl_state) + j - 1, ctrl_state="1" * (j - 1) + ctrl_state)
208
+ qc.append(mcrz, range(0, n + j))
209
+ else:
210
+ mcrz = RZGate(theta).control(len(ctrl_state), ctrl_state=ctrl_state)
211
+ qc.append(mcrz, range(0, n + j))
212
+ if xgate and j > 1:
213
+ if isinstance(xgate, (list, tuple)):
214
+ for idx, flag in enumerate(xgate):
215
+ if flag: qc.x(n + idx)
216
+ elif xgate is True: qc.x(range(n, n + j - 1))
217
+ qc.h(n + j - 1)
218
+ if lam != 0: qc.p(-lam, n + j - 1)
219
+ if j > 1: qc.cx(n + j - 1, range(n, n + j - 1))
220
+ return qc.to_gate(label=name)
221
+
222
+ def V1(nx, dt):
223
+ n = int(np.ceil(np.log2(nx)))
224
+ derivatives, blocks = QuantumRegister(2 * n), QuantumRegister(2)
225
+ qc = QuantumCircuit(derivatives, blocks)
226
+ qc.append(Wj_block(2, n, "0" * n, -dt, 0, xgate=True), list(derivatives[0:n]) + list(blocks[:]))
227
+ qc.append(Wj_block(3, n - 1, "1" * (n - 1), dt, 0, xgate=[0, 1]), list(derivatives[1:n]) + [derivatives[0]] + list(blocks[:]))
228
+ qc.append(Wj_block(1, n + 1, "0" * (n + 1), dt, 0, xgate=True), list(derivatives[n:2 * n]) + list(blocks[:]))
229
+ qc.append(Wj_block(2, n, "0" + "1" * (n - 1), -dt, 0, xgate=False), list(derivatives[n + 1:2 * n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
230
+ return qc
231
+
232
+ def V2(nx, dt):
233
+ n = int(np.ceil(np.log2(nx)))
234
+ derivatives, blocks = QuantumRegister(2 * n), QuantumRegister(2)
235
+ qc = QuantumCircuit(derivatives, blocks)
236
+ qc.append(Wj_block(2, 0, "", -2 * dt, -np.pi / 2, xgate=True), blocks[:])
237
+ for j in range(1, n + 1): qc.append(Wj_block(2 + j, 0, "", 2 * dt, -np.pi / 2, xgate=[1] * (j - 1) + [0, 1]), list(derivatives[0:j]) + list(blocks[:]))
238
+ qc.append(Wj_block(2, n, "0" * n, -dt, -np.pi / 2, xgate=True), list(derivatives[0:n]) + list(blocks[:]))
239
+ qc.append(Wj_block(2, n, "1" * n, 2 * dt, -np.pi / 2, xgate=True), list(derivatives[0:n]) + list(blocks[:]))
240
+ qc.append(Wj_block(3, n - 1, "1" * (n - 1), dt, -np.pi / 2, xgate=[0, 1]), list(derivatives[1:n]) + [derivatives[0]] + list(blocks[:]))
241
+ qc.append(Wj_block(1, 1, "0", 2 * dt, -np.pi / 2, xgate=False), blocks[:])
242
+ for j in range(1, n + 1): qc.append(Wj_block(1 + j, 1, "0", -2 * dt, -np.pi / 2, xgate=[1] * (j - 1)), [blocks[0]] + list(derivatives[n:n + j]) + [blocks[1]])
243
+ qc.append(Wj_block(1, n + 1, "0" * (n + 1), dt, -np.pi / 2, xgate=False), list(derivatives[n:2 * n]) + list(blocks[:]))
244
+ qc.append(Wj_block(1, n + 1, "0" + "1" * n, -2 * dt, -np.pi / 2, xgate=False), list(derivatives[n:2 * n]) + list(blocks[:]))
245
+ qc.append(Wj_block(2, n, "0" + "1" * (n - 1), -dt, -np.pi / 2, xgate=False), list(derivatives[n + 1:2 * n]) + [blocks[0]] + [derivatives[n]] + [blocks[1]])
246
+ return qc
247
+
248
+ def run_sim(nx, na, R, initial_state, T, snapshot_dt=None, stop_check=None, progress_callback=None):
249
+ """
250
+ Runs the quantum simulation for electromagnetic scattering with fixed dt=0.1.
251
+ Captures frames only at user-defined snapshot times: [0, Δt, 2Δt, ..., ≤ T_eff],
252
+ always including t=0 and the final solver-aligned T (T_eff = floor(T/dt)*dt).
253
+
254
+ Returns:
255
+ frames (np.ndarray), snapshot_times (np.ndarray)
256
+ """
257
+ dt = 0.1
258
+ # Validate total time and compute solver-aligned end time
259
+ try:
260
+ T_val = float(T)
261
+ except Exception:
262
+ return np.array([]), np.array([])
263
+ if T_val <= 0:
264
+ return np.array([]), np.array([])
265
+
266
+ steps = int(np.floor(T_val / dt))
267
+ if steps <= 0:
268
+ return np.array([]), np.array([])
269
+ T_eff = steps * dt
270
+
271
+ # Determine snapshot Δt on solver grid
272
+ tol = 1e-12
273
+ if snapshot_dt is None:
274
+ snapshot_dt_val = dt
275
+ else:
276
+ try:
277
+ snapshot_dt_val = float(snapshot_dt)
278
+ except Exception:
279
+ snapshot_dt_val = dt
280
+ if snapshot_dt_val < dt - tol:
281
+ snapshot_dt_val = dt
282
+ k = max(1, int(round(snapshot_dt_val / dt)))
283
+ snapshot_dt_eff = k * dt
284
+
285
+ # Build requested snapshot times on solver grid
286
+ target_times = [0.0]
287
+ t = 0.0
288
+ while t + snapshot_dt_eff <= T_eff + tol:
289
+ t = round(t + snapshot_dt_eff, 12)
290
+ if t <= T_eff + tol:
291
+ target_times.append(min(t, T_eff))
292
+ if abs(target_times[-1] - T_eff) > tol:
293
+ target_times.append(T_eff)
294
+
295
+ # Setup circuit
296
+ nq = int(np.ceil(np.log2(nx)))
297
+ dp = 2 * R * np.pi / 2 ** na
298
+ p = np.arange(-R * np.pi, R * np.pi, step=dp)
299
+ fp = np.exp(-np.abs(p))
300
+ system, ancilla = QuantumRegister(2 * nq + 2), QuantumRegister(na)
301
+ qc = QuantumCircuit(system, ancilla)
302
+ qc.append(StatePreparation(initial_state), system)
303
+ qc.append(StatePreparation(fp / np.linalg.norm(fp)), ancilla)
304
+ expA1 = V1(nx, dt).to_gate()
305
+ expA2 = V2(nx, dt)
306
+
307
+ frames = []
308
+ # Capture initial frame at t=0
309
+ sv0 = np.real(Statevector(qc)).reshape(2 ** na, 2 ** (2 * nq + 2))
310
+ frames.append(sv0[2 ** (na - 1)])
311
+ next_idx = 1 # next target_times index to capture
312
+
313
+ for i in range(steps):
314
+ if stop_check and stop_check():
315
+ print(f"Simulation interrupted at step {i}/{steps}")
316
+ break
317
+ # One solver step
318
+ qc.append(QFTGate(na), ancilla)
319
+ qc.x(ancilla[-1])
320
+ for j in range(na - 1):
321
+ qc.append(expA1.control().repeat(2 ** j), [ancilla[j]] + system[:])
322
+ qc.append(expA1.inverse().control(ctrl_state="0").repeat(2 ** (na - 1)), [ancilla[na - 1]] + system[:])
323
+ qc.append(expA2, system[:])
324
+ qc.x(ancilla[-1])
325
+ qc.append(QFTGate(na).inverse(), ancilla)
326
+
327
+ current_time = (i + 1) * dt
328
+ if next_idx < len(target_times) and abs(current_time - target_times[next_idx]) <= tol:
329
+ u = np.real(Statevector(qc)).reshape(2 ** na, 2 ** (2 * nq + 2))
330
+ frames.append(u[2 ** (na - 1)])
331
+ next_idx += 1
332
+
333
+ if progress_callback:
334
+ try:
335
+ progress = ((i + 1) / steps) * 100
336
+ progress_callback(progress)
337
+ except Exception:
338
+ pass
339
+
340
+ if progress_callback:
341
+ try:
342
+ progress_callback(100.0)
343
+ except Exception:
344
+ pass
345
+
346
+ # Ensure snapshot_times align with number of captured frames (covers early stop)
347
+ frames_arr = np.asarray(frames)
348
+ times_arr = np.asarray(target_times[: len(frames_arr)])
349
+ return frames_arr, times_arr
350
+
351
+ def create_impulse_preview_state(preview_n: int, pos01, sigma01: float = 0.02):
352
+ """
353
+ Smooth delta-like preview on a unit square using a narrow Gaussian (sigma in [0,1]).
354
+ Preview-only helper, independent of simulation grid size (nx). Use this for the
355
+ Excitation preview; use the *_from_pos() variants for the actual simulation.
356
+ """
357
+ try:
358
+ sx = float(sigma01) if sigma01 and sigma01 > 0 else 0.02
359
+ except Exception:
360
+ sx = 0.02
361
+ return create_gaussian_state_from_pos((int(preview_n), int(preview_n)), (float(pos01[0]), float(pos01[1])), (sx, sx))
362
+
363
+
364
+
365
+
366
+
367
+
368
+ ##### IBM QPU Simulation Code Below (Do Not Edit) #####
369
+
370
+ from .base_functions import *
371
+
372
+ def create_time_frames(total_time, snapshot_interval):
373
+ dt = 0.1
374
+ tol = 1e-9
375
+ try:
376
+ T_val = float(total_time)
377
+ except (ValueError, TypeError):
378
+ return []
379
+ if T_val <= 0:
380
+ return []
381
+ steps = int(np.floor(T_val / dt))
382
+ if steps <= 0:
383
+ return [0.0]
384
+ T_eff = steps * dt
385
+ try:
386
+ snapshot_dt_val = float(snapshot_interval)
387
+ except (ValueError, TypeError):
388
+ snapshot_dt_val = dt
389
+ if snapshot_dt_val < dt:
390
+ snapshot_dt_val = dt
391
+ k = max(1, int(round(snapshot_dt_val / dt)))
392
+ snapshot_dt_eff = k * dt
393
+ times = np.arange(0, T_eff + tol, snapshot_dt_eff)
394
+ if abs(times[-1] - T_eff) > tol:
395
+ times = np.append(times, T_eff)
396
+ times = np.round(times, 12)
397
+ unique_times = []
398
+ for t in times:
399
+ if not unique_times or abs(t - unique_times[-1]) > tol:
400
+ unique_times.append(float(t))
401
+ return unique_times
402
+
403
+
404
+
405
+ def run_qpu(field, x, y, T, snapshot_time, nx, initial_state, impulse_pos):
406
+ """
407
+ Compute time-series field values. Supports single-point and multi-point modes.
408
+
409
+ - Single-point (backward compatible): x, y are integers; returns list[float].
410
+ - Multi-point: x is a list/tuple of (ix, iy) integer pairs and y is None; returns dict[(ix,iy) -> list[float]].
411
+ """
412
+ na = 1
413
+ dt = 0.1
414
+ R = 4
415
+ nq = int(np.ceil(np.log2(nx)))
416
+
417
+ # Normalize monitor points input
418
+ if isinstance(x, (list, tuple)) and y is None:
419
+ points = [tuple(map(int, pt)) for pt in x]
420
+ multi = True
421
+ else:
422
+ points = [(int(x), int(y))]
423
+ multi = False
424
+
425
+ xref, yref = impulse_pos
426
+
427
+ offset = 0
428
+ grid_dims = (nx, nx)
429
+ initial_state = create_impulse_state(grid_dims, impulse_pos)
430
+
431
+ dp = 2 * R * np.pi / 2**na
432
+ p = np.arange(- R * np.pi, R * np.pi, step=dp)
433
+ fp = np.exp(-np.abs(p))
434
+ norm = np.linalg.norm(fp)
435
+
436
+ time_frames = create_time_frames(T, snapshot_time)
437
+
438
+ # Prepare outputs
439
+ if multi:
440
+ series_by_point = { (px, py): [] for (px, py) in points }
441
+ else:
442
+ series_single = []
443
+
444
+ for time in time_frames:
445
+ steps = int(math.ceil(time / dt))
446
+ # Reference Ez field at impulse location for sign
447
+ Eref = Eref_value(nx, nq, R, dt, na, steps, xref, yref, field_ref='Ez')
448
+
449
+ for (px, py) in points:
450
+ circ_magnitude = circ_for_magnitude(field, px, py, nx, na, R, dt, initial_state, steps)
451
+ magnitude = get_absolute_field_value(circ_magnitude, nq, na, offset, norm)
452
+
453
+ if field == 'Ez' and px == xref and py == yref:
454
+ Field_value = -magnitude if Eref < 0 else magnitude
455
+ else:
456
+ circsum, circdiff = circuits_for_sign(field, px, py, nx, na, dt, R, initial_state, steps, xref, yref, field_ref='Ez')
457
+ sign = get_relative_sign(circsum, circdiff, nq, na)
458
+ if (sign == 'same' and Eref > 0) or (sign == 'different' and Eref < 0):
459
+ Field_value = magnitude
460
+ else:
461
+ Field_value = -magnitude
462
+
463
+ if multi:
464
+ series_by_point[(px, py)].append(Field_value)
465
+ else:
466
+ series_single.append(Field_value)
467
+
468
+ return series_by_point if multi else series_single
utils/fdtd_demo.ipynb ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 2,
6
+ "id": "c3fb397b",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import numpy as np\n",
11
+ "import scipy.sparse as sp\n",
12
+ "import math\n",
13
+ "import matplotlib.pyplot as plt\n",
14
+ "from scipy.special import jn\n",
15
+ "from scipy.sparse import identity, csr_matrix, kron, diags, eye\n",
16
+ "from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister\n",
17
+ "from qiskit.circuit.library import MCXGate, MCPhaseGate, RXGate, CRXGate, QFTGate, StatePreparation, PauliEvolutionGate, RZGate\n",
18
+ "from qiskit.quantum_info import SparsePauliOp, Statevector, Operator, Pauli\n",
19
+ "from scipy.linalg import expm\n",
20
+ "# from tools import *\n",
21
+ "from base_functions import *\n",
22
+ "from qiskit.qasm3 import dumps # QASM 3 exporter\n",
23
+ "from qiskit.qasm3 import loads\n",
24
+ "from qiskit.circuit.library import QFT\n",
25
+ "from qiskit.primitives import StatevectorEstimator\n",
26
+ "from qiskit import transpile"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "9c5ad55d",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "# Inputs\n",
37
+ "nx = 16\n",
38
+ "grid_dims=(nx,nx)\n",
39
+ "field = 'Hx' \n",
40
+ "x = 7\n",
41
+ "y = 7\n",
42
+ "T = 3\n",
43
+ "initial_state = 'delta'\n",
44
+ "impulse_pos = (nx//2,nx//2)"
45
+ ]
46
+ },
47
+ {
48
+ "cell_type": "markdown",
49
+ "id": "ddb3e66c",
50
+ "metadata": {},
51
+ "source": [
52
+ "# Get field at any point"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": null,
58
+ "id": "f885e0a1",
59
+ "metadata": {},
60
+ "outputs": [
61
+ {
62
+ "name": "stdout",
63
+ "output_type": "stream",
64
+ "text": [
65
+ "Time step 0: Ez=2.2376563277501641e-07, Hy=2.2001300842051478e-07, Hx=2.2101993820490713e-07\n",
66
+ "Time step 1: Ez=0.03713901686823572, Hy=0.07449276224003892, Hx=-0.09915963436446357\n",
67
+ "Time step 2: Ez=0.1637729220450236, Hy=0.057764577526220356, Hx=-0.06137018919000033\n"
68
+ ]
69
+ }
70
+ ],
71
+ "source": [
72
+ "def get_field_value(field, x, y, T, nx, initial_state, impulse_pos):\n",
73
+ " na = 1\n",
74
+ " dt = 0.1\n",
75
+ " R = 4\n",
76
+ " nq = int(np.ceil(np.log2(nx)))\n",
77
+ " \n",
78
+ " xref, yref = impulse_pos\n",
79
+ "\n",
80
+ " offset = 0\n",
81
+ " # deltastate = np.zeros(4*nx*nx)\n",
82
+ " # deltastate[nx*nx//2+nx//2:nx*nx//2+nx//2+1] = 1\n",
83
+ " # deltastate[0:nx*nx] = deltastate[0:nx*nx] + offset\n",
84
+ " # norm1 = np.linalg.norm(deltastate)\n",
85
+ " # initial_state = deltastate/norm1\n",
86
+ " grid_dims = (nx,nx)\n",
87
+ " initial_state = create_impulse_state(grid_dims, impulse_pos)\n",
88
+ "\n",
89
+ " dp = 2 * R * np.pi / 2**na\n",
90
+ " p = np.arange(- R * np.pi, R * np.pi, step=dp)\n",
91
+ " fp = np.exp(-np.abs(p))\n",
92
+ " norm = np.linalg.norm(fp)\n",
93
+ "\n",
94
+ " steps = int(math.ceil(T / dt))\n",
95
+ "\n",
96
+ " Eref = Eref_value(nx, nq, R, dt, na, steps, xref, yref, field_ref = 'Ez')\n",
97
+ " \n",
98
+ " circ_magnitude = circ_for_magnitude(field, x, y, nx, na, R, dt, initial_state, steps)\n",
99
+ " magnitude = get_absolute_field_value(circ_magnitude, nq, na, offset, norm)\n",
100
+ "\n",
101
+ " if field=='Ez' and x == xref and y == yref:\n",
102
+ " if Eref< 0:\n",
103
+ " Field_value = -1*magnitude\n",
104
+ " else:\n",
105
+ " Field_value = magnitude\n",
106
+ " else: \n",
107
+ " circsum, circdiff = circuits_for_sign(field, x, y, nx, na, dt, R, initial_state, steps, xref, yref, field_ref = 'Ez')\n",
108
+ " sign = get_relative_sign(circsum, circdiff, nq, na)\n",
109
+ "\n",
110
+ " if (sign == 'same' and Eref > 0) or (sign == 'different' and Eref < 0):\n",
111
+ "\n",
112
+ " Field_value = magnitude\n",
113
+ " else:\n",
114
+ " Field_value = -1*magnitude\n",
115
+ "\n",
116
+ " return Field_value\n",
117
+ "\n",
118
+ "for i in range(T):\n",
119
+ " Exz = get_field_value('Ez', x, y, i, nx, initial_state, impulse_pos)\n",
120
+ " Hy = get_field_value('Hy', x, y, i, nx, initial_state, impulse_pos)\n",
121
+ " Hx = get_field_value('Hx', x, y, i, nx, initial_state, impulse_pos)\n",
122
+ " print(f'Time step {i}: Ez={Exz}, Hy={Hy}, Hx={Hx}')\n",
123
+ "\n",
124
+ "\n",
125
+ "# pl"
126
+ ]
127
+ },
128
+ {
129
+ "cell_type": "code",
130
+ "execution_count": null,
131
+ "id": "1c7af009",
132
+ "metadata": {},
133
+ "outputs": [],
134
+ "source": [
135
+ "import numpy as np\n",
136
+ "\n",
137
+ "def create_time_frames(total_time, snapshot_interval):\n",
138
+ " \"\"\"\n",
139
+ " Generates a list of snapshot times aligned with the quantum solver's fixed timestep.\n",
140
+ "\n",
141
+ " The solver uses a fixed internal timestep (dt=0.1). This function calculates\n",
142
+ " the effective total time and snapshot interval that align with this grid,\n",
143
+ " replicating the logic from the `run_sim` function.\n",
144
+ "\n",
145
+ " Args:\n",
146
+ " total_time (float): The desired total simulation time (T).\n",
147
+ " snapshot_interval (float): The desired time between snapshots (Δt).\n",
148
+ "\n",
149
+ " Returns:\n",
150
+ " list: A list of floats representing the actual snapshot time frames.\n",
151
+ " \"\"\"\n",
152
+ " # Solver's fixed internal timestep\n",
153
+ " dt = 0.1\n",
154
+ " tol = 1e-9\n",
155
+ "\n",
156
+ " # Validate inputs and compute the solver-aligned end time (T_eff)\n",
157
+ " try:\n",
158
+ " T_val = float(total_time)\n",
159
+ " except (ValueError, TypeError):\n",
160
+ " return []\n",
161
+ " if T_val <= 0:\n",
162
+ " return []\n",
163
+ "\n",
164
+ " steps = int(np.floor(T_val / dt))\n",
165
+ " if steps <= 0:\n",
166
+ " return [0.0] # Only the initial frame at t=0 exists\n",
167
+ " T_eff = steps * dt\n",
168
+ "\n",
169
+ " # Determine the effective snapshot interval on the solver grid (snapshot_dt_eff)\n",
170
+ " try:\n",
171
+ " snapshot_dt_val = float(snapshot_interval)\n",
172
+ " except (ValueError, TypeError):\n",
173
+ " snapshot_dt_val = dt # Default to solver dt if invalid\n",
174
+ "\n",
175
+ " if snapshot_dt_val < dt - tol:\n",
176
+ " snapshot_dt_val = dt # Clamp to the minimum possible interval\n",
177
+ " \n",
178
+ " # Snap the interval to the nearest multiple of the solver's dt\n",
179
+ " k = max(1, int(round(snapshot_dt_val / dt)))\n",
180
+ " snapshot_dt_eff = k * dt\n",
181
+ "\n",
182
+ " # Build the list of target times using the same logic as run_sim\n",
183
+ " target_times = [0.0]\n",
184
+ " current_time = 0.0\n",
185
+ " while current_time + snapshot_dt_eff <= T_eff + tol:\n",
186
+ " current_time = round(current_time + snapshot_dt_eff, 12)\n",
187
+ " target_times.append(min(current_time, T_eff))\n",
188
+ " \n",
189
+ " # Ensure the effective end time is included if the loop steps over it\n",
190
+ " if abs(target_times[-1] - T_eff) > tol:\n",
191
+ " target_times.append(T_eff)\n",
192
+ " \n",
193
+ " # Remove duplicates that might arise from floating point issues\n",
194
+ " unique_times = []\n",
195
+ " for t in target_times:\n",
196
+ " if not unique_times or abs(t - unique_times[-1]) > tol:\n",
197
+ " unique_times.append(t)\n",
198
+ "\n",
199
+ " return unique_times\n",
200
+ "\n",
201
+ "# --- Example ---\n",
202
+ "T = 1.0\n",
203
+ "# Let's use an interval that is not a multiple of 0.1\n",
204
+ "snapshot_time = 0.3\n",
205
+ "# The function will snap 0.33 to the nearest multiple of 0.1, which is 0.3 (k=3)\n",
206
+ "\n",
207
+ "time_frames = create_time_frames(T, snapshot_time)\n",
208
+ "\n",
209
+ "print(f\"Total Time: {T}\")\n",
210
+ "print(f\"Requested Snapshot Interval: {snapshot_time}\")\n",
211
+ "print(f\"Generated Time Frames: {time_frames}\")\n",
212
+ "\n",
213
+ "\n",
214
+ "\n",
215
+ "\n"
216
+ ]
217
+ },
218
+ {
219
+ "cell_type": "code",
220
+ "execution_count": null,
221
+ "id": "719cbaf4",
222
+ "metadata": {},
223
+ "outputs": [],
224
+ "source": [
225
+ "def run_qpu(field, x, y, T, snapshot_time, nx, initial_state, impulse_pos):\n",
226
+ " na = 1\n",
227
+ " dt = 0.1\n",
228
+ " R = 4\n",
229
+ " nq = int(np.ceil(np.log2(nx)))\n",
230
+ " \n",
231
+ " xref, yref = impulse_pos\n",
232
+ "\n",
233
+ " offset = 0\n",
234
+ " # deltastate = np.zeros(4*nx*nx)\n",
235
+ " # deltastate[nx*nx//2+nx//2:nx*nx//2+nx//2+1] = 1\n",
236
+ " # deltastate[0:nx*nx] = deltastate[0:nx*nx] + offset\n",
237
+ " # norm1 = np.linalg.norm(deltastate)\n",
238
+ " # initial_state = deltastate/norm1\n",
239
+ " grid_dims = (nx,nx)\n",
240
+ " initial_state = create_impulse_state(grid_dims, impulse_pos)\n",
241
+ " dp = 2 * R * np.pi / 2**na\n",
242
+ " p = np.arange(- R * np.pi, R * np.pi, step=dp)\n",
243
+ " fp = np.exp(-np.abs(p))\n",
244
+ " norm = np.linalg.norm(fp)\n",
245
+ "\n",
246
+ " time_frames = create_time_frames(T, snapshot_time)\n",
247
+ " field_values = []\n",
248
+ " for time in time_frames:\n",
249
+ " \n",
250
+ " steps = int(math.ceil(time / dt))\n",
251
+ " # use \n",
252
+ "\n",
253
+ " Eref = Eref_value(nx, nq, R, dt, na, steps, xref, yref, field_ref = 'Ez')\n",
254
+ "\n",
255
+ " circ_magnitude = circ_for_magnitude(field, x, y, nx, na, R, dt, initial_state, steps)\n",
256
+ " magnitude = get_absolute_field_value(circ_magnitude, nq, na, offset, norm)\n",
257
+ "\n",
258
+ " if field=='Ez' and x == xref and y == yref:\n",
259
+ " if Eref< 0:\n",
260
+ " Field_value = -1*magnitude\n",
261
+ " else:\n",
262
+ " Field_value = magnitude\n",
263
+ " else: \n",
264
+ " circsum, circdiff = circuits_for_sign(field, x, y, nx, na, dt, R, initial_state, steps, xref, yref, field_ref = 'Ez')\n",
265
+ " sign = get_relative_sign(circsum, circdiff, nq, na)\n",
266
+ "\n",
267
+ " if (sign == 'same' and Eref > 0) or (sign == 'different' and Eref < 0):\n",
268
+ "\n",
269
+ " Field_value = magnitude\n",
270
+ " else:\n",
271
+ " Field_value = -1*magnitude\n",
272
+ " field_values.append(Field_value)\n",
273
+ " return field_values\n",
274
+ "\n",
275
+ "\n",
276
+ "Exz = get_field_value('Ez', x, y, T, 0.3, nx, initial_state, impulse_pos)\n",
277
+ "Hy = get_field_value('Hy', x, y, T, 0.3, nx, initial_state, impulse_pos)\n",
278
+ "Hx = get_field_value('Hx', x, y, T, 0.3, nx, initial_state, impulse_pos)\n",
279
+ "# pl"
280
+ ]
281
+ },
282
+ {
283
+ "cell_type": "code",
284
+ "execution_count": 27,
285
+ "id": "76ded7b1",
286
+ "metadata": {},
287
+ "outputs": [
288
+ {
289
+ "name": "stdout",
290
+ "output_type": "stream",
291
+ "text": [
292
+ "[np.float64(2.2376563277501641e-07), np.float64(9.736590876164155e-05), np.float64(0.0044225353473238554), np.float64(0.024957214558959943), np.float64(0.03713901686823572)]\n",
293
+ "[np.float64(2.2001300842051478e-07), np.float64(0.0009704034518815665), np.float64(0.01717008973114835), np.float64(0.05764097025932368), np.float64(0.07449276224003892)]\n",
294
+ "[np.float64(2.2101993820490713e-07), np.float64(-0.0038329627633077764), np.float64(-0.029433776288711664), np.float64(-0.07996477735815476), np.float64(-0.09915963436446357)]\n"
295
+ ]
296
+ }
297
+ ],
298
+ "source": [
299
+ "print(Exz)\n",
300
+ "print(Hy)\n",
301
+ "print(Hx)"
302
+ ]
303
+ }
304
+ ],
305
+ "metadata": {
306
+ "kernelspec": {
307
+ "display_name": "Python 3",
308
+ "language": "python",
309
+ "name": "python3"
310
+ },
311
+ "language_info": {
312
+ "codemirror_mode": {
313
+ "name": "ipython",
314
+ "version": 3
315
+ },
316
+ "file_extension": ".py",
317
+ "mimetype": "text/x-python",
318
+ "name": "python",
319
+ "nbconvert_exporter": "python",
320
+ "pygments_lexer": "ipython3",
321
+ "version": "3.11.9"
322
+ }
323
+ },
324
+ "nbformat": 4,
325
+ "nbformat_minor": 5
326
+ }
utils/tempCodeRunnerFile.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from tools import *