Spaces:
Runtime error
Runtime error
Merge branch 'main' of https://huggingface.co/spaces/ansysresearch/quantum
Browse files- app.py +25 -7
- app_old.py +0 -239
- delta_impulse_generator.py +0 -360
- em/__init__.py +145 -0
- em/excitation.py +538 -0
- em/exports.py +386 -0
- em/geometry.py +306 -0
- em/globals.py +107 -0
- em/handlers.py +423 -0
- em/qpu.py +657 -0
- em/simulation.py +830 -0
- em/state.py +341 -0
- em/ui.py +738 -0
- em/utils.py +187 -0
- em_trame.py +0 -0
- pages/__init__.py +0 -4
- pages/em_page.py +0 -43
- pages/em_page_subprocess.py +0 -70
- pages/qlbm_page.py +0 -43
- pages/qlbm_page_subprocess.py +0 -128
- qlbm.py +0 -1501
- qlbm_trame.py +0 -12
- utils/fdtd_demo.ipynb +0 -326
- utils/fdtd_grid16by16_time1_dt0.1_ancilla1_offset0_fieldEz_x8_y8_adapt_aqc 1.qasm +0 -553
- utils/tempCodeRunnerFile.py +0 -1
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 |
-
|
| 54 |
|
| 55 |
# Initialize state for both modules
|
| 56 |
qlbm_embedded.init_state()
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
except Exception:
|
| 190 |
-
|
|
|
|
| 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 *
|
|
|
|
|
|