dummyQuantum / app.py
Apurva Tiwari
Remove all browser lifecycle detection - embrace state persistence
c7f9fd7
"""
Quantum Applications - Unified Single-Server App with Simplified Session Management
This app provides EM scattering and QLBM pages in a single Trame server
with simplified session management based on filesystem directories and persistent indices.
"""
import os
import errno
import uuid
os.environ["OMP_NUM_THREADS"] = "1"
from trame.app import get_server
from trame_vuetify.ui.vuetify3 import SinglePageLayout
from trame_vuetify.widgets import vuetify3
from trame.widgets import html as trame_html
import threading
import time
import base64
from simple_session_manager import SESSION_MANAGER, get_session_by_alias
# Create a single server for the entire app
server = get_server()
state, ctrl = server.state, server.controller
# --- App state
state.current_page = None # None = landing, "EM" or "QLBM"
state.session_active = False
state.session_alias = ""
state.current_session_id = None
# Session card UI state
state.session_card_visible = True
state.session_alias_input = ""
state.session_action_success = False
state.session_action_busy = False
state.session_error = ""
state.session_action_trigger = None # "create" or "load" when user clicks buttons
# --- Logo Loading ---
def _load_logo_data_uri():
base_dir = os.path.dirname(__file__)
candidates = [
os.path.join(base_dir, "ansys-part-of-synopsys-logo.svg")
]
for p in candidates:
if os.path.exists(p):
ext = os.path.splitext(p)[1].lower()
mime = "image/svg+xml" if ext == ".svg" else ("image/png" if ext == ".png" else "image/jpeg")
with open(p, "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
return f"data:{mime};base64,{b64}"
return None
state.logo_src = _load_logo_data_uri()
# --- Import Embedded Modules ---
# These are lightweight modules that build UI without creating their own servers
import qlbm_embedded
import em # Use the modular em package instead of em_embedded
# Set the shared server on both modules
qlbm_embedded.set_server(server)
em.set_server(server)
# Initialize state for both modules
qlbm_embedded.init_state()
em.init_state()
# Register EM handlers
em.register_handlers()
# --- Session Callbacks ---
def on_session_selected(session_id: str):
"""Callback when user selects a session from landing page."""
try:
session_info = SESSION_MANAGER.get_session(session_id=session_id)
if not session_info:
state.session_error = f"Session not found: {session_id}"
return
state.current_session_id = session_id
state.session_alias = session_info.get("alias", "Untitled")
state.session_active = True
print(f"[APP] Session selected: {state.session_alias} ({session_id})")
except Exception as e:
print(f"[APP] Error selecting session: {e}")
state.session_error = str(e)
state.session_active = False
# Watcher to reset navigation when no session is active
@state.change("session_active")
def _on_session_status_change(session_active, **kwargs):
"""Reset landing page if session becomes inactive."""
if not session_active and state.current_page is not None:
state.current_page = None
state.session_card_visible = True
print("[APP] Session inactive; reset to landing page")
# --- Session action watchers (respond to state changes from UI) ---
@state.change("session_action_trigger")
def _on_session_action(session_action_trigger, **kwargs):
"""Handle session create/load when user clicks buttons."""
if session_action_trigger == "create":
try:
state.session_action_busy = True
alias = state.session_alias_input.strip() if state.session_alias_input else f"session-{uuid.uuid4().hex[:6]}"
# Create new session
session_info = SESSION_MANAGER.create_session(alias=alias)
session_id = session_info["session_id"]
state.session_alias = session_info.get("alias", "Untitled")
state.session_active = True
state.current_session_id = session_id
state.current_page = None
state.session_alias_input = ""
state.session_action_success = True
state.session_error = ""
print(f"[APP] Session created: {alias} ({session_id})")
# Hide card after success animation
def _hide():
time.sleep(1.5)
state.session_card_visible = False
state.session_action_success = False
state.session_action_busy = False
state.session_action_trigger = None
threading.Thread(target=_hide, daemon=True).start()
except Exception as e:
state.session_error = str(e)
state.session_action_busy = False
state.session_action_trigger = None
print(f"[APP] Error creating session: {e}")
elif session_action_trigger == "load":
try:
state.session_action_busy = True
alias = state.session_alias_input.strip()
if not alias:
state.session_error = "Please enter an alias to load."
state.session_action_busy = False
state.session_action_trigger = None
return
# Load session by alias
session_info = get_session_by_alias(alias)
if not session_info:
state.session_error = f"No session found with alias '{alias}'."
state.session_action_busy = False
state.session_action_trigger = None
return
session_id = session_info["session_id"]
state.session_alias = session_info.get("alias", "Untitled")
state.session_active = True
state.current_session_id = session_id
state.current_page = None
state.session_alias_input = ""
state.session_action_success = True
state.session_error = ""
print(f"[APP] Session loaded: {alias} ({session_id})")
def _hide_load():
time.sleep(1.5)
state.session_card_visible = False
state.session_action_success = False
state.session_action_busy = False
state.session_action_trigger = None
threading.Thread(target=_hide_load, daemon=True).start()
except Exception as e:
state.session_error = str(e)
state.session_action_busy = False
state.session_action_trigger = None
print(f"[APP] Error loading session: {e}")
# --- Build the Layout ---
with SinglePageLayout(server) as layout:
layout.title.set_text("dummyquantum - Experiment")
layout.toolbar.classes = "pl-2 pr-1 py-1 elevation-0"
layout.toolbar.style = "background-color: #ffffff; border-bottom: 3px solid #5f259f;"
# Custom CSS
trame_html.Style("""
:root { --v-theme-primary: 95, 37, 159; }
.landing-card:hover { transform: translateY(-4px); transition: transform 0.2s ease; }
@keyframes successBounce {
0% { transform: scale(0.85); opacity: 0; }
50% { transform: scale(1.12); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
.success-bounce { animation: successBounce 0.6s ease; color: #1e9e3e; }
""")
with layout.toolbar:
vuetify3.VSpacer()
# Back button (shown when in a sub-app)
@ctrl.add("return_to_landing")
def return_to_landing():
"""Return to landing page."""
state.current_page = None
vuetify3.VBtn(
v_if="current_page",
text="Main Page",
variant="text",
color="primary",
prepend_icon="mdi-arrow-left",
click="current_page = null",
classes="mr-2",
)
# Current session indicator
vuetify3.VChip(
v_if="session_active",
label=True,
color="info",
text_color="white",
children=["Session: {{ session_alias }}"],
size="small",
classes="mr-2",
)
# Current page indicator
vuetify3.VChip(
v_if="current_page",
label=True,
color="primary",
text_color="white",
children=["{{ current_page === 'EM' ? 'Electromagnetic Scattering' : 'Quantum LBM' }}"],
classes="mr-2",
)
# Logo
vuetify3.VImg(
v_if="logo_src",
src=("logo_src", None),
style="height: 40px; width: auto;",
classes="ml-2",
)
with layout.content:
# === Simple Landing Page (compact) ===
with vuetify3.VContainer(
v_if="!current_page",
fluid=True,
classes="fill-height d-flex align-center justify-center pa-6",
):
with vuetify3.VSheet(
elevation=6,
rounded=True,
style="max-width: 1080px; width: 100%; background: linear-gradient(135deg, #fdfbff, #f3ecff);",
classes="pa-8",
):
vuetify3.VCardTitle(
"Quantum Applications Hub",
classes="text-h4 text-primary font-weight-bold mb-2 text-center",
)
vuetify3.VCardSubtitle(
"Choose a quantum CAE app",
classes="text-body-1 text-center mb-6",
)
with vuetify3.VRow(justify="center", align="stretch", class_="text-left"):
# EM Card
with vuetify3.VCol(cols=12, md=5, class_="d-flex"):
with vuetify3.VCard(elevation=4, classes="pa-6 flex-grow-1 landing-card"):
vuetify3.VIcon("mdi-radar", size=52, color="primary", classes="mb-4")
vuetify3.VCardTitle("Electromagnetic Scattering", classes="text-h5 mb-2")
vuetify3.VCardText(
"Simulate electromagnetic wave scattering using quantum Hamiltonian Simulation.",
classes="text-body-2 mb-6",
)
vuetify3.VBtn(
text="Launch EM",
color="primary",
block=True,
prepend_icon="mdi-play-circle",
size="large",
click="current_page = 'EM'",
)
# QLBM Card
with vuetify3.VCol(cols=12, md=5, class_="d-flex"):
with vuetify3.VCard(elevation=4, classes="pa-6 flex-grow-1 landing-card"):
vuetify3.VIcon("mdi-water", size=52, color="secondary", classes="mb-4")
vuetify3.VCardTitle("Fluids", classes="text-h5 mb-2")
vuetify3.VCardText(
"3D fluid simulation using a quantum analog of the classical Lattice Boltzmann method.",
classes="text-body-2 mb-6",
)
vuetify3.VBtn(
text="Launch QLBM",
color="secondary",
block=True,
prepend_icon="mdi-play-circle",
size="large",
click="current_page = 'QLBM'",
)
# Small floating corner card for session management (create / load)
with vuetify3.VContainer(v_if="session_card_visible", classes="pa-0", style="position: absolute; right: 16px; bottom: 16px; max-width: 320px;"):
with vuetify3.VCard(elevation=8, classes="pa-3", style="background: rgba(255,255,255,0.98); width: 100%;"):
vuetify3.VCardTitle("Session", classes="text-subtitle-1 mb-1")
vuetify3.VCardText("Load an existing session by alias or create a new one.", classes="text-body-2 mb-3")
vuetify3.VTextField(label="Session Alias", v_model=("session_alias_input", None), placeholder="my-session")
# App selection moved to main landing page; sessions may hold data for both apps.
with vuetify3.VRow(gutter="8"):
with vuetify3.VCol(cols=6):
vuetify3.VBtn(text="Create", color="primary", block=True, disabled=("session_action_busy", None), click="session_action_trigger = 'create'")
with vuetify3.VCol(cols=6):
vuetify3.VBtn(text="Load", color="secondary", block=True, disabled=("session_action_busy", None), click="session_action_trigger = 'load'")
# Success / error feedback
vuetify3.VRow()
with vuetify3.VCol():
vuetify3.VIcon(v_if="session_action_success", children=["mdi-check-circle"], class_="success-bounce", size=36)
vuetify3.VAlert(v_if="session_error", type="error", dense=True, text=True, children=["{{ session_error }}"])
# === EM Experience ===
with vuetify3.VContainer(
v_if="current_page === 'EM'",
fluid=True,
classes="pa-0 fill-height",
):
em.build_ui()
# === QLBM Experience ===
with vuetify3.VContainer(
v_if="current_page === 'QLBM'",
fluid=True,
classes="pa-0 fill-height",
):
qlbm_embedded.build_ui()
# Enable point picking after UI is built (prevents KeyError with Trame state)
em.enable_point_picking_on_plotter()
# Reset to landing page on every server startup (so fresh browser loads start at landing)
state.current_page = None
state.session_card_visible = True
print("[OK] Landing page state initialized on startup")
# --- Heartbeat for HuggingFace ---
def _start_hf_heartbeat_thread(interval_s: int = 5):
"""Keep the WebSocket alive for HuggingFace Spaces."""
import asyncio
def _loop():
while True:
time.sleep(interval_s)
try:
# Create a new event loop for this thread if needed
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Try to flush, but don't crash if it fails
if hasattr(server, 'controller') and hasattr(server.controller, 'flush'):
server.controller.flush()
except Exception:
# Silently continue - heartbeat is optional
pass
t = threading.Thread(target=_loop, daemon=True, name="HeartbeatThread")
t.start()
# --- Entry Point ---
if __name__ == "__main__":
_start_hf_heartbeat_thread(interval_s=5)
# Check if running on HF Spaces with launcher
import sys
if "--launcher" in sys.argv or os.environ.get("HF_SPACE_ID"):
# Running on HF Spaces with multi-user launcher
# The launcher will manage user isolation and sessions
server.start(open_browser=False)
else:
# Local development mode
host = "0.0.0.0"
port = int(os.environ.get("PORT", "7860"))
server.start(host=host, port=port, open_browser=False)