Apurva Tiwari commited on
Commit
02ae0de
·
2 Parent(s): f916858 7f9a25d

Merge branch 'main' of https://huggingface.co/spaces/ansysresearch/quantum

Browse files
app.py CHANGED
@@ -46,15 +46,18 @@ state.logo_src = _load_logo_data_uri()
46
  # --- Import Embedded Modules ---
47
  # These are lightweight modules that build UI without creating their own servers
48
  import qlbm_embedded
49
- import em_embedded
50
 
51
  # Set the shared server on both modules
52
  qlbm_embedded.set_server(server)
53
- em_embedded.set_server(server)
54
 
55
  # Initialize state for both modules
56
  qlbm_embedded.init_state()
57
- em_embedded.init_state()
 
 
 
58
 
59
  # --- Build the Layout ---
60
  with SinglePageLayout(server) as layout:
@@ -168,7 +171,7 @@ with SinglePageLayout(server) as layout:
168
  fluid=True,
169
  classes="pa-0 fill-height",
170
  ):
171
- em_embedded.build_ui()
172
 
173
  # === QLBM Experience ===
174
  with vuetify3.VContainer(
@@ -178,18 +181,33 @@ with SinglePageLayout(server) as layout:
178
  ):
179
  qlbm_embedded.build_ui()
180
 
 
 
 
181
  # --- Heartbeat for HuggingFace ---
182
  def _start_hf_heartbeat_thread(interval_s: int = 5):
183
  """Keep the WebSocket alive for HuggingFace Spaces."""
 
 
184
  def _loop():
185
  while True:
186
  time.sleep(interval_s)
187
  try:
188
- server.controller.flush()
 
 
 
 
 
 
 
 
 
189
  except Exception:
190
- break
 
191
 
192
- t = threading.Thread(target=_loop, daemon=True)
193
  t.start()
194
 
195
  # --- Entry Point ---
 
46
  # --- Import Embedded Modules ---
47
  # These are lightweight modules that build UI without creating their own servers
48
  import qlbm_embedded
49
+ import em # Use the modular em package instead of em_embedded
50
 
51
  # Set the shared server on both modules
52
  qlbm_embedded.set_server(server)
53
+ em.set_server(server)
54
 
55
  # Initialize state for both modules
56
  qlbm_embedded.init_state()
57
+ em.init_state()
58
+
59
+ # Register EM handlers (must be done after server binding)
60
+ em.register_handlers()
61
 
62
  # --- Build the Layout ---
63
  with SinglePageLayout(server) as layout:
 
171
  fluid=True,
172
  classes="pa-0 fill-height",
173
  ):
174
+ em.build_ui()
175
 
176
  # === QLBM Experience ===
177
  with vuetify3.VContainer(
 
181
  ):
182
  qlbm_embedded.build_ui()
183
 
184
+ # Enable point picking after UI is built (prevents KeyError with Trame state)
185
+ em.enable_point_picking_on_plotter()
186
+
187
  # --- Heartbeat for HuggingFace ---
188
  def _start_hf_heartbeat_thread(interval_s: int = 5):
189
  """Keep the WebSocket alive for HuggingFace Spaces."""
190
+ import asyncio
191
+
192
  def _loop():
193
  while True:
194
  time.sleep(interval_s)
195
  try:
196
+ # Create a new event loop for this thread if needed
197
+ try:
198
+ loop = asyncio.get_event_loop()
199
+ except RuntimeError:
200
+ loop = asyncio.new_event_loop()
201
+ asyncio.set_event_loop(loop)
202
+
203
+ # Try to flush, but don't crash if it fails
204
+ if hasattr(server, 'controller') and hasattr(server.controller, 'flush'):
205
+ server.controller.flush()
206
  except Exception:
207
+ # Silently continue - heartbeat is optional
208
+ pass
209
 
210
+ t = threading.Thread(target=_loop, daemon=True, name="HeartbeatThread")
211
  t.start()
212
 
213
  # --- Entry Point ---
app_old.py DELETED
@@ -1,239 +0,0 @@
1
- import os
2
- # Ensure OMP_NUM_THREADS is set to avoid libgomp errors
3
- os.environ["OMP_NUM_THREADS"] = "1"
4
-
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
- import threading
10
- from concurrent.futures import ThreadPoolExecutor
11
- import time
12
-
13
- # Import embedded page wrappers (lazy load inside tabs)
14
- from importlib import import_module
15
- em_page = None
16
- qlbm_page = None
17
-
18
- # Force embedded mode for nested pages (avoid iframes/secondary servers)
19
- os.environ.setdefault("TRAME_EMBEDDED", "1")
20
- os.environ.setdefault("TRAME_DISABLE_BROWSER", "1")
21
- os.environ.setdefault("EM_APP_EMBEDDED", "1")
22
- os.environ.setdefault("DISABLE_EM_STANDALONE", "1")
23
-
24
- # Create a single server for the multipage app
25
- server = get_server()
26
- state, ctrl = server.state, server.controller
27
-
28
- # App state: landing chooser -> specific experience
29
- state.current_page = None # "EM" or "QLBM" once selected
30
-
31
- # -------------------------------------------------------------------------
32
- # Thread pool for long-running jobs to avoid UI freezing by HF timeout
33
- # -------------------------------------------------------------------------
34
- MAX_WORKERS = int(os.environ.get("MAX_WORKERS", "4"))
35
- executor = ThreadPoolExecutor(max_workers=MAX_WORKERS)
36
-
37
- def submit_background(fn, *args, **kwargs):
38
- """
39
- Run a CPU-heavy / long job without blocking Trame's event loop.
40
-
41
- Usage from pages:
42
- from trame.app import get_server
43
- server = get_server()
44
- server.submit_background(long_fn, arg1, arg2, kw1=...)
45
- """
46
- return executor.submit(fn, *args, **kwargs)
47
-
48
- # Expose helper on the server so pages.em_page / pages.qlbm_page can use it
49
- server.submit_background = submit_background
50
-
51
- # Load Synopsys/Ansys logo as data URI for main toolbar
52
- import base64
53
-
54
- def _load_logo_data_uri():
55
- base_dir = os.path.dirname(__file__)
56
- candidates = [
57
- os.path.join(base_dir, "ansys-part-of-synopsys-logo.svg"),
58
- os.path.join(base_dir, "synopsys-logo-color-rgb.svg"),
59
- os.path.join(base_dir, "synopsys-logo-color-rgb.png"),
60
- os.path.join(base_dir, "synopsys-logo-color-rgb.jpg"),
61
- ]
62
- for p in candidates:
63
- if os.path.exists(p):
64
- ext = os.path.splitext(p)[1].lower()
65
- mime = "image/svg+xml" if ext == ".svg" else ("image/png" if ext == ".png" else "image/jpeg")
66
- with open(p, "rb") as f:
67
- b64 = base64.b64encode(f.read()).decode("ascii")
68
- return f"data:{mime};base64,{b64}"
69
- return None
70
-
71
- def _stop_subapp(module_name: str, attr_name: str):
72
- module = globals().get(attr_name)
73
- if module is None:
74
- try:
75
- module = import_module(module_name)
76
- globals()[attr_name] = module
77
- except Exception:
78
- return
79
- stop_fn = getattr(module, "stop", None)
80
- if callable(stop_fn):
81
- try:
82
- stop_fn()
83
- except Exception:
84
- pass
85
-
86
-
87
- # Safely initialize logo in state (trame state isn't a dict; avoid .get())
88
- try:
89
- if not hasattr(state, "logo_src") or state.logo_src in (None, ""):
90
- state.logo_src = _load_logo_data_uri()
91
- except Exception:
92
- state.logo_src = _load_logo_data_uri()
93
-
94
-
95
- @state.change("current_page")
96
- def _handle_page_change(current_page, **_):
97
- """Stop inactive subprocesses as users navigate between experiences."""
98
- if current_page == "EM":
99
- _stop_subapp("pages.qlbm_page", "qlbm_page")
100
- elif current_page == "QLBM":
101
- _stop_subapp("pages.em_page", "em_page")
102
- else:
103
- _stop_subapp("pages.em_page", "em_page")
104
- _stop_subapp("pages.qlbm_page", "qlbm_page")
105
-
106
- with SinglePageLayout(server) as layout:
107
- layout.title.set_text("Quantum Applications")
108
- layout.toolbar.classes = "pl-2 pr-1 py-1 elevation-0"
109
- layout.toolbar.style = "background-color: #ffffff; border-bottom: 3px solid #5f259f;"
110
-
111
- with layout.toolbar:
112
- vuetify3.VSpacer()
113
- vuetify3.VBtn(
114
- v_if="current_page",
115
- text="Main Page",
116
- variant="text",
117
- color="primary",
118
- prepend_icon="mdi-arrow-left",
119
- click="current_page = null",
120
- classes="mr-2",
121
- )
122
- vuetify3.VChip(
123
- v_if="current_page",
124
- label=True,
125
- color="primary",
126
- text_color="white",
127
- children=["{{ current_page === 'EM' ? 'Electromagnetic Scattering' : 'Quantum LBM' }}"],
128
- classes="mr-2",
129
- )
130
- vuetify3.VImg(
131
- v_if="logo_src",
132
- src=("logo_src", None),
133
- style="height: 40px; width: auto;",
134
- classes="ml-2",
135
- )
136
-
137
- with layout.content:
138
- # Landing screen
139
- with vuetify3.VContainer(
140
- v_if="!current_page",
141
- fluid=True,
142
- classes="fill-height d-flex align-center justify-center pa-6",
143
- ):
144
- with vuetify3.VSheet(
145
- elevation=6,
146
- rounded=True,
147
- style="max-width: 1080px; width: 100%; background: linear-gradient(135deg, #fdfbff, #f3ecff);",
148
- classes="pa-8",
149
- ):
150
- vuetify3.VCardTitle(
151
- "Pick a quantum experience",
152
- classes="text-h4 text-primary font-weight-bold mb-2 text-center",
153
- )
154
- vuetify3.VCardSubtitle(
155
- "Choose one workflow. We'll spin up only that server until you switch back.",
156
- classes="text-body-1 text-center mb-6",
157
- )
158
- with vuetify3.VRow(justify="center", align="stretch", class_="text-left"):
159
- with vuetify3.VCol(cols=12, md=5, class_="d-flex"):
160
- with vuetify3.VCard(elevation=4, classes="pa-6 flex-grow-1"):
161
- vuetify3.VIcon("mdi-radar", size=52, color="primary", classes="mb-4")
162
- vuetify3.VCardTitle("Electromagnetic Scattering", classes="text-h5 mb-2")
163
- vuetify3.VCardText(
164
- "Placeholder",
165
- classes="text-body-2 mb-6",
166
- )
167
- vuetify3.VBtn(
168
- text="Launch EM",
169
- color="primary",
170
- block=True,
171
- prepend_icon="mdi-play-circle",
172
- size="large",
173
- click="current_page = 'EM'",
174
- )
175
- with vuetify3.VCol(cols=12, md=5, class_="d-flex"):
176
- with vuetify3.VCard(elevation=4, classes="pa-6 flex-grow-1"):
177
- vuetify3.VIcon("mdi-water", size=52, color="secondary", classes="mb-4")
178
- vuetify3.VCardTitle("Fluids", classes="text-h5 mb-2")
179
- vuetify3.VCardText(
180
- "Placeholder",
181
- classes="text-body-2 mb-6",
182
- )
183
- vuetify3.VBtn(
184
- text="Launch QLBM",
185
- color="secondary",
186
- block=True,
187
- prepend_icon="mdi-play-circle",
188
- size="large",
189
- click="current_page = 'QLBM'",
190
- )
191
-
192
- # EM experience
193
- with vuetify3.VContainer(v_if="current_page === 'EM'", fluid=True, classes="pa-0 fill-height"):
194
- try:
195
- if not globals().get("em_page"):
196
- globals()["em_page"] = import_module("pages.em_page")
197
- em_page.build(server)
198
- except Exception as e:
199
- trame_html.Div(f"EM embed failed: {e}", style="padding:8px;color:#b00020;")
200
-
201
- # QLBM experience
202
- with vuetify3.VContainer(v_if="current_page === 'QLBM'", fluid=True, classes="pa-0 fill-height"):
203
- try:
204
- if not globals().get("qlbm_page"):
205
- globals()["qlbm_page"] = import_module("pages.qlbm_page")
206
- qlbm_page.build(server)
207
- except Exception as e:
208
- trame_html.Div(f"QLBM embed failed: {e}", style="padding:8px;color:#b00020;")
209
-
210
- # -------------------------------------------------------------------------
211
- # Heartbeat: keep HuggingFace WebSocket alive during idle periods
212
- # -------------------------------------------------------------------------
213
- def _start_hf_heartbeat_thread(interval_s: int = 5):
214
- """
215
- Start a background thread that periodically flushes the server,
216
- keeping the WebSocket "active" in the eyes of Hugging Face.
217
- """
218
- def _loop():
219
- while True:
220
- time.sleep(interval_s)
221
- try:
222
- server.controller.flush()
223
- except Exception:
224
- # If server is shutting down or flush fails, exit the thread
225
- break
226
-
227
- t = threading.Thread(target=_loop, daemon=True)
228
- t.start()
229
-
230
-
231
- if __name__ == "__main__":
232
-
233
- # Start HF heartbeat to prevent timeout
234
- _start_hf_heartbeat_thread(interval_s=5)
235
-
236
- # Allow reverse-proxy setups to pin the internal host/port independently from the public PORT
237
- port = int(os.environ.get("APP_PORT") or os.environ.get("PORT", 7860))
238
- host = os.environ.get("APP_HOST", "0.0.0.0")
239
- server.start(host=host, port=port, open_browser=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
delta_impulse_generator.py DELETED
@@ -1,360 +0,0 @@
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))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
em/__init__.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EM Embedded Package
3
+
4
+ Modular Electromagnetic Scattering Simulation Module for Trame.
5
+
6
+ This package provides a modular structure for the EM simulation application,
7
+ designed for embedded use in a shared Trame server.
8
+
9
+ Usage:
10
+ from em import set_server, init_state, build_ui
11
+
12
+ # After creating a server
13
+ set_server(server)
14
+ init_state()
15
+
16
+ # In your layout
17
+ with layout.content:
18
+ build_ui()
19
+ """
20
+
21
+ # Import core components from submodules
22
+ from .state import (
23
+ state,
24
+ ctrl,
25
+ set_server,
26
+ init_state,
27
+ get_server,
28
+ enable_point_picking_on_plotter,
29
+ )
30
+
31
+ from .globals import (
32
+ plotter,
33
+ GRID_SIZES,
34
+ DEFAULT_AXIS_TICKS,
35
+ EXCITATION_SURFACE_COLORSCALE,
36
+ qpu_ts_cache,
37
+ sim_ts_cache,
38
+ )
39
+
40
+ from .simulation import (
41
+ run_simulation_only,
42
+ reset_to_defaults,
43
+ stop_simulation_handler,
44
+ add_dotted_unit_grid,
45
+ add_dotted_unit_grid_scaled,
46
+ build_sim_timeseries_plotly,
47
+ update_value_display,
48
+ )
49
+
50
+ from .geometry import (
51
+ update_geometry_preview,
52
+ update_geometry_hole_preview,
53
+ compute_hole_edges as _compute_hole_edges,
54
+ build_geometry_placeholder as _build_geometry_placeholder,
55
+ build_square_domain_plot as _build_square_domain_plot,
56
+ )
57
+
58
+ from .excitation import (
59
+ update_initial_state_preview,
60
+ build_excitation_placeholder as _build_excitation_placeholder,
61
+ build_excitation_surface_plot as _build_excitation_surface_plot,
62
+ )
63
+
64
+ from .qpu import (
65
+ build_qpu_timeseries_plotly_multi,
66
+ rebuild_qpu_fig_filtered as _rebuild_qpu_fig_filtered,
67
+ rebuild_qpu_fig_others as _rebuild_qpu_fig_others,
68
+ refresh_qpu_plot_figures as _refresh_qpu_plot_figures,
69
+ qpu_add_monitor_config,
70
+ qpu_remove_monitor_config,
71
+ qpu_set_plot_filter,
72
+ qpu_set_plot_position_filter,
73
+ qpu_add_monitor_slot,
74
+ qpu_remove_monitor_slot,
75
+ )
76
+
77
+ from .exports import (
78
+ export_vtk,
79
+ export_vtk_all_frames,
80
+ export_mp4,
81
+ export_sim_timeseries_csv,
82
+ export_sim_timeseries_png,
83
+ export_sim_timeseries_html,
84
+ export_qpu_timeseries_csv,
85
+ export_qpu_timeseries_png,
86
+ export_qpu_timeseries_html,
87
+ )
88
+
89
+ from .handlers import (
90
+ register_handlers,
91
+ build_qubit_plot,
92
+ _determine_workflow_step,
93
+ _apply_workflow_highlights,
94
+ )
95
+
96
+ from .utils import (
97
+ load_logo_data_uri,
98
+ install_synopsys_plotly_theme,
99
+ )
100
+
101
+ from .ui import build_ui
102
+
103
+ # Install the Synopsys Plotly theme at module load time
104
+ install_synopsys_plotly_theme()
105
+
106
+ __all__ = [
107
+ # Core API
108
+ "state",
109
+ "ctrl",
110
+ "set_server",
111
+ "init_state",
112
+ "build_ui",
113
+
114
+ # Simulation
115
+ "run_simulation_only",
116
+ "reset_to_defaults",
117
+ "stop_simulation_handler",
118
+
119
+ # Previews
120
+ "update_initial_state_preview",
121
+ "update_geometry_preview",
122
+ "update_geometry_hole_preview",
123
+
124
+ # QPU
125
+ "qpu_ts_cache",
126
+ "build_qpu_timeseries_plotly_multi",
127
+
128
+ # Exports
129
+ "export_vtk",
130
+ "export_vtk_all_frames",
131
+ "export_mp4",
132
+ "export_qpu_timeseries_csv",
133
+ "export_qpu_timeseries_png",
134
+ "export_qpu_timeseries_html",
135
+ "export_sim_timeseries_csv",
136
+ "export_sim_timeseries_png",
137
+ "export_sim_timeseries_html",
138
+
139
+ # Handlers
140
+ "register_handlers",
141
+
142
+ # Globals
143
+ "plotter",
144
+ "GRID_SIZES",
145
+ ]
em/excitation.py ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EM Embedded - Excitation Module
3
+
4
+ Contains excitation preview builders and update functions.
5
+ """
6
+ import numpy as np
7
+ import pyvista as pv
8
+ import plotly.graph_objects as go
9
+
10
+ from .state import state, ctrl
11
+ from .globals import (
12
+ plotter, current_mesh, DEFAULT_AXIS_TICKS, EXCITATION_SURFACE_COLORSCALE
13
+ )
14
+ from .utils import SAMPLE_PAIR_RE
15
+
16
+ # Import simulation callback for point picking
17
+ def _get_update_value_display():
18
+ """Lazy import to avoid circular dependency."""
19
+ from .simulation import update_value_display
20
+ return update_value_display
21
+
22
+ # Import backend functions
23
+ try:
24
+ from quantum.utils.delta_impulse_generator import create_impulse_state, create_gaussian_state
25
+ except ModuleNotFoundError:
26
+ from utils.delta_impulse_generator import create_impulse_state, create_gaussian_state
27
+
28
+ __all__ = [
29
+ "build_excitation_placeholder",
30
+ "build_excitation_surface_plot",
31
+ "push_excitation_plot",
32
+ "update_initial_state_preview",
33
+ "nearest_node_index",
34
+ "snap_samples_to_grid",
35
+ "update_sim_monitor_points",
36
+ "update_qpu_sample_slot",
37
+ "hide_qpu_plots",
38
+ "update_excitation_info_message",
39
+ ]
40
+
41
+
42
+ def nearest_node_index(x: float, y: float, nx: int, ny: int = None) -> tuple:
43
+ """Map normalized [0,1] coordinates to nearest node index on an nx×ny grid."""
44
+ ny = ny or nx
45
+ i = int(round(float(x) * (nx - 1)))
46
+ j = int(round(float(y) * (ny - 1)))
47
+ i = max(0, min(nx - 1, i))
48
+ j = max(0, min(ny - 1, j))
49
+ return i, j
50
+
51
+
52
+ def update_excitation_info_message():
53
+ """Calculates and displays the coordinate snapping message."""
54
+ if state.nx is None or state.dist_type is None:
55
+ state.excitation_info_message = ""
56
+ return
57
+
58
+ try:
59
+ nx = int(state.nx)
60
+ denom = float(max(nx - 1, 1))
61
+
62
+ if state.dist_type == "Delta":
63
+ x_in = float(getattr(state, 'peak_x', 0.5))
64
+ y_in = float(getattr(state, 'peak_y', 0.5))
65
+ elif state.dist_type == "Gaussian":
66
+ x_in = float(getattr(state, 'mu_x', 0.5))
67
+ y_in = float(getattr(state, 'mu_y', 0.5))
68
+ else:
69
+ state.excitation_info_message = ""
70
+ return
71
+
72
+ ix, iy = nearest_node_index(x_in, y_in, nx)
73
+ x_snapped, y_snapped = ix / denom, iy / denom
74
+ changed = (
75
+ abs(x_in - x_snapped) > 1e-9
76
+ or abs(y_in - y_snapped) > 1e-9
77
+ )
78
+ descriptor = "adjusted" if changed else "aligned"
79
+ state.excitation_info_message = (
80
+ f"Input ({x_in:.3f}, {y_in:.3f}) {descriptor} to nearest Position "
81
+ f"({x_snapped:.3f}, {y_snapped:.3f})."
82
+ )
83
+ except Exception:
84
+ state.excitation_info_message = ""
85
+
86
+
87
+ def build_excitation_placeholder(message: str = "Select an excitation to preview.") -> go.Figure:
88
+ """Build a placeholder figure for excitation preview."""
89
+ fig = go.Figure()
90
+ fig.add_annotation(
91
+ text=message,
92
+ x=0.5,
93
+ y=0.5,
94
+ showarrow=False,
95
+ font=dict(size=18, color="#5F259F")
96
+ )
97
+ fig.update_xaxes(visible=False)
98
+ fig.update_yaxes(visible=False)
99
+ fig.update_layout(
100
+ template="plotly_white",
101
+ margin=dict(l=20, r=20, t=40, b=20),
102
+ paper_bgcolor="#ffffff",
103
+ plot_bgcolor="#ffffff",
104
+ height=520,
105
+ showlegend=False,
106
+ )
107
+ return fig
108
+
109
+
110
+ def build_excitation_surface_plot(
111
+ X: np.ndarray,
112
+ Y: np.ndarray,
113
+ field: np.ndarray,
114
+ title: str,
115
+ *,
116
+ show_grid_lines: bool = False,
117
+ grid_line_resolution: int = None,
118
+ ) -> go.Figure:
119
+ """Build a 3D surface plot for excitation preview."""
120
+ max_abs = float(np.max(np.abs(field))) if field.size else 1.0
121
+ if max_abs < 1e-12:
122
+ max_abs = 1.0
123
+ height_scale = 0.1
124
+ Z = (field / max_abs) * height_scale
125
+ fig = go.Figure()
126
+
127
+ fig.add_trace(
128
+ go.Surface(
129
+ x=X,
130
+ y=Y,
131
+ z=Z,
132
+ surfacecolor=field,
133
+ colorscale=EXCITATION_SURFACE_COLORSCALE,
134
+ cmin=-max_abs,
135
+ cmax=max_abs,
136
+ showscale=False,
137
+ opacity=0.98,
138
+ lighting=dict(ambient=0.6, diffuse=0.6, specular=0.15),
139
+ hovertemplate="x=%{x:.3f}<br>y=%{y:.3f}<br>z=%{z:.3f}<extra></extra>",
140
+ )
141
+ )
142
+
143
+ x_vals = np.unique(np.round(X[0], decimals=6))
144
+ y_vals = np.unique(np.round(Y[:, 0], decimals=6))
145
+ x_min, x_max = float(np.min(x_vals)), float(np.max(x_vals))
146
+ y_min, y_max = float(np.min(y_vals)), float(np.max(y_vals))
147
+ base_z = -0.02
148
+
149
+ if show_grid_lines:
150
+ span_x = x_max - x_min if x_max != x_min else 1.0
151
+ span_y = y_max - y_min if y_max != y_min else 1.0
152
+ if grid_line_resolution is not None and grid_line_resolution >= 2:
153
+ res = int(grid_line_resolution)
154
+ norm_vals = np.linspace(0.0, 1.0, res)
155
+ else:
156
+ norm_vals = np.asarray(DEFAULT_AXIS_TICKS)
157
+ x_grid_vals = x_min + span_x * norm_vals
158
+ y_grid_vals = y_min + span_y * norm_vals
159
+
160
+ line_x, line_y, line_z = [], [], []
161
+ for val in x_grid_vals:
162
+ val_f = float(val)
163
+ line_x.extend([val_f, val_f, np.nan])
164
+ line_y.extend([y_min, y_max, np.nan])
165
+ line_z.extend([base_z, base_z, np.nan])
166
+ for val in y_grid_vals:
167
+ val_f = float(val)
168
+ line_x.extend([x_min, x_max, np.nan])
169
+ line_y.extend([val_f, val_f, np.nan])
170
+ line_z.extend([base_z, base_z, np.nan])
171
+
172
+ fig.add_trace(
173
+ go.Scatter3d(
174
+ x=line_x,
175
+ y=line_y,
176
+ z=line_z,
177
+ mode="lines",
178
+ line=dict(color="rgba(174,139,216,0.3)", width=1),
179
+ showlegend=False,
180
+ hoverinfo="skip",
181
+ )
182
+ )
183
+
184
+ tick_positions_x = x_min + span_x * np.asarray(DEFAULT_AXIS_TICKS)
185
+ tick_positions_y = y_min + span_y * np.asarray(DEFAULT_AXIS_TICKS)
186
+ tick_labels = [f"{t:.2f}" for t in DEFAULT_AXIS_TICKS]
187
+ x_offset = x_min - 0.03 * span_x
188
+ y_offset = y_min - 0.03 * span_y
189
+ tick_plane = base_z - 0.01
190
+ fig.add_trace(
191
+ go.Scatter3d(
192
+ x=tick_positions_x,
193
+ y=[y_offset] * len(tick_positions_x),
194
+ z=[tick_plane] * len(tick_positions_x),
195
+ mode="text",
196
+ text=tick_labels,
197
+ textfont=dict(color="#5F259F", size=12),
198
+ showlegend=False,
199
+ hoverinfo="skip",
200
+ )
201
+ )
202
+ fig.add_trace(
203
+ go.Scatter3d(
204
+ x=[x_offset] * len(tick_positions_y),
205
+ y=tick_positions_y,
206
+ z=[tick_plane] * len(tick_positions_y),
207
+ mode="text",
208
+ text=tick_labels,
209
+ textfont=dict(color="#5F259F", size=12),
210
+ showlegend=False,
211
+ hoverinfo="skip",
212
+ )
213
+ )
214
+
215
+ # Domain boundary
216
+ fig.add_trace(
217
+ go.Scatter3d(
218
+ x=[x_min, x_max, x_max, x_min, x_min],
219
+ y=[y_min, y_min, y_max, y_max, y_min],
220
+ z=[base_z - 0.005] * 5,
221
+ mode="lines",
222
+ line=dict(color="rgba(255,192,203,0.9)", width=2.5),
223
+ showlegend=False,
224
+ hoverinfo="skip",
225
+ )
226
+ )
227
+
228
+ pad_x = 0.05 * (x_max - x_min if x_max != x_min else 1.0)
229
+ pad_y = 0.05 * (y_max - y_min if y_max != y_min else 1.0)
230
+
231
+ fig.update_layout(
232
+ title=title,
233
+ margin=dict(l=10, r=10, t=36, b=10),
234
+ height=620,
235
+ template="plotly_white",
236
+ scene=dict(
237
+ xaxis=dict(range=[x_min - pad_x, x_max + pad_x], showgrid=False, showline=False, showticklabels=False, zeroline=False, visible=False, showbackground=False),
238
+ yaxis=dict(range=[y_min - pad_y, y_max + pad_y], showgrid=False, showline=False, showticklabels=False, zeroline=False, visible=False, showbackground=False),
239
+ zaxis=dict(range=[-0.3, 0.3], showgrid=False, showline=False, showticklabels=False, zeroline=False, visible=False, showbackground=False),
240
+ aspectmode="cube",
241
+ camera=dict(eye=dict(x=1.2, y=1.2, z=1.0)),
242
+ ),
243
+ dragmode="orbit",
244
+ uirevision="excitation_surface",
245
+ )
246
+ return fig
247
+
248
+
249
+ def push_excitation_plot(fig: go.Figure = None):
250
+ """Push an excitation plot to the UI."""
251
+ render_fig = fig or build_excitation_placeholder()
252
+ try:
253
+ if hasattr(ctrl, "excitation_preview_update"):
254
+ ctrl.excitation_preview_update(render_fig)
255
+ except Exception:
256
+ pass
257
+
258
+
259
+ def _refresh_pyvista_view():
260
+ """Refresh the PyVista view."""
261
+ try:
262
+ if hasattr(ctrl, "view_update"):
263
+ ctrl.view_update()
264
+ except Exception:
265
+ pass
266
+
267
+
268
+ def snap_samples_to_grid(sample_str: str, nx: int) -> tuple:
269
+ """
270
+ Convert normalized sample positions to nearest integer grid indices.
271
+
272
+ Returns:
273
+ tuple: (grid_points_string, message)
274
+ """
275
+ if not sample_str or not str(sample_str).strip():
276
+ return "", ""
277
+ matches = SAMPLE_PAIR_RE.findall(str(sample_str))
278
+ if not matches:
279
+ return "", "Enter sample position(s) as (x, y) pairs in [0,1] x [0,1]."
280
+
281
+ nx_int = int(nx)
282
+ denom = float(max(nx_int - 1, 1))
283
+ tokens = []
284
+ info_lines = []
285
+
286
+ def _normalize_value(val: float) -> float:
287
+ threshold = 1.0 + 1e-9
288
+ if abs(val) <= threshold:
289
+ return max(0.0, min(1.0, val))
290
+ max_index = float(nx_int - 1)
291
+ return max(0.0, min(max_index, val)) / denom if denom else 0.0
292
+
293
+ for raw_x, raw_y in matches:
294
+ try:
295
+ x_val = float(raw_x)
296
+ y_val = float(raw_y)
297
+ except ValueError:
298
+ continue
299
+ x_norm = _normalize_value(x_val)
300
+ y_norm = _normalize_value(y_val)
301
+ ix, iy = nearest_node_index(x_norm, y_norm, nx_int)
302
+ tokens.append(f"({ix}, {iy})")
303
+ snapped_x = ix / denom
304
+ snapped_y = iy / denom
305
+ changed = (
306
+ abs(x_norm - snapped_x) > 1e-9
307
+ or abs(y_norm - snapped_y) > 1e-9
308
+ )
309
+ descriptor = "adjusted" if changed else "aligned"
310
+ info_lines.append(
311
+ f"Input ({x_val:.3f}, {y_val:.3f}) {descriptor} to nearest Position ({snapped_x:.3f}, {snapped_y:.3f})."
312
+ )
313
+
314
+ grid_str = ", ".join(tokens)
315
+ message = "\n".join(info_lines)
316
+ return grid_str, message
317
+
318
+
319
+ def update_sim_monitor_points():
320
+ """Update simulator monitor points based on state."""
321
+ sample_value = state.timeseries_points
322
+ if not sample_value or not str(sample_value).strip():
323
+ state.timeseries_gridpoints = ""
324
+ state.timeseries_point_info = ""
325
+ return
326
+ nx_val = state.nx
327
+ if nx_val is None:
328
+ state.timeseries_gridpoints = ""
329
+ state.timeseries_point_info = "Select a grid size (nx) to compute the nearest monitor positions."
330
+ return
331
+ snapped, message = snap_samples_to_grid(sample_value, int(nx_val))
332
+ state.timeseries_gridpoints = snapped
333
+ state.timeseries_point_info = message or ""
334
+
335
+
336
+ def update_qpu_sample_slot(slot_index: int):
337
+ """Update QPU sample slot based on state."""
338
+ suffix = "" if slot_index == 1 else f"_{slot_index}"
339
+ sample_attr = f"qpu_monitor_samples{suffix}"
340
+ grid_attr = f"qpu_monitor_gridpoints{suffix}"
341
+ info_attr = f"qpu_monitor_sample_info{suffix}"
342
+ sample_value = getattr(state, sample_attr, "")
343
+ nx_val = state.nx
344
+ if not sample_value or not str(sample_value).strip():
345
+ setattr(state, grid_attr, "")
346
+ setattr(state, info_attr, "")
347
+ return
348
+ if nx_val is None:
349
+ setattr(state, info_attr, "Select a grid size (nx) to compute the nearest grid position.")
350
+ setattr(state, grid_attr, "")
351
+ return
352
+ snapped, message = snap_samples_to_grid(sample_value, int(nx_val))
353
+ setattr(state, grid_attr, snapped)
354
+ setattr(state, info_attr, message or "")
355
+ if state.backend_type == "QPU":
356
+ hide_qpu_plots()
357
+
358
+
359
+ def hide_qpu_plots():
360
+ """Hide QPU plots."""
361
+ state.qpu_ts_ready = False
362
+ state.qpu_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
363
+ state.qpu_ts_other_ready = False
364
+ state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
365
+ state.qpu_plot_position_options = ["All positions"]
366
+ state.qpu_plot_position_filter = "All positions"
367
+
368
+
369
+ def update_initial_state_preview():
370
+ """
371
+ Update the initial state preview based on current geometry and excitation settings.
372
+ This is the main preview function called when state changes.
373
+ """
374
+ from .geometry import (
375
+ build_geometry_placeholder, push_geometry_plot,
376
+ update_geometry_preview, update_geometry_hole_preview
377
+ )
378
+
379
+ global current_mesh
380
+
381
+ # Don't render any preview while running
382
+ if state.is_running:
383
+ plotter.clear()
384
+ _refresh_pyvista_view()
385
+ return
386
+
387
+ # If no geometry selected, clear and stop
388
+ if not state.geometry_selection:
389
+ plotter.clear()
390
+ push_geometry_plot(build_geometry_placeholder("Select a geometry to preview."))
391
+ push_excitation_plot(None)
392
+ _refresh_pyvista_view()
393
+ return
394
+
395
+ # Geometry-only previews before initial state selection
396
+ if not state.simulation_has_run and not state.dist_type:
397
+ if state.geometry_selection == "Square Domain":
398
+ update_geometry_preview()
399
+ push_excitation_plot(None)
400
+ return
401
+ if state.geometry_selection == "Square Metallic Body":
402
+ update_geometry_hole_preview()
403
+ push_excitation_plot(None)
404
+ return
405
+ push_geometry_plot(build_geometry_placeholder("Preview not available for this geometry yet."))
406
+ push_excitation_plot(None)
407
+ return
408
+
409
+ # Plotly preview for excitation surface before any simulation run
410
+ if not state.simulation_has_run:
411
+ plotter.clear()
412
+ state.error_message = ""
413
+ preview_n = 128
414
+ nx_sel = state.nx
415
+ try:
416
+ slider_n = None
417
+ if nx_sel is not None:
418
+ slider_n = int(nx_sel)
419
+ grid_n = slider_n if slider_n is not None else preview_n
420
+ if slider_n is None:
421
+ show_grid_lines = True
422
+ grid_line_resolution = None
423
+ else:
424
+ show_grid_lines = True
425
+ grid_line_resolution = max(slider_n, 2)
426
+ grid_n = max(grid_n, 8)
427
+
428
+ if state.dist_type == "Delta":
429
+ ix, iy = nearest_node_index(float(state.impulse_x), float(state.impulse_y), grid_n)
430
+ full_state = create_impulse_state((grid_n, grid_n), (ix, iy))
431
+ title = "Delta Excitation"
432
+ elif state.dist_type == "Gaussian":
433
+ ix, iy = nearest_node_index(float(state.mu_x), float(state.mu_y), grid_n)
434
+ sx = max(float(state.sigma_x) * (grid_n - 1), 1e-9)
435
+ sy = max(float(state.sigma_y) * (grid_n - 1), 1e-9)
436
+ full_state = create_gaussian_state((grid_n, grid_n), (ix, iy), (sx, sy))
437
+ title = "Gaussian Excitation"
438
+ else:
439
+ push_excitation_plot(None)
440
+ _refresh_pyvista_view()
441
+ return
442
+
443
+ initial_grid = full_state[: grid_n * grid_n].reshape(grid_n, grid_n)
444
+ denom = float(max(grid_n - 1, 1))
445
+ coords = np.arange(grid_n) / denom
446
+ X, Y = np.meshgrid(coords, coords)
447
+ fig = build_excitation_surface_plot(
448
+ X,
449
+ Y,
450
+ initial_grid,
451
+ f"{title} (nx={grid_n})",
452
+ show_grid_lines=show_grid_lines,
453
+ grid_line_resolution=grid_line_resolution,
454
+ )
455
+ push_excitation_plot(fig)
456
+ except ValueError as e:
457
+ state.error_message = f"Parameter Error: {e}"
458
+ push_excitation_plot(build_excitation_placeholder("Check excitation parameters."))
459
+ except Exception as e:
460
+ state.error_message = f"An unexpected error occurred: {e}"
461
+ push_excitation_plot(build_excitation_placeholder("Unable to render preview."))
462
+ finally:
463
+ _refresh_pyvista_view()
464
+ return
465
+
466
+ # PyVista preview after simulation has run
467
+ plotter.clear()
468
+ state.error_message = ""
469
+ preview_n = 128
470
+ nx_sel = state.nx
471
+ show_grid_edges = nx_sel is not None
472
+
473
+ try:
474
+ grid_n = int(nx_sel) if nx_sel is not None else preview_n
475
+
476
+ if state.dist_type == "Delta":
477
+ ix, iy = nearest_node_index(float(state.impulse_x), float(state.impulse_y), grid_n)
478
+ full_state = create_impulse_state((grid_n, grid_n), (ix, iy))
479
+ elif state.dist_type == "Gaussian":
480
+ ix, iy = nearest_node_index(float(state.mu_x), float(state.mu_y), grid_n)
481
+ sx = max(float(state.sigma_x) * (grid_n - 1), 1e-9)
482
+ sy = max(float(state.sigma_y) * (grid_n - 1), 1e-9)
483
+ full_state = create_gaussian_state((grid_n, grid_n), (ix, iy), (sx, sy))
484
+ else:
485
+ return
486
+
487
+ # Build preview mesh
488
+ initial_grid = full_state[: grid_n * grid_n].reshape(grid_n, grid_n)
489
+ denom = float(max(grid_n - 1, 1))
490
+ x, y = np.arange(grid_n) / denom, np.arange(grid_n) / denom
491
+ X, Y = np.meshgrid(x, y)
492
+ max_abs = float(np.max(np.abs(initial_grid))) if initial_grid.size else 1.0
493
+ if max_abs < 1e-12:
494
+ max_abs = 1.0
495
+ height_scale = 0.15
496
+ Z = (initial_grid / max_abs) * height_scale
497
+ mesh = pv.StructuredGrid()
498
+ mesh.points = np.c_[X.ravel(), Y.ravel(), Z.ravel()]
499
+ mesh.dimensions = (grid_n, grid_n, 1)
500
+ mesh['scalars'] = initial_grid.ravel()
501
+
502
+ plotter.add_mesh(
503
+ mesh,
504
+ scalars='scalars',
505
+ cmap="Blues",
506
+ show_scalar_bar=False,
507
+ show_edges=show_grid_edges,
508
+ edge_color='grey',
509
+ line_width=0.5,
510
+ )
511
+ plotter.show_grid(
512
+ bounds=(0.0, 1.0, 0.0, 1.0, 0.0, 0.0),
513
+ xtitle="x (0–1)",
514
+ ytitle="y (0–1)",
515
+ ztitle=" ",
516
+ color="#AE8BD8",
517
+ )
518
+ plotter.add_axes()
519
+ plotter.view_isometric()
520
+ # Enable point picking for coordinate display
521
+ try:
522
+ plotter.disable_picking()
523
+ except Exception:
524
+ pass
525
+ try:
526
+ plotter.enable_point_picking(callback=_get_update_value_display(), show_message=False)
527
+ except Exception:
528
+ pass
529
+ try:
530
+ plotter.camera.parallel_projection = True
531
+ except Exception:
532
+ pass
533
+ _refresh_pyvista_view()
534
+
535
+ except ValueError as e:
536
+ state.error_message = f"Parameter Error: {e}"
537
+ except Exception as e:
538
+ state.error_message = f"An unexpected error occurred: {e}"
em/exports.py ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Export functions for VTK, MP4, CSV, PNG, and HTML output."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import tempfile
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ import numpy as np
11
+ import pyvista as pv
12
+ import plotly.graph_objects as go
13
+
14
+ from .state import state, _server
15
+ from .globals import (
16
+ plotter, simulation_data, current_mesh,
17
+ surface_clims, data_frames, X_grids, Y_grids, z_scale, snapshot_times,
18
+ qpu_ts_cache, sim_ts_cache,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ pass
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Surface / VTK Exports
27
+ # ---------------------------------------------------------------------------
28
+
29
+ def export_vtk():
30
+ """Export current surface mesh to user's Downloads as .vtp and notify via snackbar."""
31
+ if current_mesh is None:
32
+ state.export_status_message = "No mesh to export."
33
+ state.show_export_status = True
34
+ return
35
+ try:
36
+ field = state.surface_field or "Ez"
37
+ nx = int(state.nx or 16)
38
+ suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
39
+ filename = f"surface_{field}_nx{nx}_{suffix}.vtp"
40
+
41
+ # Write to a temporary file first
42
+ with tempfile.NamedTemporaryFile(suffix=".vtp", delete=False) as tmp:
43
+ current_mesh.save(tmp.name)
44
+ content = Path(tmp.name).read_bytes()
45
+ # Encode content as base64 for browser download
46
+ content_b64 = base64.b64encode(content).decode("ascii")
47
+ # Trigger browser download via JavaScript
48
+ if _server is not None:
49
+ _server.js_call("utils", "download", filename, f"data:application/octet-stream;base64,{content_b64}")
50
+ Path(tmp.name).unlink() # Clean up
51
+
52
+ state.export_status_message = f"Exported VTK to {filename}"
53
+ except Exception as e:
54
+ state.export_status_message = f"Export failed: {e}"
55
+ state.show_export_status = True
56
+
57
+
58
+ def export_vtk_all_frames():
59
+ """Export a .vtp file for each time frame of the selected component into a zip file."""
60
+ try:
61
+ if not state.simulation_has_run:
62
+ raise ValueError("Run a simulation before exporting all frames.")
63
+ field = state.surface_field or "Ez"
64
+ frames = data_frames.get(field)
65
+ if not frames:
66
+ raise ValueError(f"No frames available for {field}.")
67
+ if snapshot_times is None:
68
+ raise ValueError("Snapshot times are unavailable.")
69
+
70
+ nx = int(state.nx or 16)
71
+ suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
72
+ zip_filename = f"vtk_sequence_{field}_nx{nx}_{suffix}.zip"
73
+
74
+ import zipfile
75
+
76
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
77
+ temp_zip_path = tmp_zip.name
78
+
79
+ with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
80
+ times = np.asarray(snapshot_times)
81
+ for i, (z_data, t) in enumerate(zip(frames, times)):
82
+ points = np.c_[X_grids[field].ravel(), Y_grids[field].ravel(), z_data.ravel() * z_scale]
83
+ poly = pv.PolyData(points)
84
+ mesh = poly.delaunay_2d()
85
+ mesh["scalars"] = z_data.ravel()
86
+
87
+ fname = f"{field}_frame_{i:04d}_t{t:.3f}s.vtp"
88
+
89
+ with tempfile.NamedTemporaryFile(suffix=".vtp", delete=False) as tmp_vtp:
90
+ tmp_vtp_path = tmp_vtp.name
91
+
92
+ mesh.save(tmp_vtp_path)
93
+ zf.write(tmp_vtp_path, arcname=fname)
94
+ Path(tmp_vtp_path).unlink()
95
+
96
+ content = Path(temp_zip_path).read_bytes()
97
+ content_b64 = base64.b64encode(content).decode("ascii")
98
+ if _server is not None:
99
+ _server.js_call("utils", "download", zip_filename, f"data:application/zip;base64,{content_b64}")
100
+ Path(temp_zip_path).unlink()
101
+
102
+ state.export_status_message = f"Exported {len(frames)} frames to {zip_filename}"
103
+ except Exception as e:
104
+ state.export_status_message = f"Export failed: {e}"
105
+ finally:
106
+ state.show_export_status = True
107
+
108
+
109
+ def export_mp4():
110
+ """Export the surface plot time slider animation to MP4 using a dedicated off-screen plotter."""
111
+ try:
112
+ if not state.simulation_has_run:
113
+ raise ValueError("Run a simulation before exporting MP4.")
114
+ field = state.surface_field or "Ez"
115
+ frames = data_frames.get(field)
116
+ if not frames:
117
+ raise ValueError(f"No frames available for {field}.")
118
+ if len(frames) < 2:
119
+ raise ValueError("Only one frame available; increase T or simulation steps.")
120
+
121
+ nx = int(state.nx or 16)
122
+ suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
123
+ filename = f"surface_anim_{field}_nx{nx}_{suffix}.mp4"
124
+
125
+ # Create a temporary file for the MP4
126
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
127
+ temp_path = tmp.name
128
+
129
+ # Build with a dedicated off-screen plotter at a macro-block friendly size
130
+ movie_plotter = pv.Plotter(off_screen=True, window_size=(1280, 720))
131
+
132
+ # Initial mesh from first frame
133
+ X = X_grids.get(field)
134
+ Y = Y_grids.get(field)
135
+ if X is None or Y is None:
136
+ raise ValueError(f"Grid data not available for {field}.")
137
+
138
+ first = frames[0]
139
+ points = np.c_[X.ravel(), Y.ravel(), first.ravel() * z_scale]
140
+ poly = pv.PolyData(points)
141
+ mesh = poly.delaunay_2d()
142
+ mesh['scalars'] = first.ravel()
143
+ actor = movie_plotter.add_mesh(
144
+ mesh,
145
+ scalars='scalars',
146
+ clim=surface_clims.get(field, (-1, 1)),
147
+ cmap="RdBu",
148
+ show_scalar_bar=False,
149
+ show_edges=True,
150
+ edge_color='grey',
151
+ line_width=0.5,
152
+ )
153
+ movie_plotter.add_axes()
154
+ # Use similar camera if available, else default
155
+ try:
156
+ if hasattr(plotter, 'camera_position') and plotter.camera_position:
157
+ movie_plotter.camera_position = plotter.camera_position
158
+ else:
159
+ movie_plotter.view_isometric()
160
+ except Exception:
161
+ movie_plotter.view_isometric()
162
+
163
+ movie_plotter.open_movie(temp_path, framerate=20)
164
+ for z_data in frames:
165
+ if mesh.n_points != z_data.size:
166
+ # Rebuild mesh if topology changes (unlikely here)
167
+ points = np.c_[X.ravel(), Y.ravel(), z_data.ravel() * z_scale]
168
+ poly = pv.PolyData(points)
169
+ mesh = poly.delaunay_2d()
170
+ mesh['scalars'] = z_data.ravel()
171
+ movie_plotter.clear()
172
+ actor = movie_plotter.add_mesh(
173
+ mesh,
174
+ scalars='scalars',
175
+ clim=surface_clims.get(field, (-1, 1)),
176
+ cmap="RdBu",
177
+ show_scalar_bar=False,
178
+ show_edges=True,
179
+ edge_color='grey',
180
+ line_width=0.5,
181
+ )
182
+ else:
183
+ mesh.points[:, 2] = z_data.ravel() * z_scale
184
+ mesh['scalars'] = z_data.ravel()
185
+ movie_plotter.render()
186
+ movie_plotter.write_frame()
187
+ movie_plotter.close()
188
+
189
+ # Read the file content and trigger download
190
+ content = Path(temp_path).read_bytes()
191
+ content_b64 = base64.b64encode(content).decode("ascii")
192
+ if _server is not None:
193
+ _server.js_call("utils", "download", filename, f"data:video/mp4;base64,{content_b64}")
194
+ Path(temp_path).unlink() # Clean up
195
+
196
+ state.export_status_message = f"Exported MP4 to {filename}"
197
+ except Exception as e:
198
+ state.export_status_message = f"Export failed: {e}"
199
+ finally:
200
+ state.show_export_status = True
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # Simulator Time-Series Exports
205
+ # ---------------------------------------------------------------------------
206
+
207
+ def export_sim_timeseries_csv():
208
+ """Export Simulator time-series to CSV."""
209
+ try:
210
+ times = sim_ts_cache.get("times")
211
+ series_map = sim_ts_cache.get("series_map")
212
+ if not times or not series_map:
213
+ state.export_status_message = "No Simulator time series data to export."
214
+ state.show_export_status = True
215
+ return
216
+
217
+ suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
218
+ filename = f"sim_timeseries_{suffix}.csv"
219
+
220
+ # Build CSV content
221
+ lines = ["time"]
222
+ keys = list(series_map.keys())
223
+ for k in keys:
224
+ field, px, py = k
225
+ lines[0] += f",{field}_({px},{py})"
226
+
227
+ for i, t in enumerate(times):
228
+ row = [str(t)]
229
+ for k in keys:
230
+ vals = series_map.get(k, [])
231
+ row.append(str(vals[i]) if i < len(vals) else "")
232
+ lines.append(",".join(row))
233
+
234
+ content = "\n".join(lines)
235
+ content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
236
+ if _server is not None:
237
+ _server.js_call("utils", "download", filename, f"data:text/csv;base64,{content_b64}")
238
+
239
+ state.export_status_message = f"Exported CSV to {filename}"
240
+ except Exception as e:
241
+ state.export_status_message = f"Export failed: {e}"
242
+ finally:
243
+ state.show_export_status = True
244
+
245
+
246
+ def export_sim_timeseries_png():
247
+ """Export Simulator time-series plot to PNG."""
248
+ try:
249
+ fig = sim_ts_cache.get("figure")
250
+ if fig is None:
251
+ state.export_status_message = "No Simulator time series figure to export."
252
+ state.show_export_status = True
253
+ return
254
+
255
+ suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
256
+ filename = f"sim_timeseries_{suffix}.png"
257
+
258
+ # Export to PNG
259
+ content = fig.to_image(format="png", width=1200, height=800)
260
+ content_b64 = base64.b64encode(content).decode("ascii")
261
+ if _server is not None:
262
+ _server.js_call("utils", "download", filename, f"data:image/png;base64,{content_b64}")
263
+
264
+ state.export_status_message = f"Exported PNG to {filename}"
265
+ except Exception as e:
266
+ state.export_status_message = f"Export failed: {e}"
267
+ finally:
268
+ state.show_export_status = True
269
+
270
+
271
+ def export_sim_timeseries_html():
272
+ """Export Simulator time-series plot to interactive HTML."""
273
+ try:
274
+ fig = sim_ts_cache.get("figure")
275
+ if fig is None:
276
+ state.export_status_message = "No Simulator time series figure to export."
277
+ state.show_export_status = True
278
+ return
279
+
280
+ suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
281
+ filename = f"sim_timeseries_{suffix}.html"
282
+
283
+ # Export to HTML
284
+ content = fig.to_html(include_plotlyjs="cdn", full_html=True)
285
+ content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
286
+ if _server is not None:
287
+ _server.js_call("utils", "download", filename, f"data:text/html;base64,{content_b64}")
288
+
289
+ state.export_status_message = f"Exported HTML to {filename}"
290
+ except Exception as e:
291
+ state.export_status_message = f"Export failed: {e}"
292
+ finally:
293
+ state.show_export_status = True
294
+
295
+
296
+ # ---------------------------------------------------------------------------
297
+ # QPU Time-Series Exports
298
+ # ---------------------------------------------------------------------------
299
+
300
+ def export_qpu_timeseries_csv():
301
+ """Export QPU time-series to CSV."""
302
+ try:
303
+ times = qpu_ts_cache.get("times")
304
+ series_map = qpu_ts_cache.get("series_map")
305
+ if not times or not series_map:
306
+ state.export_status_message = "No QPU time series data to export."
307
+ state.show_export_status = True
308
+ return
309
+
310
+ suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
311
+ filename = f"qpu_timeseries_{suffix}.csv"
312
+
313
+ # Build CSV content
314
+ lines = ["time"]
315
+ keys = list(series_map.keys())
316
+ for k in keys:
317
+ field, px, py = k
318
+ lines[0] += f",{field}_({px},{py})"
319
+
320
+ for i, t in enumerate(times):
321
+ row = [str(t)]
322
+ for k in keys:
323
+ vals = series_map.get(k, [])
324
+ row.append(str(vals[i]) if i < len(vals) else "")
325
+ lines.append(",".join(row))
326
+
327
+ content = "\n".join(lines)
328
+ content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
329
+ if _server is not None:
330
+ _server.js_call("utils", "download", filename, f"data:text/csv;base64,{content_b64}")
331
+
332
+ state.export_status_message = f"Exported CSV to {filename}"
333
+ except Exception as e:
334
+ state.export_status_message = f"Export failed: {e}"
335
+ finally:
336
+ state.show_export_status = True
337
+
338
+
339
+ def export_qpu_timeseries_png():
340
+ """Export QPU time-series plot to PNG."""
341
+ try:
342
+ fig = qpu_ts_cache.get("figure")
343
+ if fig is None:
344
+ state.export_status_message = "No QPU time series figure to export."
345
+ state.show_export_status = True
346
+ return
347
+
348
+ suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
349
+ filename = f"qpu_timeseries_{suffix}.png"
350
+
351
+ # Export to PNG
352
+ content = fig.to_image(format="png", width=1200, height=800)
353
+ content_b64 = base64.b64encode(content).decode("ascii")
354
+ if _server is not None:
355
+ _server.js_call("utils", "download", filename, f"data:image/png;base64,{content_b64}")
356
+
357
+ state.export_status_message = f"Exported PNG to {filename}"
358
+ except Exception as e:
359
+ state.export_status_message = f"Export failed: {e}"
360
+ finally:
361
+ state.show_export_status = True
362
+
363
+
364
+ def export_qpu_timeseries_html():
365
+ """Export QPU time-series plot to interactive HTML."""
366
+ try:
367
+ fig = qpu_ts_cache.get("figure")
368
+ if fig is None:
369
+ state.export_status_message = "No QPU time series figure to export."
370
+ state.show_export_status = True
371
+ return
372
+
373
+ suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
374
+ filename = f"qpu_timeseries_{suffix}.html"
375
+
376
+ # Export to HTML
377
+ content = fig.to_html(include_plotlyjs="cdn", full_html=True)
378
+ content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
379
+ if _server is not None:
380
+ _server.js_call("utils", "download", filename, f"data:text/html;base64,{content_b64}")
381
+
382
+ state.export_status_message = f"Exported HTML to {filename}"
383
+ except Exception as e:
384
+ state.export_status_message = f"Export failed: {e}"
385
+ finally:
386
+ state.show_export_status = True
em/geometry.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EM Embedded - Geometry Module
3
+
4
+ Contains geometry preview builders and hole computation functions.
5
+ """
6
+ import numpy as np
7
+ import plotly.graph_objects as go
8
+
9
+ from .state import state, ctrl
10
+ from .globals import DEFAULT_AXIS_TICKS, EXCITATION_SURFACE_COLORSCALE
11
+
12
+ __all__ = [
13
+ "nearest_gridline",
14
+ "compute_hole_edges",
15
+ "build_geometry_placeholder",
16
+ "build_square_domain_plot",
17
+ "push_geometry_plot",
18
+ "update_geometry_preview",
19
+ "update_geometry_hole_preview",
20
+ ]
21
+
22
+
23
+ def nearest_gridline(val: float, nx: int) -> float:
24
+ """Snap a value to the nearest gridline."""
25
+ denom = float(max(int(nx) - 1, 1))
26
+ return round(float(val) * denom) / denom
27
+
28
+
29
+ def compute_hole_edges(nx: int, cx: float, cy: float, a: float, snap: bool = True):
30
+ """
31
+ Compute square hole edges (xL, xR, yB, yT) in [0,1].
32
+
33
+ Args:
34
+ nx: points per direction; grid lines at k/(nx-1).
35
+ cx, cy: center in (0,1).
36
+ a: edge length in (0,1].
37
+ snap: if True, snap edges to nearest grid lines; else require exact alignment.
38
+
39
+ Returns:
40
+ tuple (xL, xR, yB, yT) or None when invalid/out-of-bounds/misaligned.
41
+ """
42
+ try:
43
+ nx = int(nx)
44
+ cx = float(cx)
45
+ cy = float(cy)
46
+ a = float(a)
47
+ except Exception:
48
+ return None
49
+
50
+ if not (a > 0.0):
51
+ return None
52
+
53
+ half = a / 2.0
54
+ xL, xR = cx - half, cx + half
55
+ yB, yT = cy - half, cy + half
56
+
57
+ # Must be strictly inside domain to allow removing interior cells safely
58
+ if not (0.0 < xL < xR < 1.0 and 0.0 < yB < yT < 1.0):
59
+ return None
60
+
61
+ if snap:
62
+ xL_s = nearest_gridline(xL, nx)
63
+ xR_s = nearest_gridline(xR, nx)
64
+ yB_s = nearest_gridline(yB, nx)
65
+ yT_s = nearest_gridline(yT, nx)
66
+ # Ensure non-degenerate after snapping; attempt a minimal adjustment if equal
67
+ if xL_s >= xR_s or yB_s >= yT_s:
68
+ step = 1.0 / float(max(nx - 1, 1))
69
+ if xL_s >= xR_s:
70
+ if xL_s - step > 0.0:
71
+ xL_s -= step
72
+ elif xR_s + step < 1.0:
73
+ xR_s += step
74
+ if yB_s >= yT_s:
75
+ if yB_s - step > 0.0:
76
+ yB_s -= step
77
+ elif yT_s + step < 1.0:
78
+ yT_s += step
79
+ if xL_s >= xR_s or yB_s >= yT_s:
80
+ return None
81
+ return (xL_s, xR_s, yB_s, yT_s)
82
+ else:
83
+ # Require edges to already lie on grid lines
84
+ denom = float(max(nx - 1, 1))
85
+ tol = 1e-9
86
+ def _aligned(v: float) -> bool:
87
+ return abs(v * denom - round(v * denom)) < tol
88
+ if all(_aligned(v) for v in (xL, xR, yB, yT)):
89
+ return (xL, xR, yB, yT)
90
+ return None
91
+
92
+
93
+ def build_geometry_placeholder(message: str) -> go.Figure:
94
+ """Build a placeholder figure with a message."""
95
+ fig = go.Figure()
96
+ fig.add_annotation(
97
+ text=message,
98
+ x=0.5,
99
+ y=0.5,
100
+ showarrow=False,
101
+ font=dict(size=18, color="#5F259F"),
102
+ )
103
+ fig.update_xaxes(visible=False)
104
+ fig.update_yaxes(visible=False)
105
+ fig.update_layout(
106
+ template="plotly_white",
107
+ margin=dict(l=20, r=20, t=40, b=20),
108
+ paper_bgcolor="#ffffff",
109
+ plot_bgcolor="#ffffff",
110
+ height=460,
111
+ showlegend=False,
112
+ )
113
+ return fig
114
+
115
+
116
+ def build_square_domain_plot(
117
+ nx: int,
118
+ title: str,
119
+ hole_edges=None,
120
+ *,
121
+ show_edges: bool = True,
122
+ dense_grid: bool = False,
123
+ ) -> go.Figure:
124
+ """Build a 3D square domain plot with optional hole."""
125
+ nx = max(int(nx), 3)
126
+ grid = np.linspace(0.0, 1.0, nx)
127
+ X, Y = np.meshgrid(grid, grid, indexing="xy")
128
+ Z = np.zeros_like(X, dtype=float)
129
+ color_field = np.full_like(Z, 0.85)
130
+
131
+ if hole_edges is not None:
132
+ xL, xR, yB, yT = hole_edges
133
+ mask = (X >= xL) & (X <= xR) & (Y >= yB) & (Y <= yT)
134
+ Z = np.where(mask, np.nan, Z)
135
+ color_field = np.where(mask, np.nan, color_field)
136
+
137
+ fig = go.Figure()
138
+ fig.add_trace(
139
+ go.Surface(
140
+ x=X,
141
+ y=Y,
142
+ z=Z,
143
+ surfacecolor=np.where(np.isnan(color_field), 0.15, color_field),
144
+ colorscale=EXCITATION_SURFACE_COLORSCALE,
145
+ cmin=0.0,
146
+ cmax=1.0,
147
+ showscale=False,
148
+ opacity=0.98,
149
+ lighting=dict(ambient=0.85, diffuse=0.55, specular=0.1),
150
+ hovertemplate="x=%{x:.3f}<br>y=%{y:.3f}<extra></extra>",
151
+ )
152
+ )
153
+
154
+ if show_edges:
155
+ base_z = -0.012
156
+ grid_vals = (
157
+ np.linspace(0.0, 1.0, max(int(nx), 2))
158
+ if dense_grid
159
+ else np.asarray(DEFAULT_AXIS_TICKS)
160
+ )
161
+ line_x, line_y, line_z = [], [], []
162
+ x_min_val, x_max_val = float(grid[0]), float(grid[-1])
163
+ for val in grid_vals:
164
+ val_f = float(val)
165
+ line_x.extend([val_f, val_f, np.nan])
166
+ line_y.extend([x_min_val, x_max_val, np.nan])
167
+ line_z.extend([base_z, base_z, np.nan])
168
+ for val in grid_vals:
169
+ val_f = float(val)
170
+ line_x.extend([x_min_val, x_max_val, np.nan])
171
+ line_y.extend([val_f, val_f, np.nan])
172
+ line_z.extend([base_z, base_z, np.nan])
173
+ fig.add_trace(
174
+ go.Scatter3d(
175
+ x=line_x,
176
+ y=line_y,
177
+ z=line_z,
178
+ mode="lines",
179
+ line=dict(color="rgba(174,139,216,0.65)", width=1.6),
180
+ showlegend=False,
181
+ hoverinfo="skip",
182
+ )
183
+ )
184
+
185
+ scale_ticks = list(DEFAULT_AXIS_TICKS)
186
+ tick_text = [f"{t:.2f}" for t in scale_ticks]
187
+ tick_plane = -0.02
188
+ fig.add_trace(
189
+ go.Scatter3d(
190
+ x=scale_ticks,
191
+ y=[-0.018] * len(scale_ticks),
192
+ z=[tick_plane] * len(scale_ticks),
193
+ mode="text",
194
+ text=tick_text,
195
+ textfont=dict(color="#5F259F", size=12),
196
+ showlegend=False,
197
+ hoverinfo="skip",
198
+ )
199
+ )
200
+ fig.add_trace(
201
+ go.Scatter3d(
202
+ x=[-0.018] * len(scale_ticks),
203
+ y=scale_ticks,
204
+ z=[tick_plane] * len(scale_ticks),
205
+ mode="text",
206
+ text=tick_text,
207
+ textfont=dict(color="#5F259F", size=12),
208
+ showlegend=False,
209
+ hoverinfo="skip",
210
+ )
211
+ )
212
+
213
+ if hole_edges is not None:
214
+ xL, xR, yB, yT = hole_edges
215
+ fig.add_trace(
216
+ go.Scatter3d(
217
+ x=[xL, xR, xR, xL, xL],
218
+ y=[yB, yB, yT, yT, yB],
219
+ z=[0.0] * 5,
220
+ mode="lines",
221
+ line=dict(color="#FFFFFF", width=5),
222
+ hoverinfo="skip",
223
+ showlegend=False,
224
+ )
225
+ )
226
+
227
+ fig.update_layout(
228
+ title=title,
229
+ margin=dict(l=8, r=8, t=44, b=8),
230
+ height=620,
231
+ template="plotly_white",
232
+ scene=dict(
233
+ xaxis=dict(range=[-0.05, 1.05], visible=False, backgroundcolor="#f7f3ff"),
234
+ yaxis=dict(range=[-0.05, 1.05], visible=False, backgroundcolor="#f7f3ff"),
235
+ zaxis=dict(range=[0.1, 0.1], visible=False, backgroundcolor="#f7f3ff"),
236
+ aspectmode="cube",
237
+ camera=dict(eye=dict(x=1.25, y=1.25, z=0.85)),
238
+ ),
239
+ dragmode="orbit",
240
+ uirevision="geometry_surface",
241
+ )
242
+ return fig
243
+
244
+
245
+ def push_geometry_plot(fig: go.Figure):
246
+ """Push a geometry plot to the UI."""
247
+ try:
248
+ if hasattr(ctrl, "geometry_preview_update"):
249
+ ctrl.geometry_preview_update(fig)
250
+ except Exception:
251
+ pass
252
+
253
+
254
+ def update_geometry_preview():
255
+ """Update the geometry preview based on current state."""
256
+ if not state.bound:
257
+ return
258
+
259
+ geo = state.geometry_selection
260
+ if geo in (None, "None"):
261
+ fig = build_geometry_placeholder("Select a geometry to preview.")
262
+ elif geo == "Square Domain":
263
+ nx = int(state.nx or 16)
264
+ fig = build_square_domain_plot(nx, "Square Domain Preview", None, show_edges=True)
265
+ elif geo == "Square Metallic Body":
266
+ nx = int(state.nx or 16)
267
+ # Use hole parameters from state
268
+ edges = compute_hole_edges(
269
+ nx,
270
+ float(state.hole_center_x or 0.5),
271
+ float(state.hole_center_y or 0.5),
272
+ float(state.hole_size_edge or 0.2),
273
+ snap=getattr(state, "hole_snap", True),
274
+ )
275
+ fig = build_square_domain_plot(nx, "Square Metallic Body Preview", edges, show_edges=True)
276
+ else:
277
+ fig = build_geometry_placeholder(f"Geometry: {geo}")
278
+
279
+ push_geometry_plot(fig)
280
+
281
+
282
+ def update_geometry_hole_preview():
283
+ """Update geometry preview with current hole settings."""
284
+ if not state.bound:
285
+ return
286
+
287
+ geo = state.geometry_selection
288
+ if geo != "Square Metallic Body":
289
+ return
290
+
291
+ nx = int(state.nx or 16)
292
+ edges = compute_hole_edges(
293
+ nx,
294
+ float(state.hole_center_x or 0.5),
295
+ float(state.hole_center_y or 0.5),
296
+ float(state.hole_size_edge or 0.2),
297
+ snap=getattr(state, "hole_snap", True),
298
+ )
299
+
300
+ if edges is None:
301
+ state.hole_error_message = "Invalid hole configuration (edges out of bounds or degenerate)"
302
+ else:
303
+ state.hole_error_message = ""
304
+
305
+ fig = build_square_domain_plot(nx, "Square Metallic Body Preview", edges, show_edges=True)
306
+ push_geometry_plot(fig)
em/globals.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EM Embedded - Global Variables and Constants
3
+
4
+ Contains PyVista plotter, simulation data, caches, and constants
5
+ shared across the EM module.
6
+ """
7
+ import numpy as np
8
+ import pyvista as pv
9
+
10
+ # Set PyVista to use off-screen rendering for Trame
11
+ pv.OFF_SCREEN = True
12
+
13
+ __all__ = [
14
+ "plotter",
15
+ "simulation_data",
16
+ "current_mesh",
17
+ "data_frames",
18
+ "X_grids",
19
+ "Y_grids",
20
+ "surface_clims",
21
+ "stop_simulation",
22
+ "snapshot_times",
23
+ "z_scale",
24
+ "GRID_SIZES",
25
+ "DEFAULT_AXIS_TICKS",
26
+ "EXCITATION_SURFACE_COLORSCALE",
27
+ "qpu_ts_cache",
28
+ "sim_ts_cache",
29
+ "qpu_cfg_id_counter",
30
+ ]
31
+
32
+ # --- Constants ---
33
+ GRID_SIZES = ["16", "32", "64", "128", "256", "512"]
34
+ DEFAULT_AXIS_TICKS = (0.0, 0.25, 0.5, 0.75, 1.0)
35
+
36
+ EXCITATION_SURFACE_COLORSCALE = [
37
+ [0.0, "#001219"],
38
+ [0.25, "#005F73"],
39
+ [0.5, "#94D2BD"],
40
+ [0.75, "#EE9B00"],
41
+ [1.0, "#CA6702"],
42
+ ]
43
+
44
+ # --- PyVista and Simulation Data ---
45
+ plotter = pv.Plotter()
46
+ simulation_data = None
47
+ current_mesh = None
48
+ data_frames = None
49
+ z_scale = 1.0
50
+ X_grids = {} # Dictionary: field -> meshgrid X
51
+ Y_grids = {} # Dictionary: field -> meshgrid Y
52
+ surface_clims = {}
53
+ stop_simulation = False # Flag to stop running simulation
54
+ snapshot_times = None # Times corresponding to saved frames
55
+
56
+ # --- Caches ---
57
+ # Cache for QPU Plotly export/selection
58
+ qpu_ts_cache = {
59
+ "times": None,
60
+ "series_map": None,
61
+ "field": None,
62
+ "fig": None,
63
+ "positions_by_field": {"All": []},
64
+ "key_to_label": {},
65
+ "label_to_keys": {},
66
+ "nx": None,
67
+ "unique_fields": [],
68
+ }
69
+
70
+ # Cache for Simulator Time Series
71
+ sim_ts_cache = {
72
+ "fig": None,
73
+ "field": None,
74
+ }
75
+
76
+ # Stable id generator for QPU monitor config rows
77
+ qpu_cfg_id_counter = 1
78
+
79
+
80
+ def new_monitor_cfg(field: str = "Ez", points: str = "(8, 8)") -> dict:
81
+ """Create a new QPU monitor configuration."""
82
+ global qpu_cfg_id_counter
83
+ cfg = {"id": qpu_cfg_id_counter, "field": field, "points": points}
84
+ qpu_cfg_id_counter += 1
85
+ return cfg
86
+
87
+
88
+ def reset_globals():
89
+ """Reset global simulation state."""
90
+ global simulation_data, current_mesh, data_frames, X_grids, Y_grids
91
+ global surface_clims, stop_simulation, snapshot_times, z_scale
92
+
93
+ simulation_data = None
94
+ current_mesh = None
95
+ data_frames = None
96
+ X_grids = {}
97
+ Y_grids = {}
98
+ surface_clims = {}
99
+ stop_simulation = False
100
+ snapshot_times = None
101
+ z_scale = 1.0
102
+
103
+
104
+ def set_stop_simulation(value: bool):
105
+ """Set the stop simulation flag."""
106
+ global stop_simulation
107
+ stop_simulation = value
em/handlers.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """State change handlers for the EM module.
2
+
3
+ All @state.change decorated functions that respond to UI state changes.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from typing import TYPE_CHECKING
9
+
10
+ from .state import state, ctrl
11
+ from .geometry import compute_hole_edges as _compute_hole_edges, update_geometry_hole_preview
12
+ from .excitation import update_initial_state_preview, update_excitation_info_message
13
+ from .qpu import (
14
+ update_qpu_sample_slot as _update_qpu_sample_slot,
15
+ refresh_all_qpu_sample_slots as _refresh_all_qpu_sample_slots,
16
+ hide_qpu_plots as _hide_qpu_plots,
17
+ )
18
+ from .simulation import update_sim_monitor_points as _update_sim_monitor_points
19
+
20
+ if TYPE_CHECKING:
21
+ pass
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Workflow Highlights (visual feedback for UI cards)
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def _determine_workflow_step() -> int:
29
+ """Determine which configuration step the user is on (1-6)."""
30
+ try:
31
+ if not state.problem_selection:
32
+ return 1
33
+ if not state.geometry_selection:
34
+ return 2
35
+ if not state.dist_type:
36
+ return 3
37
+ nx = state.nx
38
+ if nx is None:
39
+ return 4
40
+ if not state.backend_type:
41
+ return 5
42
+ return 6
43
+ except Exception:
44
+ return 1
45
+
46
+
47
+ def _apply_workflow_highlights(step: int):
48
+ """Set card styles based on current workflow step."""
49
+ try:
50
+ base_style = "font-size: 0.8rem;"
51
+ highlight = "font-size: 0.8rem; border: 2px solid #5F259F; box-shadow: 0 0 8px rgba(95,37,159,0.25);"
52
+ state.overview_card_style = highlight if step == 1 else base_style
53
+ state.geometry_card_style = highlight if step == 2 else base_style
54
+ state.excitation_card_style = highlight if step == 3 else base_style
55
+ state.meshing_card_style = highlight if step == 4 else base_style
56
+ state.backend_card_style = highlight if step == 5 else base_style
57
+ state.output_card_style = highlight if step == 6 else base_style
58
+ except Exception:
59
+ pass
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Qubit Plot (meshing slider)
64
+ # ---------------------------------------------------------------------------
65
+
66
+ def build_qubit_plot(grid_size: int):
67
+ """Build a Plotly figure showing qubit requirements vs grid size."""
68
+ import numpy as np
69
+ import plotly.graph_objects as go
70
+
71
+ x_sizes = np.array([16, 32, 64, 128, 256, 512])
72
+ y_qubits = 2 * np.ceil(np.log2(x_sizes)).astype(int) + 4
73
+ current_nq = int(2 * np.ceil(np.log2(max(1, int(grid_size)))) + 4)
74
+
75
+ fig = go.Figure()
76
+ fig.add_trace(go.Scatter(x=x_sizes, y=y_qubits, mode='lines', name='Total Qubits', line=dict(color='#7A3DB5', width=3)))
77
+ fig.add_trace(go.Scatter(x=[grid_size], y=[current_nq], mode='markers', marker=dict(size=10, color='#5F259F'), name='Current Selection'))
78
+
79
+ x_min = int(x_sizes.min())
80
+ x_max = int(x_sizes.max())
81
+ y_min = int(y_qubits.min())
82
+ y_max = int(max(y_qubits.max(), current_nq))
83
+ fig.update_xaxes(
84
+ range=[x_min - 8, x_max + 8],
85
+ tickmode='array',
86
+ tickvals=x_sizes,
87
+ ticktext=[str(v) for v in x_sizes],
88
+ title_text="Grid Size (nx)",
89
+ gridcolor='rgba(95,37,159,0.1)',
90
+ zerolinecolor='rgba(95,37,159,0.3)'
91
+ )
92
+ fig.update_yaxes(
93
+ range=[y_min - 1, y_max + 1],
94
+ dtick=1,
95
+ title_text="Total Qubits (nq)",
96
+ gridcolor='rgba(95,37,159,0.1)',
97
+ zerolinecolor='rgba(95,37,159,0.3)'
98
+ )
99
+ fig.update_layout(
100
+ margin=dict(l=30, r=10, t=10, b=30),
101
+ autosize=True,
102
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1),
103
+ font=dict(color='#1A1A1A'),
104
+ paper_bgcolor='#FFFFFF',
105
+ plot_bgcolor='#FFFFFF',
106
+ colorway=['#5F259F', '#7A3DB5', '#AE8BD8', '#5F259F'],
107
+ )
108
+ return fig
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # State Change Handlers - Registered after server binding
113
+ # ---------------------------------------------------------------------------
114
+
115
+ def register_handlers():
116
+ """Register all @state.change handlers. Call after server is bound."""
117
+ if not state.bound:
118
+ return
119
+
120
+ @state.change("nx")
121
+ def update_qubit_plot_handler(nx, **kwargs):
122
+ try:
123
+ ctrl.qubit_plot_update(build_qubit_plot(int(nx)))
124
+ except Exception:
125
+ pass
126
+
127
+ @state.change("hole_size_edge", "hole_center_x", "hole_center_y", "geometry_selection", "hole_snap")
128
+ def validate_hole_inputs(**kwargs):
129
+ # Only validate when Square Metallic Body is selected
130
+ if state.geometry_selection != "Square Metallic Body":
131
+ state.hole_error_message = ""
132
+ return
133
+ try:
134
+ s = float(state.hole_size_edge)
135
+ cx = float(state.hole_center_x)
136
+ cy = float(state.hole_center_y)
137
+ except Exception:
138
+ state.hole_error_message = "Hole size and center must be numeric."
139
+ return
140
+
141
+ # Use selected nx, fall back to a safe default
142
+ try:
143
+ nx = int(state.nx or 32)
144
+ except Exception:
145
+ nx = 32
146
+
147
+ if s > 1.0:
148
+ state.hole_error_message = "Hole edge length must be <= 1."
149
+ return
150
+ if not (0.0 < cx < 1.0) or not (0.0 < cy < 1.0):
151
+ state.hole_error_message = "Hole center must be strictly within (0, 1) for both X and Y."
152
+ return
153
+
154
+ # Alignment check (strict vs snap)
155
+ mode_snap = bool(state.hole_snap)
156
+ edges = _compute_hole_edges(nx, cx, cy, s, snap=mode_snap)
157
+ if not mode_snap and edges is None:
158
+ state.hole_error_message = "Hole edges must align with grid lines; enable Snap to auto-align."
159
+ return
160
+
161
+ # Inputs valid; clear error and refresh preview
162
+ state.hole_error_message = ""
163
+ update_geometry_hole_preview()
164
+
165
+ @state.change("hole_center_pair")
166
+ def sync_hole_center_pair(hole_center_pair, **kwargs):
167
+ """Parse bracket-format pair (x, y) from dropdown into numeric center fields."""
168
+ try:
169
+ m = re.match(r"\(\s*([-+]?[0-9]*\.?[0-9]+)\s*,\s*([-+]?[0-9]*\.?[0-9]+)\s*\)", str(hole_center_pair))
170
+ if not m:
171
+ raise ValueError("Invalid format")
172
+ state.hole_center_x = float(m.group(1))
173
+ state.hole_center_y = float(m.group(2))
174
+ state.hole_error_message = ""
175
+ except Exception:
176
+ state.hole_error_message = "Invalid hole center. Use format (x, y)."
177
+
178
+ @state.change("sigma_pair")
179
+ def sync_sigma_pair(sigma_pair, **kwargs):
180
+ """Parse bracket-format pair (x, y) for Sigma and update sigma_x/sigma_y."""
181
+ try:
182
+ m = re.match(r"\(\s*([-+]?[0-9]*\.?[0-9]+)\s*,\s*([-+]?[0-9]*\.?[0-9]+)\s*\)", str(sigma_pair))
183
+ if not m:
184
+ raise ValueError("Invalid format")
185
+ x = max(0.0, min(1.0, float(m.group(1))))
186
+ y = max(0.0, min(1.0, float(m.group(2))))
187
+ state.sigma_x = x
188
+ state.sigma_y = y
189
+ state.excitation_error_message = ""
190
+ except Exception:
191
+ state.excitation_error_message = "Invalid Sigma. Use format (x, y) in [0,1]."
192
+
193
+ @state.change("dist_type")
194
+ def normalize_dist_type(dist_type, **kwargs):
195
+ # Allow unselecting via 'None'
196
+ if dist_type in (None, "", "None"):
197
+ state.dist_type = None
198
+ update_initial_state_preview()
199
+ _apply_workflow_highlights(_determine_workflow_step())
200
+ return
201
+ update_excitation_info_message()
202
+ _apply_workflow_highlights(_determine_workflow_step())
203
+
204
+ @state.change("qpu_monitor_samples")
205
+ def on_qpu_monitor_samples_change(**kwargs):
206
+ _update_qpu_sample_slot(1)
207
+
208
+ @state.change("qpu_monitor_samples_2")
209
+ def on_qpu_monitor_samples_2_change(**kwargs):
210
+ _update_qpu_sample_slot(2)
211
+
212
+ @state.change("qpu_monitor_samples_3")
213
+ def on_qpu_monitor_samples_3_change(**kwargs):
214
+ _update_qpu_sample_slot(3)
215
+
216
+ @state.change("qpu_monitor_samples_4")
217
+ def on_qpu_monitor_samples_4_change(**kwargs):
218
+ _update_qpu_sample_slot(4)
219
+
220
+ @state.change("qpu_monitor_samples_5")
221
+ def on_qpu_monitor_samples_5_change(**kwargs):
222
+ _update_qpu_sample_slot(5)
223
+
224
+ @state.change("nx")
225
+ def on_nx_change_refresh_qpu_samples(nx, **kwargs):
226
+ _refresh_all_qpu_sample_slots()
227
+ _update_sim_monitor_points()
228
+ _apply_workflow_highlights(_determine_workflow_step())
229
+
230
+ @state.change("dt_user")
231
+ def validate_dt_user(dt_user, **kwargs):
232
+ """Validate snapshot Δt: must be >= 0.1 (solver dt) and a multiple of 0.1."""
233
+ try:
234
+ dt_val = float(dt_user)
235
+ except Exception:
236
+ state.temporal_warning = "Δt must be numeric. Frames are captured every Δt."
237
+ return
238
+ tol = 1e-9
239
+ if dt_val < 0.1 - tol:
240
+ state.temporal_warning = "Δt < 0.1 is unsupported (solver dt = 0.1 s)."
241
+ elif abs((dt_val / 0.1) - round(dt_val / 0.1)) > 1e-9:
242
+ state.temporal_warning = "Δt must be a multiple of 0.1 s."
243
+ else:
244
+ state.temporal_warning = ""
245
+
246
+ @state.change("backend_type")
247
+ def on_backend_change(backend_type, **kwargs):
248
+ if backend_type == "QPU":
249
+ _hide_qpu_plots()
250
+ _apply_workflow_highlights(_determine_workflow_step())
251
+
252
+ @state.change("selected_qpu")
253
+ def on_selected_qpu_change(selected_qpu, **kwargs):
254
+ if state.backend_type == "QPU":
255
+ _hide_qpu_plots()
256
+
257
+ @state.change("qpu_plot_filter")
258
+ def on_qpu_plot_filter_change(qpu_plot_filter, **kwargs):
259
+ # No-op: updates handled by controller bound to the VSelect to avoid double refresh
260
+ return
261
+
262
+ @state.change("problem_selection")
263
+ def on_problem_change(problem_selection, **kwargs):
264
+ """Update geometry options based on problem selection."""
265
+ # Filter geometry options based on problem
266
+ if problem_selection == "Propagation in a given medium (no bodies)":
267
+ # Hide "Square Metallic Body" for propagation problem
268
+ state.geometry_options = ["None", "Square Domain", "Geometry 2", "Add"]
269
+ elif problem_selection == "Scattering from a perfectly conducting body":
270
+ # Hide "Square Domain" for scattering problem
271
+ state.geometry_options = ["None", "Square Metallic Body", "Geometry 2", "Add"]
272
+ else:
273
+ # Show all options when no problem selected
274
+ state.geometry_options = ["None", "Square Metallic Body", "Square Domain", "Geometry 2", "Add"]
275
+
276
+ # Reset geometry selection if current selection is no longer valid
277
+ if state.geometry_selection not in state.geometry_options:
278
+ state.geometry_selection = None
279
+
280
+ _apply_workflow_highlights(_determine_workflow_step())
281
+
282
+ @state.change("geometry_selection")
283
+ def on_geometry_change(geometry_selection, **kwargs):
284
+ _apply_workflow_highlights(_determine_workflow_step())
285
+
286
+ @state.change("peak_pair")
287
+ def sync_peak_pair(peak_pair, **kwargs):
288
+ """Parse peak pair (x, y) and validate."""
289
+ try:
290
+ m = re.match(r"\(\s*([-+]?[0-9]*\.?[0-9]+)\s*,\s*([-+]?[0-9]*\.?[0-9]+)\s*\)", str(peak_pair))
291
+ if not m:
292
+ raise ValueError("Invalid format")
293
+ x = float(m.group(1))
294
+ y = float(m.group(2))
295
+ if not (0.0 <= x <= 1.0) or not (0.0 <= y <= 1.0):
296
+ state.excitation_error_message = "Peak must be in [0,1]."
297
+ return
298
+ state.peak_x = x
299
+ state.peak_y = y
300
+ state.excitation_error_message = ""
301
+ update_initial_state_preview()
302
+ except Exception:
303
+ state.excitation_error_message = "Invalid peak. Use format (x, y)."
304
+
305
+ @state.change("mu_pair")
306
+ def sync_mu_pair(mu_pair, **kwargs):
307
+ """Parse mu pair (x, y) and validate."""
308
+ try:
309
+ m = re.match(r"\(\s*([-+]?[0-9]*\.?[0-9]+)\s*,\s*([-+]?[0-9]*\.?[0-9]+)\s*\)", str(mu_pair))
310
+ if not m:
311
+ raise ValueError("Invalid format")
312
+ x = float(m.group(1))
313
+ y = float(m.group(2))
314
+ if not (0.0 <= x <= 1.0) or not (0.0 <= y <= 1.0):
315
+ state.excitation_error_message = "Mu must be in [0,1]."
316
+ return
317
+ state.mu_x = x
318
+ state.mu_y = y
319
+ state.excitation_error_message = ""
320
+ update_initial_state_preview()
321
+ except Exception:
322
+ state.excitation_error_message = "Invalid mu. Use format (x, y)."
323
+
324
+ @state.change("sigma_x", "sigma_y")
325
+ def on_sigma_change(sigma_x, sigma_y, **kwargs):
326
+ update_initial_state_preview()
327
+
328
+ # -----------------------------------------------------------------------
329
+ # Additional handlers for simulation workflow
330
+ # -----------------------------------------------------------------------
331
+
332
+ @state.change("nx_slider_index")
333
+ def on_slider_index_change(nx_slider_index, **kwargs):
334
+ """Handle grid size slider changes."""
335
+ from .globals import GRID_SIZES
336
+ if nx_slider_index is None:
337
+ state.nx = None
338
+ else:
339
+ try:
340
+ state.nx = int(GRID_SIZES[int(nx_slider_index)])
341
+ except Exception:
342
+ state.nx = None
343
+ update_excitation_info_message()
344
+
345
+ @state.change("nx", "T", "dist_type", "impulse_x", "impulse_y", "mu_x", "mu_y", "sigma_x", "sigma_y", "coeff_permittivity", "coeff_permeability")
346
+ def on_input_parameter_change(**kwargs):
347
+ """Handle changes to input parameters."""
348
+ from .simulation import generate_plot
349
+
350
+ if state.is_running:
351
+ return
352
+
353
+ update_excitation_info_message()
354
+
355
+ if state.backend_type == "QPU":
356
+ state.qpu_ts_ready = False
357
+ state.qpu_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
358
+ state.qpu_ts_other_ready = False
359
+ state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
360
+
361
+ changed_keys = set(kwargs.keys())
362
+
363
+ if state.simulation_has_run:
364
+ state.run_button_text = "Re-run!"
365
+ return
366
+
367
+ preview_params = {"nx", "dist_type", "impulse_x", "impulse_y", "mu_x", "mu_y", "sigma_x", "sigma_y"}
368
+ if changed_keys & preview_params:
369
+ update_initial_state_preview()
370
+
371
+ @state.change("output_type", "timeseries_field", "timeseries_points")
372
+ def on_output_config_change(**kwargs):
373
+ """Handle changes to output configuration."""
374
+ from .simulation import generate_plot
375
+ _update_sim_monitor_points()
376
+ if state.simulation_has_run:
377
+ generate_plot()
378
+
379
+ @state.change("timeseries_points")
380
+ def on_timeseries_points_text_change(**kwargs):
381
+ """Handle changes to timeseries points text."""
382
+ _update_sim_monitor_points()
383
+
384
+ @state.change("surface_field")
385
+ def on_surface_field_change(surface_field, **kwargs):
386
+ """Handle changes to surface field selection."""
387
+ from .simulation import redraw_surface_plot
388
+ if state.simulation_has_run and state.output_type == "Surface Plot":
389
+ redraw_surface_plot()
390
+
391
+ @state.change("time_val")
392
+ def on_time_change(time_val, **kwargs):
393
+ """Handle changes to time value."""
394
+ from .simulation import redraw_surface_plot
395
+ if not state.simulation_has_run or state.output_type != "Surface Plot":
396
+ return
397
+ redraw_surface_plot()
398
+
399
+ @state.change("geometry_selection")
400
+ def handle_geometry_add(geometry_selection, **kwargs):
401
+ """Handle geometry selection changes."""
402
+ if geometry_selection in (None, "", "None"):
403
+ state.geometry_selection = None
404
+ update_initial_state_preview()
405
+ _apply_workflow_highlights(_determine_workflow_step())
406
+ return
407
+ if geometry_selection == "Add":
408
+ state.show_upload_dialog = True
409
+ state.geometry_selection = None
410
+ _apply_workflow_highlights(_determine_workflow_step())
411
+ return
412
+ update_initial_state_preview()
413
+ _apply_workflow_highlights(_determine_workflow_step())
414
+
415
+ @state.change("uploaded_file_info")
416
+ def handle_file_upload(uploaded_file_info, **kwargs):
417
+ """Handle file upload completion."""
418
+ if uploaded_file_info:
419
+ file_name = uploaded_file_info.get("name", "unknown file")
420
+ print(f"File selected (dummy upload): {file_name}")
421
+ state.show_upload_dialog = False
422
+ state.upload_status_message = f"File '{file_name}' uploaded."
423
+ state.show_upload_status = True
em/qpu.py ADDED
@@ -0,0 +1,657 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EM Embedded - QPU Module
3
+
4
+ Contains QPU timeseries, caching, filters, and handlers.
5
+ """
6
+ import re
7
+ import numpy as np
8
+ import plotly.graph_objects as go
9
+ from collections import defaultdict
10
+ from matplotlib import cm as _mpl_cm
11
+
12
+ from .state import state, ctrl
13
+ from .globals import qpu_ts_cache
14
+ from .utils import normalized_position_label, format_grid_label
15
+
16
+ # Import backend functions
17
+ try:
18
+ from quantum.utils.delta_impulse_generator import create_time_frames, run_qpu
19
+ import quantum.utils.delta_impulse_generator as qutils
20
+ except ModuleNotFoundError:
21
+ from utils.delta_impulse_generator import create_time_frames, run_qpu
22
+ import utils.delta_impulse_generator as qutils
23
+
24
+ __all__ = [
25
+ "cmap_for_field",
26
+ "update_qpu_position_options",
27
+ "filter_series_keys",
28
+ "refresh_qpu_plot_figures",
29
+ "build_qpu_timeseries_plotly_multi",
30
+ "rebuild_qpu_fig_filtered",
31
+ "rebuild_qpu_fig_others",
32
+ "on_qpu_ts_click",
33
+ "on_qpu_ts_clear",
34
+ "qpu_add_monitor_config",
35
+ "qpu_remove_monitor_config",
36
+ "qpu_set_monitor_field",
37
+ "qpu_set_monitor_points",
38
+ "qpu_set_plot_filter",
39
+ "qpu_set_plot_position_filter",
40
+ "qpu_add_monitor_slot",
41
+ "qpu_remove_monitor_slot",
42
+ # Internal functions needed by handlers
43
+ "hide_qpu_plots",
44
+ "update_qpu_sample_slot",
45
+ "refresh_all_qpu_sample_slots",
46
+ ]
47
+
48
+
49
+ def hide_qpu_plots():
50
+ """Hide QPU timeseries plots."""
51
+ state.qpu_ts_ready = False
52
+ state.qpu_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
53
+ state.qpu_ts_other_ready = False
54
+ state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
55
+ state.qpu_plot_position_options = ["All positions"]
56
+ state.qpu_plot_position_filter = "All positions"
57
+
58
+
59
+ def update_qpu_sample_slot(slot: int):
60
+ """Update QPU sample slot with grid point info."""
61
+ try:
62
+ samples_var = f"qpu_monitor_samples{'_' + str(slot) if slot > 1 else ''}"
63
+ info_var = f"qpu_monitor_sample_info{'_' + str(slot) if slot > 1 else ''}"
64
+ gp_var = f"qpu_monitor_gridpoints{'_' + str(slot) if slot > 1 else ''}"
65
+
66
+ samples = getattr(state, samples_var, "")
67
+ nx = int(state.nx or 16)
68
+
69
+ if not samples:
70
+ setattr(state, info_var, "")
71
+ setattr(state, gp_var, "")
72
+ return
73
+
74
+ # Parse sample pairs
75
+ pairs = re.findall(r'\(\s*([-+]?\d*\.?\d+)\s*,\s*([-+]?\d*\.?\d+)\s*\)', str(samples))
76
+ if not pairs:
77
+ setattr(state, info_var, "Invalid format. Use (x, y).")
78
+ setattr(state, gp_var, "")
79
+ return
80
+
81
+ gps = []
82
+ labels = []
83
+ for x_str, y_str in pairs:
84
+ x = float(x_str)
85
+ y = float(y_str)
86
+ i = int(round(x * (nx - 1)))
87
+ j = int(round(y * (nx - 1)))
88
+ i = max(0, min(nx - 1, i))
89
+ j = max(0, min(nx - 1, j))
90
+ gps.append((i, j))
91
+ labels.append(f"({i}, {j})")
92
+
93
+ setattr(state, gp_var, "; ".join(labels))
94
+ setattr(state, info_var, f"Grid points: {', '.join(labels)}")
95
+ except Exception:
96
+ pass
97
+
98
+
99
+ def refresh_all_qpu_sample_slots():
100
+ """Refresh all QPU sample slots."""
101
+ for slot in range(1, 6):
102
+ update_qpu_sample_slot(slot)
103
+
104
+
105
+ def cmap_for_field(field: str):
106
+ """Choose colormap per field (Ez→Reds, Hx→Greens, Hy→Blues)."""
107
+ f = str(field)
108
+ if f == 'Ez':
109
+ return _mpl_cm.Reds
110
+ if f == 'Hx':
111
+ return _mpl_cm.Greens
112
+ return _mpl_cm.Blues
113
+
114
+
115
+ def update_qpu_position_options(current_field: str = "All"):
116
+ """Update QPU position filter options based on current field."""
117
+ try:
118
+ field_key = (current_field or "All").strip() or "All"
119
+ pos_map = qpu_ts_cache.get("positions_by_field") or {}
120
+ entries = pos_map.get(field_key) or pos_map.get("All") or []
121
+ labels = []
122
+ for entry in entries:
123
+ label = None
124
+ if isinstance(entry, dict):
125
+ label = entry.get("label")
126
+ if not label:
127
+ coords = entry.get("coords") or (None, None)
128
+ fld = entry.get("field")
129
+ label = format_grid_label(coords[0], coords[1], fld)
130
+ elif isinstance(entry, (list, tuple)) and len(entry) >= 2:
131
+ lbl_field = entry[2] if len(entry) >= 3 else (field_key if field_key not in ("", "All") else None)
132
+ label = format_grid_label(entry[0], entry[1], lbl_field)
133
+ if label:
134
+ labels.append(label)
135
+ labels = list(dict.fromkeys(labels))
136
+ options = ["All positions"] + labels if labels else ["All positions"]
137
+ state.qpu_plot_position_options = options
138
+ if state.qpu_plot_position_filter not in options:
139
+ state.qpu_plot_position_filter = options[0]
140
+ except Exception:
141
+ pass
142
+
143
+
144
+ def filter_series_keys(series_map, field_filter: str, position_filter: str):
145
+ """Filter series keys based on field and position filters."""
146
+ keys = list(series_map.keys())
147
+ ff = (field_filter or "All").strip()
148
+ pf = (position_filter or "All positions").strip()
149
+ if ff not in ("", "All"):
150
+ keys = [k for k in keys if str(k[0]) == ff]
151
+ if pf not in ("", "All", "All positions"):
152
+ label_map = qpu_ts_cache.get("label_to_keys") or {}
153
+ label_keys = label_map.get(pf)
154
+ if not label_keys:
155
+ return []
156
+ allowed = {(str(fld), int(px), int(py)) for (fld, px, py) in label_keys}
157
+ keys = [k for k in keys if (str(k[0]), int(k[1]), int(k[2])) in allowed]
158
+ return keys
159
+
160
+
161
+ def refresh_qpu_plot_figures():
162
+ """Refresh QPU plot figures with current filter settings."""
163
+ try:
164
+ field_filter = (state.qpu_plot_filter or "All").strip()
165
+ except Exception:
166
+ field_filter = "All"
167
+ try:
168
+ position_filter = (state.qpu_plot_position_filter or "All positions").strip()
169
+ except Exception:
170
+ position_filter = "All positions"
171
+
172
+ fig_all = qpu_ts_cache.get("fig")
173
+ times = qpu_ts_cache.get("times") or []
174
+ series_map = qpu_ts_cache.get("series_map") or {}
175
+ if fig_all is None or not times or not series_map:
176
+ return
177
+
178
+ update_qpu_position_options(field_filter)
179
+ fig_primary = rebuild_qpu_fig_filtered(field_filter, position_filter)
180
+ if fig_primary is None:
181
+ fig_primary = fig_all
182
+
183
+ if fig_primary is not None:
184
+ try:
185
+ ctrl.qpu_ts_update(fig_primary)
186
+ except Exception:
187
+ pass
188
+ state.qpu_ts_ready = True
189
+ state.qpu_plot_style = "width: 900px; height: 660px; margin: 0 auto;"
190
+ else:
191
+ state.qpu_ts_ready = False
192
+ state.qpu_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
193
+
194
+ if field_filter not in ("", "All") and position_filter in ("", "All", "All positions"):
195
+ fig_oth = rebuild_qpu_fig_others(field_filter, position_filter)
196
+ if fig_oth is not None and getattr(fig_oth, "data", None):
197
+ try:
198
+ ctrl.qpu_ts_other_update(fig_oth)
199
+ except Exception:
200
+ pass
201
+ state.qpu_ts_other_ready = True
202
+ state.qpu_other_plot_style = "width: 900px; height: 660px; margin: 0 auto;"
203
+ else:
204
+ state.qpu_ts_other_ready = False
205
+ state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
206
+ else:
207
+ state.qpu_ts_other_ready = False
208
+ state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
209
+
210
+
211
+ def build_qpu_timeseries_plotly_multi(configs, nx: int, T: float, snapshot_dt: float, impulse_pos, progress_callback=None, print_callback=None):
212
+ """Build multi-config Plotly time series for QPU results."""
213
+ times = qutils.create_time_frames(T, snapshot_dt)
214
+ fig = go.Figure()
215
+
216
+ all_triplets = []
217
+ cfg_expanded = []
218
+
219
+ for cfg in (configs or []):
220
+ field_type = (cfg.get("field") or "Ez").strip()
221
+ pts_str = str(cfg.get("points") or "").strip()
222
+ fields = ('Ez', 'Hx', 'Hy') if field_type == 'All' else (field_type,)
223
+ raw_pts = [tuple(map(int, m)) for m in re.findall(r"\((\d+)\s*,\s*(\d+)\)", pts_str)] or [impulse_pos]
224
+ for f in fields:
225
+ if f == 'Ez':
226
+ gw, gh = nx, nx
227
+ elif f == 'Hx':
228
+ gw, gh = nx, nx - 1
229
+ else:
230
+ gw, gh = nx - 1, nx
231
+ valid = []
232
+ for (px, py) in raw_pts:
233
+ if 0 <= px < gw and 0 <= py < gh:
234
+ valid.append((int(px), int(py)))
235
+ if not valid:
236
+ continue
237
+ cfg_expanded.append((f, valid))
238
+ all_triplets.extend((f, px, py) for (px, py) in valid)
239
+
240
+ max_sum = max(((px + py) for (_, px, py) in all_triplets), default=1)
241
+ if max_sum <= 0:
242
+ max_sum = 1
243
+
244
+ series_map = {}
245
+ positions_by_field = defaultdict(dict)
246
+ key_to_label = {}
247
+ label_to_keys = defaultdict(set)
248
+ max_abs = 0.0
249
+ dashes = ["solid", "dash", "dot", "dashdot"]
250
+ markers = ["circle", "square", "diamond", "triangle-up", "x"]
251
+
252
+ total_configs = len(cfg_expanded)
253
+ for idx, (field_type, valid_positions) in enumerate(cfg_expanded):
254
+ def _sub_progress(p):
255
+ if progress_callback:
256
+ base = (idx / total_configs) * 100
257
+ fraction = (1 / total_configs) * 100
258
+ total_p = base + (p / 100.0) * fraction
259
+ progress_callback(total_p)
260
+
261
+ try:
262
+ series_map_field = qutils.run_qpu(
263
+ field_type, valid_positions, None,
264
+ float(T), float(snapshot_dt), int(nx),
265
+ None, impulse_pos,
266
+ progress_callback=_sub_progress,
267
+ print_callback=print_callback
268
+ )
269
+ except Exception as e:
270
+ msg = f"QPU error for {field_type} positions {valid_positions}: {e}"
271
+ if print_callback:
272
+ print_callback(msg)
273
+ continue
274
+
275
+ cmap = cmap_for_field(field_type)
276
+ num_pts = len(valid_positions)
277
+
278
+ for i, (px, py) in enumerate(valid_positions):
279
+ ys = (series_map_field or {}).get((px, py), [])
280
+ if not ys or len(ys) != len(times):
281
+ continue
282
+ series_map[(field_type, px, py)] = list(ys)
283
+ try:
284
+ max_abs = max(max_abs, max((abs(float(v)) for v in ys)))
285
+ except Exception:
286
+ pass
287
+
288
+ if num_pts > 1:
289
+ s_index = i / (num_pts - 1)
290
+ s_light = 0.3 + 0.6 * s_index
291
+ else:
292
+ s_light = 0.6
293
+
294
+ rgba = cmap(s_light)
295
+ color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
296
+
297
+ if field_type == 'Ez':
298
+ gw, gh = nx, nx
299
+ elif field_type == 'Hx':
300
+ gw, gh = nx, nx - 1
301
+ else:
302
+ gw, gh = nx - 1, nx
303
+
304
+ label = normalized_position_label(px, py, gw, gh)
305
+ key = (str(field_type), int(px), int(py))
306
+ key_to_label[key] = label
307
+ label_to_keys[label].add(key)
308
+ positions_by_field[str(field_type)][(int(px), int(py))] = {
309
+ "coords": (int(px), int(py)),
310
+ "label": label,
311
+ "field": str(field_type),
312
+ }
313
+
314
+ fig.add_trace(
315
+ go.Scatter(
316
+ x=times,
317
+ y=ys,
318
+ mode='lines+markers',
319
+ name=label,
320
+ line=dict(color=color_hex, width=2.5, dash=dashes[i % len(dashes)]),
321
+ marker=dict(size=7, symbol=markers[i % len(markers)], color=color_hex),
322
+ hovertemplate=f"{field_type} | t=%{{x:.3f}}s<br>Value=%{{y:.6g}}<extra>{label}</extra>",
323
+ )
324
+ )
325
+
326
+ unique_fields = sorted({f for (f, _, _) in series_map.keys()})
327
+ fig.update_layout(
328
+ title=f"Time Series ({', '.join(unique_fields) if unique_fields else '—'})",
329
+ height=660, width=900,
330
+ margin=dict(l=50, r=30, t=50, b=50),
331
+ hovermode="x unified",
332
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1, title_text=""),
333
+ paper_bgcolor="#FFFFFF",
334
+ plot_bgcolor="#FFFFFF",
335
+ )
336
+ fig.update_xaxes(title_text="Time (s)", title_font=dict(size=22), tickfont=dict(size=16), showgrid=True, gridcolor="rgba(0,0,0,.06)")
337
+ fig.update_yaxes(title_text="Field Value", title_font=dict(size=22), tickfont=dict(size=16), showgrid=True, gridcolor="rgba(0,0,0,.06)")
338
+ if max_abs > 0:
339
+ pad = 0.12 * max_abs
340
+ fig.update_yaxes(range=[-max_abs - pad, max_abs + pad])
341
+
342
+ # Update cache
343
+ qpu_ts_cache["times"] = list(times)
344
+ qpu_ts_cache["series_map"] = series_map
345
+ qpu_ts_cache["field"] = ",".join(unique_fields) if len(unique_fields) == 1 else ("multi" if unique_fields else "")
346
+ qpu_ts_cache["fig"] = fig
347
+ qpu_ts_cache["unique_fields"] = list(unique_fields)
348
+
349
+ try:
350
+ positions_map_sorted = {}
351
+ all_entries = {}
352
+ for field_name, entry_map in positions_by_field.items():
353
+ entries = [entry_map[key] for key in sorted(entry_map.keys(), key=lambda xy: (xy[0], xy[1]))]
354
+ positions_map_sorted[field_name] = entries
355
+ for entry in entries:
356
+ all_entries.setdefault(entry["label"], entry)
357
+ positions_map_sorted["All"] = sorted(all_entries.values(), key=lambda entry: (entry["coords"][0], entry["coords"][1]))
358
+ qpu_ts_cache["positions_by_field"] = positions_map_sorted
359
+ qpu_ts_cache["key_to_label"] = key_to_label
360
+ qpu_ts_cache["label_to_keys"] = {lbl: sorted(list(vals)) for lbl, vals in label_to_keys.items()}
361
+ qpu_ts_cache["nx"] = int(nx)
362
+ except Exception:
363
+ qpu_ts_cache["positions_by_field"] = {"All": []}
364
+ qpu_ts_cache["key_to_label"] = {}
365
+ qpu_ts_cache["label_to_keys"] = {}
366
+
367
+ try:
368
+ state.qpu_plot_field_options = ["All"] + list(unique_fields)
369
+ state.qpu_plot_filter = "All"
370
+ update_qpu_position_options("All")
371
+ except Exception:
372
+ pass
373
+
374
+ return fig
375
+
376
+
377
+ def rebuild_qpu_fig_filtered(filter_value: str, position_filter: str = "All positions"):
378
+ """Rebuild QPU figure with field/position filters applied."""
379
+ try:
380
+ fv = (filter_value or "All").strip()
381
+ pf = (position_filter or "All positions").strip()
382
+ fig_all = qpu_ts_cache.get("fig")
383
+ times = qpu_ts_cache.get("times") or []
384
+ series_map = qpu_ts_cache.get("series_map") or {}
385
+ if fig_all is None or not times or not series_map:
386
+ return fig_all
387
+
388
+ use_base = fv in ("", "All") and pf in ("", "All", "All positions")
389
+ if use_base:
390
+ return fig_all
391
+
392
+ keys = filter_series_keys(series_map, fv, pf)
393
+ if not keys:
394
+ return None
395
+
396
+ fig = go.Figure()
397
+ dashes = ["solid", "dash", "dot", "dashdot"]
398
+ markers = ["circle", "square", "diamond", "triangle-up", "x"]
399
+ max_abs = 0.0
400
+ label_map = qpu_ts_cache.get("key_to_label") or {}
401
+ sorted_keys = sorted(keys, key=lambda x: (str(x[0]), x[1], x[2]))
402
+ num_keys = len(sorted_keys)
403
+
404
+ for i, k in enumerate(sorted_keys):
405
+ field_name, px, py = k
406
+ ys = series_map.get(k) or []
407
+ if not ys or len(ys) != len(times):
408
+ continue
409
+ try:
410
+ max_abs = max(max_abs, max((abs(float(v)) for v in ys)))
411
+ except Exception:
412
+ pass
413
+
414
+ cmap = cmap_for_field(field_name)
415
+ if num_keys > 1:
416
+ s_index = i / (num_keys - 1)
417
+ s_light = 0.3 + 0.6 * s_index
418
+ else:
419
+ s_light = 0.6
420
+
421
+ rgba = cmap(s_light)
422
+ color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
423
+ label = label_map.get((str(field_name), int(px), int(py))) or format_grid_label(px, py, field_name)
424
+
425
+ fig.add_trace(go.Scatter(
426
+ x=times,
427
+ y=ys,
428
+ mode='lines+markers',
429
+ name=label,
430
+ line=dict(color=color_hex, width=2.5, dash=dashes[i % len(dashes)]),
431
+ marker=dict(size=7, symbol=markers[i % len(markers)], color=color_hex),
432
+ hovertemplate=f"{field_name} | t=%{{x:.3f}}s<br>Value=%{{y:.6g}}<extra>{label}</extra>",
433
+ ))
434
+
435
+ title_parts = []
436
+ if fv not in ("", "All"):
437
+ title_parts.append(fv)
438
+ if pf not in ("", "All", "All positions"):
439
+ title_parts.append(pf)
440
+ suffix = " - ".join(title_parts) if title_parts else "Filtered"
441
+
442
+ fig.update_layout(
443
+ title=f"IBM QPU Time Series ({suffix})",
444
+ height=660, width=900,
445
+ margin=dict(l=50, r=30, t=50, b=50),
446
+ hovermode="x unified",
447
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
448
+ )
449
+ fig.update_xaxes(title_text="Time (s)", title_font=dict(size=22), tickfont=dict(size=16))
450
+ fig.update_yaxes(title_text="Field Value", title_font=dict(size=22), tickfont=dict(size=16))
451
+ if max_abs > 0:
452
+ pad = 0.12 * max_abs
453
+ fig.update_yaxes(range=[-max_abs - pad, max_abs + pad])
454
+
455
+ return fig
456
+ except Exception:
457
+ return qpu_ts_cache.get("fig")
458
+
459
+
460
+ def rebuild_qpu_fig_others(selected_field: str, position_filter: str = "All positions"):
461
+ """Build Plotly figure for all components except the selected one."""
462
+ try:
463
+ times = qpu_ts_cache.get("times") or []
464
+ series_map = qpu_ts_cache.get("series_map") or {}
465
+ if not times or not series_map:
466
+ return None
467
+
468
+ all_fields = sorted({str(k[0]) for k in series_map.keys()})
469
+ other_fields = [f for f in all_fields if f != selected_field]
470
+ if not other_fields:
471
+ return None
472
+
473
+ keys = [k for k in series_map.keys() if str(k[0]) in other_fields]
474
+ if not keys:
475
+ return None
476
+
477
+ fig = go.Figure()
478
+ dashes = ["solid", "dash", "dot", "dashdot"]
479
+ markers = ["circle", "square", "diamond", "triangle-up", "x"]
480
+ max_abs = 0.0
481
+ label_map = qpu_ts_cache.get("key_to_label") or {}
482
+ sorted_keys = sorted(keys, key=lambda x: (str(x[0]), x[1], x[2]))
483
+ num_keys = len(sorted_keys)
484
+
485
+ for i, k in enumerate(sorted_keys):
486
+ field_name, px, py = k
487
+ ys = series_map.get(k) or []
488
+ if not ys or len(ys) != len(times):
489
+ continue
490
+ try:
491
+ max_abs = max(max_abs, max((abs(float(v)) for v in ys)))
492
+ except Exception:
493
+ pass
494
+
495
+ cmap = cmap_for_field(field_name)
496
+ s_light = 0.6 if num_keys == 1 else 0.3 + 0.6 * (i / (num_keys - 1))
497
+ rgba = cmap(s_light)
498
+ color_hex = f"#{int(rgba[0]*255):02x}{int(rgba[1]*255):02x}{int(rgba[2]*255):02x}"
499
+ label = label_map.get((str(field_name), int(px), int(py))) or format_grid_label(px, py, field_name)
500
+
501
+ fig.add_trace(go.Scatter(
502
+ x=times,
503
+ y=ys,
504
+ mode='lines+markers',
505
+ name=label,
506
+ line=dict(color=color_hex, width=2.5, dash=dashes[i % len(dashes)]),
507
+ marker=dict(size=7, symbol=markers[i % len(markers)], color=color_hex),
508
+ hovertemplate=f"{field_name} | t=%{{x:.3f}}s<br>Value=%{{y:.6g}}<extra>{label}</extra>",
509
+ ))
510
+
511
+ fig.update_layout(
512
+ title=f"Other Components ({', '.join(other_fields)})",
513
+ height=660, width=900,
514
+ margin=dict(l=50, r=30, t=50, b=50),
515
+ hovermode="x unified",
516
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
517
+ )
518
+ if max_abs > 0:
519
+ pad = 0.12 * max_abs
520
+ fig.update_yaxes(range=[-max_abs - pad, max_abs + pad])
521
+
522
+ return fig
523
+ except Exception:
524
+ return None
525
+
526
+
527
+ # Click handlers
528
+ def on_qpu_ts_click(evt):
529
+ """Handle click on QPU time series plot."""
530
+ try:
531
+ if not evt or "points" not in evt or not evt["points"]:
532
+ return
533
+ x = float(evt["points"][0].get("x"))
534
+ times = qpu_ts_cache.get("times") or []
535
+ fig = qpu_ts_cache.get("fig")
536
+ if not times or fig is None:
537
+ return
538
+ idx = int(np.argmin(np.abs(np.asarray(times) - x)))
539
+ sel_t = float(times[idx])
540
+ fig.update_layout(shapes=[dict(
541
+ type="line", x0=sel_t, x1=sel_t, y0=0, y1=1,
542
+ xref="x", yref="paper",
543
+ line=dict(color="#5F259F", width=2, dash="dot")
544
+ )])
545
+ qpu_ts_cache["fig"] = fig
546
+ try:
547
+ ctrl.qpu_ts_update(fig)
548
+ except Exception:
549
+ pass
550
+ state.qpu_ts_selected_time = sel_t
551
+ except Exception:
552
+ pass
553
+
554
+
555
+ def on_qpu_ts_clear():
556
+ """Clear QPU time series selection."""
557
+ try:
558
+ fig = qpu_ts_cache.get("fig")
559
+ if fig is None:
560
+ return
561
+ fig.update_layout(shapes=[])
562
+ qpu_ts_cache["fig"] = fig
563
+ try:
564
+ ctrl.qpu_ts_update(fig)
565
+ except Exception:
566
+ pass
567
+ state.qpu_ts_selected_time = None
568
+ except Exception:
569
+ pass
570
+
571
+
572
+ # Register click handlers on controller
573
+ ctrl.on_qpu_ts_click = on_qpu_ts_click
574
+ ctrl.on_qpu_ts_clear = on_qpu_ts_clear
575
+
576
+
577
+ # Monitor config management
578
+ def qpu_add_monitor_config():
579
+ """Add a new QPU monitor configuration."""
580
+ from .globals import new_monitor_cfg
581
+ configs = list(state.qpu_monitor_configs or [])
582
+ configs.append(new_monitor_cfg())
583
+ state.qpu_monitor_configs = configs
584
+
585
+
586
+ def qpu_remove_monitor_config(index):
587
+ """Remove a QPU monitor configuration by index."""
588
+ configs = list(state.qpu_monitor_configs or [])
589
+ if 0 <= index < len(configs):
590
+ configs.pop(index)
591
+ state.qpu_monitor_configs = configs
592
+
593
+
594
+ def qpu_set_monitor_field(index, value):
595
+ """Set the field for a monitor config."""
596
+ configs = list(state.qpu_monitor_configs or [])
597
+ if 0 <= index < len(configs):
598
+ configs[index]["field"] = value
599
+ state.qpu_monitor_configs = configs
600
+
601
+
602
+ def qpu_set_monitor_points(index, value):
603
+ """Set the points for a monitor config."""
604
+ configs = list(state.qpu_monitor_configs or [])
605
+ if 0 <= index < len(configs):
606
+ configs[index]["points"] = value
607
+ state.qpu_monitor_configs = configs
608
+
609
+
610
+ def qpu_set_plot_filter(value):
611
+ """Set the component filter and refresh chart."""
612
+ state.qpu_plot_filter = value
613
+ update_qpu_position_options(value)
614
+ refresh_qpu_plot_figures()
615
+
616
+
617
+ def qpu_set_plot_position_filter(value):
618
+ """Set the position filter and refresh chart."""
619
+ state.qpu_plot_position_filter = value
620
+ refresh_qpu_plot_figures()
621
+
622
+
623
+ def qpu_add_monitor_slot():
624
+ """Add a new QPU monitor slot."""
625
+ try:
626
+ cnt = int(state.qpu_monitor_count or 0)
627
+ except Exception:
628
+ cnt = 0
629
+ if cnt < 4:
630
+ state.qpu_monitor_count = cnt + 1
631
+
632
+
633
+ def qpu_remove_monitor_slot(slot_index):
634
+ """Remove a QPU monitor slot."""
635
+ try:
636
+ cnt = int(state.qpu_monitor_count or 0)
637
+ except Exception:
638
+ cnt = 0
639
+ if slot_index <= cnt:
640
+ # Shift remaining slots up
641
+ for i in range(slot_index, cnt):
642
+ src = i + 1
643
+ setattr(state, f"qpu_field_components_{i}", getattr(state, f"qpu_field_components_{src}", "Ez"))
644
+ setattr(state, f"qpu_monitor_gridpoints_{i}", getattr(state, f"qpu_monitor_gridpoints_{src}", ""))
645
+ setattr(state, f"qpu_monitor_samples_{i}", getattr(state, f"qpu_monitor_samples_{src}", ""))
646
+ setattr(state, f"qpu_monitor_sample_info_{i}", getattr(state, f"qpu_monitor_sample_info_{src}", ""))
647
+ state.qpu_monitor_count = max(0, cnt - 1)
648
+
649
+
650
+ # Register on controller
651
+ ctrl.qpu_remove_monitor_config = qpu_remove_monitor_config
652
+ ctrl.qpu_set_monitor_field = qpu_set_monitor_field
653
+ ctrl.qpu_set_monitor_points = qpu_set_monitor_points
654
+ ctrl.qpu_set_plot_filter = qpu_set_plot_filter
655
+ ctrl.qpu_set_plot_position_filter = qpu_set_plot_position_filter
656
+ ctrl.qpu_add_monitor_slot = qpu_add_monitor_slot
657
+ ctrl.qpu_remove_monitor_slot = qpu_remove_monitor_slot
em/simulation.py ADDED
@@ -0,0 +1,830 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EM Embedded - Simulation Module
3
+
4
+ Contains simulation logic including run_simulation_only, reset_to_defaults,
5
+ and stop handlers.
6
+ """
7
+ import numpy as np
8
+
9
+ from .state import state, ctrl, _apply_workflow_highlights
10
+ from .globals import (
11
+ plotter, simulation_data, current_mesh, snapshot_times,
12
+ stop_simulation, qpu_ts_cache, sim_ts_cache, set_stop_simulation, reset_globals
13
+ )
14
+
15
+ # Import backend functions
16
+ try:
17
+ from quantum.utils.delta_impulse_generator import (
18
+ create_impulse_state, create_gaussian_state,
19
+ create_impulse_state_from_pos, create_gaussian_state_from_pos,
20
+ run_sim, create_time_frames
21
+ )
22
+ import quantum.utils.delta_impulse_generator as qutils
23
+ except ModuleNotFoundError:
24
+ from utils.delta_impulse_generator import (
25
+ create_impulse_state, create_gaussian_state,
26
+ create_impulse_state_from_pos, create_gaussian_state_from_pos,
27
+ run_sim, create_time_frames
28
+ )
29
+ import utils.delta_impulse_generator as qutils
30
+
31
+ __all__ = [
32
+ "run_simulation_only",
33
+ "reset_to_defaults",
34
+ "stop_simulation_handler",
35
+ "log_to_console",
36
+ "log_message",
37
+ "setup_surface_plot_data",
38
+ "generate_plot",
39
+ "redraw_surface_plot",
40
+ "update_sim_monitor_points",
41
+ "add_dotted_unit_grid",
42
+ "add_dotted_unit_grid_scaled",
43
+ "build_sim_timeseries_plotly",
44
+ "update_value_display",
45
+ ]
46
+
47
+
48
+ def update_sim_monitor_points():
49
+ """Update simulator monitor points based on timeseries_points input."""
50
+ from .utils import snap_samples_to_grid
51
+
52
+ sample_value = state.timeseries_points
53
+ if not sample_value or not str(sample_value).strip():
54
+ state.timeseries_gridpoints = ""
55
+ state.timeseries_point_info = ""
56
+ return
57
+ nx_val = state.nx
58
+ if nx_val is None:
59
+ state.timeseries_gridpoints = ""
60
+ state.timeseries_point_info = "Select a grid size (nx) to compute the nearest monitor positions."
61
+ return
62
+ snapped, message = snap_samples_to_grid(sample_value, int(nx_val))
63
+ state.timeseries_gridpoints = snapped
64
+ state.timeseries_point_info = message or ""
65
+
66
+
67
+ def log_message(message, level="INFO"):
68
+ """Log a message to the console."""
69
+ from datetime import datetime
70
+ timestamp = datetime.now().strftime("%H:%M:%S")
71
+ log_line = f"[{timestamp}] [{level}] {message}\n"
72
+ current = state.console_logs or ""
73
+ state.console_logs = current + log_line
74
+
75
+
76
+ def log_to_console(message):
77
+ """Log a message to the console output."""
78
+ current = state.console_output or ""
79
+ state.console_output = current + message + "\n"
80
+
81
+
82
+ def setup_surface_plot_data(sim_data, nx):
83
+ """Setup surface plot data from simulation results - matches em_embedded.py exactly."""
84
+ from . import globals as g
85
+
86
+ nx = int(nx)
87
+ mask = np.arange(1, nx * nx + 1) % nx != 0
88
+
89
+ g.data_frames = {'Ez': [], 'Hx': [], 'Hy': []}
90
+ g.surface_clims = {'Ez': [np.inf, -np.inf], 'Hx': [np.inf, -np.inf], 'Hy': [np.inf, -np.inf]}
91
+
92
+ for u in sim_data:
93
+ ez = u[:nx*nx].reshape(nx, nx)
94
+ hx = u[2*nx*nx:3*nx*nx-nx].reshape(nx-1, nx)
95
+ hy = u[-nx*nx:][mask].reshape(nx, nx-1)
96
+
97
+ g.data_frames['Ez'].append(ez)
98
+ g.data_frames['Hx'].append(hx)
99
+ g.data_frames['Hy'].append(hy)
100
+
101
+ if ez.size > 0:
102
+ g.surface_clims['Ez'][0] = min(g.surface_clims['Ez'][0], ez.min())
103
+ g.surface_clims['Ez'][1] = max(g.surface_clims['Ez'][1], ez.max())
104
+ if hx.size > 0:
105
+ g.surface_clims['Hx'][0] = min(g.surface_clims['Hx'][0], hx.min())
106
+ g.surface_clims['Hx'][1] = max(g.surface_clims['Hx'][1], hx.max())
107
+ if hy.size > 0:
108
+ g.surface_clims['Hy'][0] = min(g.surface_clims['Hy'][0], hy.min())
109
+ g.surface_clims['Hy'][1] = max(g.surface_clims['Hy'][1], hy.max())
110
+
111
+ # Prevent zero-range clims
112
+ for key in g.surface_clims:
113
+ if g.surface_clims[key][0] == g.surface_clims[key][1]:
114
+ g.surface_clims[key][0] -= 1e-9
115
+ g.surface_clims[key][1] += 1e-9
116
+
117
+ # Use integer grid coordinates (like em_embedded.py / app.py)
118
+ x = np.arange(nx)
119
+ y = np.arange(nx)
120
+ x_m1 = np.arange(nx - 1)
121
+ y_m1 = np.arange(nx - 1)
122
+
123
+ g.X_grids['Ez'], g.Y_grids['Ez'] = np.meshgrid(x, y)
124
+ g.X_grids['Hx'], g.Y_grids['Hx'] = np.meshgrid(x, y_m1)
125
+ g.X_grids['Hy'], g.Y_grids['Hy'] = np.meshgrid(x_m1, y)
126
+
127
+ # Compute z_scale for visualization
128
+ finite_vals = [abs(float(v)) for pair in g.surface_clims.values() for v in pair if np.isfinite(v)]
129
+ max_abs = max(finite_vals) if finite_vals else 1e-9
130
+ g.z_scale = (nx / 2) / max(max_abs, 1e-9)
131
+
132
+ g.simulation_data = sim_data
133
+
134
+
135
+ def generate_plot():
136
+ """Generate the plot based on output_type selection."""
137
+ import re
138
+ from . import globals as g
139
+
140
+ if not state.simulation_has_run:
141
+ return
142
+
143
+ plotter.clear()
144
+ try:
145
+ plotter.disable_picking()
146
+ except Exception:
147
+ pass
148
+
149
+ nx = int(state.nx)
150
+
151
+ if state.output_type == "Surface Plot":
152
+ redraw_surface_plot()
153
+ else: # Time Series -> Plotly for Simulator
154
+ try:
155
+ points_str = state.timeseries_gridpoints or ""
156
+ positions = [tuple(map(int, match)) for match in re.findall(r'\((\d+)\s*,\s*(\d+)\)', points_str)]
157
+ if not positions and (state.timeseries_points or "").strip():
158
+ raise ValueError("No valid monitor positions found. Enter (x, y) pairs in [0,1] x [0,1].")
159
+
160
+ fig = build_sim_timeseries_plotly(state.timeseries_field, positions, nx, g.snapshot_times, g.simulation_data)
161
+ if fig is not None:
162
+ # Cache the figure for export
163
+ g.sim_ts_cache["fig"] = fig
164
+ g.sim_ts_cache["field"] = state.timeseries_field
165
+ try:
166
+ ctrl.sim_ts_update(fig)
167
+ except Exception:
168
+ pass
169
+ except Exception as e:
170
+ state.error_message = f"Plotting Error: {e}"
171
+
172
+ ctrl.view_update()
173
+
174
+
175
+ def redraw_surface_plot():
176
+ """Redraw the surface plot with current field and time - matches em_embedded.py."""
177
+ import pyvista as pv
178
+ from . import globals as g
179
+
180
+ plotter.clear()
181
+
182
+ field = state.surface_field
183
+ if g.data_frames is None or not g.data_frames.get(field):
184
+ return
185
+ if g.snapshot_times is None or len(g.snapshot_times) == 0:
186
+ return
187
+
188
+ # Find nearest snapshot index to requested time and clamp to available frames
189
+ req_t = float(state.time_val)
190
+ times = np.asarray(g.snapshot_times)
191
+ idx = int(np.argmin(np.abs(times - req_t)))
192
+ max_idx = len(g.data_frames[field]) - 1
193
+ idx = max(0, min(idx, max_idx))
194
+
195
+ z_data = g.data_frames[field][idx]
196
+ X = g.X_grids[field]
197
+ Y = g.Y_grids[field]
198
+
199
+ points = np.c_[X.ravel(), Y.ravel(), z_data.ravel() * g.z_scale]
200
+ poly = pv.PolyData(points)
201
+ mesh = poly.delaunay_2d()
202
+ mesh['scalars'] = z_data.ravel()
203
+ g.current_mesh = mesh
204
+
205
+ # Add mesh with styling matching em_embedded.py
206
+ plotter.add_mesh(
207
+ mesh,
208
+ scalars='scalars',
209
+ clim=g.surface_clims[field],
210
+ cmap="Blues",
211
+ show_scalar_bar=False,
212
+ show_edges=True,
213
+ edge_color='grey',
214
+ line_width=0.5
215
+ )
216
+ plotter.add_scalar_bar(title=f"{field} Amplitude")
217
+
218
+ # Enable point picking
219
+ try:
220
+ plotter.disable_picking()
221
+ except Exception:
222
+ pass
223
+ plotter.enable_point_picking(callback=update_value_display, show_message=False)
224
+
225
+ plotter.add_axes()
226
+ plotter.view_isometric()
227
+ try:
228
+ plotter.camera.parallel_projection = True
229
+ except Exception:
230
+ pass
231
+ ctrl.view_update()
232
+
233
+
234
+ def run_simulation_only():
235
+ """Run the simulation based on current state settings."""
236
+ from . import globals as g
237
+ from .excitation import nearest_node_index
238
+ from .qpu import build_qpu_timeseries_plotly_multi
239
+
240
+ # Require selections before running
241
+ if not state.geometry_selection:
242
+ state.error_message = "Please select a geometry before running the simulation."
243
+ log_to_console("Error: Please select a geometry before running.")
244
+ state.status_visible = True
245
+ state.status_message = "Error: Please select a geometry before running."
246
+ state.status_type = "error"
247
+ state.show_progress = False
248
+ state.is_running = False
249
+ state.run_button_text = "RUN!"
250
+ return
251
+
252
+ if not state.dist_type:
253
+ state.error_message = "Please select an initial state before running the simulation."
254
+ log_to_console("Error: Please select an initial state before running.")
255
+ state.status_visible = True
256
+ state.status_message = "Error: Please select an initial state before running."
257
+ state.status_type = "error"
258
+ state.show_progress = False
259
+ state.is_running = False
260
+ state.run_button_text = "RUN!"
261
+ return
262
+
263
+ # Show status: Starting simulation
264
+ state.status_visible = True
265
+ state.status_message = "Initializing simulation..."
266
+ log_to_console("Initializing simulation...")
267
+ state.status_type = "info"
268
+ state.show_progress = True
269
+ state.simulation_progress = 0
270
+
271
+ last_logged_percent = [0]
272
+ def _progress_callback(percent):
273
+ state.simulation_progress = percent
274
+ if percent - last_logged_percent[0] >= 10:
275
+ log_to_console(f"Simulation progress: {int(percent)}%")
276
+ last_logged_percent[0] = percent
277
+
278
+ # Reset stop flag and enable Stop button at start
279
+ set_stop_simulation(False)
280
+ state.stop_button_disabled = False
281
+
282
+ plotter.clear()
283
+ g.current_mesh = None
284
+ state.error_message = ""
285
+ state.is_running = True
286
+ state.simulation_has_run = False
287
+ state.run_button_text = "Running"
288
+ try:
289
+ ctrl.view_update()
290
+ except Exception:
291
+ pass
292
+
293
+ nx, T = int(state.nx), float(state.T)
294
+ na, R = 1, 4
295
+
296
+ try:
297
+ state.status_message = "Creating initial state..."
298
+ state.simulation_progress = 10
299
+ if state.dist_type == "Delta":
300
+ initial_state = create_impulse_state_from_pos(
301
+ (nx, nx),
302
+ (float(state.impulse_x), float(state.impulse_y))
303
+ )
304
+ else:
305
+ initial_state = create_gaussian_state_from_pos(
306
+ (nx, nx),
307
+ (float(state.mu_x), float(state.mu_y)),
308
+ (float(state.sigma_x), float(state.sigma_y))
309
+ )
310
+ except ValueError as e:
311
+ state.error_message = f"Initial State Error: {e}"
312
+ state.status_message = f"Error: {e}"
313
+ state.status_type = "error"
314
+ state.show_progress = False
315
+ state.is_running = False
316
+ state.run_button_text = "RUN!"
317
+ state.stop_button_disabled = True
318
+ return
319
+
320
+ # If QPU selected, build QPU time series chart and return
321
+ if state.backend_type == "QPU":
322
+ try:
323
+ log_to_console("Running QPU...")
324
+ state.status_message = "Running QPU simulation..."
325
+ state.simulation_progress = 20
326
+ state.qpu_ts_ready = False
327
+ state.qpu_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
328
+ state.qpu_ts_other_ready = False
329
+ state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
330
+
331
+ # Inputs for QPU
332
+ snapshot_dt = float(state.dt_user)
333
+ ix_imp, iy_imp = nearest_node_index(float(state.impulse_x), float(state.impulse_y), nx)
334
+ impulse_pos = (ix_imp, iy_imp)
335
+
336
+ # Build configs from primitive slots
337
+ configs = [{
338
+ "field": (state.qpu_field_components or "Ez"),
339
+ "points": (state.qpu_monitor_gridpoints or ""),
340
+ }]
341
+ try:
342
+ cnt = int(state.qpu_monitor_count or 0)
343
+ except Exception:
344
+ cnt = 0
345
+ for slot_num in range(2, 2 + cnt):
346
+ f = getattr(state, f"qpu_field_components_{slot_num}", "Ez") or "Ez"
347
+ p = getattr(state, f"qpu_monitor_gridpoints_{slot_num}", "") or ""
348
+ configs.append({"field": f, "points": p})
349
+
350
+ state.status_message = "Building QPU time series..."
351
+ state.simulation_progress = 60
352
+
353
+ # Build and render Plotly chart
354
+ fig = build_qpu_timeseries_plotly_multi(
355
+ configs, nx, T, snapshot_dt, impulse_pos,
356
+ progress_callback=_progress_callback,
357
+ print_callback=log_to_console
358
+ )
359
+ qpu_ts_cache["fig"] = fig
360
+
361
+ try:
362
+ ctrl.qpu_ts_update(fig)
363
+ except Exception:
364
+ pass
365
+
366
+ state.simulation_has_run = True
367
+ state.run_button_text = "Successful!"
368
+ state.simulation_progress = 100
369
+ state.status_message = "QPU simulation completed successfully!"
370
+ log_to_console("Simulation Completed")
371
+ state.status_type = "success"
372
+ state.show_progress = False
373
+
374
+ ready = bool(getattr(fig, "data", None)) and len(fig.data) > 0
375
+ state.qpu_ts_ready = ready
376
+ state.qpu_plot_style = (
377
+ "width: 900px; height: 660px; margin: 0 auto;"
378
+ if ready else "display: none; width: 900px; height: 660px; margin: 0 auto;"
379
+ )
380
+ state.qpu_ts_other_ready = False
381
+ state.qpu_other_plot_style = "display: none; width: 900px; height: 660px; margin: 0 auto;"
382
+
383
+ if not ready:
384
+ state.error_message = "No QPU time series generated. Check Δt, T, nx, and monitor points."
385
+ state.status_message = "Warning: No QPU time series generated."
386
+ state.status_type = "warning"
387
+ log_to_console("QPU complete.")
388
+
389
+ except Exception as e:
390
+ state.error_message = f"QPU run failed: {e}"
391
+ state.status_message = f"QPU Error: {e}"
392
+ state.status_type = "error"
393
+ state.show_progress = False
394
+ state.run_button_text = "RUN!"
395
+ state.qpu_ts_ready = False
396
+ log_to_console(f"QPU error: {e}")
397
+ finally:
398
+ state.is_running = False
399
+ state.stop_button_disabled = True
400
+ try:
401
+ ctrl.view_update()
402
+ except Exception:
403
+ pass
404
+ return
405
+
406
+ # Simulator path
407
+ log_to_console("Running simulation...")
408
+ state.status_message = "Running simulation... This may take a while."
409
+ state.simulation_progress = 30
410
+
411
+ snapshot_dt = float(state.dt_user)
412
+
413
+ def _stop_check():
414
+ return g.stop_simulation
415
+
416
+ state.simulation_progress = 50
417
+ sim_data, times = run_sim(
418
+ nx, na, R, initial_state, T,
419
+ snapshot_dt=snapshot_dt,
420
+ stop_check=_stop_check,
421
+ progress_callback=_progress_callback,
422
+ print_callback=log_to_console
423
+ )
424
+ g.simulation_data = sim_data
425
+ g.snapshot_times = times
426
+ log_to_console("Simulation complete.")
427
+
428
+ state.simulation_progress = 80
429
+ state.status_message = "Processing simulation results..."
430
+
431
+ if sim_data.size > 0:
432
+ setup_surface_plot_data(sim_data, nx)
433
+ state.simulation_has_run = True
434
+ state.run_button_text = "Successful!"
435
+ state.simulation_progress = 100
436
+ state.status_message = "Simulation completed successfully!"
437
+ state.status_type = "success"
438
+ state.show_progress = False
439
+ generate_plot()
440
+ else:
441
+ state.error_message = "Simulation produced no data. Check parameters (e.g., T > 0)."
442
+ state.status_message = "Error: Simulation produced no data."
443
+ state.status_type = "error"
444
+ state.show_progress = False
445
+ state.run_button_text = "RUN!"
446
+
447
+ state.is_running = False
448
+ state.stop_button_disabled = True
449
+
450
+
451
+ def reset_to_defaults():
452
+ """Reset all parameters to their default values."""
453
+ from .excitation import update_initial_state_preview, update_sim_monitor_points
454
+ from . import globals as g
455
+
456
+ # Stop any running simulation
457
+ set_stop_simulation(True)
458
+
459
+ # Reset global variables
460
+ reset_globals()
461
+
462
+ # Reset state to default values
463
+ state.update({
464
+ "dist_type": None,
465
+ "impulse_x": 0.5,
466
+ "impulse_y": 0.5,
467
+ "peak_pair": "(0.5, 0.5)",
468
+ "mu_x": 0.5,
469
+ "mu_y": 0.5,
470
+ "sigma_x": 0.25,
471
+ "sigma_y": 0.15,
472
+ "mu_pair": "(0.5, 0.5)",
473
+ "sigma_pair": "(0.25, 0.15)",
474
+ "nx": None,
475
+ "T": 10.0,
476
+ "time_val": 0.0,
477
+ "output_type": "Surface Plot",
478
+ "surface_field": "Ez",
479
+ "timeseries_field": "Ez",
480
+ "timeseries_points": "(0.5, 0.5)",
481
+ "timeseries_gridpoints": "",
482
+ "timeseries_point_info": "",
483
+ "error_message": "",
484
+ "excitation_info_message": "",
485
+ "excitation_config_open": False,
486
+ "is_running": False,
487
+ "simulation_has_run": False,
488
+ "geometry_selection": None,
489
+ "coeff_permittivity": 1.0,
490
+ "coeff_permeability": 1.0,
491
+ "run_button_text": "RUN!",
492
+ "backend_type": None,
493
+ "selected_simulator": "IBM Qiskit simulator",
494
+ "selected_qpu": "IBM QPU",
495
+ "stop_button_disabled": True,
496
+ "export_format": "vtk",
497
+ "nx_slider_index": None,
498
+ "dt_user": 0.1,
499
+ "temporal_warning": "",
500
+ "qpu_field_components": "Ez",
501
+ "qpu_monitor_gridpoints": "",
502
+ "qpu_monitor_samples": "(0.5, 0.5)",
503
+ "qpu_monitor_sample_info": "",
504
+ "qpu_monitor_count": 0,
505
+ "qpu_plot_filter": "All",
506
+ "qpu_plot_field_options": ["All"],
507
+ "qpu_plot_position_filter": "All positions",
508
+ "qpu_plot_position_options": ["All positions"],
509
+ "qpu_ts_ready": False,
510
+ "qpu_plot_style": "display: none; width: 900px; height: 660px; margin: 0 auto;",
511
+ "qpu_ts_other_ready": False,
512
+ "qpu_other_plot_style": "display: none; width: 900px; height: 660px; margin: 0 auto;",
513
+ "pyvista_view_style": "aspect-ratio: 1 / 1; width: 100%;",
514
+ })
515
+
516
+ # Reset QPU cache
517
+ qpu_ts_cache.update({
518
+ "times": None,
519
+ "series_map": None,
520
+ "field": None,
521
+ "fig": None,
522
+ "positions_by_field": {"All": []},
523
+ "key_to_label": {},
524
+ "label_to_keys": {},
525
+ "nx": None,
526
+ })
527
+
528
+ # Ensure stop flag is cleared for next run
529
+ set_stop_simulation(False)
530
+
531
+ # Update monitors
532
+ update_sim_monitor_points()
533
+ _apply_workflow_highlights(0)
534
+
535
+ # Update the preview with default values
536
+ update_initial_state_preview()
537
+ print("Reset to default settings")
538
+
539
+
540
+ def stop_simulation_handler():
541
+ """Stop the currently running simulation."""
542
+ set_stop_simulation(True)
543
+ state.status_message = "Stopping simulation..."
544
+ state.status_type = "warning"
545
+ log_to_console("Stopping simulation...")
546
+
547
+
548
+ # ---------------------------------------------------------------------------
549
+ # Grid overlay helpers for PyVista plots
550
+ # ---------------------------------------------------------------------------
551
+
552
+ def add_dotted_unit_grid(pl, ticks=(0.0, 0.25, 0.5, 0.75, 1.0), segments=48, gap_ratio=0.4, color="#AE8BD8", line_width=0.2):
553
+ """Add a dotted unit grid (0..1) overlay in light Synopsys purple."""
554
+ import pyvista as pv
555
+ try:
556
+ step = 1.0 / float(max(segments, 1))
557
+ seg_len = step * float(max(0.0, min(1.0, 1.0 - gap_ratio)))
558
+ pts = []
559
+ lines = []
560
+ # Horizontal dotted lines at given y=tick
561
+ for y in ticks:
562
+ pos = 0.0
563
+ while pos < 1.0 - 1e-9:
564
+ y0, y1 = pos, min(pos + seg_len, 1.0)
565
+ pts.extend([(0.0, y, 0.0), (1.0, y, 0.0)])
566
+ pts[-2] = (pos, y, 0.0)
567
+ pts[-1] = (y1 if seg_len > 0 else pos, y, 0.0)
568
+ i0 = len(pts) - 2
569
+ lines.extend([2, i0, i0 + 1])
570
+ pos += step
571
+ # Vertical dotted lines at given x=tick
572
+ for x in ticks:
573
+ pos = 0.0
574
+ while pos < 1.0 - 1e-9:
575
+ y0, y1 = pos, min(pos + seg_len, 1.0)
576
+ pts.extend([(x, pos, 0.0), (x, y1 if seg_len > 0 else pos, 0.0)])
577
+ i0 = len(pts) - 2
578
+ lines.extend([2, i0, i0 + 1])
579
+ pos += step
580
+ if pts and lines:
581
+ poly = pv.PolyData(np.array(pts))
582
+ poly.lines = np.array(lines)
583
+ pl.add_mesh(poly, color=color, line_width=line_width, name="dotted_unit_grid", pickable=False)
584
+ except Exception:
585
+ pass
586
+
587
+
588
+ def add_dotted_unit_grid_scaled(pl, 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"):
589
+ """Overlay a 0–1 dotted grid scaled to [0, denom] on the XY plane."""
590
+ import pyvista as pv
591
+ from . import globals as g
592
+ try:
593
+ step = 1.0 / float(max(segments, 1))
594
+ seg_len = step * float(max(0.0, min(1.0, 1.0 - gap_ratio)))
595
+ # Set a z slightly below mesh to avoid z-fighting
596
+ try:
597
+ z0 = float(g.current_mesh.points[:, 2].min()) - 1e-6 if g.current_mesh is not None else 0.0
598
+ except Exception:
599
+ z0 = 0.0
600
+ pts, lines = [], []
601
+ # Vertical lines at x = t * denom
602
+ for t in ticks:
603
+ x = float(t) * float(denom)
604
+ pos = 0.0
605
+ while pos < 1.0 - 1e-9:
606
+ y0 = pos * denom
607
+ y1 = min(pos + seg_len, 1.0) * denom
608
+ pts.extend([(x, y0, z0), (x, y1, z0)])
609
+ i0 = len(pts) - 2
610
+ lines.extend([2, i0, i0 + 1])
611
+ pos += step
612
+ # Horizontal lines at y = t * denom
613
+ for t in ticks:
614
+ y = float(t) * float(denom)
615
+ pos = 0.0
616
+ while pos < 1.0 - 1e-9:
617
+ x0 = pos * denom
618
+ x1 = min(pos + seg_len, 1.0) * denom
619
+ pts.extend([(x0, y, z0), (x1, y, z0)])
620
+ i0 = len(pts) - 2
621
+ lines.extend([2, i0, i0 + 1])
622
+ pos += step
623
+ try:
624
+ pl.remove_actor(name)
625
+ except Exception:
626
+ pass
627
+ if pts and lines:
628
+ poly = pv.PolyData(np.array(pts))
629
+ poly.lines = np.array(lines)
630
+ pl.add_mesh(poly, color=color, line_width=line_width, name=name, pickable=False)
631
+ except Exception:
632
+ pass
633
+
634
+
635
+ # ---------------------------------------------------------------------------
636
+ # Simulator timeseries plot builder
637
+ # ---------------------------------------------------------------------------
638
+
639
+ def build_sim_timeseries_plotly(field_type: str, positions, nx: int, times, sim_data):
640
+ """Build a Plotly figure for simulator timeseries data."""
641
+ import plotly.graph_objects as go
642
+ from matplotlib import cm as _cm
643
+ from .utils import normalized_position_label
644
+
645
+ try:
646
+ def _rgba_to_hex(rgba):
647
+ r, g, b, a = rgba
648
+ return "#%02x%02x%02x" % (int(r*255), int(g*255), int(b*255))
649
+
650
+ n_frames = int(sim_data.shape[0]) if sim_data is not None else 0
651
+ time_axis = np.asarray(times) if times is not None else np.arange(n_frames)
652
+
653
+ def _dims(f):
654
+ if f == 'Ez':
655
+ return nx, nx
656
+ if f == 'Hx':
657
+ return nx, nx - 1
658
+ return nx - 1, nx # Hy
659
+
660
+ def _valid_positions(f, pts):
661
+ gw, gh = _dims(f)
662
+ out = []
663
+ for (px, py) in pts:
664
+ if 0 <= px < gw and 0 <= py < gh:
665
+ out.append((int(px), int(py)))
666
+ return out
667
+
668
+ fig = go.Figure()
669
+
670
+ if not positions or sim_data is None or n_frames == 0:
671
+ fig.update_layout(
672
+ title="Time Series (Simulator)",
673
+ height=660, width=900,
674
+ margin=dict(l=50, r=30, t=50, b=50),
675
+ xaxis=dict(title="Time (s)", title_font=dict(size=22), tickfont=dict(size=16), showline=True, linewidth=1, linecolor="rgba(0,0,0,.3)", gridcolor="rgba(0,0,0,.06)", showspikes=True, spikemode='across', spikesnap='cursor'),
676
+ yaxis=dict(title="Field Amplitude", title_font=dict(size=22), tickfont=dict(size=16), showline=True, linewidth=1, linecolor="rgba(0,0,0,.3)", gridcolor="rgba(0,0,0,.06)", zeroline=True, zerolinecolor="rgba(0,0,0,.25)"),
677
+ hovermode="x unified",
678
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1),
679
+ )
680
+ return fig
681
+
682
+ max_sum = max((px + py) for (px, py) in positions) if positions else 1
683
+ if max_sum <= 0:
684
+ max_sum = 1
685
+
686
+ cmap_map = {
687
+ 'Ez': _cm.Reds,
688
+ 'Hx': _cm.Greens,
689
+ 'Hy': _cm.Blues,
690
+ }
691
+
692
+ def _add_field_traces(f_name: str, pts):
693
+ nonlocal fig
694
+ gw, gh = _dims(f_name)
695
+ valid_pts = _valid_positions(f_name, pts)
696
+ if not valid_pts:
697
+ return 0.0, 0
698
+ max_abs_local = 0.0
699
+ num_keys = len(valid_pts)
700
+ for i, (px, py) in enumerate(valid_pts):
701
+ if f_name == 'Ez':
702
+ values = sim_data[:, py * gw + px]
703
+ elif f_name == 'Hx':
704
+ block = sim_data[:, 2*nx*nx : 3*nx*nx-nx].reshape(n_frames, gh, gw)
705
+ values = block[:, py, px]
706
+ else: # Hy
707
+ mask = np.arange(1, nx * nx + 1) % nx != 0
708
+ raw_block = sim_data[:, -nx*nx:]
709
+ values = np.array([raw_block[t, mask].reshape(nx, nx - 1)[py, px] for t in range(n_frames)])
710
+ try:
711
+ max_abs_local = max(max_abs_local, float(np.max(np.abs(values))))
712
+ except Exception:
713
+ pass
714
+
715
+ if num_keys > 1:
716
+ s_index = i / (num_keys - 1)
717
+ s_light = 0.3 + 0.6 * s_index
718
+ else:
719
+ s_light = 0.6
720
+
721
+ rgba = cmap_map.get(f_name, _cm.Blues)(s_light)
722
+ color_hex = _rgba_to_hex(rgba)
723
+ dash_styles = ["solid", "dash", "dot", "dashdot"]
724
+ marker_symbols = ["circle", "square", "diamond", "triangle-up", "x"]
725
+ label = normalized_position_label(px, py, gw, gh)
726
+ fig.add_trace(go.Scatter(
727
+ x=time_axis,
728
+ y=values,
729
+ mode='lines+markers',
730
+ name=label,
731
+ line=dict(color=color_hex, width=2.5, dash=dash_styles[i % len(dash_styles)]),
732
+ marker=dict(size=7, symbol=marker_symbols[i % len(marker_symbols)], color=color_hex, line=dict(width=0)),
733
+ hovertemplate=f"{f_name} | t=%{{x:.3f}}s<br>Value=%{{y:.6g}}<extra>{label}</extra>",
734
+ ))
735
+ return max_abs_local, len(valid_pts)
736
+
737
+ max_abs = 0.0
738
+ total_traces = 0
739
+ if str(field_type) == 'All':
740
+ for f in ('Ez', 'Hx', 'Hy'):
741
+ m, n_tr = _add_field_traces(f, positions)
742
+ max_abs = max(max_abs, m)
743
+ total_traces += n_tr
744
+ else:
745
+ m, n_tr = _add_field_traces(str(field_type), positions)
746
+ max_abs = max(max_abs, m)
747
+ total_traces += n_tr
748
+
749
+ title_suffix = str(field_type) if str(field_type) != 'All' else 'Ez, Hx, Hy'
750
+ fig.update_layout(
751
+ title=f"Time Series (Simulator: {title_suffix})",
752
+ height=660, width=900,
753
+ margin=dict(l=50, r=30, t=50, b=50),
754
+ hovermode="x unified",
755
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1, title_text=""),
756
+ paper_bgcolor="#FFFFFF",
757
+ plot_bgcolor="#FFFFFF",
758
+ )
759
+ fig.update_xaxes(
760
+ title_text="Time (s)", title_font=dict(size=22), tickfont=dict(size=16),
761
+ showgrid=True, gridcolor="rgba(95,37,159,0.08)", zeroline=False,
762
+ showline=True, linewidth=1, linecolor="rgba(0,0,0,.2)",
763
+ showspikes=True, spikemode='across', spikesnap='cursor'
764
+ )
765
+ fig.update_yaxes(
766
+ title_text="Field Amplitude", title_font=dict(size=22), tickfont=dict(size=16),
767
+ showgrid=True, gridcolor="rgba(95,37,159,0.08)", zeroline=True, zerolinecolor="rgba(0,0,0,.25)",
768
+ showline=True, linewidth=1, linecolor="rgba(0,0,0,.2)"
769
+ )
770
+ if max_abs > 0:
771
+ pad = 0.12 * max_abs
772
+ fig.update_yaxes(range=[-max_abs - pad, max_abs + pad])
773
+ return fig
774
+ except Exception:
775
+ import plotly.graph_objects as go
776
+ return go.Figure(layout=dict(height=660, width=900))
777
+
778
+
779
+ # ---------------------------------------------------------------------------
780
+ # Value display for picked points on the mesh
781
+ # ---------------------------------------------------------------------------
782
+
783
+ def update_value_display(point):
784
+ """Update value display when a point is picked on the mesh."""
785
+ from . import globals as g
786
+
787
+ if g.current_mesh is None:
788
+ return
789
+ try:
790
+ plotter.remove_actor("value_text")
791
+ except Exception:
792
+ pass
793
+
794
+ closest_id = g.current_mesh.find_closest_point(point)
795
+ if closest_id == -1:
796
+ return
797
+
798
+ value = g.current_mesh['scalars'][closest_id] if 'scalars' in g.current_mesh.array_names else 0.0
799
+ px, py, pz = g.current_mesh.points[closest_id]
800
+ px = float(px)
801
+ py = float(py)
802
+
803
+ xmin, xmax, ymin, ymax, _, _ = g.current_mesh.bounds
804
+ is_unit_square = (xmax <= 1.00001 and ymax <= 1.00001)
805
+
806
+ if not state.simulation_has_run and is_unit_square:
807
+ text = f"Position: ({px:.3f}, {py:.3f})\nValue: {value:.3e}"
808
+ else:
809
+ nx_val = int(state.nx)
810
+ denom = max(float(nx_val - 1), 1.0)
811
+ if is_unit_square:
812
+ ix = int(round(px * denom))
813
+ iy = int(round(py * denom))
814
+ x_code = max(0.0, min(1.0, px))
815
+ y_code = max(0.0, min(1.0, py))
816
+ else:
817
+ ix = int(round(px))
818
+ iy = int(round(py))
819
+ x_code = max(0.0, min(1.0, px / denom))
820
+ y_code = max(0.0, min(1.0, py / denom))
821
+ ix = max(0, min(ix, nx_val - 1))
822
+ iy = max(0, min(iy, nx_val - 1))
823
+ if state.simulation_has_run:
824
+ time = float(state.time_val)
825
+ text = f"Index: ({ix}, {iy}) | Position: ({x_code:.3f}, {y_code:.3f})\nTime: {time:.2f}s\nValue: {value:.3e}"
826
+ else:
827
+ text = f"Index: ({ix}, {iy}) | Position: ({x_code:.3f}, {y_code:.3f})\nValue: {value:.3e}"
828
+
829
+ plotter.add_text(text, name="value_text", position="lower_left", color="black", font_size=10)
830
+ ctrl.view_update()
em/state.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EM Embedded - State Management Module
3
+
4
+ Contains deferred state/controller proxy classes and state initialization.
5
+ These allow using state.update(), @state.change(), and ctrl.xxx at module
6
+ load time, then applying them when set_server() is called.
7
+ """
8
+
9
+ __all__ = [
10
+ "state", "ctrl", "set_server", "init_state",
11
+ "enable_point_picking_on_plotter",
12
+ "_apply_workflow_highlights", "_determine_workflow_step",
13
+ ]
14
+
15
+
16
+ class _DeferredStateProxy:
17
+ """
18
+ A proxy that collects state defaults and change decorators at module load time,
19
+ then applies them to the real server.state when bind() is called.
20
+ """
21
+
22
+ def __init__(self):
23
+ self._state = None
24
+ self._defaults = {}
25
+ self._pending_changes = [] # list of (keys, func)
26
+
27
+ def bind(self, real_state):
28
+ """Bind to the real state object and apply all pending operations."""
29
+ self._state = real_state
30
+ # Apply all collected defaults
31
+ if self._defaults:
32
+ real_state.update(self._defaults)
33
+ self._defaults.clear()
34
+ # Apply all pending @state.change decorators
35
+ for keys, func in self._pending_changes:
36
+ real_state.change(*keys)(func)
37
+ self._pending_changes.clear()
38
+
39
+ @property
40
+ def bound(self):
41
+ return self._state is not None
42
+
43
+ def update(self, d):
44
+ """Collect defaults; apply immediately if bound."""
45
+ if self._state is not None:
46
+ self._state.update(d)
47
+ else:
48
+ self._defaults.update(d)
49
+
50
+ def change(self, *keys):
51
+ """
52
+ Decorator factory that mimics @state.change("key1", "key2").
53
+ If already bound, apply immediately. Otherwise, queue for later.
54
+ """
55
+ def decorator(func):
56
+ if self._state is not None:
57
+ # Already bound - register directly
58
+ self._state.change(*keys)(func)
59
+ else:
60
+ # Queue for later
61
+ self._pending_changes.append((keys, func))
62
+ return func
63
+ return decorator
64
+
65
+ def __getattr__(self, name):
66
+ if name.startswith("_"):
67
+ raise AttributeError(name)
68
+ if self._state is None:
69
+ raise AttributeError(f"State not bound yet; cannot access '{name}'")
70
+ return getattr(self._state, name)
71
+
72
+ def __setattr__(self, name, value):
73
+ if name.startswith("_"):
74
+ object.__setattr__(self, name, value)
75
+ elif self._state is not None:
76
+ setattr(self._state, name, value)
77
+ else:
78
+ # Store as a default
79
+ self._defaults[name] = value
80
+
81
+
82
+ class _DeferredControllerProxy:
83
+ """
84
+ A proxy that collects controller attribute assignments at module load time,
85
+ then applies them when bind() is called.
86
+ """
87
+
88
+ def __init__(self):
89
+ self._ctrl = None
90
+ self._pending = {}
91
+
92
+ def bind(self, real_ctrl):
93
+ """Bind to the real controller and apply pending attributes."""
94
+ self._ctrl = real_ctrl
95
+ for name, value in self._pending.items():
96
+ setattr(real_ctrl, name, value)
97
+ self._pending.clear()
98
+
99
+ @property
100
+ def bound(self):
101
+ return self._ctrl is not None
102
+
103
+ def __getattr__(self, name):
104
+ if name.startswith("_"):
105
+ raise AttributeError(name)
106
+ if self._ctrl is None:
107
+ raise AttributeError(f"Controller not bound yet; cannot access '{name}'")
108
+ return getattr(self._ctrl, name)
109
+
110
+ def __setattr__(self, name, value):
111
+ if name.startswith("_"):
112
+ object.__setattr__(self, name, value)
113
+ elif self._ctrl is not None:
114
+ setattr(self._ctrl, name, value)
115
+ else:
116
+ self._pending[name] = value
117
+
118
+
119
+ # Module-level proxies (will be bound when set_server is called)
120
+ _server = None
121
+ state = _DeferredStateProxy()
122
+ ctrl = _DeferredControllerProxy()
123
+
124
+
125
+ def _noop(*_, **__):
126
+ """No-op placeholder for controller methods."""
127
+ pass
128
+
129
+
130
+ # Pre-register controller methods as no-ops
131
+ ctrl.qpu_ts_update = _noop
132
+ ctrl.qpu_ts_other_update = _noop
133
+ ctrl.view_update = _noop
134
+ ctrl.sim_ts_update = _noop
135
+ ctrl.geometry_preview_update = _noop
136
+ ctrl.excitation_preview_update = _noop
137
+ ctrl.qubit_plot_update = _noop
138
+
139
+
140
+ def set_server(server):
141
+ """Bind the embedded EM module to the shared Trame server."""
142
+ global _server
143
+ _server = server
144
+ state.bind(server.state)
145
+ ctrl.bind(server.controller)
146
+
147
+
148
+ def get_server():
149
+ """Get the bound server instance."""
150
+ return _server
151
+
152
+
153
+ # --- Workflow Highlighting ---
154
+ _WORKFLOW_CARD_KEYS = [
155
+ "overview_card_style",
156
+ "geometry_card_style",
157
+ "excitation_card_style",
158
+ "meshing_card_style",
159
+ "backend_card_style",
160
+ "output_card_style",
161
+ ]
162
+
163
+
164
+ def _workflow_highlight_style(active: bool) -> str:
165
+ base = "font-size: 0.8rem; border: 1px solid transparent; transition: box-shadow 0.2s ease;"
166
+ return f"{base} box-shadow: 0 0 0 2px #6200ea;" if active else base
167
+
168
+
169
+ def _determine_workflow_step() -> int:
170
+ """Determine the current workflow step based on state."""
171
+ if not state.bound:
172
+ return 0
173
+ if state.problem_selection is None:
174
+ return 0
175
+ if state.geometry_selection is None:
176
+ return 1
177
+ if state.dist_type is None:
178
+ return 2
179
+ if state.nx is None:
180
+ return 3
181
+ if state.backend_type is None:
182
+ return 4
183
+ return 5
184
+
185
+
186
+ def _apply_workflow_highlights(step_index: int):
187
+ """Apply highlight styles to workflow cards."""
188
+ for i, key in enumerate(_WORKFLOW_CARD_KEYS):
189
+ setattr(state, key, _workflow_highlight_style(i == step_index))
190
+
191
+
192
+ # --- State Defaults ---
193
+ def _init_state_defaults():
194
+ """Initialize all EM state defaults."""
195
+ state.update({
196
+ "problem_selection": None,
197
+ "geometry_options": ["None", "Square Metallic Body", "Square Domain", "Geometry 2", "Add"],
198
+ "dist_type": None,
199
+ "impulse_x": 0.5,
200
+ "impulse_y": 0.5,
201
+ "peak_pair": "(0.5, 0.5)",
202
+ "mu_x": 0.5,
203
+ "mu_y": 0.5,
204
+ "sigma_x": 0.25,
205
+ "sigma_y": 0.15,
206
+ "mu_pair": "(0.5, 0.5)",
207
+ "sigma_pair": "(0.25, 0.15)",
208
+ "nx": None,
209
+ "T": 1.0,
210
+ "time_val": 0.0,
211
+ "L": 1.0,
212
+ "output_type": "Surface Plot",
213
+ "surface_field": "Ez",
214
+ "timeseries_field": "Ez",
215
+ "timeseries_points": "(0.5, 0.5)",
216
+ "timeseries_gridpoints": "",
217
+ "timeseries_point_info": "",
218
+ "error_message": "",
219
+ "is_running": False,
220
+ "simulation_has_run": False,
221
+ "geometry_selection": None,
222
+ "show_upload_dialog": False,
223
+ "uploaded_file_info": None,
224
+ "show_upload_status": False,
225
+ "upload_status_message": "",
226
+ "coeff_permittivity": 1.0,
227
+ "coeff_permeability": 1.0,
228
+ "run_button_text": "RUN!",
229
+ "backend_type": None,
230
+ "selected_simulator": "IBM Qiskit simulator",
231
+ "selected_qpu": "IBM QPU",
232
+ "stop_button_disabled": True,
233
+ "export_format": "vtk",
234
+ "nx_slider_index": None,
235
+ "show_export_status": False,
236
+ "export_status_message": "",
237
+ "logo_src": None,
238
+ # Geometry-hole controls
239
+ "hole_size_edge": 0.2,
240
+ "hole_center_x": 0.5,
241
+ "hole_center_y": 0.5,
242
+ "hole_center_pair": "(0.5, 0.5)",
243
+ "hole_error_message": "",
244
+ "excitation_error_message": "",
245
+ "excitation_info_message": "",
246
+ "excitation_config_open": False,
247
+ "dt_user": 0.1,
248
+ "temporal_warning": "",
249
+ # QPU monitor controls
250
+ "qpu_field_components": "Ez",
251
+ "qpu_monitor_gridpoints": "",
252
+ "qpu_monitor_samples": "(0.5, 0.5)",
253
+ "qpu_monitor_sample_info": "",
254
+ "console_output": "Console initialized.\n",
255
+ "pyvista_view_style": "aspect-ratio: 1 / 1; width: 100%;",
256
+ # QPU Plotly controls
257
+ "qpu_ts_fig": None,
258
+ "qpu_ts_selected_time": None,
259
+ "qpu_ts_ready": False,
260
+ "qpu_plot_style": "display: none; width: 900px; height: 660px; margin: 0 auto;",
261
+ "qpu_ts_other_ready": False,
262
+ "qpu_other_plot_style": "display: none; width: 900px; height: 660px; margin: 0 auto;",
263
+ "qpu_monitor_configs": [],
264
+ "qpu_plot_filter": "All",
265
+ "qpu_plot_field_options": ["All"],
266
+ "qpu_plot_position_filter": "All positions",
267
+ "qpu_plot_position_options": ["All positions"],
268
+ # Additional QPU monitor slots
269
+ "qpu_monitor_count": 0,
270
+ "qpu_field_components_2": "Ez",
271
+ "qpu_monitor_gridpoints_2": "",
272
+ "qpu_monitor_samples_2": "",
273
+ "qpu_monitor_sample_info_2": "",
274
+ "qpu_field_components_3": "Ez",
275
+ "qpu_monitor_gridpoints_3": "",
276
+ "qpu_monitor_samples_3": "",
277
+ "qpu_monitor_sample_info_3": "",
278
+ "qpu_field_components_4": "Ez",
279
+ "qpu_monitor_gridpoints_4": "",
280
+ "qpu_monitor_samples_4": "",
281
+ "qpu_monitor_sample_info_4": "",
282
+ "qpu_field_components_5": "Ez",
283
+ "qpu_monitor_gridpoints_5": "",
284
+ "qpu_monitor_samples_5": "",
285
+ "qpu_monitor_sample_info_5": "",
286
+ # Status
287
+ "status_visible": False,
288
+ "status_message": "Ready",
289
+ "status_type": "info",
290
+ "simulation_progress": 0,
291
+ "show_progress": False,
292
+ "console_logs": "Console initialized...\n",
293
+ })
294
+
295
+ # Ensure hole snap state exists
296
+ state.hole_snap = True
297
+
298
+
299
+ def init_state(force: bool = False):
300
+ """Initialize EM state. Called after set_server()."""
301
+ if not state.bound:
302
+ return
303
+
304
+ # Apply workflow highlights
305
+ _apply_workflow_highlights(0)
306
+
307
+ # Load logo
308
+ from .utils import load_logo_data_uri
309
+ state.logo_src = load_logo_data_uri()
310
+
311
+ # NOTE: Point picking is enabled later, after the UI is built,
312
+ # by calling enable_point_picking_on_plotter() or in redraw_surface_plot()
313
+
314
+ if force:
315
+ from .simulation import reset_to_defaults
316
+ reset_to_defaults()
317
+
318
+ # Initialize preview
319
+ try:
320
+ from .excitation import update_initial_state_preview
321
+ update_initial_state_preview()
322
+ except Exception:
323
+ pass
324
+
325
+
326
+ def enable_point_picking_on_plotter():
327
+ """Enable point picking on the plotter. Call AFTER build_ui()."""
328
+ from .globals import plotter
329
+ from .simulation import update_value_display
330
+ try:
331
+ plotter.disable_picking()
332
+ except Exception:
333
+ pass
334
+ try:
335
+ plotter.enable_point_picking(callback=update_value_display, show_message=False)
336
+ except Exception:
337
+ pass
338
+
339
+
340
+ # Initialize state defaults at module load time
341
+ _init_state_defaults()
em/ui.py ADDED
@@ -0,0 +1,738 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """UI components and build_ui function for the EM module."""
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ import plotly.graph_objects as go
7
+ from trame.widgets import html as trame_html, vuetify3, plotly as plotly_widgets
8
+ from pyvista.trame.ui import plotter_ui
9
+
10
+ from .state import state, ctrl, get_server
11
+ from .globals import plotter
12
+ from .geometry import build_geometry_placeholder as _build_geometry_placeholder
13
+ from .excitation import build_excitation_placeholder as _build_excitation_placeholder
14
+ from .simulation import run_simulation_only, reset_to_defaults, stop_simulation_handler
15
+ from .exports import (
16
+ export_vtk, export_vtk_all_frames, export_mp4,
17
+ export_sim_timeseries_csv, export_sim_timeseries_png, export_sim_timeseries_html,
18
+ export_qpu_timeseries_csv, export_qpu_timeseries_png, export_qpu_timeseries_html,
19
+ )
20
+ from .handlers import build_qubit_plot
21
+
22
+ if TYPE_CHECKING:
23
+ pass
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Placeholder builders for preview areas
28
+ # ---------------------------------------------------------------------------
29
+
30
+ def _build_empty_figure(message: str = "") -> go.Figure:
31
+ """Build an empty placeholder figure with optional message."""
32
+ fig = go.Figure()
33
+ if message:
34
+ fig.add_annotation(
35
+ text=message,
36
+ x=0.5, y=0.5, showarrow=False,
37
+ font=dict(size=16, color="#888")
38
+ )
39
+ fig.update_layout(
40
+ margin=dict(l=20, r=20, t=40, b=20),
41
+ paper_bgcolor="#ffffff",
42
+ plot_bgcolor="#ffffff",
43
+ height=400,
44
+ )
45
+ fig.update_xaxes(visible=False)
46
+ fig.update_yaxes(visible=False)
47
+ return fig
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Main UI Builder
52
+ # ---------------------------------------------------------------------------
53
+
54
+ def build_ui():
55
+ """Render the EM UI inside the host layout."""
56
+ _server = get_server()
57
+ if _server is None or not state.bound or not ctrl.bound:
58
+ raise RuntimeError('Call set_server(server) before build_ui().')
59
+
60
+ with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
61
+ # Upload Dialog
62
+ with vuetify3.VDialog(v_model=("show_upload_dialog", False), max_width="500px"):
63
+ with vuetify3.VCard():
64
+ vuetify3.VCardTitle("Upload Geometry")
65
+ with vuetify3.VCardText(classes="py-1 px-2"):
66
+ vuetify3.VFileInput(
67
+ show_size=True,
68
+ label="Select geometry file",
69
+ accept=".vtp,.vtk,.glb,.stl",
70
+ update_binary=("uploaded_file_info", 1),
71
+ )
72
+ with vuetify3.VCardActions():
73
+ vuetify3.VSpacer()
74
+ vuetify3.VBtn("Cancel", click="show_upload_dialog = false")
75
+
76
+ # Snackbars for status messages
77
+ vuetify3.VSnackbar(
78
+ v_model=("show_upload_status", False),
79
+ children=["{{ upload_status_message }}"],
80
+ timeout=4000,
81
+ location="bottom right",
82
+ color="primary",
83
+ variant="tonal",
84
+ )
85
+ vuetify3.VSnackbar(
86
+ v_model=("show_export_status", False),
87
+ children=["{{ export_status_message }}"],
88
+ timeout=4000,
89
+ location="bottom right",
90
+ color="primary",
91
+ variant="tonal",
92
+ )
93
+
94
+ with vuetify3.VRow(no_gutters=True, classes="fill-height"):
95
+ # Left Column - Configuration
96
+ with vuetify3.VCol(cols=5, classes="pa-1 d-flex flex-column"):
97
+ _build_overview_card()
98
+ _build_geometry_card()
99
+ _build_excitation_card()
100
+ _build_material_card()
101
+ _build_time_card()
102
+ _build_meshing_card()
103
+ _build_backends_card()
104
+ _build_output_preferences_card()
105
+ _build_run_buttons()
106
+ vuetify3.VSpacer()
107
+ _build_reset_button()
108
+
109
+ # Right Column - Visualization
110
+ with vuetify3.VCol(cols=7, classes="pa-1 d-flex flex-column"):
111
+ _build_output_config_card()
112
+ _build_main_plot_area()
113
+ _build_qpu_plot_area()
114
+ _build_no_geometry_placeholder()
115
+ _build_status_console()
116
+
117
+ # Floating status window
118
+ _build_status_window()
119
+
120
+
121
+ def _build_overview_card():
122
+ """Build the Overview/Introduction card."""
123
+ with vuetify3.VCard(classes="mb-1", style=("overview_card_style", "font-size: 0.8rem;")):
124
+ with vuetify3.VCardTitle("Overview", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
125
+ pass
126
+ with vuetify3.VCardText(classes="py-1 px-2"):
127
+ vuetify3.VSelect(
128
+ label="Select a problem",
129
+ v_model=("problem_selection", None),
130
+ items=(
131
+ "problem_options",
132
+ [
133
+ "Propagation in a given medium (no bodies)",
134
+ "Scattering from a perfectly conducting body",
135
+ ],
136
+ ),
137
+ placeholder="Select a problem",
138
+ density="compact",
139
+ color="primary",
140
+ )
141
+ vuetify3.VDivider(classes="my-0")
142
+ vuetify3.VCardSubtitle("Governing Equations", classes="text-caption font-weight-bold mt-0", style="font-size: 0.7rem;")
143
+ vuetify3.VDivider(classes="mb-0")
144
+ vuetify3.VListItemTitle("Maxwell's time-domain, 2D, TEz polarized.", classes="text-caption", style="font-size: 0.7rem;")
145
+ vuetify3.VCardSubtitle("Inputs", classes="text-caption font-weight-bold mt-0", style="font-size: 0.7rem;")
146
+ vuetify3.VDivider(classes="mb-0")
147
+ vuetify3.VListItemTitle("Geometry, excitation, medium, output visualization preferences.", classes="text-caption", style="font-size: 0.7rem;")
148
+ vuetify3.VCardSubtitle("Outputs", classes="text-caption font-weight-bold mt-0", style="font-size: 0.7rem;")
149
+ vuetify3.VDivider(classes="mb-0")
150
+ vuetify3.VListItemTitle("Surface plots of field components OR time evolution of field components at specified points.", classes="text-caption", style="font-size: 0.7rem;")
151
+
152
+
153
+ def _build_geometry_card():
154
+ """Build the Geometry configuration card."""
155
+ with vuetify3.VCard(classes="mb-1", style=("geometry_card_style", "font-size: 0.8rem;")):
156
+ with vuetify3.VCardTitle("Geometry", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
157
+ pass
158
+ with vuetify3.VCardText(classes="py-1 px-2"):
159
+ vuetify3.VSelect(
160
+ label="Select",
161
+ v_model=("geometry_selection", None),
162
+ items=("geometry_options",),
163
+ placeholder="Select",
164
+ density="compact",
165
+ color="primary",
166
+ )
167
+ with vuetify3.VContainer(v_if="geometry_selection === 'Square Metallic Body'", classes="pa-0 mt-2"):
168
+ with vuetify3.VRow(dense=True):
169
+ with vuetify3.VCol():
170
+ with vuetify3.VTooltip("Square hole edge length s in domain units [0,1]. Must be ≤ 1. UI-only.", location="bottom", color="primary"):
171
+ with vuetify3.Template(v_slot_activator="{ props }"):
172
+ vuetify3.VTextField(
173
+ v_bind="props",
174
+ v_model=("hole_size_edge", 0.2),
175
+ label="Hole Edge Length [0 - 1]",
176
+ type="number",
177
+ step=0.05,
178
+ min=0,
179
+ density="compact",
180
+ color="primary",
181
+ )
182
+ with vuetify3.VCol():
183
+ 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"):
184
+ with vuetify3.Template(v_slot_activator="{ props }"):
185
+ vuetify3.VTextField(
186
+ v_bind="props",
187
+ v_model=("hole_center_pair", "(0.5, 0.5)"),
188
+ label="Hole Center (X, Y)",
189
+ density="compact",
190
+ color="primary",
191
+ )
192
+ with vuetify3.VRow(dense=True, classes="mt-1"):
193
+ with vuetify3.VCol(cols=12):
194
+ vuetify3.VSwitch(
195
+ v_model=("hole_snap", True),
196
+ label="Snap edges to nearest grid lines",
197
+ color="primary",
198
+ inset=True,
199
+ density="compact",
200
+ )
201
+ vuetify3.VAlert(
202
+ v_if="hole_error_message",
203
+ type="error",
204
+ variant="tonal",
205
+ density="compact",
206
+ children=["{{ hole_error_message }}"],
207
+ classes="mt-1",
208
+ )
209
+
210
+
211
+ def _build_excitation_card():
212
+ """Build the Excitation/Initial State configuration card."""
213
+ with vuetify3.VCard(classes="mb-1", style=("excitation_card_style", "font-size: 0.8rem;")):
214
+ with vuetify3.VCardTitle("Excitation: Initial State", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
215
+ pass
216
+ with vuetify3.VCardText(classes="py-1 px-2"):
217
+ with vuetify3.VRow(classes="d-flex align-center", dense=True, no_gutters=True):
218
+ with vuetify3.VCol(classes="flex-grow-1"):
219
+ vuetify3.VSelect(
220
+ label="Select",
221
+ v_model=("dist_type", None),
222
+ items=("dist_type_options", ["None", "Delta", "Gaussian"]),
223
+ placeholder="Select",
224
+ density="compact",
225
+ color="primary",
226
+ )
227
+ with vuetify3.VCol(cols="auto", classes="ml-1"):
228
+ with vuetify3.VTooltip("Show or hide configuration for the selected excitation", location="bottom", color="primary"):
229
+ with vuetify3.Template(v_slot_activator="{ props }"):
230
+ with vuetify3.VBtn(
231
+ icon=True,
232
+ density="compact",
233
+ variant="text",
234
+ click="excitation_config_open = !excitation_config_open",
235
+ disabled=("!dist_type", False),
236
+ v_bind="props",
237
+ ):
238
+ vuetify3.VIcon("mdi-cog", color=("excitation_config_open ? 'primary' : 'grey'",))
239
+ trame_html.Span("Toggle parameter inputs for the chosen excitation")
240
+ # Delta config
241
+ with vuetify3.VExpandTransition():
242
+ with vuetify3.VSheet(
243
+ v_if="excitation_config_open && dist_type === 'Delta'",
244
+ classes="pa-2 mt-1 rounded-lg",
245
+ style="background-color: rgba(95,37,159,0.03); border: 1px solid rgba(95,37,159,0.15);",
246
+ ):
247
+ with vuetify3.VTooltip("Impulse position (x, y) in [0,1]. Example: (0.6, 0.6).", location="bottom", color="primary"):
248
+ with vuetify3.Template(v_slot_activator="{ props }"):
249
+ vuetify3.VTextField(
250
+ v_bind="props",
251
+ v_model=("peak_pair", "(0.5, 0.5)"),
252
+ label="Peak (x, y) in [0,1]",
253
+ density="compact",
254
+ color="primary",
255
+ )
256
+ # Gaussian config
257
+ with vuetify3.VExpandTransition():
258
+ with vuetify3.VSheet(
259
+ v_if="excitation_config_open && dist_type === 'Gaussian'",
260
+ classes="pa-2 mt-1 rounded-lg",
261
+ style="background-color: rgba(95,37,159,0.03); border: 1px solid rgba(95,37,159,0.15);",
262
+ ):
263
+ with vuetify3.VRow(dense=True):
264
+ with vuetify3.VCol():
265
+ with vuetify3.VTooltip("Gaussian center μ (x, y) in [0,1]. Example: (0.5, 0.5).", location="bottom", color="primary"):
266
+ with vuetify3.Template(v_slot_activator="{ props }"):
267
+ vuetify3.VTextField(
268
+ v_bind="props",
269
+ v_model=("mu_pair", "(0.5, 0.5)"),
270
+ label="Mu (x, y) in [0,1]",
271
+ density="compact",
272
+ color="primary",
273
+ )
274
+ with vuetify3.VRow(dense=True, classes="mt-1"):
275
+ with vuetify3.VCol():
276
+ with vuetify3.VTooltip("Gaussian spread σx in [0,1] of domain length.", location="bottom", color="primary"):
277
+ with vuetify3.Template(v_slot_activator="{ props }"):
278
+ vuetify3.VTextField(
279
+ v_bind="props",
280
+ v_model=("sigma_x", 0.25),
281
+ label="Sigma X (0–1)",
282
+ type="number",
283
+ step="0.01",
284
+ density="compact",
285
+ color="primary",
286
+ )
287
+ with vuetify3.VCol():
288
+ with vuetify3.VTooltip("Gaussian spread σy in [0,1] of domain length.", location="bottom", color="primary"):
289
+ with vuetify3.Template(v_slot_activator="{ props }"):
290
+ vuetify3.VTextField(
291
+ v_bind="props",
292
+ v_model=("sigma_y", 0.15),
293
+ label="Sigma Y (0–1)",
294
+ type="number",
295
+ step="0.01",
296
+ density="compact",
297
+ color="primary",
298
+ )
299
+ vuetify3.VAlert(v_if="excitation_error_message", type="error", variant="tonal", density="compact", children=["{{ excitation_error_message }}"], classes="mt-1")
300
+ vuetify3.VAlert(
301
+ v_if="excitation_info_message",
302
+ type="info",
303
+ variant="tonal",
304
+ density="compact",
305
+ children=["{{ excitation_info_message }}"],
306
+ classes="mt-1",
307
+ style="white-space: pre-line;",
308
+ )
309
+
310
+
311
+ def _build_material_card():
312
+ """Build the Material Properties card."""
313
+ with vuetify3.VCard(classes="mb-1", style="font-size: 0.8rem;"):
314
+ with vuetify3.VCardTitle("Material Properties (Medium)", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
315
+ pass
316
+ with vuetify3.VCardText(classes="py-1 px-2"):
317
+ with vuetify3.VRow(dense=True):
318
+ with vuetify3.VCol(cols="6"):
319
+ with vuetify3.VTooltip("Relative permittivity. ε_r = ε / ε₀. Default 1.0 (free space).", location="bottom", color="primary"):
320
+ with vuetify3.Template(v_slot_activator="{ props }"):
321
+ vuetify3.VTextField(v_bind="props", v_model=("coeff_permittivity", 1.0), label="Permittivity (εr)", type="number", step="0.1", density="compact", color="primary")
322
+ with vuetify3.VCol(cols="6"):
323
+ with vuetify3.VTooltip("Relative permeability. μ_r = μ / μ₀. Default 1.0 (non-magnetic).", location="bottom", color="primary"):
324
+ with vuetify3.Template(v_slot_activator="{ props }"):
325
+ vuetify3.VTextField(v_bind="props", v_model=("coeff_permeability", 1.0), label="Permeability (μr)", type="number", step="0.1", density="compact", color="primary")
326
+
327
+
328
+ def _build_time_card():
329
+ """Build the Time configuration card."""
330
+ with vuetify3.VCard(classes="mb-1", style="font-size: 0.8rem;"):
331
+ with vuetify3.VCardTitle("Time", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
332
+ pass
333
+ with vuetify3.VCardText(classes="py-1 px-2"):
334
+ with vuetify3.VTooltip("Sets the total duration for the simulation to run.", location="bottom", color="primary"):
335
+ with vuetify3.Template(v_slot_activator="{ props }"):
336
+ vuetify3.VTextField(
337
+ v_bind="props",
338
+ v_model=("T", 1.0),
339
+ label="Total Time (T)",
340
+ type="number",
341
+ step="0.1",
342
+ density="compact",
343
+ color="primary",
344
+ )
345
+
346
+
347
+ def _build_meshing_card():
348
+ """Build the Meshing card with qubit plot hover."""
349
+ with vuetify3.VCard(classes="mb-1", style=("meshing_card_style", "font-size: 0.8rem;")):
350
+ with vuetify3.VCardTitle("Meshing", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
351
+ pass
352
+ with vuetify3.VCardText(classes="py-1 px-2"):
353
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"):
354
+ with vuetify3.Template(v_slot_activator="{ props }"):
355
+ with vuetify3.VSlider(
356
+ v_bind="props",
357
+ v_model=("nx_slider_index", None),
358
+ label="No. of points per direction:",
359
+ min=0,
360
+ max=5,
361
+ step=1,
362
+ show_ticks="always",
363
+ thumb_label="always",
364
+ density="compact",
365
+ color="primary",
366
+ ):
367
+ vuetify3.Template(v_slot_thumb_label="{ modelValue }", children=["{{ modelValue === null ? 'Select' : [16, 32, 64, 128, 256, 512][modelValue] }}"])
368
+ # Hover content: enlarged Plotly graph
369
+ with vuetify3.VSheet(classes="pa-2", elevation=6, rounded=True, style="width: 644px;"):
370
+ qubit_fig_widget = plotly_widgets.Figure(
371
+ figure=build_qubit_plot(int(state.nx or 16)),
372
+ responsive=True,
373
+ style="width: 616px; height: 364px; min-height: 364px;",
374
+ )
375
+ ctrl.qubit_plot_update = qubit_fig_widget.update
376
+
377
+
378
+ def _build_backends_card():
379
+ """Build the Backends selection card."""
380
+ with vuetify3.VCard(classes="mb-1", style=("backend_card_style", "font-size: 0.8rem;")):
381
+ with vuetify3.VCardTitle("Backends", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
382
+ pass
383
+ with vuetify3.VCardText(classes="py-1 px-2"):
384
+ with vuetify3.VRow(dense=True, classes="mb-2"):
385
+ with vuetify3.VCol():
386
+ vuetify3.VAlert(
387
+ type="info",
388
+ color="primary",
389
+ variant="tonal",
390
+ density="compact",
391
+ children=[
392
+ "Selected: ",
393
+ "{{ backend_type || '—' }}",
394
+ " - ",
395
+ "{{ backend_type === 'Simulator' ? selected_simulator : (backend_type === 'QPU' ? selected_qpu : '—') }}",
396
+ ],
397
+ )
398
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"):
399
+ with vuetify3.Template(v_slot_activator="{ props }"):
400
+ vuetify3.VBtn(v_bind="props", text="Choose Backend", color="primary", variant="tonal", block=True)
401
+ with vuetify3.VList(density="compact"):
402
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8):
403
+ with vuetify3.Template(v_slot_activator="{ props }"):
404
+ vuetify3.VListItem(v_bind="props", title="Simulator", prepend_icon="mdi-robot-outline", append_icon="mdi-chevron-right")
405
+ with vuetify3.VList(density="compact"):
406
+ vuetify3.VListItem(title="IBM Qiskit simulator", click="backend_type = 'Simulator'; selected_simulator = 'IBM Qiskit simulator'")
407
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8):
408
+ with vuetify3.Template(v_slot_activator="{ props }"):
409
+ vuetify3.VListItem(v_bind="props", title="QPU", prepend_icon="mdi-chip", append_icon="mdi-chevron-right")
410
+ with vuetify3.VList(density="compact"):
411
+ vuetify3.VListItem(title="IBM QPU", click="backend_type = 'QPU'; selected_qpu = 'IBM QPU'")
412
+ vuetify3.VListItem(title="IonQ QPU", click="backend_type = 'QPU'; selected_qpu = 'IonQ QPU'")
413
+
414
+
415
+ def _build_output_preferences_card():
416
+ """Build the Output Preferences card (appears after backend selection)."""
417
+ with vuetify3.VCard(v_if="backend_type", classes="mb-0", style=("output_card_style", "font-size: 0.8rem;")):
418
+ with vuetify3.VCardTitle("Output Preferences", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
419
+ pass
420
+ with vuetify3.VCardText(classes="py-1 px-2"):
421
+ vuetify3.VCardSubtitle("Select Δt intervals for plotting output snapshots", classes="text-caption font-weight-bold mt-1", style="font-size: 0.75rem;")
422
+ with vuetify3.VTooltip("Snapshot interval (Δt). Solver runs at fixed 0.1 s; frames are saved every Δt.", location="bottom", color="primary"):
423
+ with vuetify3.Template(v_slot_activator="{ props }"):
424
+ vuetify3.VTextField(v_bind="props", v_model=("dt_user", 0.1), label="Δt", type="number", step="0.1", density="compact", color="primary", classes="mt-1")
425
+ vuetify3.VAlert(v_if="temporal_warning", type="warning", variant="tonal", density="compact", children=["{{ temporal_warning }}"], classes="mt-1")
426
+
427
+ # QPU monitor options
428
+ with vuetify3.VContainer(v_if="backend_type === 'QPU'", classes="pa-0 mt-2"):
429
+ with vuetify3.VRow(dense=True, classes="mb-1 align-center"):
430
+ with vuetify3.VCol(cols=4, sm=3, md=3):
431
+ vuetify3.VSelect(
432
+ label="Field",
433
+ v_model=("qpu_field_components", "Ez"),
434
+ items=("qpu_field_options", ["All", "Ez", "Hx", "Hy"]),
435
+ density="compact",
436
+ color="primary",
437
+ hide_details=True,
438
+ style="max-width: 160px;",
439
+ )
440
+ with vuetify3.VCol(cols=8, sm=9, md=9):
441
+ vuetify3.VTextField(
442
+ label="Sample position(s) (x, y) in [0,1]",
443
+ v_model=("qpu_monitor_samples", "(0.5, 0.5)"),
444
+ density="compact",
445
+ color="primary",
446
+ hide_details=True,
447
+ style="max-width: 320px;",
448
+ )
449
+ vuetify3.VAlert(
450
+ v_if="qpu_monitor_sample_info",
451
+ type="info",
452
+ variant="tonal",
453
+ density="compact",
454
+ children=["{{ qpu_monitor_sample_info }}"],
455
+ classes="mb-1",
456
+ style="white-space: pre-line;",
457
+ )
458
+
459
+
460
+ def _build_run_buttons():
461
+ """Build the Run and Stop buttons row."""
462
+ with vuetify3.VRow(dense=True, classes="mb-2"):
463
+ with vuetify3.VCol(cols=9):
464
+ with vuetify3.VTooltip("Starts the quantum simulation with the specified parameters.", location="bottom", color="primary"):
465
+ with vuetify3.Template(v_slot_activator="{ props }"):
466
+ vuetify3.VBtn(
467
+ v_bind="props",
468
+ text=("run_button_text", "RUN!"),
469
+ click=run_simulation_only,
470
+ color="primary",
471
+ block=True,
472
+ disabled=("is_running || run_button_text === 'Successful!' || !geometry_selection || !dist_type || !!temporal_warning || nx === null || !backend_type", False),
473
+ )
474
+ with vuetify3.VCol(cols=3):
475
+ with vuetify3.VTooltip("Stop the running simulation", location="bottom", color="primary"):
476
+ with vuetify3.Template(v_slot_activator="{ props }"):
477
+ vuetify3.VBtn(
478
+ v_bind="props",
479
+ text="Stop",
480
+ click=stop_simulation_handler,
481
+ color="error",
482
+ block=True,
483
+ disabled=("stop_button_disabled", True),
484
+ )
485
+
486
+
487
+ def _build_reset_button():
488
+ """Build the Reset button."""
489
+ with trame_html.Div(style="flex: 0 0 auto;"):
490
+ with vuetify3.VTooltip("Reset all parameters to their default values", location="bottom", color="primary"):
491
+ with vuetify3.Template(v_slot_activator="{ props }"):
492
+ vuetify3.VBtn(
493
+ v_bind="props",
494
+ text="Reset",
495
+ click=reset_to_defaults,
496
+ color="secondary",
497
+ block=True,
498
+ )
499
+
500
+
501
+ def _build_output_config_card():
502
+ """Build the Output Configuration card (appears after simulation, not for QPU)."""
503
+ with vuetify3.VCard(v_if="simulation_has_run && backend_type !== 'QPU'", classes="mb-1", style="font-size: 0.8rem;"):
504
+ with vuetify3.VCardSubtitle("Output Configuration", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px; font-weight: 600; color: #1A1A1A;"):
505
+ with vuetify3.VCardText(classes="py-1 px-2", style="color: #1A1A1A;"):
506
+ with vuetify3.VRadioGroup(v_model=("output_type", "Surface Plot"), row=True, density="compact", color="primary"):
507
+ vuetify3.VRadio(label="Surface", value="Surface Plot", style="font-weight: 500;")
508
+ vuetify3.VRadio(label="Time Series", value="Time Series Plot", style="font-weight: 500;")
509
+
510
+ # Surface Plot options
511
+ with vuetify3.VContainer(v_if="output_type === 'Surface Plot'", classes="pa-0"):
512
+ vuetify3.VSelect(v_model=("surface_field", "Ez"), items=("surface_field_options", ["Ez", "Hx", "Hy"]), label="Field Component", density="compact", color="primary", style="font-weight: 500;")
513
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"):
514
+ with vuetify3.Template(v_slot_activator="{ props }"):
515
+ vuetify3.VBtn(v_bind="props", text="DOWNLOAD", color="primary", variant="tonal", block=True, classes="mt-1")
516
+ with vuetify3.VList(density="compact"):
517
+ vuetify3.VListSubheader("VTK")
518
+ vuetify3.VListItem(title="Current frame (VTK)", prepend_icon="mdi-download", click=export_vtk)
519
+ vuetify3.VListItem(title="All frames (VTK sequence)", prepend_icon="mdi-download-multiple", click=export_vtk_all_frames)
520
+ vuetify3.VDivider()
521
+ vuetify3.VListItem(title="Animation (MP4)", prepend_icon="mdi-movie", click=export_mp4)
522
+
523
+ # Time Series options
524
+ with vuetify3.VContainer(v_if="output_type === 'Time Series Plot'", classes="pa-0"):
525
+ vuetify3.VSelect(v_model=("timeseries_field", "Ez"), items=("timeseries_field_options", ["All", "Ez", "Hx", "Hy"]), label="Field Component", density="compact", color="primary", style="font-weight: 500;")
526
+ vuetify3.VTextarea(
527
+ v_model=("timeseries_points", "(0.5, 0.5)"),
528
+ label="Monitor Position(s) (x, y) in [0,1]",
529
+ hint="e.g., (0.50, 0.50) or multiple comma-separated pairs",
530
+ rows=2,
531
+ auto_grow=True,
532
+ color="primary",
533
+ style="font-weight: 500;",
534
+ )
535
+ vuetify3.VAlert(
536
+ v_if="timeseries_point_info",
537
+ type="info",
538
+ variant="tonal",
539
+ density="compact",
540
+ children=["{{ timeseries_point_info }}"],
541
+ classes="mt-1",
542
+ style="white-space: pre-line;",
543
+ )
544
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"):
545
+ with vuetify3.Template(v_slot_activator="{ props }"):
546
+ vuetify3.VBtn(v_bind="props", text="DOWNLOAD", color="primary", variant="tonal", block=True, classes="mt-1")
547
+ with vuetify3.VList(density="compact"):
548
+ vuetify3.VListItem(title="Download CSV", prepend_icon="mdi-download", click=export_sim_timeseries_csv)
549
+ vuetify3.VListItem(title="Download PNG", prepend_icon="mdi-image", click=export_sim_timeseries_png)
550
+ vuetify3.VListItem(title="Download HTML", prepend_icon="mdi-file-html", click=export_sim_timeseries_html)
551
+
552
+
553
+ def _build_main_plot_area():
554
+ """Build the main plot area (PyVista for Simulator)."""
555
+ with vuetify3.VCard(
556
+ v_if="geometry_selection && (backend_type !== 'QPU' || !qpu_ts_ready)",
557
+ classes="mb-1 flex-grow-1 d-flex flex-column",
558
+ style="min-height: 0;",
559
+ ):
560
+ # Running indicator
561
+ with vuetify3.VContainer(v_if="is_running", fluid=True, classes="fill-height d-flex flex-column align-center justify-center"):
562
+ vuetify3.VProgressCircular(indeterminate=True, size=64, color="primary")
563
+ vuetify3.VCardSubtitle("Running simulation...", classes="mt-4")
564
+
565
+ # Geometry preview (no excitation selected yet)
566
+ with vuetify3.VContainer(
567
+ v_if="!is_running && geometry_selection && !dist_type",
568
+ fluid=True,
569
+ classes="pa-0 flex-grow-1 d-flex align-center justify-center",
570
+ style="width: 100%; min-height: 560px;",
571
+ ):
572
+ geometry_preview_widget = plotly_widgets.Figure(
573
+ figure=_build_geometry_placeholder("Select a geometry to preview."),
574
+ responsive=True,
575
+ style="width: 100%; height: 100%;",
576
+ )
577
+ ctrl.geometry_preview_update = geometry_preview_widget.update
578
+
579
+ # Excitation preview (before simulation runs)
580
+ with vuetify3.VContainer(
581
+ v_if="!is_running && dist_type && !simulation_has_run",
582
+ fluid=True,
583
+ classes="pa-0 flex-grow-1 d-flex align-center justify-center",
584
+ style="width: 100%; min-height: 580px;",
585
+ ):
586
+ excitation_preview_widget = plotly_widgets.Figure(
587
+ figure=_build_excitation_placeholder("Select an excitation to preview."),
588
+ responsive=True,
589
+ style="width: 100%; height: 100%;",
590
+ )
591
+ ctrl.excitation_preview_update = excitation_preview_widget.update
592
+
593
+ # Surface Plot: PyVista view
594
+ with vuetify3.VContainer(v_if="!is_running && simulation_has_run && output_type === 'Surface Plot'", fluid=True, classes="pa-0", style=("pyvista_view_style", "aspect-ratio: 1 / 1; width: 100%;")):
595
+ view = plotter_ui(plotter)
596
+ ctrl.view_update = view.update
597
+
598
+ # Time Series: Plotly figure
599
+ with vuetify3.VContainer(v_if="!is_running && simulation_has_run && output_type === 'Time Series Plot'", fluid=True, classes="d-flex align-center justify-center pa-2", style="overflow: hidden;"):
600
+ sim_ts = plotly_widgets.Figure(
601
+ figure=go.Figure(layout=dict(width=900, height=660)),
602
+ responsive=False,
603
+ style="width: 900px; height: 660px;",
604
+ )
605
+ ctrl.sim_ts_update = sim_ts.update
606
+
607
+ # Time slider for surface plot
608
+ with vuetify3.VContainer(v_if="simulation_has_run && output_type === 'Surface Plot' && backend_type !== 'QPU'", fluid=True, classes="pa-0 mt-2"):
609
+ 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")
610
+
611
+
612
+ def _build_qpu_plot_area():
613
+ """Build the QPU plot area (Plotly time series)."""
614
+ with vuetify3.VCard(v_if="geometry_selection && backend_type === 'QPU' && (is_running || qpu_ts_ready)", classes="flex-grow-1", style="min-height: 0;"):
615
+ # Running indicator
616
+ with vuetify3.VContainer(v_if="is_running", fluid=True, classes="fill-height d-flex flex-column align-center justify-center"):
617
+ vuetify3.VProgressCircular(indeterminate=True, size=64, color="primary")
618
+ vuetify3.VCardSubtitle("Running QPU...", classes="mt-4")
619
+
620
+ # QPU timeseries with toolbar
621
+ with vuetify3.VContainer(v_if="!is_running && qpu_ts_ready", fluid=True, classes="pa-2"):
622
+ with vuetify3.VToolbar(density="compact", flat=True, color="transparent", classes="px-0"):
623
+ vuetify3.VSelect(
624
+ label="Component",
625
+ v_model=("qpu_plot_filter", "All"),
626
+ items=("qpu_plot_field_options", ["All"]),
627
+ density="compact",
628
+ color="primary",
629
+ hide_details=True,
630
+ style="max-width: 180px; margin-right: 8px;",
631
+ disabled=("!qpu_ts_ready", True),
632
+ update_modelValue=(ctrl.qpu_set_plot_filter, "[$event]")
633
+ )
634
+ vuetify3.VSelect(
635
+ label="Position",
636
+ v_model=("qpu_plot_position_filter", "All positions"),
637
+ items=("qpu_plot_position_options", ["All positions"]),
638
+ density="compact",
639
+ color="primary",
640
+ hide_details=True,
641
+ style="max-width: 200px; margin-right: 8px;",
642
+ disabled=("!qpu_ts_ready", True),
643
+ update_modelValue=(ctrl.qpu_set_plot_position_filter, "[$event]")
644
+ )
645
+ vuetify3.VSpacer()
646
+ vuetify3.VBtn(
647
+ text="Clear Selection",
648
+ color="secondary",
649
+ variant="text",
650
+ click=ctrl.on_qpu_ts_clear,
651
+ disabled=("!qpu_ts_ready", True),
652
+ )
653
+ with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8):
654
+ with vuetify3.Template(v_slot_activator="{ props }"):
655
+ vuetify3.VBtn(
656
+ v_bind="props",
657
+ text="DOWNLOAD",
658
+ color="primary",
659
+ variant="tonal",
660
+ disabled=("!qpu_ts_ready", True),
661
+ )
662
+ with vuetify3.VList(density="compact"):
663
+ vuetify3.VListItem(title="Download CSV", prepend_icon="mdi-download", click=export_qpu_timeseries_csv, disabled=("!qpu_ts_ready", True))
664
+ vuetify3.VListItem(title="Download PNG", prepend_icon="mdi-image", click=export_qpu_timeseries_png, disabled=("!qpu_ts_ready", True))
665
+ vuetify3.VListItem(title="Download HTML", prepend_icon="mdi-file-html", click=export_qpu_timeseries_html, disabled=("!qpu_ts_ready", True))
666
+
667
+ trame_html.Div(style="height: 6px;")
668
+
669
+ # Plotly figure for QPU timeseries
670
+ qpu_ts_widget = plotly_widgets.Figure(
671
+ figure=go.Figure(layout=dict(width=900, height=660)),
672
+ responsive=False,
673
+ style=("qpu_plot_style", "display: none; width: 900px; height: 660px; margin: 0 auto;"),
674
+ click=ctrl.on_qpu_ts_click,
675
+ )
676
+ ctrl.qpu_ts_update = qpu_ts_widget.update
677
+
678
+
679
+ def _build_no_geometry_placeholder():
680
+ """Build placeholder when no geometry is selected."""
681
+ with vuetify3.VContainer(v_if="!geometry_selection", fluid=True, classes="flex-grow-1 d-flex align-center justify-center text-medium-emphasis"):
682
+ vuetify3.VCardText("Select a geometry to display the preview and results.")
683
+
684
+
685
+ def _build_status_console():
686
+ """Build the Status/Console card."""
687
+ with vuetify3.VCard(classes="mt-2", style="font-size: 0.8rem;"):
688
+ with vuetify3.VCardTitle("Status", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
689
+ pass
690
+ with vuetify3.VCardText(classes="py-1 px-2", style="height: 150px; overflow-y: auto; background-color: #f5f5f5; font-family: monospace;"):
691
+ vuetify3.VTextarea(
692
+ v_model=("console_output", ""),
693
+ readonly=True,
694
+ auto_grow=False,
695
+ rows=6,
696
+ variant="plain",
697
+ hide_details=True,
698
+ style="font-family: monospace; width: 100%; height: 100%;"
699
+ )
700
+
701
+
702
+ def _build_status_window():
703
+ """Build the floating status window (bottom right)."""
704
+ with vuetify3.VCard(
705
+ v_if="status_visible",
706
+ style="position: fixed; bottom: 16px; right: 16px; z-index: 1000; min-width: 320px; max-width: 450px;",
707
+ elevation=8
708
+ ):
709
+ with vuetify3.VCardTitle(classes="d-flex align-center", style="font-size: 0.95rem; padding: 8px 12px;"):
710
+ vuetify3.VIcon("mdi-information-outline", size="small", classes="mr-2")
711
+ trame_html.Span("Simulation Status")
712
+ vuetify3.VSpacer()
713
+ vuetify3.VBtn(
714
+ icon="mdi-close",
715
+ size="x-small",
716
+ variant="text",
717
+ click="status_visible = false"
718
+ )
719
+ vuetify3.VDivider()
720
+ with vuetify3.VCardText(classes="py-2 px-3"):
721
+ vuetify3.VAlert(
722
+ type=("status_type", "info"),
723
+ variant="tonal",
724
+ density="compact",
725
+ children=["{{ status_message }}"]
726
+ )
727
+ with vuetify3.VContainer(v_if="show_progress", classes="pa-0 mt-2"):
728
+ vuetify3.VProgressLinear(
729
+ model_value=("simulation_progress", 0),
730
+ color="primary",
731
+ height=6,
732
+ striped=True
733
+ )
734
+ trame_html.Div(
735
+ "{{ simulation_progress }}% complete",
736
+ classes="text-caption text-center mt-1",
737
+ style="font-size: 0.75rem;"
738
+ )
em/utils.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EM Embedded - Utility Functions
3
+
4
+ Contains shared utilities for grid snapping, coordinate helpers,
5
+ regex patterns, and logo loading.
6
+ """
7
+ import re
8
+ import os
9
+ import base64
10
+ import numpy as np
11
+
12
+ __all__ = [
13
+ "SAMPLE_PAIR_RE",
14
+ "nearest_node_index",
15
+ "snap_samples_to_grid",
16
+ "nearest_gridline",
17
+ "load_logo_data_uri",
18
+ "normalized_position_label",
19
+ "format_grid_label",
20
+ ]
21
+
22
+ # Regex pattern for parsing coordinate pairs like "(0.5, 0.5)"
23
+ SAMPLE_PAIR_RE = re.compile(r"\(\s*([-+]?\d*\.?\d+)\s*,\s*([-+]?\d*\.?\d+)\s*\)")
24
+
25
+
26
+ def nearest_node_index(x: float, y: float, nx: int, ny: int = None) -> tuple:
27
+ """Map normalized [0,1] coordinates to nearest node index on an nx×ny grid."""
28
+ if ny is None:
29
+ ny = nx
30
+ ix = int(round(x * (nx - 1)))
31
+ iy = int(round(y * (ny - 1)))
32
+ ix = max(0, min(nx - 1, ix))
33
+ iy = max(0, min(ny - 1, iy))
34
+ return ix, iy
35
+
36
+
37
+ def nearest_gridline(val: float, nx: int) -> float:
38
+ """Snap a value to the nearest gridline."""
39
+ return round(val * (nx - 1)) / (nx - 1) if nx > 1 else val
40
+
41
+
42
+ def snap_samples_to_grid(sample_str: str, nx: int) -> tuple:
43
+ """
44
+ Parse and snap sample points to grid.
45
+
46
+ Returns:
47
+ tuple: (snapped_gridpoints_str, display_info_str)
48
+ """
49
+ if nx is None or nx < 2:
50
+ return "", ""
51
+
52
+ matches = SAMPLE_PAIR_RE.findall(sample_str)
53
+ if not matches:
54
+ return "", "Enter sample position(s) as (x, y) pairs in [0,1] x [0,1]."
55
+
56
+ snapped = []
57
+ info_parts = []
58
+
59
+ for x_str, y_str in matches:
60
+ try:
61
+ x_norm = float(x_str)
62
+ y_norm = float(y_str)
63
+ # Clamp to [0, 1]
64
+ x_norm = max(0.0, min(1.0, x_norm))
65
+ y_norm = max(0.0, min(1.0, y_norm))
66
+ # Snap to grid
67
+ ix = int(round(x_norm * (nx - 1)))
68
+ iy = int(round(y_norm * (nx - 1)))
69
+ ix = max(0, min(nx - 1, ix))
70
+ iy = max(0, min(nx - 1, iy))
71
+ snapped.append((ix, iy))
72
+ # Compute snapped normalized coords for display
73
+ x_snapped = ix / (nx - 1) if nx > 1 else 0.0
74
+ y_snapped = iy / (nx - 1) if nx > 1 else 0.0
75
+ changed = (abs(x_norm - x_snapped) > 1e-9 or abs(y_norm - y_snapped) > 1e-9)
76
+ descriptor = "adjusted" if changed else "aligned"
77
+ info_parts.append(f"Input ({x_norm:.3f}, {y_norm:.3f}) {descriptor} to ({x_snapped:.3f}, {y_snapped:.3f}) → ({ix}, {iy})")
78
+ except (ValueError, ZeroDivisionError):
79
+ continue
80
+
81
+ gridpoints_str = ", ".join(f"({ix}, {iy})" for ix, iy in snapped)
82
+ info_str = "\n".join(info_parts)
83
+
84
+ return gridpoints_str, info_str
85
+
86
+
87
+ def normalized_position_label(px: int, py: int, gw: int, gh: int) -> str:
88
+ """Create a normalized position label like 'Position (0.500, 0.500)'."""
89
+ px_i, py_i = int(px), int(py)
90
+ denom_x = float(max(gw - 1, 1))
91
+ denom_y = float(max(gh - 1, 1))
92
+ x_norm = px_i / denom_x
93
+ y_norm = py_i / denom_y
94
+ return f"Position ({x_norm:.3f}, {y_norm:.3f})"
95
+
96
+
97
+ def format_grid_label(px: int, py: int, field: str = None, nx: int = None, label_map: dict = None) -> str:
98
+ """Format a grid position label, optionally using a label map."""
99
+ px_i, py_i = int(px), int(py)
100
+
101
+ if label_map and field:
102
+ label = label_map.get((str(field), px_i, py_i))
103
+ if label:
104
+ return label
105
+
106
+ if label_map:
107
+ for (fld, gx, gy), label in label_map.items():
108
+ if gx == px_i and gy == py_i:
109
+ return label
110
+
111
+ if nx:
112
+ denom = float(max(int(nx) - 1, 1))
113
+ return f"Position ({px_i / denom:.3f}, {py_i / denom:.3f})"
114
+
115
+ return f"Position ({px_i}, {py_i})"
116
+
117
+
118
+ def load_logo_data_uri() -> str:
119
+ """Load the Synopsys logo as a data URI."""
120
+ base_dir = os.path.dirname(os.path.dirname(__file__)) # quantum_embedded folder
121
+ candidates = [
122
+ os.path.join(base_dir, "ansys-part-of-synopsys-logo.svg"),
123
+ os.path.join(base_dir, "synopsys-logo-color-rgb.svg"),
124
+ os.path.join(base_dir, "synopsys-logo-color-rgb.png"),
125
+ os.path.join(base_dir, "synopsys-logo-color-rgb.jpg"),
126
+ # Also check in parent quantum folder
127
+ os.path.join(os.path.dirname(base_dir), "quantum", "ansys-part-of-synopsys-logo.svg"),
128
+ ]
129
+
130
+ for p in candidates:
131
+ if os.path.exists(p):
132
+ ext = os.path.splitext(p)[1].lower()
133
+ if ext == ".svg":
134
+ mime = "image/svg+xml"
135
+ elif ext == ".png":
136
+ mime = "image/png"
137
+ else:
138
+ mime = "image/jpeg"
139
+ try:
140
+ with open(p, "rb") as f:
141
+ b64 = base64.b64encode(f.read()).decode("ascii")
142
+ return f"data:{mime};base64,{b64}"
143
+ except Exception:
144
+ continue
145
+
146
+ return None
147
+
148
+
149
+ def install_synopsys_plotly_theme():
150
+ """Install a Synopsys-aligned Plotly theme."""
151
+ import plotly.io as pio
152
+ import plotly.graph_objects as go
153
+
154
+ base = go.layout.Template(pio.templates["plotly_white"])
155
+ base.layout.update(
156
+ font=dict(
157
+ family="Inter, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
158
+ size=13,
159
+ color="#1A1A1A",
160
+ ),
161
+ paper_bgcolor="#FFFFFF",
162
+ plot_bgcolor="#FFFFFF",
163
+ colorway=["#5F259F", "#7A3DB5", "#AE8BD8", "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"],
164
+ hoverlabel=dict(bgcolor="#FFFFFF", bordercolor="#5F259F", font=dict(color="#1A1A1A")),
165
+ legend=dict(orientation="h", x=1, xanchor="right", y=1.02, yanchor="bottom", title_text=""),
166
+ margin=dict(l=40, r=20, t=40, b=40),
167
+ )
168
+ base.layout.xaxis.update(
169
+ showgrid=True,
170
+ gridcolor="rgba(95,37,159,0.1)",
171
+ zeroline=False,
172
+ linecolor="rgba(0,0,0,.2)",
173
+ ticks="outside",
174
+ tickformat=".2f",
175
+ )
176
+ base.layout.yaxis.update(
177
+ showgrid=True,
178
+ gridcolor="rgba(95,37,159,0.1)",
179
+ zeroline=True,
180
+ zerolinecolor="rgba(0,0,0,.25)",
181
+ linecolor="rgba(0,0,0,.2)",
182
+ ticks="outside",
183
+ tickformat=".3g",
184
+ )
185
+ pio.templates["syn_white"] = base
186
+ pio.templates.default = "syn_white"
187
+
em_trame.py DELETED
The diff for this file is too large to render. See raw diff
 
pages/__init__.py DELETED
@@ -1,4 +0,0 @@
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/em_page.py DELETED
@@ -1,43 +0,0 @@
1
- """
2
- EM Page - Embedded Mode Wrapper
3
-
4
- This module provides the EM experience for the unified app.
5
- It uses the embedded module instead of spawning a subprocess.
6
- """
7
- from trame_vuetify.widgets import vuetify3
8
- from trame.widgets import html as trame_html
9
- import os
10
- import sys
11
-
12
- # Add parent directory to path for imports
13
- sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
14
-
15
- try:
16
- import em_embedded
17
- _EM_AVAILABLE = True
18
- except ImportError as e:
19
- _EM_AVAILABLE = False
20
- _EM_ERROR = str(e)
21
-
22
-
23
- def build(server):
24
- """Build the EM UI using the embedded module."""
25
- if not _EM_AVAILABLE:
26
- with vuetify3.VContainer(fluid=True, classes="pa-4"):
27
- trame_html.Div(
28
- f"EM module failed to load: {_EM_ERROR}",
29
- style="color: #b00020; padding: 12px;"
30
- )
31
- return
32
-
33
- # Set the server and initialize state
34
- em_embedded.set_server(server)
35
- em_embedded.init_state()
36
-
37
- # Build the UI
38
- em_embedded.build_ui()
39
-
40
-
41
- def stop():
42
- """Stop any running EM processes (no-op in embedded mode)."""
43
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/em_page_subprocess.py DELETED
@@ -1,70 +0,0 @@
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
- _EM_HOST = os.environ.get("EM_HOST", "127.0.0.1")
11
- _EM_IFRAME_SRC = os.environ.get("EM_IFRAME_SRC", "").strip()
12
-
13
-
14
- def _kill_em_process():
15
- """Ensure any running EM process is terminated."""
16
- global _em_proc
17
- if _em_proc and _em_proc.poll() is None:
18
- try:
19
- _em_proc.terminate()
20
- _em_proc.wait(timeout=2)
21
- except Exception:
22
- try:
23
- _em_proc.kill()
24
- except Exception:
25
- pass
26
- _em_proc = None
27
-
28
-
29
- def _ensure_em_process_started():
30
- global _em_proc
31
- # Check if process is still running
32
- if (_em_proc and _em_proc.poll() is None):
33
- return
34
-
35
- # Kill any stale process first
36
- _kill_em_process()
37
-
38
- base_dir = os.path.dirname(os.path.dirname(__file__))
39
- em_path = os.path.join(base_dir, "em_trame.py")
40
- env = os.environ.copy()
41
- # Prevent hosted platforms from forcing the subprocess to bind to $PORT
42
- env.pop("PORT", None)
43
- env.pop("HF_PORT", None)
44
- # Port used by iframe
45
- env.setdefault("EM_APP_PORT", env.get("PORT_EM", "8701"))
46
- env.setdefault("EM_HOST", _EM_HOST)
47
- # Start em_trame.py in a separate process
48
- python_exe = sys.executable or "python"
49
- _em_proc = subprocess.Popen([python_exe, em_path], cwd=base_dir, env=env)
50
-
51
-
52
- # Register cleanup on exit
53
- atexit.register(_kill_em_process)
54
-
55
-
56
- def build(server):
57
- """Render the EM app via iframe and ensure its process is running."""
58
- _ensure_em_process_started()
59
- port = os.environ.get("EM_APP_PORT", os.environ.get("PORT_EM", "8701"))
60
- host = os.environ.get("EM_HOST", _EM_HOST)
61
- iframe_src = _EM_IFRAME_SRC or f"http://{host}:{port}/"
62
- with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
63
- trame_html.Iframe(
64
- src=("em_iframe_src", iframe_src),
65
- style="border:0; width:100%; height: calc(100vh - 64px);",
66
- )
67
- trame_html.Div(
68
- "If the EM page is blank, wait a few seconds for the subprocess to start.",
69
- style="color: rgba(0,0,0,.6); padding: 6px;",
70
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/qlbm_page.py DELETED
@@ -1,43 +0,0 @@
1
- """
2
- QLBM Page - Embedded Mode Wrapper
3
-
4
- This module provides the QLBM experience for the unified app.
5
- It uses the embedded module instead of spawning a subprocess.
6
- """
7
- from trame_vuetify.widgets import vuetify3
8
- from trame.widgets import html as trame_html
9
- import os
10
- import sys
11
-
12
- # Add parent directory to path for imports
13
- sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
14
-
15
- try:
16
- import qlbm_embedded
17
- _QLBM_AVAILABLE = True
18
- except ImportError as e:
19
- _QLBM_AVAILABLE = False
20
- _QLBM_ERROR = str(e)
21
-
22
-
23
- def build(server):
24
- """Build the QLBM UI using the embedded module."""
25
- if not _QLBM_AVAILABLE:
26
- with vuetify3.VContainer(fluid=True, classes="pa-4"):
27
- trame_html.Div(
28
- f"QLBM module failed to load: {_QLBM_ERROR}",
29
- style="color: #b00020; padding: 12px;"
30
- )
31
- return
32
-
33
- # Set the server and initialize state
34
- qlbm_embedded.set_server(server)
35
- qlbm_embedded.init_state()
36
-
37
- # Build the UI
38
- qlbm_embedded.build_ui()
39
-
40
-
41
- def stop():
42
- """Stop any running QLBM processes (no-op in embedded mode)."""
43
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/qlbm_page_subprocess.py DELETED
@@ -1,128 +0,0 @@
1
- """Embedded QLBM fluids page wrapper.
2
- Starts the standalone qlbm.py server in a background subprocess so the main
3
- multi-page app only needs `python app.py`.
4
-
5
- Environment variables:
6
- QLBM_APP_PORT / PORT_QLBM -> port (default 8702)
7
- QLBM_HOST -> host interface (default 127.0.0.1)
8
- """
9
- from __future__ import annotations
10
- import os, sys, subprocess, atexit
11
- from trame_vuetify.widgets import vuetify3
12
- from trame.widgets import html as trame_html
13
- import shutil
14
-
15
- _gpu_status = {
16
- "checked": False,
17
- "available": False,
18
- "reason": ""
19
- }
20
-
21
-
22
- def _detect_gpu_availability(force_recheck=False):
23
- """Best-effort CUDA GPU detection for hosted environments."""
24
- global _gpu_status
25
- if _gpu_status["checked"] and not force_recheck:
26
- return _gpu_status["available"], _gpu_status["reason"]
27
-
28
- allow_missing = os.environ.get("QLBM_IGNORE_GPU_CHECK", "0") == "1"
29
- if allow_missing:
30
- _gpu_status.update({"checked": True, "available": True, "reason": ""})
31
- return True, ""
32
-
33
- def _has_devices(val: str | None) -> bool:
34
- if not val:
35
- return False
36
- lowered = val.strip().lower()
37
- return lowered not in ("", "none", "nodevfiles", "-1")
38
-
39
- nvidia_devices = os.environ.get("NVIDIA_VISIBLE_DEVICES")
40
- cuda_devices = os.environ.get("CUDA_VISIBLE_DEVICES")
41
- saw_env_nvidia = _has_devices(nvidia_devices)
42
- saw_env_cuda = _has_devices(cuda_devices)
43
-
44
- # Heuristics: presence of device files or nvidia-smi
45
- has_proc_entry = os.path.exists("/proc/driver/nvidia/version")
46
- has_nvidia_smi = shutil.which("nvidia-smi") is not None
47
- has_gpu = (saw_env_nvidia or saw_env_cuda or has_proc_entry or has_nvidia_smi)
48
-
49
- if has_gpu:
50
- _gpu_status.update({"checked": True, "available": True, "reason": ""})
51
- return True, ""
52
-
53
- reason = "No NVIDIA GPU detected (CUDA_VISIBLE_DEVICES/NVIDIA_VISIBLE_DEVICES unset and no nvidia drivers present)."
54
- _gpu_status.update({"checked": True, "available": False, "reason": reason})
55
- return False, reason
56
-
57
- _qlbm_proc = None
58
- _QLBM_HOST = os.environ.get("QLBM_HOST", "127.0.0.1")
59
- _QLBM_IFRAME_SRC = os.environ.get("QLBM_IFRAME_SRC", "").strip()
60
-
61
-
62
- def _kill_qlbm_process():
63
- global _qlbm_proc
64
- if _qlbm_proc and _qlbm_proc.poll() is None:
65
- try:
66
- _qlbm_proc.terminate()
67
- _qlbm_proc.wait(timeout=2)
68
- except Exception:
69
- try:
70
- _qlbm_proc.kill()
71
- except Exception:
72
- pass
73
- _qlbm_proc = None
74
-
75
-
76
- def _ensure_qlbm_process_started():
77
- global _qlbm_proc
78
- if _qlbm_proc and _qlbm_proc.poll() is None:
79
- return
80
- _kill_qlbm_process()
81
- base_dir = os.path.dirname(os.path.dirname(__file__))
82
- qlbm_path = os.path.join(base_dir, "qlbm.py")
83
- env = os.environ.copy()
84
- env.pop("PORT", None)
85
- env.pop("HF_PORT", None)
86
- env.setdefault("QLBM_APP_PORT", env.get("PORT_QLBM", "8702"))
87
- env.setdefault("QLBM_HOST", _QLBM_HOST)
88
- py = sys.executable or "python"
89
- _qlbm_proc = subprocess.Popen([py, qlbm_path], cwd=base_dir, env=env)
90
-
91
-
92
- atexit.register(_kill_qlbm_process)
93
-
94
-
95
- def build(server): # signature matches app.py expectation
96
- if os.environ.get("DISABLE_SUBAPPS", "").strip() == "1":
97
- with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
98
- trame_html.Div(
99
- "This tab is disabled in single-port environments. Please run locally to enable the QLBM view.",
100
- style="padding:12px;color:#555;",
101
- )
102
- return
103
- gpu_ok, gpu_reason = _detect_gpu_availability()
104
- if not gpu_ok:
105
- with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
106
- trame_html.Div(
107
- "QLBM requires an NVIDIA CUDA-capable GPU, which is not available in this environment.",
108
- style="padding:12px;color:#b00020;font-weight:600;",
109
- )
110
- trame_html.Div(
111
- f"Details: {gpu_reason} Run the FLUIDS tab locally with a GPU or set QLBM_IGNORE_GPU_CHECK=1 to bypass this guard.",
112
- style="padding:8px 12px;color:#555;",
113
- )
114
- return
115
- _ensure_qlbm_process_started()
116
- port = os.environ.get("QLBM_APP_PORT", os.environ.get("PORT_QLBM", "8702"))
117
- host = os.environ.get("QLBM_HOST", _QLBM_HOST)
118
- iframe_src = _QLBM_IFRAME_SRC or f"http://{host}:{port}/"
119
- with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
120
- trame_html.Iframe(
121
- src=("qlbm_iframe_src", iframe_src),
122
- style="border:0;width:100%;height:100%;min-height:0;",
123
- )
124
- trame_html.Div(
125
- "If the QLBM view is blank, wait a few seconds for the subprocess to start.",
126
- style="color:rgba(0,0,0,.6);padding:6px;",
127
- )
128
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
qlbm.py DELETED
@@ -1,1501 +0,0 @@
1
- import os
2
- os.environ["OMP_NUM_THREADS"] = "1"
3
-
4
- import numpy as np
5
- import math
6
- import time
7
- import tempfile
8
- from datetime import datetime
9
- from pathlib import Path
10
- import plotly.graph_objects as go
11
- import pyvista as pv
12
- from trame.app import get_server
13
- from trame_vuetify.ui.vuetify3 import SinglePageLayout
14
- from trame_vuetify.widgets import vuetify3
15
- from trame.widgets import html # <--- IMPORT ADDED HERE
16
- from trame_plotly.widgets import plotly as plotly_widgets
17
- from pyvista.trame.ui import plotter_ui
18
-
19
-
20
- def _env_flag(name: str) -> bool:
21
- value = os.environ.get(name)
22
- return str(value).lower() in {"1", "true", "yes", "on"} if value is not None else False
23
-
24
-
25
- def _should_disable_quantum_backend():
26
- if _env_flag("FORCE_ENABLE_CUDAQ"):
27
- return False, ""
28
-
29
- reasons = []
30
- if _env_flag("DISABLE_CUDAQ"):
31
- reasons.append("disabled via DISABLE_CUDAQ=1")
32
-
33
- hf_space = os.environ.get("SPACE_ID") or os.environ.get("HF_SPACE_ID")
34
- # if hf_space:
35
- # reasons.append("running inside Hugging Face Spaces (CPU runtime)")
36
-
37
- cuda_visible = os.environ.get("CUDA_VISIBLE_DEVICES")
38
- if cuda_visible is not None and cuda_visible.strip() in {"", "-1"}:
39
- reasons.append("no CUDA device exposed (CUDA_VISIBLE_DEVICES is empty)")
40
-
41
- nvidia_visible = os.environ.get("NVIDIA_VISIBLE_DEVICES")
42
- if nvidia_visible is not None and nvidia_visible.strip() in {"", "void"}:
43
- reasons.append("no NVIDIA device exposed (NVIDIA_VISIBLE_DEVICES is empty)")
44
-
45
- if reasons:
46
- msg = "; ".join(reasons)
47
- return True, f"Quantum backend disabled because {msg}. Set FORCE_ENABLE_CUDAQ=1 to override."
48
-
49
- return False, ""
50
-
51
-
52
- simulate_qlbm_3D_and_animate = None
53
- _SIMULATION_BACKEND_DISABLED, _SIMULATION_DISABLED_REASON = _should_disable_quantum_backend()
54
- _CPU_DEMO_AVAILABLE = True
55
- _CPU_DEMO_MAX_GRID = 48
56
-
57
- if not _SIMULATION_BACKEND_DISABLED:
58
- try:
59
- from fluid3d_pyvista import simulate_qlbm_3D_and_animate # type: ignore
60
- except ImportError:
61
- try:
62
- from quantum.fluid3d_pyvista import simulate_qlbm_3D_and_animate # type: ignore
63
- except ImportError as exc:
64
- _SIMULATION_DISABLED_REASON = "Simulation module not found."
65
- simulate_qlbm_3D_and_animate = None
66
- print(f"Warning: {exc}")
67
- except Exception as exc: # pragma: no cover - defensive guard for HF runtime
68
- _SIMULATION_DISABLED_REASON = f"Failed to load CUDA-Q backend: {exc}"
69
- simulate_qlbm_3D_and_animate = None
70
- else:
71
- _SIMULATION_DISABLED_REASON = _SIMULATION_DISABLED_REASON or "Quantum backend disabled by environment settings."
72
-
73
- _SIMULATION_BACKEND_READY = simulate_qlbm_3D_and_animate is not None
74
- if not _SIMULATION_BACKEND_READY and _SIMULATION_DISABLED_REASON:
75
- print(f"Warning: {_SIMULATION_DISABLED_REASON}")
76
-
77
- if _SIMULATION_BACKEND_READY:
78
- _SIMULATION_BACKEND_NOTE = ""
79
- _SIMULATION_MODE_LABEL = "Quantum CUDA-Q backend"
80
- else:
81
- if _CPU_DEMO_AVAILABLE:
82
- reason = _SIMULATION_DISABLED_REASON or "Quantum backend is unavailable in this environment."
83
- _SIMULATION_BACKEND_NOTE = (
84
- f"CPU demo mode active ({reason}). Results are approximate. "
85
- "Set FORCE_ENABLE_CUDAQ=1 on a GPU host to use the quantum backend."
86
- )
87
- _SIMULATION_MODE_LABEL = "CPU demo backend"
88
- else:
89
- _SIMULATION_BACKEND_NOTE = _SIMULATION_DISABLED_REASON or "Simulation backend unavailable."
90
- _SIMULATION_MODE_LABEL = "Unavailable"
91
-
92
- _SIMULATION_CAN_RUN = _SIMULATION_BACKEND_READY or _CPU_DEMO_AVAILABLE
93
-
94
- # --- Server Setup ---
95
- server = get_server()
96
- state, ctrl = server.state, server.controller
97
-
98
- def log_to_console(message):
99
- timestamp = datetime.now().strftime("%H:%M:%S")
100
- new_line = f"[{timestamp}] {message}\n"
101
- state.console_output = (state.console_output or "") + new_line
102
-
103
- pv.OFF_SCREEN = True
104
-
105
- # --- State Initialization ---
106
- GRID_SIZES = ["8", "16", "32", "64", "128"]
107
-
108
- _WORKFLOW_BASE_STYLE = "font-size: 0.8rem; border: 1px solid transparent; transition: box-shadow 0.2s ease;"
109
-
110
- state.update({
111
- "console_output": "Console initialized.\n",
112
- "status_visible": True,
113
- "status_message": "Ready",
114
- "status_type": "info",
115
- "simulation_progress": 0,
116
- "show_progress": False,
117
- "dist_modes": ["Sinusoidal", "Gaussian"],
118
- "dist_type": None,
119
- "nx_slider_index": 2, # Default to 32
120
- "nx": 32,
121
- "show_edges": False,
122
- "custom_dist_params": False,
123
- "sine_k_x": 1.0,
124
- "sine_k_y": 1.0,
125
- "sine_k_z": 1.0,
126
- "gauss_cx": 16,
127
- "gauss_cy": 16,
128
- "gauss_cz": 16,
129
- "gauss_sigma": 6.0,
130
- # Added from qlbm.py
131
- "problems_selection": None,
132
- "geometry_selection": None,
133
- "domain_L": 1.0,
134
- "domain_W": 1.0,
135
- "domain_H": 1.0,
136
- # Added for new sections
137
- "boundary_condition": "Periodic",
138
- "advecting_field": None,
139
- "show_advect_params": False,
140
- "vx_expr": "0.2",
141
- "vy_expr": "-0.15",
142
- "vz_expr": "0.3",
143
- "grid_index": 2,
144
- "grid_size": 32,
145
- "time_steps": 100,
146
- "backend_type": None,
147
- "selected_simulator": "IBM Qiskit simulator",
148
- "selected_qpu": "IBM QPU",
149
- "is_running": False,
150
- "run_error": "",
151
- "qubit_grid_info": "Grid Size: 32 × 32 × 32",
152
- "qubit_warning": "",
153
- # Animation state
154
- "simulation_has_run": False,
155
- "time_val": 0,
156
- "max_time_step": 0,
157
- "time_slider_labels": [], # Formatted time labels for slider thumb
158
- "simulation_backend_ready": _SIMULATION_CAN_RUN,
159
- "simulation_backend_note": _SIMULATION_BACKEND_NOTE,
160
- "simulation_backend_mode": _SIMULATION_MODE_LABEL,
161
- # Workflow guidance styles
162
- "workflow_step": 0,
163
- "overview_card_style": _WORKFLOW_BASE_STYLE,
164
- "geometry_card_style": _WORKFLOW_BASE_STYLE,
165
- "distribution_card_style": _WORKFLOW_BASE_STYLE,
166
- "advect_card_style": _WORKFLOW_BASE_STYLE,
167
- "meshing_card_style": _WORKFLOW_BASE_STYLE,
168
- "backend_card_style": _WORKFLOW_BASE_STYLE,
169
- })
170
-
171
- _WORKFLOW_CARD_KEYS = [
172
- "overview_card_style",
173
- "distribution_card_style",
174
- "advect_card_style",
175
- "backend_card_style",
176
- ]
177
-
178
- _PROBLEM_GEOMETRY_MAP = {
179
- "Scalar advection-diffusion in a box": "Cube",
180
- "Laminar flow & heat transfer for a heated body in water.": "Rectangular domain with a heated box (3D)",
181
- }
182
-
183
-
184
- def _workflow_highlight_style(active: bool) -> str:
185
- accent = "border: 2px solid #5F259F; box-shadow: 0 0 12px rgba(95,37,159,0.45); background-color: rgba(95,37,159,0.02);"
186
- return f"{_WORKFLOW_BASE_STYLE} {accent}" if active else _WORKFLOW_BASE_STYLE
187
-
188
-
189
- def _determine_workflow_step() -> int:
190
- if not state.problems_selection:
191
- return 0
192
- if not state.dist_type:
193
- return 1
194
- if not state.advecting_field:
195
- return 2
196
- if not state.backend_type:
197
- return 3
198
- return len(_WORKFLOW_CARD_KEYS)
199
-
200
-
201
- def _apply_workflow_highlights(step_index: int):
202
- state.workflow_step = step_index
203
- for idx, key in enumerate(_WORKFLOW_CARD_KEYS):
204
- highlight = idx == step_index if step_index < len(_WORKFLOW_CARD_KEYS) else False
205
- setattr(state, key, _workflow_highlight_style(highlight))
206
-
207
-
208
- _apply_workflow_highlights(0)
209
-
210
- # --- Plotter Setup ---
211
- plotter = pv.Plotter()
212
- current_grid = None
213
- current_grid_size = 0
214
- _picking_enabled = False
215
- _INFO_TEXT_ACTOR = "pick_info_text"
216
-
217
- # Global simulation data storage
218
- simulation_data_frames = []
219
- simulation_times = []
220
- current_grid_object = None
221
-
222
- def _set_pick_text(message: str):
223
- try:
224
- plotter.remove_actor(_INFO_TEXT_ACTOR)
225
- except Exception:
226
- pass
227
- plotter.add_text(message, name=_INFO_TEXT_ACTOR, position="lower_left", font_size=12, color="black")
228
-
229
- def _ensure_point_picking(callback):
230
- global _picking_enabled
231
- if not _picking_enabled:
232
- plotter.enable_point_picking(callback=callback, show_message=False, point_size=10, color="red")
233
- _picking_enabled = True
234
-
235
- def get_initial_distribution_figure(distribution_type: str, grid_size: int = 32, show_edges: bool = False):
236
- """
237
- Generates a 3D Plotly isosurface figure for a given distribution type.
238
- """
239
- N = grid_size
240
-
241
- # --- 1. Define the mathematical functions ---
242
- if distribution_type == "Sinusoidal":
243
- kx = max(1.0, round(state.sine_k_x))
244
- ky = max(1.0, round(state.sine_k_y))
245
- kz = max(1.0, round(state.sine_k_z))
246
-
247
- selected_func = lambda x, y, z: \
248
- np.sin(x * 2 * np.pi * kx / N) * \
249
- np.sin(y * 2 * np.pi * ky / N) * \
250
- np.sin(z * 2 * np.pi * kz / N) + 1
251
- title = f"Sinusoidal Distribution (N={N})"
252
-
253
- elif distribution_type == "Gaussian":
254
- cx, cy, cz = state.gauss_cx, state.gauss_cy, state.gauss_cz
255
- sigma = state.gauss_sigma if state.gauss_sigma > 0 else 0.1
256
-
257
- selected_func = lambda x, y, z: \
258
- np.exp(-((x - cx)**2 / (2 * sigma**2) +
259
- (y - cy)**2 / (2 * sigma**2) +
260
- (z - cz)**2 / (2 * sigma**2))) * 1.8 + 0.2
261
- title = f"Gaussian Distribution (N={N})"
262
-
263
- else:
264
- return go.Figure()
265
-
266
- # --- 2. Create the 3D grid ---
267
- x_indices = np.linspace(0, 1, N)
268
- y_indices = np.linspace(0, 1, N)
269
- z_indices = np.linspace(0, 1, N)
270
-
271
- X, Y, Z = np.meshgrid(x_indices, y_indices, z_indices, indexing='ij')
272
-
273
- # --- 3. Calculate the distribution values at every point ---
274
- xi = np.arange(0, N)
275
- yi = np.arange(0, N)
276
- zi = np.arange(0, N)
277
- Xi, Yi, Zi = np.meshgrid(xi, yi, zi, indexing='ij')
278
- values = selected_func(Xi, Yi, Zi)
279
-
280
- # --- 4. Create the Plotly visualization ---
281
-
282
- # Default settings
283
- isomin = np.min(values)
284
- isomax = np.max(values)
285
- surface_count = 5
286
-
287
- if distribution_type == "Sinusoidal":
288
- # Avoid drawing the boundary plane at value=1.0
289
- # Range is [0, 2]. 1.0 is the background.
290
- # We use count=4 and slightly cropped range to avoid 1.0
291
- isomin = 0.1
292
- isomax = 1.9
293
- surface_count = 4
294
-
295
- data = [go.Isosurface(
296
- x=X.flatten(),
297
- y=Y.flatten(),
298
- z=Z.flatten(),
299
- value=values.flatten(),
300
- isomin=isomin,
301
- isomax=isomax,
302
- surface_count=surface_count,
303
- colorscale='Blues',
304
- opacity=0.35,
305
- caps=dict(x_show=False, y_show=False, z_show=False)
306
- )]
307
-
308
- if show_edges:
309
- # Create grid lines using np.nan to separate segments
310
- # 1. Lines along X (vary x, fixed y, z)
311
- Y_yz, Z_yz = np.meshgrid(y_indices, z_indices, indexing='ij')
312
- Y_flat, Z_flat = Y_yz.flatten(), Z_yz.flatten()
313
- num_lines = len(Y_flat)
314
-
315
- xe = np.full(num_lines * 3, np.nan)
316
- xe[0::3], xe[1::3] = 0, 1
317
- ye = np.full(num_lines * 3, np.nan)
318
- ye[0::3] = ye[1::3] = Y_flat
319
- ze = np.full(num_lines * 3, np.nan)
320
- ze[0::3] = ze[1::3] = Z_flat
321
-
322
- # 2. Lines along Y (vary y, fixed x, z)
323
- X_xz, Z_xz = np.meshgrid(x_indices, z_indices, indexing='ij')
324
- X_flat, Z_flat = X_xz.flatten(), Z_xz.flatten()
325
- num_lines = len(X_flat)
326
-
327
- xe_y = np.full(num_lines * 3, np.nan)
328
- xe_y[0::3] = xe_y[1::3] = X_flat
329
- ye_y = np.full(num_lines * 3, np.nan)
330
- ye_y[0::3], ye_y[1::3] = 0, 1
331
- ze_y = np.full(num_lines * 3, np.nan)
332
- ze_y[0::3] = ze_y[1::3] = Z_flat
333
-
334
- # 3. Lines along Z (vary z, fixed x, y)
335
- X_xy, Y_xy = np.meshgrid(x_indices, y_indices, indexing='ij')
336
- X_flat, Y_flat = X_xy.flatten(), Y_xy.flatten()
337
- num_lines = len(X_flat)
338
-
339
- xe_z = np.full(num_lines * 3, np.nan)
340
- xe_z[0::3] = xe_z[1::3] = X_flat
341
- ye_z = np.full(num_lines * 3, np.nan)
342
- ye_z[0::3] = ye_z[1::3] = Y_flat
343
- ze_z = np.full(num_lines * 3, np.nan)
344
- ze_z[0::3], ze_z[1::3] = 0, 1
345
-
346
- # Combine
347
- x_all = np.concatenate([xe, xe_y, xe_z])
348
- y_all = np.concatenate([ye, ye_y, ye_z])
349
- z_all = np.concatenate([ze, ze_y, ze_z])
350
-
351
- data.append(go.Scatter3d(
352
- x=x_all, y=y_all, z=z_all,
353
- mode='lines',
354
- line=dict(color='black', width=1), # Black lines
355
- opacity=0.22, # Reduced opacity
356
- name='Grid Edges'
357
- ))
358
-
359
- fig = go.Figure(data=data)
360
-
361
- fig.update_layout(
362
- title=title,
363
- scene=dict(
364
- xaxis=dict(backgroundcolor="white", showbackground=True, gridcolor="lightgrey", zerolinecolor="lightgrey", title='X'),
365
- yaxis=dict(backgroundcolor="white", showbackground=True, gridcolor="lightgrey", zerolinecolor="lightgrey", title='Y'),
366
- zaxis=dict(backgroundcolor="white", showbackground=True, gridcolor="lightgrey", zerolinecolor="lightgrey", title='Z'),
367
- ),
368
- margin=dict(l=0, r=0, b=0, t=40),
369
- width=800,
370
- height=700
371
- )
372
- return fig
373
-
374
- def update_view():
375
- global current_grid, current_grid_size
376
-
377
- # If simulation has run, we don't update the preview
378
- if state.simulation_has_run:
379
- return
380
-
381
- try:
382
- N = int(state.nx)
383
- distribution_type = state.dist_type
384
-
385
- # Update Plotly Preview
386
- show_edges = state.show_edges
387
- fig = get_initial_distribution_figure(distribution_type, N, show_edges)
388
- if hasattr(ctrl, "preview_update"):
389
- ctrl.preview_update(fig)
390
-
391
- except Exception as e:
392
- print(f"Error: {e}")
393
-
394
- def on_pick_point(point, *_) -> None:
395
- if point is None or current_grid_object is None: return
396
- closest_id = current_grid_object.find_closest_point(point)
397
- if closest_id == -1: return
398
- values = current_grid_object.point_data.get('scalars')
399
- if values is None: return
400
-
401
- coords = current_grid_object.points[closest_id]
402
- val = float(values[closest_id])
403
-
404
- x, y, z = coords
405
- _set_pick_text(f"Position: ({x:.3f}, {y:.3f}, {z:.3f})\nValue: {val:.4g}")
406
- ctrl.view_update()
407
-
408
- # --- Helpers for New Sections ---
409
-
410
- def update_qubit_3D_info(grid_size: int):
411
- """Generate qubit requirement plot and info strings."""
412
- try:
413
- num_reg_qubits = int(math.log2(grid_size)) if grid_size > 0 else 3
414
- x = np.array([16, 32, 64, 128, 256])
415
- y = np.log2(x).astype(int)
416
- fig = go.Figure()
417
- fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='Qubits/Direction', line=dict(color='#7A3DB5', width=3)))
418
- fig.add_trace(go.Scatter(x=[grid_size], y=[num_reg_qubits], mode='markers',
419
- marker=dict(size=12, color='red'), name='Current Selection'))
420
- fig.update_layout(
421
- xaxis_title="Grid Size (Points/Direction)",
422
- yaxis_title="Qubits/Direction",
423
- width=616,
424
- height=320,
425
- margin=dict(l=40, r=20, t=20, b=40)
426
- )
427
- grid_display = f"Grid Size: {grid_size} × {grid_size} × {grid_size}"
428
- warning = "⚠️ Warning: Grid sizes > 64 may exceed simulator/memory limits!" if grid_size > 64 else ""
429
- return fig, grid_display, warning
430
- except Exception:
431
- return go.Figure(), "Grid Size: N/A", ""
432
-
433
- def set_velocity_preset(preset_name):
434
- """Map velocity preset buttons to expression triplets."""
435
- mapping = {
436
- "Uniform": ("0.2", "-0.15", "0.3"),
437
- "Swirl": ("0.3*sin(-2*pi*z)", "0.2", "0.3*sin(2*pi*x)"),
438
- "Shear": ("abs(z-0.5)*1.2-0.3", "0", "0"),
439
- "TGV": ("0.15*cos(2*pi*x)*sin(2*pi*y)*sin(2*pi*z)", "-0.3*sin(2*pi*x)*cos(2*pi*y)*sin(2*pi*z)", "0.15*sin(2*pi*x)*sin(2*pi*y)*cos(2*pi*z)"),
440
- }
441
- vx, vy, vz = mapping.get(preset_name, mapping["Uniform"])
442
- state.advecting_field = preset_name
443
- state.vx_expr = vx
444
- state.vy_expr = vy
445
- state.vz_expr = vz
446
-
447
- def make_velocity_func(expr):
448
- """Convert a string expression into a function of (x, y, z)."""
449
- def func(x, y, z):
450
- # Safe-ish eval context with numpy functions
451
- context = {
452
- "x": x, "y": y, "z": z,
453
- "sin": np.sin, "cos": np.cos, "tan": np.tan,
454
- "pi": np.pi, "abs": np.abs, "exp": np.exp, "sqrt": np.sqrt
455
- }
456
- try:
457
- # Evaluate expression
458
- return eval(str(expr), {"__builtins__": {}}, context)
459
- except Exception as e:
460
- print(f"Error evaluating velocity expression '{expr}': {e}")
461
- # Return a safe default (scalar or array depending on input)
462
- return np.zeros_like(x) if isinstance(x, np.ndarray) else 0.0
463
- return func
464
-
465
-
466
- def _safe_velocity_sample(func) -> float:
467
- try:
468
- val = func(0.5, 0.5, 0.5)
469
- if isinstance(val, np.ndarray):
470
- val = float(np.mean(val))
471
- return float(val)
472
- except Exception:
473
- return 0.0
474
-
475
-
476
- def _cpu_distribution_field(distribution_type: str, Xi, Yi, Zi, grid_size: int, drift, phase_fraction: float):
477
- if distribution_type == "Sinusoidal":
478
- kx = max(1.0, round(float(state.sine_k_x))) if hasattr(state, "sine_k_x") else 1.0
479
- ky = max(1.0, round(float(state.sine_k_y))) if hasattr(state, "sine_k_y") else 1.0
480
- kz = max(1.0, round(float(state.sine_k_z))) if hasattr(state, "sine_k_z") else 1.0
481
- x_term = np.sin((np.mod(Xi + drift[0], grid_size)) * 2 * np.pi * kx / grid_size)
482
- y_term = np.sin((np.mod(Yi + drift[1], grid_size)) * 2 * np.pi * ky / grid_size)
483
- z_term = np.sin((np.mod(Zi + drift[2], grid_size)) * 2 * np.pi * kz / grid_size)
484
- field = x_term * y_term * z_term + 1.0
485
- else:
486
- # Gaussian (default fallback)
487
- nx_val = max(1.0, float(state.nx)) if hasattr(state, "nx") else float(grid_size)
488
- cx = float(state.gauss_cx) if hasattr(state, "gauss_cx") else nx_val / 2
489
- cy = float(state.gauss_cy) if hasattr(state, "gauss_cy") else nx_val / 2
490
- cz = float(state.gauss_cz) if hasattr(state, "gauss_cz") else nx_val / 2
491
- sigma = float(state.gauss_sigma) if hasattr(state, "gauss_sigma") else nx_val / 6
492
- scale = (grid_size - 1) / nx_val if nx_val else 1.0
493
- cx = cx * scale + drift[0]
494
- cy = cy * scale + drift[1]
495
- cz = cz * scale + drift[2]
496
- sigma = max(1.0, sigma * scale)
497
- field = np.exp(-(((Xi - cx) ** 2 + (Yi - cy) ** 2 + (Zi - cz) ** 2) / (2 * sigma ** 2))) * 1.8 + 0.2
498
-
499
- modulation = 0.15 * np.sin(2 * np.pi * phase_fraction + (Xi + Yi + Zi) * np.pi / max(1, grid_size))
500
- return field + modulation
501
-
502
-
503
- def _run_cpu_demo_simulation(grid_size: int, T: int, distribution_type: str, vx_func, vy_func, vz_func, progress_callback=None):
504
- grid_size = int(max(8, min(grid_size, _CPU_DEMO_MAX_GRID)))
505
- idx_coords = np.linspace(0, grid_size - 1, grid_size, dtype=np.float32)
506
- Xi, Yi, Zi = np.meshgrid(idx_coords, idx_coords, idx_coords, indexing='ij')
507
- geom_coords = np.linspace(0, 1, grid_size, dtype=np.float32)
508
- Xg, Yg, Zg = np.meshgrid(geom_coords, geom_coords, geom_coords, indexing='ij')
509
-
510
- if T <= 0:
511
- target = 1.0
512
- else:
513
- target = float(T)
514
- num_frames = min(30, max(2, int(min(target, 20)) + 1))
515
- timeline = list(np.linspace(0.0, target, num_frames))
516
- if len(timeline) < 2:
517
- timeline.append(target)
518
-
519
- vx = _safe_velocity_sample(vx_func)
520
- vy = _safe_velocity_sample(vy_func)
521
- vz = _safe_velocity_sample(vz_func)
522
- drift_scale = 0.25 * grid_size
523
-
524
- frames = []
525
- for idx, t_val in enumerate(timeline):
526
- phase_fraction = idx / (len(timeline) - 1) if len(timeline) > 1 else 0.0
527
- drift = (
528
- vx * phase_fraction * drift_scale,
529
- vy * phase_fraction * drift_scale,
530
- vz * phase_fraction * drift_scale,
531
- )
532
- field = _cpu_distribution_field(distribution_type, Xi, Yi, Zi, grid_size, drift, phase_fraction)
533
- frames.append(field.astype(np.float32))
534
-
535
- if progress_callback:
536
- percent = int(((idx + 1) / len(timeline)) * 100)
537
- progress_callback(percent)
538
-
539
- grid = pv.StructuredGrid()
540
- grid.points = np.column_stack((Xg.ravel(), Yg.ravel(), Zg.ravel()))
541
- grid.dimensions = [grid_size, grid_size, grid_size]
542
- grid["scalars"] = frames[0].ravel()
543
-
544
- times = [float(t) for t in timeline]
545
- return frames, times, grid
546
-
547
- def get_geometry_figure():
548
- """Generates a 3D Plotly figure for the selected geometry."""
549
- geom = state.geometry_selection
550
-
551
- if geom == "Cube":
552
- # Draw a unit cube
553
- fig = _create_box_figure(1, 1, 1, "Cube")
554
-
555
- elif geom == "Rectangular domain with a heated box (3D)":
556
- try:
557
- L = float(state.domain_L)
558
- W = float(state.domain_W)
559
- H = float(state.domain_H)
560
- except:
561
- L, W, H = 1.0, 1.0, 1.0
562
-
563
- # Normalize so max dimension is 1
564
- max_dim = max(L, W, H)
565
- if max_dim > 0:
566
- L /= max_dim
567
- W /= max_dim
568
- H /= max_dim
569
-
570
- fig = _create_box_figure(L, W, H, "Rectangular Domain")
571
-
572
- else:
573
- # Empty figure
574
- fig = go.Figure()
575
- fig.update_layout(
576
- scene=dict(xaxis=dict(visible=False), yaxis=dict(visible=False), zaxis=dict(visible=False)),
577
- margin=dict(l=0, r=0, b=0, t=0),
578
- )
579
- return fig
580
-
581
- fig.update_layout(
582
- scene=dict(
583
- xaxis=dict(visible=False),
584
- yaxis=dict(visible=False),
585
- zaxis=dict(visible=False),
586
- aspectmode='data'
587
- ),
588
- margin=dict(l=0, r=0, b=0, t=30),
589
- )
590
- return fig
591
-
592
- def _create_box_figure(lx, ly, lz, title):
593
- # Vertices
594
- x = [0, lx, lx, 0, 0, lx, lx, 0]
595
- y = [0, 0, ly, ly, 0, 0, ly, ly]
596
- z = [0, 0, 0, 0, lz, lz, lz, lz]
597
-
598
- fig = go.Figure()
599
-
600
- # Solid Mesh (semi-transparent)
601
- fig.add_trace(go.Mesh3d(
602
- x=x, y=y, z=z,
603
- # Indices for triangles (2 per face, 6 faces)
604
- i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
605
- j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
606
- k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
607
- opacity=0.2,
608
- color='blue',
609
- flatshading=True,
610
- name=title,
611
- showscale=False
612
- ))
613
-
614
- # Wireframe Edges
615
- # Define lines to connect vertices
616
- xe = [0, lx, lx, 0, 0, None, 0, lx, lx, 0, 0, None, 0, 0, None, lx, lx, None, lx, lx, None, 0, 0]
617
- ye = [0, 0, ly, ly, 0, None, 0, 0, ly, ly, 0, None, 0, 0, None, 0, 0, None, ly, ly, None, ly, ly]
618
- ze = [0, 0, 0, 0, 0, None, lz, lz, lz, lz, lz, None, 0, lz, None, 0, lz, None, 0, lz, None, 0, lz]
619
-
620
- fig.add_trace(go.Scatter3d(
621
- x=xe, y=ye, z=ze,
622
- mode='lines',
623
- line=dict(color='black', width=3),
624
- showlegend=False
625
- ))
626
-
627
- fig.update_layout(title=title)
628
- return fig
629
-
630
- def update_geometry_view():
631
- try:
632
- fig = get_geometry_figure()
633
- if hasattr(ctrl, "geometry_plot_update"):
634
- ctrl.geometry_plot_update(fig)
635
- except Exception as e:
636
- print(f"Error updating geometry view: {e}")
637
-
638
-
639
- def export_simulation_vtk():
640
- """Download the current simulation volume/isosurface as a VTK file."""
641
- global current_grid_object
642
-
643
- if not state.simulation_has_run or current_grid_object is None:
644
- log_to_console("VTK export unavailable: run a simulation first.")
645
- return
646
-
647
- temp_path = None
648
- try:
649
- suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
650
- grid_size = int(state.grid_size or 0)
651
- filename = f"qlbm_volume_n{grid_size}_{suffix}.vts"
652
-
653
- tmp = tempfile.NamedTemporaryFile(suffix=".vts", delete=False)
654
- tmp.close()
655
- temp_path = Path(tmp.name)
656
-
657
- current_grid_object.save(str(temp_path))
658
- server.controller.download_file(temp_path.read_bytes(), filename)
659
- log_to_console(f"Exported VTK to {filename}")
660
- except Exception as exc:
661
- log_to_console(f"VTK export failed: {exc}")
662
- finally:
663
- if temp_path and temp_path.exists():
664
- try:
665
- temp_path.unlink()
666
- except Exception:
667
- pass
668
-
669
-
670
- def export_simulation_mp4():
671
- """Render the simulation frames to an MP4 animation for download."""
672
- global simulation_data_frames, current_grid_object
673
-
674
- if not state.simulation_has_run or not simulation_data_frames:
675
- log_to_console("MP4 export unavailable: run a simulation first.")
676
- return
677
- if current_grid_object is None:
678
- log_to_console("MP4 export failed: missing grid data.")
679
- return
680
-
681
- temp_path = None
682
- movie_plotter = None
683
- try:
684
- suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
685
- grid_size = int(state.grid_size or 0)
686
- filename = f"qlbm_animation_n{grid_size}_{suffix}.mp4"
687
-
688
- tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
689
- tmp.close()
690
- temp_path = Path(tmp.name)
691
-
692
- movie_plotter = pv.Plotter(off_screen=True, window_size=(1280, 720))
693
- try:
694
- camera_position = plotter.camera_position if plotter.camera_position else None
695
- except Exception:
696
- camera_position = None
697
-
698
- base_grid = current_grid_object.copy()
699
- movie_plotter.open_movie(str(temp_path), framerate=15)
700
-
701
- for frame_data in simulation_data_frames:
702
- base_grid["scalars"] = np.asarray(frame_data).ravel()
703
- iso_mesh = base_grid.contour(isosurfaces=7, scalars="scalars")
704
- movie_plotter.clear()
705
- movie_plotter.add_mesh(
706
- iso_mesh,
707
- cmap="Blues",
708
- opacity=0.35,
709
- show_scalar_bar=False,
710
- )
711
- movie_plotter.add_axes()
712
- if camera_position:
713
- try:
714
- movie_plotter.camera_position = camera_position
715
- except Exception:
716
- pass
717
- else:
718
- movie_plotter.view_isometric()
719
- movie_plotter.render()
720
- movie_plotter.write_frame()
721
-
722
- movie_plotter.close()
723
- movie_plotter = None
724
-
725
- server.controller.download_file(temp_path.read_bytes(), filename)
726
- log_to_console(f"Exported MP4 to {filename}")
727
- except Exception as exc:
728
- log_to_console(f"MP4 export failed: {exc}")
729
- finally:
730
- if movie_plotter is not None:
731
- try:
732
- movie_plotter.close()
733
- except Exception:
734
- pass
735
- if temp_path and temp_path.exists():
736
- try:
737
- temp_path.unlink()
738
- except Exception:
739
- pass
740
-
741
- def run_simulation():
742
- global simulation_data_frames, simulation_times, current_grid_object
743
-
744
- if not _SIMULATION_CAN_RUN:
745
- msg = _SIMULATION_DISABLED_REASON or "Simulation backend is not available on this platform."
746
- state.run_error = msg
747
- log_to_console(f"Error: {msg}")
748
- state.status_message = "Error: Backend unavailable"
749
- state.status_type = "error"
750
- return
751
-
752
- state.is_running = True
753
- state.run_error = ""
754
- state.simulation_has_run = False
755
- state.show_progress = True
756
- state.simulation_progress = 0
757
- state.status_message = "Running simulation..."
758
- state.status_type = "info"
759
-
760
- # Log initial configuration
761
- config_lines = [
762
- "Job Initiated",
763
- f" Grid Size: {state.grid_size} × {state.grid_size} × {state.grid_size}",
764
- f" Time Steps: {state.time_steps}",
765
- f" Distribution: {state.dist_type}",
766
- f" Boundary: {state.boundary_condition}",
767
- f" Backend: {state.selected_backend}",
768
- f" Velocity: vx={state.vx_expr}, vy={state.vy_expr}, vz={state.vz_expr}",
769
- ]
770
- for line in config_lines:
771
- log_to_console(line)
772
-
773
- last_logged_percent = 0
774
- def _progress_callback(percent):
775
- nonlocal last_logged_percent
776
- state.simulation_progress = percent
777
- if percent - last_logged_percent >= 10:
778
- log_to_console(f"Simulation progress: {int(percent)}%")
779
- last_logged_percent = percent
780
-
781
- try:
782
- # 1. Prepare Parameters
783
- grid_size = int(state.grid_size)
784
- num_reg_qubits = int(math.log2(grid_size)) if grid_size > 0 else 3
785
- T = int(state.time_steps)
786
- distribution_type = state.dist_type
787
- boundary_condition = state.boundary_condition
788
-
789
- # Velocity functions
790
- vx_func = make_velocity_func(state.vx_expr)
791
- vy_func = make_velocity_func(state.vy_expr)
792
- vz_func = make_velocity_func(state.vz_expr)
793
-
794
- # Update progress to indicate start
795
- _progress_callback(0)
796
-
797
- if simulate_qlbm_3D_and_animate is not None:
798
- # 2a. Run CUDA-Q Simulation
799
- log_to_console("Running CUDA-Q Simulation...")
800
- plotter.clear()
801
- # Note: This call is blocking and doesn't report progress back to UI during execution
802
- _, frames, times, grid_obj = simulate_qlbm_3D_and_animate(
803
- num_reg_qubits=num_reg_qubits,
804
- T=T,
805
- distribution_type=distribution_type,
806
- vx_input=vx_func,
807
- vy_input=vy_func,
808
- vz_input=vz_func,
809
- boundary_condition=boundary_condition,
810
- plotter=plotter,
811
- add_slider=False,
812
- progress_callback=_progress_callback
813
- )
814
- else:
815
- # 2b. CPU Demo Simulation (approximate)
816
- log_to_console("Running CPU Demo Simulation...")
817
- frames, times, grid_obj = _run_cpu_demo_simulation(
818
- grid_size=grid_size,
819
- T=T,
820
- distribution_type=distribution_type or "Sinusoidal",
821
- vx_func=vx_func,
822
- vy_func=vy_func,
823
- vz_func=vz_func,
824
- progress_callback=_progress_callback
825
- )
826
-
827
- _progress_callback(100)
828
-
829
- # Force Blues colormap
830
- if grid_obj:
831
- plotter.clear()
832
- isosurfaces = grid_obj.contour(isosurfaces=7, scalars="scalars")
833
- plotter.add_mesh(isosurfaces, cmap="Blues", opacity=0.3, show_scalar_bar=True)
834
- plotter.add_axes()
835
- plotter.show_grid()
836
-
837
- # Remove the built-in slider widget from fluid3d_pyvista if present,
838
- # as we are using the Trame UI slider.
839
- # try:
840
- # plotter.clear_slider_widgets()
841
- # except AttributeError:
842
- # pass
843
-
844
- # 3. Store Results
845
- if frames and len(frames) > 0:
846
- simulation_data_frames = frames
847
- simulation_times = times
848
- current_grid_object = grid_obj
849
-
850
- state.max_time_step = len(frames) - 1
851
- state.time_val = 0
852
- # Build formatted time labels for slider thumb (e.g., "0.0", "0.5", "1.0", ...)
853
- state.time_slider_labels = [f"{t:.1f}" for t in times] if times else [str(i) for i in range(len(frames))]
854
- state.simulation_has_run = True
855
-
856
- # The plotter is already updated by the function, but we ensure view update
857
- # Enable picking
858
- _ensure_point_picking(on_pick_point)
859
-
860
- ctrl.view_update()
861
- log_to_console("Simulation completed successfully.")
862
- state.status_message = "Simulation completed successfully."
863
- state.status_type = "success"
864
- state.simulation_progress = 100
865
- else:
866
- state.run_error = "Simulation produced no data."
867
- log_to_console("Error: Simulation produced no data.")
868
- state.status_message = "Error: No data produced"
869
- state.status_type = "error"
870
-
871
- except Exception as e:
872
- state.run_error = f"Simulation failed: {str(e)}"
873
- log_to_console(f"Simulation Error: {e}")
874
- print(f"Simulation Error: {e}")
875
- state.status_message = "Simulation failed"
876
- state.status_type = "error"
877
- finally:
878
- state.is_running = False
879
- # Keep progress visible for a moment or hide it? em_trame keeps it if we don't hide it.
880
- # But usually we want to hide it after some time or keep it at 100%.
881
- # For now, we leave show_progress=True so user sees 100%.
882
- # But if we want to hide it on next run, we reset it at start.
883
- if state.status_type == "success":
884
- # Optional: Auto-hide after delay? Trame doesn't have easy setTimeout in python without async.
885
- pass
886
- else:
887
- state.show_progress = False
888
-
889
- def stop_simulation():
890
- state.is_running = False
891
-
892
- def reset_simulation():
893
- state.is_running = False
894
- state.run_error = ""
895
- state.simulation_has_run = False
896
- state.dist_type = None
897
- state.show_edges = False
898
- state.problems_selection = None
899
- state.geometry_selection = None
900
- state.backend_type = None
901
- state.advecting_field = None
902
- state.show_advect_params = False
903
- plotter.clear()
904
- ctrl.view_update()
905
- _apply_workflow_highlights(_determine_workflow_step())
906
-
907
- # --- Handlers ---
908
-
909
- @state.change("advecting_field")
910
- def _on_advect_dropdown_change(advecting_field, **_):
911
- if advecting_field:
912
- set_velocity_preset(advecting_field)
913
- _apply_workflow_highlights(_determine_workflow_step())
914
-
915
- @state.change("grid_index")
916
- def _on_grid_index_change(grid_index, **_):
917
- """Map discrete slider index to allowed grid sizes [8,16,32,64,128,256]."""
918
- try:
919
- allowed = [8, 16, 32, 64, 128, 256]
920
- if grid_index is None:
921
- return
922
- if isinstance(grid_index, (int, float)):
923
- idx = int(grid_index)
924
- idx = max(0, min(idx, len(allowed) - 1))
925
- val = allowed[idx]
926
-
927
- # Update grid_size state
928
- if state.grid_size != val:
929
- state.grid_size = val
930
- # Also update the qubit plot
931
- fig, info, warn = update_qubit_3D_info(val)
932
- state.qubit_grid_info = info
933
- state.qubit_warning = warn
934
- if hasattr(ctrl, "qubit_plot_update"):
935
- ctrl.qubit_plot_update(fig)
936
-
937
- # Update nx state and view (replacing on_grid_change logic)
938
- if state.nx != val:
939
- state.nx = val
940
- state.gauss_cx = val / 2
941
- state.gauss_cy = val / 2
942
- state.gauss_cz = val / 2
943
- state.show_edges = True # Enable edges when user interacts with slider
944
- update_view()
945
-
946
- except Exception:
947
- pass
948
- finally:
949
- _apply_workflow_highlights(_determine_workflow_step())
950
-
951
- @state.change("problems_selection")
952
- def _on_problem_selection_change(problems_selection, **_):
953
- """Auto-select geometry based on the chosen problem and show read-only."""
954
- try:
955
- if not problems_selection:
956
- state.geometry_selection = None
957
- return
958
-
959
- if isinstance(problems_selection, str):
960
- normalized = problems_selection.strip()
961
- state.geometry_selection = _PROBLEM_GEOMETRY_MAP.get(normalized)
962
- else:
963
- state.geometry_selection = None
964
- except Exception:
965
- state.geometry_selection = None
966
- finally:
967
- _apply_workflow_highlights(_determine_workflow_step())
968
-
969
- @state.change("dist_type")
970
- def _on_dist_type_change(dist_type, **_):
971
- # Whenever the user picks a new excitation, hide edges until they touch the slider again
972
- if state.show_edges:
973
- state.show_edges = False
974
- update_view()
975
- _apply_workflow_highlights(_determine_workflow_step())
976
-
977
-
978
- @state.change("show_edges", "sine_k_x", "sine_k_y", "sine_k_z", "gauss_cx", "gauss_cy", "gauss_cz", "gauss_sigma")
979
- def on_param_change(**kwargs):
980
- update_view()
981
- _apply_workflow_highlights(_determine_workflow_step())
982
-
983
-
984
- @state.change("geometry_selection", "domain_L", "domain_W", "domain_H")
985
- def _on_geometry_selection_change(**_):
986
- update_geometry_view()
987
- _apply_workflow_highlights(_determine_workflow_step())
988
-
989
-
990
- @state.change("backend_type")
991
- def _on_backend_type_change(**_):
992
- _apply_workflow_highlights(_determine_workflow_step())
993
-
994
-
995
- @state.change("time_val")
996
- def update_time_frame(time_val, **_):
997
- """Update the plotter with the frame corresponding to time_val."""
998
- global simulation_data_frames, simulation_times, current_grid_object
999
-
1000
- if not state.simulation_has_run or not simulation_data_frames or current_grid_object is None:
1001
- return
1002
-
1003
- try:
1004
- idx = int(time_val)
1005
- if 0 <= idx < len(simulation_data_frames):
1006
- # Update grid scalars
1007
- current_grid_object["scalars"] = simulation_data_frames[idx].flatten()
1008
-
1009
- # Re-compute isosurfaces
1010
- # Note: contour() returns a new mesh. We need to replace the actor.
1011
- isosurfaces = current_grid_object.contour(isosurfaces=7, scalars="scalars")
1012
-
1013
- plotter.clear()
1014
- plotter.add_mesh(isosurfaces, cmap="Blues", opacity=0.3, show_scalar_bar=True)
1015
- plotter.add_axes()
1016
- plotter.show_grid()
1017
-
1018
- # Update time label (use actual time from simulation_times)
1019
- t_val = simulation_times[idx] if idx < len(simulation_times) else idx
1020
- state.current_time_label = f"{t_val:.2f}" if isinstance(t_val, float) else str(t_val)
1021
- plotter.add_text(f"Time: {t_val:.2f}" if isinstance(t_val, float) else f"Time: {t_val}", name="time_label", position="upper_right")
1022
-
1023
- # Enable picking
1024
- _ensure_point_picking(on_pick_point)
1025
-
1026
- ctrl.view_update()
1027
- except Exception as e:
1028
- print(f"Error updating time frame: {e}")
1029
-
1030
- # --- UI Layout ---
1031
-
1032
- with SinglePageLayout(server) as layout:
1033
- layout.title.set_text("")
1034
- layout.toolbar.hide = True
1035
-
1036
- html.Style("""
1037
- :root{ --v-theme-primary:95,37,159; }
1038
- .example-img{ max-width:100%; border-radius:4px; }
1039
- .warn-text{ color:#b71c1c; font-size:0.85rem; }
1040
- """)
1041
-
1042
- with layout.content:
1043
- with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
1044
- with vuetify3.VRow(no_gutters=True, classes="fill-height"):
1045
-
1046
- # --- Left Column: Controls ---
1047
- with vuetify3.VCol(cols=5, classes="pa-2 d-flex flex-column" , style="overflow-y: auto; max-height: 200vh;"):
1048
- # Overview cell (from qlbm)
1049
- with vuetify3.VCard(classes="mb-2", style=("overview_card_style", "font-size: 0.8rem;")):
1050
- vuetify3.VCardTitle("Overview", classes="text-subtitle-2 font-weight-bold text-primary")
1051
- with vuetify3.VCardText():
1052
- vuetify3.VDivider(classes="my-2")
1053
- vuetify3.VCardSubtitle("Problems", classes="text-caption font-weight-bold mt-2")
1054
- vuetify3.VSelect(
1055
- key="overview_problems",
1056
- label="Select a problem",
1057
- v_model=("problems_selection", None),
1058
- items=(
1059
- "qlbm_problems",
1060
- [
1061
- "Scalar advection-diffusion in a box",
1062
- "Laminar flow & heat transfer for a heated body in water.",
1063
- ],
1064
- ),
1065
- placeholder="Select",
1066
- density="compact",
1067
- hide_details=True,
1068
- color="primary",
1069
- classes="mb-2"
1070
- )
1071
- vuetify3.VCardSubtitle("Governing Equations", classes="text-caption font-weight-bold mt-2")
1072
- vuetify3.VListItemTitle("Laminar Navier-Stokes including energy", classes="text-caption")
1073
- vuetify3.VCardSubtitle("Inputs", classes="text-caption font-weight-bold mt-2")
1074
- vuetify3.VListItemTitle("Geometry, Boundary conditions - temperature and flow", classes="text-caption")
1075
- vuetify3.VCardSubtitle("Outputs", classes="text-caption font-weight-bold mt-2")
1076
- vuetify3.VListItemTitle("Surface plots on sections OR sampling through a line in 3D domain", classes="text-caption")
1077
-
1078
- # Geometry cell
1079
- with vuetify3.VCard(classes="mb-2"):
1080
- vuetify3.VCardTitle("Geometry", classes="text-subtitle-2 font-weight-bold text-primary")
1081
- with vuetify3.VCardText():
1082
- # Read-only geometry display auto-set by problem selection
1083
- vuetify3.VAlert(
1084
- v_if="geometry_selection",
1085
- type="info",
1086
- variant="tonal",
1087
- density="compact",
1088
- color="primary",
1089
- children=["Selected Geometry: ", "{{ geometry_selection }}"],
1090
- classes="mb-2"
1091
- )
1092
- vuetify3.VAlert(
1093
- v_if="!geometry_selection",
1094
- type="info",
1095
- variant="tonal",
1096
- density="compact",
1097
- color="primary",
1098
- children=["No geometry selected. Choose a problem to auto-set."],
1099
- classes="mb-2"
1100
- )
1101
- with vuetify3.VContainer(v_if="geometry_selection === 'Rectangular domain with a heated box (3D)'", classes="pa-0 mt-2"):
1102
- vuetify3.VCardSubtitle("Domain dimensions", classes="text-caption font-weight-bold mb-2")
1103
- with vuetify3.VRow(dense=True):
1104
- # Length
1105
- with vuetify3.VCol():
1106
- with vuetify3.VTooltip(location="top"):
1107
- with vuetify3.Template(v_slot_activator="{ props }"):
1108
- vuetify3.VTextField(label="Length (L)", v_model=("domain_L", 1.0), type="number", step="0.1", density="compact", hide_details=True, color="primary", v_bind="props")
1109
- html.Span("Relative Length. The domain is scaled so the largest dimension is 1.0.")
1110
- # Width
1111
- with vuetify3.VCol():
1112
- with vuetify3.VTooltip(location="top"):
1113
- with vuetify3.Template(v_slot_activator="{ props }"):
1114
- vuetify3.VTextField(label="Width (W)", v_model=("domain_W", 1.0), type="number", step="0.1", density="compact", hide_details=True, color="primary", v_bind="props")
1115
- html.Span("Relative Width. The domain is scaled so the largest dimension is 1.0.")
1116
- # Height
1117
- with vuetify3.VCol():
1118
- with vuetify3.VTooltip(location="top"):
1119
- with vuetify3.Template(v_slot_activator="{ props }"):
1120
- vuetify3.VTextField(label="Height (H)", v_model=("domain_H", 1.0), type="number", step="0.1", density="compact", hide_details=True, color="primary", v_bind="props")
1121
- html.Span("Relative Height. The domain is scaled so the largest dimension is 1.0.")
1122
-
1123
- with vuetify3.VCard(classes="mb-2", style=("distribution_card_style", "font-size: 0.8rem;")):
1124
- with vuetify3.VCardTitle("Initial Distribution", classes="text-subtitle-2 font-weight-bold text-primary"): pass
1125
- with vuetify3.VCardText():
1126
-
1127
- # Row with Select + Customize Button
1128
- with vuetify3.VRow(classes="d-flex align-center mb-2", no_gutters=True):
1129
- with vuetify3.VCol(cols="auto", classes="flex-grow-1"):
1130
-
1131
- # TOOLTIP FOR DROPDOWN
1132
- with vuetify3.VTooltip(location="top"):
1133
- with vuetify3.Template(v_slot_activator="{ props }"):
1134
- vuetify3.VSelect(
1135
- label="Initial Distribution",
1136
- v_model=("dist_type", None),
1137
- items=("dist_modes",),
1138
- density="compact",
1139
- hide_details=True,
1140
- v_bind="props"
1141
- )
1142
- # FIXED: Using html.Span instead of vuetify3.Span
1143
- html.Span("Select the mathematical function used to initialize the field.")
1144
-
1145
- with vuetify3.VCol(cols="auto", classes="ml-2"):
1146
-
1147
- # TOOLTIP FOR CUSTOMIZE BUTTON
1148
- with vuetify3.VTooltip(location="top"):
1149
- with vuetify3.Template(v_slot_activator="{ props }"):
1150
- with vuetify3.VBtn(
1151
- icon=True, density="compact", variant="text",
1152
- click="custom_dist_params = !custom_dist_params",
1153
- v_bind="props"
1154
- ):
1155
- vuetify3.VIcon("mdi-cog", color=("custom_dist_params ? 'primary' : 'grey'",))
1156
- html.Span("Toggle advanced parameters (Frequency / Position / Width).")
1157
-
1158
- # --- Conditional Controls ---
1159
-
1160
- # 1. SINUSOIDAL CONTROLS
1161
- with vuetify3.VCard(classes="mb-2", v_if="custom_dist_params && dist_type === 'Sinusoidal'"):
1162
- with vuetify3.VCardTitle("Sinusoidal Frequencies"): pass
1163
- with vuetify3.VCardText():
1164
- for axis in ['x', 'y', 'z']:
1165
- with vuetify3.VTooltip(location="start"):
1166
- with vuetify3.Template(v_slot_activator="{ props }"):
1167
- vuetify3.VSlider(
1168
- label=f"Freq {axis.upper()}",
1169
- v_model=(f"sine_k_{axis}", 1.0),
1170
- min=1, max=5, step=1,
1171
- thumb_label="always", density="compact",
1172
- v_bind="props"
1173
- )
1174
- html.Span(f"Number of full wave cycles along the {axis.upper()} axis. Must be an integer to maintain periodic boundaries.")
1175
-
1176
- # 2. GAUSSIAN CONTROLS
1177
- with vuetify3.VCard(classes="mb-2", v_if="custom_dist_params && dist_type === 'Gaussian'"):
1178
- with vuetify3.VCardTitle("Gaussian Parameters"): pass
1179
- with vuetify3.VCardText():
1180
- # Centers
1181
- for axis in ['x', 'y', 'z']:
1182
- with vuetify3.VTooltip(location="start"):
1183
- with vuetify3.Template(v_slot_activator="{ props }"):
1184
- vuetify3.VSlider(
1185
- label=f"Center {axis.upper()}",
1186
- v_model=(f"gauss_c{axis}", 16),
1187
- min=0, max=("nx", 32), step=1,
1188
- thumb_label="always", density="compact",
1189
- v_bind="props"
1190
- )
1191
- html.Span(f"Shift the center (Mean) of the blob along the {axis.upper()} axis.")
1192
-
1193
- # Width
1194
- with vuetify3.VTooltip(location="start"):
1195
- with vuetify3.Template(v_slot_activator="{ props }"):
1196
- vuetify3.VSlider(
1197
- label="Width (Sigma)",
1198
- v_model=("gauss_sigma", 6.0),
1199
- min=1.0, max=20.0, step=0.5,
1200
- thumb_label="always", density="compact",
1201
- v_bind="props"
1202
- )
1203
- html.Span("Controls the spread (Standard Deviation). Higher Sigma = wider, fuzzier blob.")
1204
-
1205
- # Boundary Conditions
1206
- with vuetify3.VCard(classes="mb-2"):
1207
- vuetify3.VCardTitle("Boundary Conditions", classes="text-subtitle-2 font-weight-bold text-primary")
1208
- with vuetify3.VCardText():
1209
- vuetify3.VSelect(label="Boundary Condition", v_model=("boundary_condition", "Periodic"), items=("['Periodic']",), density="compact", hide_details=True, color="primary")
1210
-
1211
- # Advecting Fields
1212
- with vuetify3.VCard(classes="mb-2", style=("advect_card_style", "font-size: 0.8rem;")):
1213
- vuetify3.VCardTitle("Advecting Fields", classes="text-subtitle-2 font-weight-bold text-primary")
1214
- with vuetify3.VCardText():
1215
- with vuetify3.VRow(classes="d-flex align-center mb-2", no_gutters=True):
1216
- with vuetify3.VCol(cols="auto", classes="flex-grow-1"):
1217
- vuetify3.VSelect(
1218
- label="Select Advecting field",
1219
- v_model=("advecting_field", None),
1220
- items=("['Uniform', 'Swirl', 'Shear', 'TGV']",),
1221
- density="compact",
1222
- hide_details=True,
1223
- color="primary",
1224
- placeholder="Select",
1225
- )
1226
- with vuetify3.VCol(cols="auto", classes="ml-2"):
1227
- with vuetify3.VTooltip(location="top"):
1228
- with vuetify3.Template(v_slot_activator="{ props }"):
1229
- with vuetify3.VBtn(
1230
- icon=True,
1231
- density="compact",
1232
- variant="text",
1233
- click="show_advect_params = !show_advect_params",
1234
- v_bind="props",
1235
- ):
1236
- vuetify3.VIcon("mdi-cog", color=("show_advect_params ? 'primary' : 'grey'",))
1237
- html.Span("Toggle velocity component inputs.")
1238
- with vuetify3.VContainer(v_if="show_advect_params", classes="pa-0 mt-2"):
1239
- html.Div("Velocity Components", classes="text-caption mb-1")
1240
- vuetify3.VTextField(label="Velocity vx", v_model=("vx_expr", "0.2"), density="compact", hide_details=True, color="primary", classes="mb-1")
1241
- vuetify3.VTextField(label="Velocity vy", v_model=("vy_expr", "-0.15"), density="compact", hide_details=True, color="primary", classes="mb-1")
1242
- vuetify3.VTextField(label="Velocity vz", v_model=("vz_expr", "0.3"), density="compact", hide_details=True, color="primary")
1243
-
1244
- # Meshing
1245
- with vuetify3.VCard(classes="mb-2", style=("meshing_card_style", "font-size: 0.8rem;")):
1246
- vuetify3.VCardTitle("Meshing", classes="text-subtitle-2 font-weight-bold text-primary")
1247
- with vuetify3.VCardText():
1248
- with vuetify3.VMenu(open_on_hover=True, close_on_content_click=False, location="end"):
1249
- with vuetify3.Template(v_slot_activator="{ props }"):
1250
- with vuetify3.VSlider(
1251
- v_bind="props",
1252
- label="Number of Points / Direction",
1253
- v_model=("grid_index", 2),
1254
- min=0, max=5, step=1,
1255
- thumb_label="always",
1256
- show_ticks="always",
1257
- color="primary",
1258
- density="compact",
1259
- hide_details=True
1260
- ):
1261
- vuetify3.Template(v_slot_thumb_label="{ modelValue }", children=["{{ ['8','16','32','64','128','256'][modelValue] }}"])
1262
- with vuetify3.VSheet(classes="pa-2", elevation=6, rounded=True, style="width: 700px;"):
1263
- with vuetify3.VContainer(fluid=True, classes="pa-0"):
1264
- qubit_fig = plotly_widgets.Figure(figure=go.Figure(), style="width: 616px; height: 320px; min-height: 320px;", responsive=True)
1265
- ctrl.qubit_plot_update = qubit_fig.update
1266
- html.Div("{{ qubit_grid_info }}", classes="mt-2 text-caption")
1267
- html.Div("{{ qubit_warning }}", classes="warn-text")
1268
- vuetify3.VAlert(v_if="grid_size > 32", type="warning", variant="tonal", density="compact", children=["Warning: High grid size may impact performance."], classes="mt-2")
1269
-
1270
- # Time
1271
- with vuetify3.VCard(classes="mb-2"):
1272
- vuetify3.VCardTitle("Time", classes="text-subtitle-2 font-weight-bold text-primary")
1273
- with vuetify3.VCardText():
1274
- vuetify3.VSlider(label="Total Time", v_model=("time_steps", 10), min=0, max=2000, step=10, thumb_label="always", show_ticks="always", color="primary", density="compact", hide_details=True)
1275
- vuetify3.VAlert(v_if="time_steps > 100", type="warning", variant="tonal", density="compact", children=["Warning: High time steps may increase runtime."], classes="mt-2")
1276
-
1277
- # Backends
1278
- with vuetify3.VCard(classes="mb-2", style=("backend_card_style", "font-size: 0.8rem;")):
1279
- vuetify3.VCardTitle("Backends", classes="text-subtitle-2 font-weight-bold text-primary")
1280
- with vuetify3.VCardText():
1281
- with vuetify3.VRow(dense=True, classes="mb-2"):
1282
- with vuetify3.VCol():
1283
- vuetify3.VAlert(
1284
- type="info",
1285
- color="primary",
1286
- variant="tonal",
1287
- density="compact",
1288
- children=[
1289
- "Selected: ",
1290
- "{{ backend_type || '—' }}",
1291
- " - ",
1292
- "{{ backend_type === 'Simulator' ? selected_simulator : (backend_type === 'QPU' ? selected_qpu : '—') }}",
1293
- ],
1294
- )
1295
- with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"):
1296
- with vuetify3.Template(v_slot_activator="{ props }"):
1297
- vuetify3.VBtn(v_bind="props", text="Choose Backend", color="primary", variant="tonal", block=True)
1298
- with vuetify3.VList(density="compact"):
1299
- with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8):
1300
- with vuetify3.Template(v_slot_activator="{ props }"):
1301
- vuetify3.VListItem(v_bind="props", title="Simulator", prepend_icon="mdi-robot-outline", append_icon="mdi-chevron-right")
1302
- with vuetify3.VList(density="compact"):
1303
- vuetify3.VListItem(title="IBM Qiskit simulator", click="backend_type = 'Simulator'; selected_simulator = 'IBM Qiskit simulator'")
1304
- with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8):
1305
- with vuetify3.Template(v_slot_activator="{ props }"):
1306
- vuetify3.VListItem(v_bind="props", title="QPU", prepend_icon="mdi-chip", append_icon="mdi-chevron-right")
1307
- with vuetify3.VList(density="compact"):
1308
- vuetify3.VListItem(title="IBM QPU", click="backend_type = 'QPU'; selected_qpu = 'IBM QPU'")
1309
- vuetify3.VListItem(title="IonQ QPU", click="backend_type = 'QPU'; selected_qpu = 'IonQ QPU'")
1310
- vuetify3.VDivider(classes="my-3")
1311
- vuetify3.VBtn(
1312
- text="Run",
1313
- color="primary",
1314
- block=True,
1315
- disabled=("is_running || !simulation_backend_ready", False),
1316
- click=run_simulation,
1317
- style=("is_running ? '' : 'background-color:#87CEFA;'", ""),
1318
- )
1319
- html.Div("Backend: {{ simulation_backend_mode }}", classes="text-caption text-medium-emphasis mt-2")
1320
- vuetify3.VAlert(
1321
- v_if="simulation_backend_note",
1322
- type="info",
1323
- variant="tonal",
1324
- density="compact",
1325
- children=["{{ simulation_backend_note }}"],
1326
- classes="mt-2",
1327
- )
1328
- with vuetify3.VRow(dense=True, classes="mt-2"):
1329
- with vuetify3.VCol(cols=6):
1330
- vuetify3.VBtn(
1331
- text="Reset",
1332
- color="#8BC34A",
1333
- variant="tonal",
1334
- block=True,
1335
- disabled=("is_running", False),
1336
- click=reset_simulation,
1337
- )
1338
- with vuetify3.VCol(cols=6):
1339
- vuetify3.VBtn(
1340
- text="STOP",
1341
- color="#FF7043",
1342
- variant="tonal",
1343
- block=True,
1344
- click=stop_simulation,
1345
- disabled=("!is_running", True),
1346
- )
1347
-
1348
- # --- Right Column: Plotter ---
1349
- with vuetify3.VCol(cols=7, classes="pa-1 d-flex flex-column"):
1350
- # Main Plot Card
1351
- with vuetify3.VCard(classes="mb-1 flex-grow-1 d-flex flex-column", elevation=2, style="min-height: 0;"):
1352
-
1353
- # 0. Geometry Preview (Plotly) - Show when NOT simulation_has_run AND NO dist_type AND geometry_selection
1354
- with vuetify3.VContainer(v_if="!simulation_has_run && !dist_type && geometry_selection", fluid=True, classes="pa-0 flex-grow-1", style="width: 100%; height: 100%;"):
1355
- geom_fig = plotly_widgets.Figure(figure=go.Figure(), style="width: 100%; height: 100%;", responsive=True)
1356
- ctrl.geometry_plot_update = geom_fig.update
1357
-
1358
- # 1. Preview (Plotly) - Show when NOT simulation_has_run AND dist_type is selected
1359
- with vuetify3.VContainer(v_if="!simulation_has_run && dist_type", fluid=True, classes="pa-0 flex-grow-1", style="width: 100%; height: 100%;"):
1360
- preview_fig = plotly_widgets.Figure(figure=go.Figure(), style="width:100%; height:100%;", responsive=True)
1361
- ctrl.preview_update = preview_fig.update
1362
-
1363
- # Download controls (shown once simulation data exists)
1364
- with vuetify3.VContainer(v_if="simulation_has_run", classes="px-4 pt-3 pb-1 d-flex justify-end", style="width: 100%; flex: 0 0 auto;"):
1365
- with vuetify3.VMenu(location="bottom end"):
1366
- with vuetify3.Template(v_slot_activator="{ props }"):
1367
- vuetify3.VBtn(
1368
- v_bind="props",
1369
- text="Download",
1370
- color="primary",
1371
- variant="tonal",
1372
- prepend_icon="mdi-download"
1373
- )
1374
- with vuetify3.VList(density="compact"):
1375
- vuetify3.VListItem(
1376
- title="Export as VTK",
1377
- prepend_icon="mdi-content-save",
1378
- click=export_simulation_vtk
1379
- )
1380
- vuetify3.VListItem(
1381
- title="Export as MP4",
1382
- prepend_icon="mdi-movie",
1383
- click=export_simulation_mp4
1384
- )
1385
-
1386
- # 2. Simulation Result (PyVista) - Show when simulation_has_run
1387
- with vuetify3.VContainer(v_if="simulation_has_run", fluid=True, classes="pa-0 flex-grow-1", style="width: 100%; height: 100%;"):
1388
- view = plotter_ui(plotter)
1389
- ctrl.view_update = view.update
1390
-
1391
- # Time Slider (visible only after simulation run)
1392
- with vuetify3.VContainer(v_if="simulation_has_run", classes="px-4 pb-4", style="width: 90%; flex: 0 0 auto;"):
1393
- with vuetify3.VSlider(
1394
- v_model=("time_val", 0),
1395
- min=0,
1396
- max=("max_time_step", 10),
1397
- step=1,
1398
- label="Time",
1399
- thumb_label="always",
1400
- density="compact",
1401
- hide_details=True,
1402
- color="primary"
1403
- ):
1404
- vuetify3.Template(
1405
- v_slot_thumb_label="{ modelValue }",
1406
- children=["{{ time_slider_labels[modelValue] || modelValue }}"]
1407
- )
1408
-
1409
- # Console Window (In-column)
1410
- with vuetify3.VCard(classes="mt-1", style="font-size: 0.8rem; flex: 0 0 auto;"):
1411
- with vuetify3.VCardTitle("Status", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"):
1412
- pass
1413
- with vuetify3.VCardText(classes="py-1 px-2", style="height: 150px; overflow-y: auto; background-color: #f5f5f5; font-family: monospace;"):
1414
- vuetify3.VTextarea(
1415
- v_model=("console_output", ""),
1416
- readonly=True,
1417
- auto_grow=False,
1418
- rows=6,
1419
- variant="plain",
1420
- hide_details=True,
1421
- style="font-family: monospace; width: 100%; height: 100%;"
1422
- )
1423
-
1424
- # Status Window - Fixed at bottom right (Floating, for progress)
1425
- with vuetify3.VCard(
1426
- v_if="status_visible",
1427
- style="position: fixed; bottom: 16px; right: 16px; z-index: 1000; min-width: 320px; max-width: 450px;",
1428
- elevation=8
1429
- ):
1430
- with vuetify3.VCardTitle(classes="d-flex align-center", style="font-size: 0.95rem; padding: 8px 12px;"):
1431
- vuetify3.VIcon("mdi-information-outline", size="small", classes="mr-2")
1432
- html.Span("Simulation Status")
1433
- vuetify3.VSpacer()
1434
- vuetify3.VBtn(
1435
- icon="mdi-close",
1436
- size="x-small",
1437
- variant="text",
1438
- click="status_visible = false"
1439
- )
1440
- vuetify3.VDivider()
1441
- with vuetify3.VCardText(classes="py-2 px-3"):
1442
- # Status message
1443
- vuetify3.VAlert(
1444
- type=("status_type", "info"),
1445
- variant="tonal",
1446
- density="compact",
1447
- children=["{{ status_message }}"]
1448
- )
1449
- # Progress bar (shown when simulation is running)
1450
- with vuetify3.VContainer(v_if="show_progress", classes="pa-0 mt-2"):
1451
- vuetify3.VProgressLinear(
1452
- model_value=("simulation_progress", 0),
1453
- color="primary",
1454
- height=6,
1455
- striped=True
1456
- )
1457
- html.Div(
1458
- "{{ simulation_progress }}% complete",
1459
- classes="text-caption text-center mt-1",
1460
- style="font-size: 0.75rem;"
1461
- )
1462
-
1463
- # --- Entry point ---
1464
- if __name__ == "__main__":
1465
- import argparse
1466
- import errno
1467
- import os
1468
-
1469
- update_view()
1470
-
1471
- parser = argparse.ArgumentParser(description="Start 3D Isosurface Explorer Trame server")
1472
- parser.add_argument("--host", default=None, help="Host/IP to bind (default: 127.0.0.1 locally; 0.0.0.0 if PORT/HF_PORT set)")
1473
- parser.add_argument("--port", type=int, default=None, help="Port to bind (default: TRIAL_APP_PORT or 8702 locally)")
1474
- args = parser.parse_args()
1475
-
1476
- env_port = os.environ.get("PORT") or os.environ.get("HF_PORT")
1477
- # If platform provides a port (e.g., Hugging Face Spaces), bind exactly there with 0.0.0.0 and do not auto-fallback.
1478
- if env_port:
1479
- host = args.host or "0.0.0.0"
1480
- port = int(env_port)
1481
- server.start(host=host, port=port, open_browser=False)
1482
- else:
1483
- # Local dev: allow CLI/env override and auto-increment to find a free port if busy.
1484
- base_port = args.port or int(os.environ.get("TRIAL_APP_PORT", "8702"))
1485
- host = args.host or os.environ.get("TRIAL_HOST", "127.0.0.1")
1486
- max_tries = 20
1487
- last_err = None
1488
- for i in range(max_tries):
1489
- try:
1490
- port = base_port + i
1491
- server.start(host=host, port=port, open_browser=False)
1492
- break
1493
- except OSError as e:
1494
- last_err = e
1495
- # EADDRINUSE -> try next
1496
- if getattr(e, 'errno', None) in (errno.EADDRINUSE, 98):
1497
- continue
1498
- # Other bind errors -> raise immediately
1499
- raise
1500
- else:
1501
- raise RuntimeError(f"Failed to bind after {max_tries} attempts starting at port {base_port}: {last_err}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
qlbm_trame.py DELETED
@@ -1,12 +0,0 @@
1
- import os
2
- import sys
3
-
4
- # Configure VTK for headless rendering BEFORE any VTK imports
5
- if os.environ.get("PORT") or os.environ.get("HF_PORT") or os.environ.get("SPACE_ID"):
6
- os.environ["MESA_GL_VERSION_OVERRIDE"] = "3.2"
7
- os.environ["MESA_GLSL_VERSION_OVERRIDE"] = "150"
8
- os.environ["GALLIUM_DRIVER"] = "llvmpipe"
9
- # Force VTK to use OSMesa for offscreen rendering
10
- os.environ["VTK_DEFAULT_EGL_DEVICE_INDEX"] = "-1"
11
-
12
- # ...existing imports...
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/fdtd_demo.ipynb DELETED
@@ -1,326 +0,0 @@
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/fdtd_grid16by16_time1_dt0.1_ancilla1_offset0_fieldEz_x8_y8_adapt_aqc 1.qasm DELETED
@@ -1,553 +0,0 @@
1
- OPENQASM 3.0;
2
- include "stdgates.inc";
3
- qubit[10] system;
4
- qubit[1] ancilla;
5
- qubit[1] Naimark;
6
- rx(-0.009929826774008) system[0];
7
- ry(0.004724135008185915) system[2];
8
- rx(-0.0686202769901092) system[4];
9
- ry(0.0029229585023471394) system[5];
10
- rz(-2.1854592787170617) system[7];
11
- cx system[0], system[9];
12
- ry(0.027400034915722626) system[0];
13
- cx system[0], system[1];
14
- rz(-0.7463947655035743) system[0];
15
- rx(0.02042320381868934) system[0];
16
- cx system[0], system[9];
17
- rz(-1.8230334240531008) system[0];
18
- ry(-0.027350205506260217) system[0];
19
- cx system[0], system[8];
20
- rz(2.5671372108276835) system[0];
21
- ry(0.039786737396934946) system[0];
22
- ry(0.007676353616585585) system[1];
23
- ry(-0.001805718052178884) system[9];
24
- cx system[4], Naimark[0];
25
- ry(0.0057912581312564715) Naimark[0];
26
- rz(2.9347571909778827) system[4];
27
- ry(-0.012528680630899736) system[4];
28
- cx system[4], system[5];
29
- rz(0.4859678147514519) system[4];
30
- ry(-0.012019499696038105) system[4];
31
- cx system[0], system[4];
32
- rz(0.5693250047567826) system[0];
33
- rx(-0.0309915671580967) system[0];
34
- cx system[0], Naimark[0];
35
- ry(-0.004061605248347311) Naimark[0];
36
- rz(0.20054303336987656) system[0];
37
- rx(-0.0654168174177241) system[0];
38
- cx system[0], system[9];
39
- rz(0.88253193990888) system[0];
40
- ry(0.040010415867415716) system[0];
41
- ry(0.00799719255391218) system[4];
42
- cx system[0], system[4];
43
- rz(3.1172080946137664) system[0];
44
- rx(0.05797126294265653) system[0];
45
- ry(0.0033198555528590745) system[4];
46
- ry(0.021232804271263506) system[4];
47
- rz(-0.25852474968713945) system[5];
48
- cx system[0], system[5];
49
- rz(0.3196851116661861) system[0];
50
- ry(0.03318974506177552) system[0];
51
- rz(-3.1288178532632527) system[5];
52
- ry(0.018811277080629907) system[5];
53
- cx system[5], system[6];
54
- rz(0.5377005190620405) system[5];
55
- rx(0.019763079252683946) system[5];
56
- ry(-0.0037382199378650505) system[9];
57
- cx system[0], system[9];
58
- rz(-2.4486180001926314) system[0];
59
- ry(0.06063543646138503) system[0];
60
- cx system[0], system[8];
61
- rz(-2.774363599584902) system[0];
62
- rx(-0.0206401099975011) system[0];
63
- cx system[0], system[1];
64
- rz(-0.3187491632873889) system[0];
65
- ry(-0.03589693966842655) system[0];
66
- rx(0.0037864611143736404) system[1];
67
- ry(-0.03539478262076856) system[1];
68
- rz(3.09594607920261) system[8];
69
- cx system[5], system[8];
70
- rz(0.6657455137535487) system[5];
71
- ry(-0.025849856763279222) system[5];
72
- rz(3.059875645069256) system[8];
73
- ry(0.029113654640829045) system[8];
74
- cx system[8], Naimark[0];
75
- ry(0.0101221689454849) Naimark[0];
76
- rz(0.018068722269390713) system[8];
77
- rx(0.025345162645482144) system[8];
78
- rx(-0.0011829648386165736) system[9];
79
- cx system[0], system[9];
80
- rz(1.3781527904622721) system[0];
81
- ry(-0.028711392721448403) system[0];
82
- cx system[0], system[4];
83
- rz(-0.11143542611262425) system[0];
84
- rx(-0.05603774712094567) system[0];
85
- rx(0.0010447338966630415) system[4];
86
- ry(-0.04479042313876458) system[4];
87
- cx system[4], Naimark[0];
88
- rz(-3.0844929025229275) Naimark[0];
89
- rz(-0.08255572981249815) system[4];
90
- ry(0.023804914814244738) system[4];
91
- rz(3.103546440492554) system[9];
92
- rz(2.7799319025884284) system[9];
93
- cx system[1], system[9];
94
- rz(-1.2883775125138963) system[1];
95
- ry(0.01822672749196208) system[1];
96
- cx system[1], Naimark[0];
97
- ry(0.009377744466731341) Naimark[0];
98
- ry(0.03179438408982582) Naimark[0];
99
- rz(0.33903485027948355) system[1];
100
- rx(0.043572158586145715) system[1];
101
- rz(2.9623962882808392) system[9];
102
- ry(0.0027440497246247197) system[9];
103
- cx system[8], system[9];
104
- rz(-0.6592370478820975) system[8];
105
- ry(-0.013271281403817392) system[8];
106
- cx system[4], system[8];
107
- ry(-0.02070698518802172) system[4];
108
- rz(-0.007670035645202189) system[4];
109
- rz(0.06496056026088182) system[8];
110
- ry(0.009329273119684345) system[8];
111
- cx system[0], system[8];
112
- rz(2.4880621327793584) system[0];
113
- rx(0.04481168580667649) system[0];
114
- ry(0.0013522036871222998) system[8];
115
- cx system[1], system[8];
116
- rz(0.0031448285399942044) system[1];
117
- ry(-0.010972742328514418) system[1];
118
- ry(0.012713967945394034) system[8];
119
- rx(-0.0039036910533283287) system[9];
120
- cx system[5], system[9];
121
- rz(0.7599795586518138) system[5];
122
- rx(-0.01608254297217182) system[5];
123
- cx system[5], system[6];
124
- rz(-0.5690509388859626) system[5];
125
- rx(-0.0017186276266858425) system[5];
126
- ry(-0.0020154056388657082) system[6];
127
- ry(0.012944280233578853) system[9];
128
- cx system[0], system[9];
129
- rz(0.16644237678417806) system[0];
130
- rx(0.047793563158643915) system[0];
131
- cx system[0], Naimark[0];
132
- ry(-0.030252812233535264) Naimark[0];
133
- rz(1.0747300496502332) system[0];
134
- ry(-0.05803895137493731) system[0];
135
- cx system[0], system[8];
136
- rz(-0.6262596737268766) system[0];
137
- rx(0.08712099324802214) system[0];
138
- cx system[0], system[1];
139
- rz(-0.6605360455225995) system[0];
140
- ry(-0.06147490007082812) system[0];
141
- ry(0.01281991274398031) system[1];
142
- rx(0.039844381573964416) system[1];
143
- ry(0.007638305504808374) system[8];
144
- rx(0.002803525410413066) system[8];
145
- ry(-0.0075099151033604095) system[9];
146
- ry(0.038341771256415136) system[9];
147
- cx system[1], system[9];
148
- rz(0.2513196725730653) system[1];
149
- rx(-0.09285400642538222) system[1];
150
- cx system[1], Naimark[0];
151
- rz(2.9678621472280016) Naimark[0];
152
- ry(0.006650622157603037) Naimark[0];
153
- rz(0.51983357989024) system[1];
154
- rx(0.036377523310496596) system[1];
155
- cx system[1], system[4];
156
- rz(-0.4187039274666453) system[1];
157
- ry(-0.03848288516583609) system[1];
158
- cx system[1], system[5];
159
- rz(2.686071905243066) system[1];
160
- rx(-0.05880384911951242) system[1];
161
- ry(0.020979733647543464) system[4];
162
- rx(-0.025601507276202584) system[4];
163
- ry(-0.00856253456519851) system[5];
164
- ry(-0.049286631253192104) system[9];
165
- cx system[4], system[9];
166
- rz(-0.5820865977218292) system[4];
167
- ry(-0.03725764097066575) system[4];
168
- rz(3.085511090235535) system[9];
169
- rx(0.03160455813525376) system[9];
170
- cx system[9], Naimark[0];
171
- cx system[1], Naimark[0];
172
- ry(-0.0018817320591488773) Naimark[0];
173
- ry(-0.0027210076906778458) Naimark[0];
174
- rz(2.7470896394107465) system[1];
175
- ry(0.04709223295866849) system[1];
176
- cx system[1], system[8];
177
- rz(0.6216830491259504) system[1];
178
- ry(0.004110733310723758) system[1];
179
- cx system[0], system[1];
180
- rz(-0.2892381135035573) system[0];
181
- ry(0.05645238747821191) system[0];
182
- rx(-0.0013960436853235336) system[1];
183
- rx(0.04081528878589302) system[1];
184
- cx system[4], Naimark[0];
185
- ry(-0.10551914519200922) Naimark[0];
186
- rz(0.12857028301657714) system[4];
187
- rx(-0.04399271417926798) system[4];
188
- ry(-0.03878890504120425) system[8];
189
- rx(-0.0015147246189397556) system[8];
190
- cx system[4], system[8];
191
- rz(0.07241238464453548) system[4];
192
- ry(0.03669256854019509) system[4];
193
- ry(-0.01770081312317262) system[8];
194
- rz(0.12459413032775957) system[8];
195
- rz(-0.2653529309684237) system[9];
196
- rx(-0.0013686267420343068) system[9];
197
- cx system[1], system[9];
198
- rz(0.5063195889939947) system[1];
199
- rx(0.10222195125830269) system[1];
200
- cx system[1], system[5];
201
- rz(-0.7977244858244403) system[1];
202
- ry(-0.046322495855074086) system[1];
203
- cx system[1], system[8];
204
- rz(-0.10869711872819798) system[1];
205
- ry(-0.09101808478741935) system[1];
206
- ry(0.004836890553977735) system[5];
207
- ry(0.00970972689547378) system[5];
208
- cx system[4], system[5];
209
- rz(0.060466255775309286) system[4];
210
- ry(-0.03969720896057649) system[4];
211
- rx(0.0874396836489264) system[5];
212
- rz(2.9507284270126486) system[8];
213
- ry(-0.07133706577571108) system[8];
214
- cx system[8], Naimark[0];
215
- ry(0.03727795909100573) Naimark[0];
216
- ry(0.09148208132826485) Naimark[0];
217
- rz(0.2141306628280446) system[8];
218
- rx(0.0015169954436846655) system[8];
219
- ry(-0.02420646846586383) system[9];
220
- rx(0.0016274085362881774) system[9];
221
- cx system[1], system[9];
222
- rz(-0.5623594724392595) system[1];
223
- ry(0.03963678154940564) system[1];
224
- cx system[1], system[8];
225
- rz(0.41733591513441026) system[1];
226
- rx(-0.05984077583854952) system[1];
227
- ry(0.02838922019871526) system[8];
228
- rx(-0.0028228116420585536) system[8];
229
- ry(-0.04155371454950818) system[9];
230
- cx system[5], system[9];
231
- rz(-3.0409168946253438) system[5];
232
- rx(-0.11288558925464542) system[5];
233
- cx system[5], Naimark[0];
234
- ry(-0.04473822542252326) Naimark[0];
235
- cx system[1], Naimark[0];
236
- ry(-0.02574440517596477) Naimark[0];
237
- rx(-0.005636549065758389) Naimark[0];
238
- rz(1.2826395874064411) system[1];
239
- rx(-0.04225791588566974) system[1];
240
- rz(-0.9015889398647352) system[5];
241
- rx(-0.03009966961945465) system[5];
242
- cx system[5], system[8];
243
- rz(0.7445856051283412) system[5];
244
- ry(-0.054524584043191826) system[5];
245
- cx system[1], system[5];
246
- rz(0.37091099168271024) system[1];
247
- rx(-0.0898531103887843) system[1];
248
- cx system[1], system[4];
249
- rz(0.7564792893941548) system[1];
250
- rx(0.09981868556331253) system[1];
251
- ry(0.12344133435873705) system[4];
252
- rx(0.0024245345906948046) system[4];
253
- ry(0.13832268856975505) system[5];
254
- cx system[1], system[5];
255
- rz(-1.5753513635986032) system[1];
256
- ry(-0.03270821544082847) system[1];
257
- ry(-0.016621300847767584) system[5];
258
- rx(-0.0012713157358927862) system[5];
259
- ry(-0.011070386482335604) system[8];
260
- ry(-0.01745015764595026) system[8];
261
- ry(0.03466982935076057) system[9];
262
- cx system[0], system[9];
263
- rz(0.5303440543333249) system[0];
264
- rx(-0.0747857776754477) system[0];
265
- cx system[0], system[8];
266
- rz(-0.291361770804053) system[0];
267
- ry(-0.1389306266725212) system[0];
268
- cx system[0], system[5];
269
- rz(2.1960785513194647) system[0];
270
- rx(0.06321166653231858) system[0];
271
- cx system[0], system[4];
272
- rz(-0.028653765602788983) system[0];
273
- rx(-0.11959846497038962) system[0];
274
- ry(-0.03870090499536838) system[4];
275
- ry(-0.09366818441174174) system[4];
276
- ry(-0.11466460599841577) system[5];
277
- rx(0.008805439279890415) system[5];
278
- rx(-0.002055429357030958) system[8];
279
- ry(-0.394681875468919) system[8];
280
- ry(-0.06064788641082086) system[9];
281
- rx(-0.0015336520636346496) system[9];
282
- cx system[1], system[9];
283
- rz(-0.17415695728317626) system[1];
284
- ry(0.0010960690587447086) system[1];
285
- ry(0.03723615393708113) system[9];
286
- ry(0.06644213517603537) system[9];
287
- cx system[0], system[9];
288
- rz(0.10830219207778047) system[0];
289
- ry(0.1313312713232644) system[0];
290
- cx system[0], Naimark[0];
291
- ry(0.08189302119764386) Naimark[0];
292
- rx(0.0016846656325815168) Naimark[0];
293
- rz(-0.25290503511083906) system[0];
294
- rx(0.12805862775623278) system[0];
295
- cx system[0], system[8];
296
- rz(2.6981863529702057) system[0];
297
- ry(0.06693998697527759) system[0];
298
- cx system[0], system[4];
299
- rz(3.071286737319054) system[0];
300
- rx(-0.08168592794383223) system[0];
301
- rx(-0.0014337668799899728) system[4];
302
- rx(0.0718854509265685) system[4];
303
- cx system[4], Naimark[0];
304
- ry(-0.04012471668101325) Naimark[0];
305
- rx(0.0012784437756034883) Naimark[0];
306
- rz(0.6448845966655425) system[4];
307
- rx(0.08505137767400872) system[4];
308
- ry(0.08958179102726471) system[8];
309
- rx(0.005730351508522302) system[8];
310
- rx(-0.00775267698863602) system[9];
311
- ry(-0.23062193969579026) system[9];
312
- cx system[0], system[9];
313
- rz(0.48155714023882856) system[0];
314
- ry(0.0827800965674177) system[0];
315
- cx system[0], system[1];
316
- rz(1.0680170538671268) system[0];
317
- ry(0.08619252106664987) system[0];
318
- rx(-0.0010902753266142096) system[1];
319
- ry(0.005085954782033886) system[1];
320
- ry(0.2504325304773376) system[9];
321
- rx(0.005537036085017766) system[9];
322
- cx system[0], system[9];
323
- rz(-0.5717613074707804) system[0];
324
- rx(0.10628215142116226) system[0];
325
- cx system[0], system[5];
326
- rz(0.09079765519686522) system[0];
327
- rx(-0.06959294142319883) system[0];
328
- cx system[0], system[8];
329
- rz(0.20973454851435402) system[0];
330
- rx(0.10031819775844308) system[0];
331
- ry(0.055605888538766024) system[5];
332
- ry(0.06653811687500566) system[5];
333
- ry(0.030126133746601447) system[8];
334
- rx(-0.0020961597276425437) system[8];
335
- ry(-0.238670595031655) system[9];
336
- rx(-0.004623536631043423) system[9];
337
- cx system[4], system[9];
338
- rz(-2.661855934314122) system[4];
339
- rx(-0.08527350073761375) system[4];
340
- cx system[4], system[8];
341
- rz(-0.1998476494051984) system[4];
342
- ry(-0.04297022074225376) system[4];
343
- cx system[0], system[4];
344
- rz(-2.7849458311730793) system[0];
345
- ry(0.059411390245743156) system[0];
346
- rx(-0.009589470933319966) system[4];
347
- rx(0.11490496896779456) system[4];
348
- ry(0.2753466744203168) system[8];
349
- ry(-0.032066080762984894) system[8];
350
- ry(0.2906305776340279) system[9];
351
- rx(-0.003449301863866161) system[9];
352
- cx system[0], system[9];
353
- rz(-0.40195480113888116) system[0];
354
- rx(-0.20186835265966563) system[0];
355
- cx system[0], system[1];
356
- rz(-0.5936042810024307) system[0];
357
- ry(0.2984549611053353) system[0];
358
- rx(-0.13575127552725208) system[1];
359
- ry(-0.1138658130251542) system[9];
360
- rx(0.0018681198802035226) system[9];
361
- cx system[4], system[9];
362
- rz(0.4956137828075491) system[4];
363
- ry(-0.14233398471874326) system[4];
364
- cx system[4], Naimark[0];
365
- ry(-0.0020256314263702446) Naimark[0];
366
- ry(0.016917054323362768) Naimark[0];
367
- rz(0.3296212453899574) system[4];
368
- rx(0.13383590218503838) system[4];
369
- cx system[4], system[5];
370
- rz(-0.7299538823430041) system[4];
371
- rx(-0.2253041737847825) system[4];
372
- ry(-0.10297200708214094) system[5];
373
- rx(-0.10785492671022312) system[5];
374
- ry(0.014726977342930825) system[9];
375
- ry(0.004424357873839879) system[9];
376
- cx system[5], system[9];
377
- rz(-1.0825552703309635) system[5];
378
- rx(-0.12354014292037108) system[5];
379
- cx system[5], system[8];
380
- rz(-1.3327255450052085) system[5];
381
- rx(0.10700134977535769) system[5];
382
- ry(-0.02150165088434286) system[8];
383
- ry(-0.040921142843525216) system[8];
384
- cx system[4], system[8];
385
- rz(-3.0165077784432737) system[4];
386
- ry(0.10721942699433051) system[4];
387
- cx system[4], Naimark[0];
388
- ry(0.016909955963348988) Naimark[0];
389
- ry(-0.013849213350811018) Naimark[0];
390
- cx system[1], Naimark[0];
391
- rx(0.008525982198229753) Naimark[0];
392
- ry(-0.0016211623084037008) Naimark[0];
393
- rz(0.16014213952223888) system[1];
394
- rx(-0.24926195834993492) system[1];
395
- rz(0.9337750932487947) system[4];
396
- rx(0.20641770348784738) system[4];
397
- rx(0.1762044810859893) system[8];
398
- rx(-0.0032417273765092958) system[9];
399
- ry(0.6056054020082107) system[9];
400
- cx system[8], system[9];
401
- rz(-0.24203094093886945) system[8];
402
- cx system[4], system[8];
403
- rz(-1.368986365009464) system[4];
404
- ry(-0.5166676350309127) system[4];
405
- rx(0.0035204203108085697) system[8];
406
- rx(0.06013670550180161) system[8];
407
- cx system[8], Naimark[0];
408
- rx(-0.0031968166715314883) Naimark[0];
409
- ry(0.007218043585037082) Naimark[0];
410
- rz(-0.2640744642441992) system[8];
411
- rx(0.013587674765100921) system[8];
412
- cx system[5], system[8];
413
- rz(-1.0872544057120672) system[5];
414
- rx(-0.4178980739027367) system[5];
415
- ry(0.44110596187410867) system[8];
416
- rx(-0.008850922667052563) system[8];
417
- ry(-0.43025361473080403) system[9];
418
- rx(0.0020794168451256922) system[9];
419
- cx system[1], system[9];
420
- rz(-0.2319503977186672) system[1];
421
- ry(0.21459915242482386) system[1];
422
- cx system[1], system[4];
423
- rz(-0.13400317563256214) system[1];
424
- ry(-0.040034220751908434) system[1];
425
- cx system[1], system[8];
426
- rz(2.336554940954409) system[1];
427
- ry(-0.43109598558810136) system[1];
428
- ry(0.021285237070470142) system[4];
429
- ry(0.15177411596675205) system[4];
430
- ry(-0.11711147575717051) system[8];
431
- rx(0.11448740603139851) system[8];
432
- ry(-0.13928261987447077) system[9];
433
- cx system[0], system[9];
434
- rz(-1.4994430205134193) system[0];
435
- ry(0.2662551424588313) system[0];
436
- cx system[0], system[4];
437
- rz(0.19404541603156344) system[0];
438
- ry(0.41019736289877273) system[0];
439
- cx system[0], system[1];
440
- rz(0.9757422864800007) system[0];
441
- rx(-0.22899008354397554) system[0];
442
- cx system[0], system[8];
443
- rz(-0.43097993078264807) system[0];
444
- rx(-0.26038289599157705) system[0];
445
- rx(0.015366706849811784) system[1];
446
- ry(0.3840418049603911) system[1];
447
- cx system[0], system[1];
448
- rz(0.17780867828202584) system[0];
449
- rx(-0.5275082283320511) system[0];
450
- ry(-0.2225594714278405) system[1];
451
- rx(0.3993641158494119) system[1];
452
- rx(0.092775239486667) system[4];
453
- ry(1.009272431427935) system[4];
454
- ry(0.05726105758227318) system[8];
455
- ry(0.11814497024118209) system[8];
456
- rx(-0.0059943199557608295) system[9];
457
- ry(0.3059885458578182) system[9];
458
- cx system[8], system[9];
459
- rz(-0.7462186688835626) system[8];
460
- ry(0.12766951911634217) system[8];
461
- cx system[5], system[8];
462
- rz(0.2825300257962977) system[5];
463
- rx(0.03140621251805942) system[5];
464
- ry(-0.1074325777895122) system[8];
465
- ry(-0.41969046446302194) system[8];
466
- cx system[4], system[8];
467
- rz(0.1347096567698438) system[4];
468
- ry(0.7074042146737217) system[4];
469
- cx system[4], system[5];
470
- rz(-2.548839895628648) system[4];
471
- ry(-0.12082456711065381) system[4];
472
- cx system[4], ancilla[0];
473
- rz(0.902506756001441) ancilla[0];
474
- rz(-0.31027577400284834) system[4];
475
- rz(0.6522620948404828) system[4];
476
- cx system[4], ancilla[0];
477
- ry(0.0030079357456544997) ancilla[0];
478
- rx(-3.1415914730683694) ancilla[0];
479
- ry(2.5720621305328004) system[4];
480
- rx(3.1301603542392495) system[4];
481
- ry(-0.015007284229247908) system[5];
482
- rx(2.876549947970843) system[5];
483
- ry(0.02901438214868346) system[8];
484
- ry(0.5853137058931843) system[8];
485
- cx system[8], Naimark[0];
486
- rx(0.17018509010453697) Naimark[0];
487
- rz(-1.8354993729731843) Naimark[0];
488
- rz(-0.06014166040857649) system[8];
489
- ry(-0.3868045312467434) system[8];
490
- ry(-0.10794626185486034) system[9];
491
- ry(0.28399112152493156) system[9];
492
- cx system[0], system[9];
493
- rz(-0.32353895135774935) system[0];
494
- ry(-0.9855039222221569) system[0];
495
- cx system[0], system[8];
496
- rz(-1.3990109674227673) system[0];
497
- ry(0.08412752501309795) system[0];
498
- cx system[0], system[1];
499
- rx(-0.04174725535065971) system[0];
500
- rz(2.8648233845523943) system[0];
501
- cx system[0], Naimark[0];
502
- rz(0.7485989322823312) Naimark[0];
503
- rz(0.2827342329340039) Naimark[0];
504
- ry(0.4961560477853224) system[0];
505
- rx(-0.004551723549076403) system[0];
506
- rz(-0.34153839906589) system[1];
507
- ry(0.5845302822346644) system[1];
508
- cx system[1], system[2];
509
- ry(0.02123360876465652) system[1];
510
- rz(0.03553790436213222) system[2];
511
- ry(-0.004760148102602724) system[2];
512
- cx system[2], system[3];
513
- rz(0.4487867341168277) system[2];
514
- rz(-2.728262252691723) system[3];
515
- rx(3.1415926260200457) system[3];
516
- ry(1.2375733008116148) system[8];
517
- rx(-0.7173215629078751) system[8];
518
- ry(-0.5540876384757851) system[9];
519
- rx(-0.1456420303973338) system[9];
520
- cx system[5], system[9];
521
- rz(0.19080047575453074) system[5];
522
- ry(-0.24909272808783633) system[5];
523
- cx system[5], system[6];
524
- ry(-0.023133001258463626) system[5];
525
- rx(-3.140729951618603) system[5];
526
- rz(0.22267515796325865) system[6];
527
- ry(-0.0024204591614060966) system[6];
528
- cx system[6], system[7];
529
- rz(0.20775055645975837) system[6];
530
- rx(3.1415923571646704) system[6];
531
- rz(3.064820763340073) system[7];
532
- rx(-0.10315332046222125) system[9];
533
- rx(-2.1046704610655773) system[9];
534
- cx system[9], Naimark[0];
535
- rz(-2.4930064762935715) Naimark[0];
536
- rx(0.014744786370962437) Naimark[0];
537
- rz(-0.8950669647447036) system[9];
538
- rx(1.403106574660132) system[9];
539
- cx system[8], system[9];
540
- rx(-1.002390734054222) system[8];
541
- rz(-2.4613948492245314) system[8];
542
- rz(-2.3374964486073853) system[9];
543
- rz(1.2776954221707313) system[9];
544
- cx system[8], system[9];
545
- ry(-0.2931773145826284) system[8];
546
- rx(0.45965008077031144) system[8];
547
- rx(0.007625686218259542) system[9];
548
- rx(0.022230607766974275) system[9];
549
- cx system[9], Naimark[0];
550
- rx(0.25437914021569696) Naimark[0];
551
- rx(0.12265636795048418) Naimark[0];
552
- rx(0.25353036390056394) system[9];
553
- rx(-2.9292236880862497) system[9];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/tempCodeRunnerFile.py DELETED
@@ -1 +0,0 @@
1
- from tools import *