Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |
| 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) --- | |
| 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) | |
| 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) | |