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

Upload 8 files

Browse files

" First Commit: Application and other files added"

.gitattributes CHANGED
@@ -33,3 +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
 
 
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
Dockerfile ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Start from a slim Python 3.11 base image
2
+ FROM python:3.11-slim
3
+
4
+ # 2. Set Environment Variables
5
+ # - DEBIAN_FRONTEND: Prevents installers from asking interactive questions
6
+ # - PYTHONUNBUFFERED/PYTHONDONTWRITEBYTECODE: Standard Python-in-Docker settings
7
+ # - PYVISTA_OFF_SCREEN/DISPLAY: Crucial for running PyVista headless (off-screen)
8
+ # by telling it to use a "virtual" display at address :99
9
+ ENV DEBIAN_FRONTEND=noninteractive \
10
+ PYTHONUNBUFFERED=1 \
11
+ PYTHONDONTWRITEBYTECODE=1 \
12
+ HOME=/home/user \
13
+ PATH=/home/user/.local/bin:$PATH \
14
+ PYVISTA_OFF_SCREEN=true \
15
+ DISPLAY=:99 \
16
+ VTK_SILENCE_GET_VOID_POINTER_WARNINGS=1
17
+
18
+ # 3. Install System Dependencies
19
+ # This is the most critical part for PyVista/VTK. We need the OS
20
+ # graphics libraries (libosmesa, libgl1) and the X Virtual FrameBuffer (xvfb).
21
+ #
22
+ # *** UPDATED this section to use current package names (e.g., libgl1, libegl1) ***
23
+ #
24
+ RUN apt-get update && apt-get install -y --no-install-recommends \
25
+ build-essential cmake wget xvfb \
26
+ libosmesa6 libosmesa6-dev \
27
+ libgl1 libgl1-mesa-dev \
28
+ libegl1 libegl1-mesa-dev \
29
+ libglu1-mesa libglu1-mesa-dev \
30
+ libgles2-mesa-dev \
31
+ libx11-6 libxt6 libxrender1 libsm6 libice6 \
32
+ && rm -rf /var/lib/apt/lists/*
33
+
34
+ # 4. Create a non-root user for security
35
+ RUN useradd -m -u 1000 user
36
+ WORKDIR /home/user/app
37
+
38
+ # 5. Install Python dependencies (optimized)
39
+ # We copy *only* requirements.txt first and install it.
40
+ # This "layer" is cached by Docker. If you only change app.py later,
41
+ # Docker skips this step, making builds much faster.
42
+ COPY requirements.txt .
43
+ RUN python3 -m pip install --upgrade pip setuptools wheel \
44
+ && python3 -m pip install --no-cache-dir -r requirements.txt
45
+
46
+ # 6. Copy the rest of the application code
47
+ # This copies app.py, delta_impulse_generator.py, etc.
48
+ # We set the owner to our new 'user'.
49
+ COPY --chown=user:user . .
50
+
51
+ # 7. Switch to the non-root user
52
+ USER user
53
+
54
+ # 8. Expose the port the app will run on
55
+ EXPOSE 7860
56
+
57
+ # 9. Healthcheck (good practice for hosting platforms)
58
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
59
+ CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT:-7860}/ || exit 1
60
+
61
+ # 10. Start Command
62
+ # This command does two things:
63
+ # a) Starts the X Virtual FrameBuffer (Xvfb) in the background (&) on display :99
64
+ # b) 'exec' runs your app. Using 'exec' is important as it makes the Python
65
+ # process the main one, which properly handles signals (like stopping the container).
66
+ # '--host 0.0.0.0' is ESSENTIAL to make the server accessible from outside the container.
67
+ CMD ["sh", "-c", "Xvfb :99 -screen 0 1024x768x24 >/dev/null 2>&1 & exec python3 app.py --server --host 0.0.0.0 --port ${PORT:-7860}"]
68
+
69
+
README.md CHANGED
@@ -1,10 +1,10 @@
1
- ---
2
- title: Quantum
3
- emoji: 📈
4
- colorFrom: gray
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: Quantum
3
+ emoji: 📈
4
+ colorFrom: gray
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,1547 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"),
87
+ ]
88
+ for p in candidates:
89
+ if os.path.exists(p):
90
+ ext = os.path.splitext(p)[1].lower()
91
+ mime = "image/svg+xml" if ext == ".svg" else ("image/png" if ext == ".png" else "image/jpeg")
92
+ with open(p, "rb") as f:
93
+ b64 = base64.b64encode(f.read()).decode("ascii")
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
+
delta_impulse_generator.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from qiskit.circuit import QuantumCircuit, QuantumRegister
3
+ from qiskit.circuit.library import StatePreparation, QFTGate, RZGate
4
+ from qiskit.quantum_info import Statevector
5
+ import pyvista as pv
6
+
7
+ def create_impulse_state(grid_dims, impulse_pos):
8
+ """
9
+ Creates an initial state vector with a single delta impulse at a specified grid position.
10
+
11
+ The 2D grid is flattened into a 1D vector in row-major order, and this
12
+ vector is then padded to match the full simulation state space size (4x).
13
+
14
+ Args:
15
+ grid_dims (tuple): A tuple (width, height) defining the simulation grid dimensions.
16
+ For your original code, this would be (nx, nx).
17
+ impulse_pos (tuple): A tuple (x, y) for the position of the impulse.
18
+ Coordinates are 0-indexed.
19
+
20
+ Returns:
21
+ numpy.ndarray: The full, padded initial state vector with a single 1.
22
+
23
+ Raises:
24
+ ValueError: If the impulse position is outside the grid dimensions.
25
+ """
26
+ grid_width, grid_height = grid_dims
27
+ impulse_x, impulse_y = impulse_pos
28
+
29
+ # --- Input Validation ---
30
+ # Ensure the requested impulse position is actually on the grid.
31
+ if not (0 <= impulse_x < grid_width and 0 <= impulse_y < grid_height):
32
+ raise ValueError(f"Impulse position ({impulse_x}, {impulse_y}) is outside the "
33
+ f"grid dimensions ({grid_width}x{grid_height}).")
34
+
35
+ # --- 1. Calculate the 1D Array Index ---
36
+ # Convert the (x, y) coordinate to a single index in a flattened 1D array.
37
+ # The formula for row-major order is: index = y_coord * width + x_coord
38
+ flat_index = impulse_y * grid_width + impulse_x
39
+
40
+ # --- 2. Create the Full, Padded State Vector ---
41
+ grid_size = grid_width * grid_height
42
+ total_size = 4 * grid_size # The simulation space is 4x the grid size.
43
+ initial_state = np.zeros(total_size)
44
+
45
+ # --- 3. Set the Delta Impulse ---
46
+ initial_state[flat_index] = 1
47
+
48
+ return initial_state
49
+
50
+ def create_gaussian_state(grid_dims, mu, sigma):
51
+ """
52
+ Creates an initial state vector with a 2D Gaussian distribution.
53
+
54
+ The state is normalized and padded to match the full simulation state space size (4x).
55
+
56
+ Args:
57
+ grid_dims (tuple): A tuple (width, height) defining the grid dimensions.
58
+ mu (tuple): A tuple (mu_x, mu_y) for the center (mean) of the Gaussian.
59
+ sigma (tuple): A tuple (sigma_x, sigma_y) for the standard deviation (spread).
60
+
61
+ Returns:
62
+ numpy.ndarray: The full, padded initial state vector for the Gaussian state.
63
+
64
+ Raises:
65
+ ValueError: If sigma values are not positive.
66
+ """
67
+ grid_width, grid_height = grid_dims
68
+ mu_x, mu_y = mu
69
+ sigma_x, sigma_y = sigma
70
+
71
+ if sigma_x <= 0 or sigma_y <= 0:
72
+ raise ValueError("Sigma values (spread) must be positive.")
73
+
74
+ # --- 1. Create a Coordinate Grid ---
75
+ x = np.arange(0, grid_width)
76
+ y = np.arange(0, grid_height)
77
+ X, Y = np.meshgrid(x, y)
78
+
79
+ # --- 2. Calculate the 2D Gaussian Function ---
80
+ gaussian_2d = np.exp(-((X - mu_x)**2 / (2 * sigma_x**2)) -
81
+ ((Y - mu_y)**2 / (2 * sigma_y**2)))
82
+
83
+ # --- 3. Normalize the State Vector ---
84
+ # For a valid quantum state, the L2 norm (sum of squares of amplitudes) must be 1.
85
+ norm = np.linalg.norm(gaussian_2d)
86
+ if norm > 0:
87
+ gaussian_2d = gaussian_2d / norm
88
+
89
+ # --- 4. Flatten and Pad the Vector ---
90
+ gaussian_flat = gaussian_2d.flatten()
91
+ grid_size = grid_width * grid_height
92
+ total_size = 4 * grid_size
93
+ initial_state = np.pad(gaussian_flat, (0, total_size - grid_size), mode='constant')
94
+
95
+ return initial_state
96
+
97
+
98
+
99
+
100
+
101
+ # --- New: Continuous-position helpers for excitation before meshing ---
102
+ def _normalize_to_unit(vec: np.ndarray) -> np.ndarray:
103
+ n = np.linalg.norm(vec)
104
+ return vec / n if n > 0 else vec
105
+
106
+
107
+
108
+
109
+ def create_impulse_state_from_pos(grid_dims, pos01):
110
+ """
111
+ Create a delta-like initial state from continuous position pos01=(x,y) in [0,1].
112
+
113
+ Why grid_dims?
114
+ - Simulation runs on a discrete nx×ny lattice; the continuous position must be
115
+ discretized onto that grid to produce the state vector fed into the solver.
116
+ - grid_dims provides (nx, ny) so we can map (x,y)∈[0,1]→grid coordinates via
117
+ gx = x*(nx-1), gy = y*(ny-1), then distribute amplitude bilinearly to the 4
118
+ neighboring nodes. This is required only for the simulation state, not the preview.
119
+
120
+ The preview uses create_impulse_preview_state(), which renders a smooth bump on a
121
+ fixed unit-square grid independent of nx for visualization.
122
+ """
123
+ grid_width, grid_height = grid_dims
124
+ px, py = pos01
125
+ px = float(max(0.0, min(1.0, px)))
126
+ py = float(max(0.0, min(1.0, py)))
127
+
128
+ gx = px * (grid_width - 1)
129
+ gy = py * (grid_height - 1)
130
+ i0, j0 = int(np.floor(gx)), int(np.floor(gy))
131
+ i1, j1 = min(i0 + 1, grid_width - 1), min(j0 + 1, grid_height - 1)
132
+ dx, dy = gx - i0, gy - j0
133
+
134
+ w00 = (1 - dx) * (1 - dy)
135
+ w10 = dx * (1 - dy)
136
+ w01 = (1 - dx) * dy
137
+ w11 = dx * dy
138
+
139
+ grid_size = grid_width * grid_height
140
+ total_size = 4 * grid_size
141
+ field = np.zeros(grid_size)
142
+ field[j0 * grid_width + i0] += w00
143
+ field[j0 * grid_width + i1] += w10
144
+ field[j1 * grid_width + i0] += w01
145
+ field[j1 * grid_width + i1] += w11
146
+ field = _normalize_to_unit(field)
147
+
148
+ initial_state = np.zeros(total_size)
149
+ initial_state[:grid_size] = field
150
+ return initial_state
151
+
152
+
153
+ def create_gaussian_state_from_pos(grid_dims, mu01, sigma01):
154
+ """
155
+ Create a Gaussian initial state with center mu01=(x,y) and spreads sigma01=(sx,sy)
156
+ in [0,1] of the domain, then discretize to the solver grid given by grid_dims.
157
+
158
+ Why grid_dims?
159
+ - The quantum solver expects a vector aligned to the chosen nx×ny simulation grid.
160
+ We convert normalized μ and σ (fractions of the domain) into grid units using
161
+ (nx-1) and (ny-1). This step is necessary for the simulation, not for the preview.
162
+
163
+ For preview-only rendering, use create_impulse_preview_state() to keep the visuals
164
+ continuous and independent of nx.
165
+ """
166
+ grid_width, grid_height = grid_dims
167
+ mu_x01, mu_y01 = mu01
168
+ sig_x01, sig_y01 = sigma01
169
+
170
+ mu_x01 = float(max(0.0, min(1.0, mu_x01)))
171
+ mu_y01 = float(max(0.0, min(1.0, mu_y01)))
172
+ sig_x01 = float(sig_x01)
173
+ sig_y01 = float(sig_y01)
174
+ if sig_x01 <= 0 or sig_y01 <= 0:
175
+ raise ValueError("Sigma values (spread) must be positive.")
176
+
177
+ mu_x = mu_x01 * (grid_width - 1)
178
+ mu_y = mu_y01 * (grid_height - 1)
179
+ sigma_x = sig_x01 * (grid_width - 1)
180
+ sigma_y = sig_y01 * (grid_height - 1)
181
+
182
+ x = np.arange(0, grid_width)
183
+ y = np.arange(0, grid_height)
184
+ X, Y = np.meshgrid(x, y)
185
+ gaussian_2d = np.exp(-((X - mu_x) ** 2) / (2 * sigma_x ** 2) - ((Y - mu_y) ** 2) / (2 * sigma_y ** 2))
186
+
187
+ field = _normalize_to_unit(gaussian_2d.ravel())
188
+ grid_size = grid_width * grid_height
189
+ total_size = 4 * grid_size
190
+ initial_state = np.zeros(total_size)
191
+ initial_state[:grid_size] = field
192
+ return initial_state
193
+
194
+ # --- Simulation Code (from previous context) ---
195
+ def Wj_block(j, n, ctrl_state, theta, lam, name='Wj_block', xgate=False):
196
+ qc = QuantumCircuit(n + j, name=name)
197
+ if j > 1: qc.cx(n + j - 1, range(n, n + j - 1))
198
+ if lam != 0: qc.p(lam, n + j - 1)
199
+ qc.h(n + j - 1)
200
+ if xgate and j > 1:
201
+ if isinstance(xgate, (list, tuple)):
202
+ for idx, flag in enumerate(xgate):
203
+ if flag: qc.x(n + idx)
204
+ elif xgate is True: qc.x(range(n, n + j - 1))
205
+ if j > 1:
206
+ mcrz = RZGate(theta).control(len(ctrl_state) + j - 1, ctrl_state="1" * (j - 1) + ctrl_state)
207
+ qc.append(mcrz, range(0, n + j))
208
+ else:
209
+ mcrz = RZGate(theta).control(len(ctrl_state), ctrl_state=ctrl_state)
210
+ qc.append(mcrz, range(0, n + j))
211
+ if xgate and j > 1:
212
+ if isinstance(xgate, (list, tuple)):
213
+ for idx, flag in enumerate(xgate):
214
+ if flag: qc.x(n + idx)
215
+ elif xgate is True: qc.x(range(n, n + j - 1))
216
+ qc.h(n + j - 1)
217
+ if lam != 0: qc.p(-lam, n + j - 1)
218
+ if j > 1: qc.cx(n + j - 1, range(n, n + j - 1))
219
+ return qc.to_gate(label=name)
220
+
221
+ def V1(nx, dt):
222
+ n = int(np.ceil(np.log2(nx)))
223
+ derivatives, blocks = QuantumRegister(2 * n), QuantumRegister(2)
224
+ qc = QuantumCircuit(derivatives, blocks)
225
+ qc.append(Wj_block(2, n, "0" * n, -dt, 0, xgate=True), list(derivatives[0:n]) + list(blocks[:]))
226
+ qc.append(Wj_block(3, n - 1, "1" * (n - 1), dt, 0, xgate=[0, 1]), list(derivatives[1:n]) + [derivatives[0]] + list(blocks[:]))
227
+ qc.append(Wj_block(1, n + 1, "0" * (n + 1), dt, 0, xgate=True), list(derivatives[n:2 * n]) + list(blocks[:]))
228
+ 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]])
229
+ return qc
230
+
231
+ def V2(nx, dt):
232
+ n = int(np.ceil(np.log2(nx)))
233
+ derivatives, blocks = QuantumRegister(2 * n), QuantumRegister(2)
234
+ qc = QuantumCircuit(derivatives, blocks)
235
+ qc.append(Wj_block(2, 0, "", -2 * dt, -np.pi / 2, xgate=True), blocks[:])
236
+ 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[:]))
237
+ qc.append(Wj_block(2, n, "0" * n, -dt, -np.pi / 2, xgate=True), list(derivatives[0:n]) + list(blocks[:]))
238
+ qc.append(Wj_block(2, n, "1" * n, 2 * dt, -np.pi / 2, xgate=True), list(derivatives[0:n]) + list(blocks[:]))
239
+ 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[:]))
240
+ qc.append(Wj_block(1, 1, "0", 2 * dt, -np.pi / 2, xgate=False), blocks[:])
241
+ 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]])
242
+ qc.append(Wj_block(1, n + 1, "0" * (n + 1), dt, -np.pi / 2, xgate=False), list(derivatives[n:2 * n]) + list(blocks[:]))
243
+ qc.append(Wj_block(1, n + 1, "0" + "1" * n, -2 * dt, -np.pi / 2, xgate=False), list(derivatives[n:2 * n]) + list(blocks[:]))
244
+ 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]])
245
+ return qc
246
+
247
+ def run_sim(nx, na, R, initial_state, T, snapshot_dt=None, stop_check=None, progress_callback=None):
248
+ """
249
+ Runs the quantum simulation for electromagnetic scattering with fixed dt=0.1.
250
+ Captures frames only at user-defined snapshot times: [0, Δt, 2Δt, ..., ≤ T_eff],
251
+ always including t=0 and the final solver-aligned T (T_eff = floor(T/dt)*dt).
252
+
253
+ Returns:
254
+ frames (np.ndarray), snapshot_times (np.ndarray)
255
+ """
256
+ dt = 0.1
257
+ # Validate total time and compute solver-aligned end time
258
+ try:
259
+ T_val = float(T)
260
+ except Exception:
261
+ return np.array([]), np.array([])
262
+ if T_val <= 0:
263
+ return np.array([]), np.array([])
264
+
265
+ steps = int(np.floor(T_val / dt))
266
+ if steps <= 0:
267
+ return np.array([]), np.array([])
268
+ T_eff = steps * dt
269
+
270
+ # Determine snapshot Δt on solver grid
271
+ tol = 1e-12
272
+ if snapshot_dt is None:
273
+ snapshot_dt_val = dt
274
+ else:
275
+ try:
276
+ snapshot_dt_val = float(snapshot_dt)
277
+ except Exception:
278
+ snapshot_dt_val = dt
279
+ if snapshot_dt_val < dt - tol:
280
+ snapshot_dt_val = dt
281
+ k = max(1, int(round(snapshot_dt_val / dt)))
282
+ snapshot_dt_eff = k * dt
283
+
284
+ # Build requested snapshot times on solver grid
285
+ target_times = [0.0]
286
+ t = 0.0
287
+ while t + snapshot_dt_eff <= T_eff + tol:
288
+ t = round(t + snapshot_dt_eff, 12)
289
+ if t <= T_eff + tol:
290
+ target_times.append(min(t, T_eff))
291
+ if abs(target_times[-1] - T_eff) > tol:
292
+ target_times.append(T_eff)
293
+
294
+ # Setup circuit
295
+ nq = int(np.ceil(np.log2(nx)))
296
+ dp = 2 * R * np.pi / 2 ** na
297
+ p = np.arange(-R * np.pi, R * np.pi, step=dp)
298
+ fp = np.exp(-np.abs(p))
299
+ system, ancilla = QuantumRegister(2 * nq + 2), QuantumRegister(na)
300
+ qc = QuantumCircuit(system, ancilla)
301
+ qc.append(StatePreparation(initial_state), system)
302
+ qc.append(StatePreparation(fp / np.linalg.norm(fp)), ancilla)
303
+ expA1 = V1(nx, dt).to_gate()
304
+ expA2 = V2(nx, dt)
305
+
306
+ frames = []
307
+ # Capture initial frame at t=0
308
+ sv0 = np.real(Statevector(qc)).reshape(2 ** na, 2 ** (2 * nq + 2))
309
+ frames.append(sv0[2 ** (na - 1)])
310
+ next_idx = 1 # next target_times index to capture
311
+
312
+ for i in range(steps):
313
+ if stop_check and stop_check():
314
+ print(f"Simulation interrupted at step {i}/{steps}")
315
+ break
316
+ # One solver step
317
+ qc.append(QFTGate(na), ancilla)
318
+ qc.x(ancilla[-1])
319
+ for j in range(na - 1):
320
+ qc.append(expA1.control().repeat(2 ** j), [ancilla[j]] + system[:])
321
+ qc.append(expA1.inverse().control(ctrl_state="0").repeat(2 ** (na - 1)), [ancilla[na - 1]] + system[:])
322
+ qc.append(expA2, system[:])
323
+ qc.x(ancilla[-1])
324
+ qc.append(QFTGate(na).inverse(), ancilla)
325
+
326
+ current_time = (i + 1) * dt
327
+ if next_idx < len(target_times) and abs(current_time - target_times[next_idx]) <= tol:
328
+ u = np.real(Statevector(qc)).reshape(2 ** na, 2 ** (2 * nq + 2))
329
+ frames.append(u[2 ** (na - 1)])
330
+ next_idx += 1
331
+
332
+ if progress_callback:
333
+ try:
334
+ progress = ((i + 1) / steps) * 100
335
+ progress_callback(progress)
336
+ except Exception:
337
+ pass
338
+
339
+ if progress_callback:
340
+ try:
341
+ progress_callback(100.0)
342
+ except Exception:
343
+ pass
344
+
345
+ # Ensure snapshot_times align with number of captured frames (covers early stop)
346
+ frames_arr = np.asarray(frames)
347
+ times_arr = np.asarray(target_times[: len(frames_arr)])
348
+ return frames_arr, times_arr
349
+
350
+ def create_impulse_preview_state(preview_n: int, pos01, sigma01: float = 0.02):
351
+ """
352
+ Smooth delta-like preview on a unit square using a narrow Gaussian (sigma in [0,1]).
353
+ Preview-only helper, independent of simulation grid size (nx). Use this for the
354
+ Excitation preview; use the *_from_pos() variants for the actual simulation.
355
+ """
356
+ try:
357
+ sx = float(sigma01) if sigma01 and sigma01 > 0 else 0.02
358
+ except Exception:
359
+ sx = 0.02
360
+ return create_gaussian_state_from_pos((int(preview_n), int(preview_n)), (float(pos01[0]), float(pos01[1])), (sx, sx))
requirements.txt ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core scientific computing
2
+ numpy==2.2.6
3
+ scipy==1.16.2 # Updated to actual latest (1.16.1 doesn't exist)
4
+
5
+ # 3D Visualization
6
+ pyvista==0.46.3
7
+ vtk==9.4.2
8
+ scooby==0.10.1
9
+
10
+ # Trame Web Framework
11
+ trame==3.12.0
12
+ trame-client==3.11.2
13
+ trame-server==3.6.3
14
+ trame-vtk==2.10.0
15
+ trame-vuetify==3.1.0
16
+ wslink==2.4.0
17
+
18
+ # Qiskit Quantum Computing
19
+ qiskit==2.0.0
20
+ qiskit-aer==0.17.1
21
+ rustworkx==0.16.0
22
+
23
+ # Plotting
24
+ matplotlib==3.10.1
25
+ plotly
26
+ trame_plotly
27
+
28
+ # Core dependencies
29
+ Pillow==10.4.0
30
+ packaging==25.0
31
+ python-dateutil==2.9.0.post0
synopsys-logo-color-rgb.png ADDED
synopsys-logo-color-rgb.svg ADDED