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