Spaces:
Runtime error
Runtime error
| """ | |
| Quantum Applications - Unified Single-Server App | |
| This app provides both EM Scattering and QLBM experiences in a single Trame server, | |
| avoiding the multi-server/iframe approach that causes issues on HuggingFace Spaces. | |
| """ | |
| import os | |
| import errno | |
| 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 | |
| # 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" | |
| # --- 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 (must be done after server binding) | |
| em.register_handlers() | |
| # --- Build the Layout --- | |
| with SinglePageLayout(server) as layout: | |
| layout.title.set_text("Quantum Applications") | |
| 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; } | |
| """) | |
| with layout.toolbar: | |
| vuetify3.VSpacer() | |
| # Back button (shown when in a sub-app) | |
| 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 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: | |
| # === Landing Page === | |
| 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. Time-domain Maxwell equations are re-cast to Schrödinger-type equations and an equivalent Hamiltonian is computed, and evolved over time. " | |
| "Configure geometry, excitation, and visualize field propagation.", | |
| 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 the classical Lattice Boltzmann method." | |
| " Explore advection-diffusion with quantum-enhanced computation." | |
| " Configure geometry, initial condition, and visualize field propagation.", | |
| 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'", | |
| ) | |
| # === 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() | |
| # --- 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 heartbeat | |
| _start_hf_heartbeat_thread(interval_s=5) | |
| # Get port from environment (HuggingFace) or use default | |
| base_port = int(os.environ.get("APP_PORT") or os.environ.get("PORT", 7860)) | |
| host = os.environ.get("APP_HOST", "0.0.0.0") | |
| max_attempts = 10 | |
| print(f"Starting Quantum Applications server on {host}:{base_port}") | |
| print("This is a SINGLE SERVER serving both EM and QLBM experiences.") | |
| for attempt in range(max_attempts): | |
| port = base_port + attempt | |
| try: | |
| server.start(host=host, port=port, open_browser=False) | |
| break | |
| except OSError as exc: | |
| if getattr(exc, "errno", None) == errno.EADDRINUSE and attempt < max_attempts - 1: | |
| print(f"Port {port} busy, retrying on port {port + 1}...") | |
| continue | |
| raise | |