"""UI components and build_ui function for the EM module.""" from __future__ import annotations from typing import TYPE_CHECKING import plotly.graph_objects as go from trame.widgets import html as trame_html, vuetify3, plotly as plotly_widgets from pyvista.trame.ui import plotter_ui from .state import state, ctrl, get_server from .globals import plotter, GRID_SIZES from .geometry import build_geometry_placeholder as _build_geometry_placeholder from .excitation import build_excitation_placeholder as _build_excitation_placeholder from .simulation import run_simulation_only, reset_to_defaults, stop_simulation_handler, process_uploaded_em_job_result from .exports import ( export_vtk, export_vtk_all_frames, export_mp4, export_sim_timeseries_csv, export_sim_timeseries_png, export_sim_timeseries_html, export_qpu_timeseries_csv, export_qpu_timeseries_png, export_qpu_timeseries_html, ) from .handlers import build_qubit_plot if TYPE_CHECKING: pass # --------------------------------------------------------------------------- # Placeholder builders for preview areas # --------------------------------------------------------------------------- def _build_empty_figure(message: str = "") -> go.Figure: """Build an empty placeholder figure with optional message.""" fig = go.Figure() if message: fig.add_annotation( text=message, x=0.5, y=0.5, showarrow=False, font=dict(size=16, color="#888") ) fig.update_layout( margin=dict(l=20, r=20, t=40, b=20), paper_bgcolor="#ffffff", plot_bgcolor="#ffffff", height=400, ) fig.update_xaxes(visible=False) fig.update_yaxes(visible=False) return fig # --------------------------------------------------------------------------- # Main UI Builder # --------------------------------------------------------------------------- def build_ui(): """Render the EM UI inside the host layout.""" _server = get_server() if _server is None or not state.bound or not ctrl.bound: raise RuntimeError('Call set_server(server) before build_ui().') with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"): # Upload Dialog with vuetify3.VDialog(v_model=("show_upload_dialog", False), max_width="500px"): with vuetify3.VCard(): vuetify3.VCardTitle("Upload Geometry") with vuetify3.VCardText(classes="py-1 px-2"): vuetify3.VFileInput( show_size=True, label="Select geometry file", accept=".vtp,.vtk,.glb,.stl", update_binary=("uploaded_file_info", 1), ) with vuetify3.VCardActions(): vuetify3.VSpacer() vuetify3.VBtn("Cancel", click="show_upload_dialog = false") # Snackbars for status messages vuetify3.VSnackbar( v_model=("show_upload_status", False), children=["{{ upload_status_message }}"], timeout=4000, location="bottom right", color="primary", variant="tonal", ) vuetify3.VSnackbar( v_model=("show_export_status", False), children=["{{ export_status_message }}"], timeout=4000, location="bottom right", color="primary", variant="tonal", ) with vuetify3.VRow(no_gutters=True, classes="fill-height"): # Left Column - Configuration with vuetify3.VCol(cols=5, classes="pa-1 d-flex flex-column"): _build_overview_card() _build_geometry_card() _build_excitation_card() _build_material_card() _build_time_card() _build_meshing_card() _build_backends_card() _build_output_preferences_card() _build_run_buttons() _build_job_upload_card() vuetify3.VSpacer() _build_reset_button() # Right Column - Visualization with vuetify3.VCol(cols=7, classes="pa-1 d-flex flex-column"): _build_output_config_card() _build_main_plot_area() _build_qpu_plot_area() _build_no_geometry_placeholder() _build_status_console() # Floating status window _build_status_window() def _build_overview_card(): """Build the Overview/Introduction card.""" with vuetify3.VCard(classes="mb-1", style=("overview_card_style", "font-size: 0.8rem;")): with vuetify3.VCardTitle("Overview", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2"): vuetify3.VSelect( label="Select a problem", v_model=("problem_selection", None), items=( "problem_options", [ "Propagation in a given medium (no bodies)", "Scattering from a perfectly conducting body", ], ), placeholder="Select a problem", density="compact", color="primary", ) def _build_geometry_card(): """Build the Geometry configuration card.""" with vuetify3.VCard(classes="mb-1", style=("geometry_card_style", "font-size: 0.8rem;")): with vuetify3.VCardTitle("Geometry", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2"): vuetify3.VSelect( label="Select", v_model=("geometry_selection", None), items=("geometry_options",), placeholder="Select", density="compact", color="primary", ) with vuetify3.VContainer(v_if="geometry_selection === 'Square Metallic Body'", classes="pa-0 mt-2"): with vuetify3.VRow(dense=True): with vuetify3.VCol(): with vuetify3.VTooltip("Square hole edge length s in domain units [0,1]. Must be ≤ 1. UI-only.", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", v_model=("hole_size_edge", 0.2), label="Hole Edge Length [0 - 1]", type="number", step=0.05, min=0, density="compact", color="primary", ) with vuetify3.VCol(): 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"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", v_model=("hole_center_pair", "(0.5, 0.5)"), label="Hole Center (X, Y)", density="compact", color="primary", ) with vuetify3.VRow(dense=True, classes="mt-1"): with vuetify3.VCol(cols=12): vuetify3.VSwitch( v_model=("hole_snap", True), label="Snap edges to nearest grid lines", color="primary", inset=True, density="compact", ) vuetify3.VAlert( v_if="hole_error_message", type="error", variant="tonal", density="compact", children=["{{ hole_error_message }}"], classes="mt-1", ) def _build_excitation_card(): """Build the Excitation/Initial State configuration card.""" with vuetify3.VCard(classes="mb-1", style=("excitation_card_style", "font-size: 0.8rem;")): with vuetify3.VCardTitle("Excitation: Initial State", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2"): with vuetify3.VRow(classes="d-flex align-center", dense=True, no_gutters=True): with vuetify3.VCol(classes="flex-grow-1"): vuetify3.VSelect( label="Select", v_model=("dist_type", None), items=("dist_type_options", ["None", "Delta", "Gaussian"]), placeholder="Select", density="compact", color="primary", ) with vuetify3.VCol(cols="auto", classes="ml-1"): with vuetify3.VTooltip("Show or hide configuration for the selected excitation", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): with vuetify3.VBtn( icon=True, density="compact", variant="text", click="excitation_config_open = !excitation_config_open", disabled=("!dist_type", False), v_bind="props", ): vuetify3.VIcon("mdi-cog", color=("excitation_config_open ? 'primary' : 'grey'",)) trame_html.Span("Toggle parameter inputs for the chosen excitation") # Delta config with vuetify3.VExpandTransition(): with vuetify3.VSheet( v_if="excitation_config_open && dist_type === 'Delta'", classes="pa-2 mt-1 rounded-lg", style="background-color: rgba(95,37,159,0.03); border: 1px solid rgba(95,37,159,0.15);", ): with vuetify3.VTooltip("Impulse position (x, y) in [0,1]. Example: (0.6, 0.6).", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", v_model=("peak_pair", "(0.5, 0.5)"), label="Peak (x, y) in [0,1]", density="compact", color="primary", ) # Gaussian config with vuetify3.VExpandTransition(): with vuetify3.VSheet( v_if="excitation_config_open && dist_type === 'Gaussian'", classes="pa-2 mt-1 rounded-lg", style="background-color: rgba(95,37,159,0.03); border: 1px solid rgba(95,37,159,0.15);", ): with vuetify3.VRow(dense=True): with vuetify3.VCol(): with vuetify3.VTooltip("Gaussian center μ (x, y) in [0,1]. Example: (0.5, 0.5).", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", v_model=("mu_pair", "(0.5, 0.5)"), label="Mu (x, y) in [0,1]", density="compact", color="primary", ) with vuetify3.VRow(dense=True, classes="mt-1"): with vuetify3.VCol(): with vuetify3.VTooltip("Gaussian spread σx in [0,1] of domain length.", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): 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", ) with vuetify3.VCol(): with vuetify3.VTooltip("Gaussian spread σy in [0,1] of domain length.", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): 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", ) vuetify3.VAlert(v_if="excitation_error_message", type="error", variant="tonal", density="compact", children=["{{ excitation_error_message }}"], classes="mt-1") vuetify3.VAlert( v_if="excitation_info_message", type="info", variant="tonal", density="compact", children=["{{ excitation_info_message }}"], classes="mt-1", style="white-space: pre-line;", ) def _build_material_card(): """Build the Material Properties card.""" with vuetify3.VCard(classes="mb-1", style="font-size: 0.8rem;"): with vuetify3.VCardTitle("Material Properties (Medium)", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2"): with vuetify3.VRow(dense=True): with vuetify3.VCol(cols="6"): with vuetify3.VTooltip("Relative permittivity. ε_r = ε / ε₀. Default 1.0 (free space).", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField(v_bind="props", v_model=("coeff_permittivity", 1.0), label="Permittivity (εr)", type="number", step="0.1", density="compact", color="primary") with vuetify3.VCol(cols="6"): with vuetify3.VTooltip("Relative permeability. μ_r = μ / μ₀. Default 1.0 (non-magnetic).", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField(v_bind="props", v_model=("coeff_permeability", 1.0), label="Permeability (μr)", type="number", step="0.1", density="compact", color="primary") def _build_time_card(): """Build the Time configuration card.""" with vuetify3.VCard(classes="mb-1", style="font-size: 0.8rem;"): with vuetify3.VCardTitle("Time", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2"): with vuetify3.VTooltip("Sets the total duration for the simulation to run.", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", v_model=("T", 1.0), label="Total Time (T)", type="number", step="0.1", density="compact", color="primary", ) def _build_meshing_card(): """Build the Meshing card with qubit plot hover.""" with vuetify3.VCard(classes="mb-1", style=("meshing_card_style", "font-size: 0.8rem;")): with vuetify3.VCardTitle("Meshing", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2"): with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"): with vuetify3.Template(v_slot_activator="{ props }"): with vuetify3.VSlider( v_bind="props", v_model=("nx_slider_index", None), label="No. of points per direction:", min=0, max=len(GRID_SIZES) - 1, step=1, show_ticks="always", thumb_label="always", density="compact", color="primary", ): vuetify3.Template( v_slot_thumb_label="{ modelValue }", children=["{{ modelValue === null ? 'Select' : grid_size_labels[modelValue] }}"], ) # Hover content: enlarged Plotly graph with vuetify3.VSheet(classes="pa-2", elevation=6, rounded=True, style="width: 644px;"): qubit_fig_widget = plotly_widgets.Figure( figure=build_qubit_plot(int(state.nx or 16)), responsive=True, style="width: 616px; height: 364px; min-height: 364px;", ) ctrl.qubit_plot_update = qubit_fig_widget.update def _build_backends_card(): """Build the Backends selection card.""" with vuetify3.VCard(classes="mb-1", style=("backend_card_style", "font-size: 0.8rem;")): with vuetify3.VCardTitle("Backends", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2"): with vuetify3.VRow(dense=True, classes="mb-2"): with vuetify3.VCol(): vuetify3.VAlert( type="info", color="primary", variant="tonal", density="compact", children=[ "Selected: ", "{{ backend_type || '—' }}", " - ", "{{ backend_type === 'Simulator' ? selected_simulator : (backend_type === 'QPU' ? selected_qpu : '—') }}", ], ) with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VBtn(v_bind="props", text="Choose Backend", color="primary", variant="tonal", block=True) with vuetify3.VList(density="compact"): with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VListItem(v_bind="props", title="Simulator", prepend_icon="mdi-robot-outline", append_icon="mdi-chevron-right") with vuetify3.VList(density="compact"): vuetify3.VListItem(title="IBM Qiskit simulator", click="backend_type = 'Simulator'; selected_simulator = 'IBM Qiskit simulator'") vuetify3.VListItem(title="Statevector Estimator", click="backend_type = 'Simulator'; selected_simulator = 'Statevector Estimator'") with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VListItem(v_bind="props", title="QPU", prepend_icon="mdi-chip", append_icon="mdi-chevron-right") with vuetify3.VList(density="compact"): vuetify3.VListItem(title="IBM QPU", click="backend_type = 'QPU'; selected_qpu = 'IBM QPU'") vuetify3.VListItem(title="IonQ QPU", click="backend_type = 'QPU'; selected_qpu = 'IonQ QPU'") def _build_output_preferences_card(): """Build the Output Preferences card (appears after backend selection).""" with vuetify3.VCard(v_if="backend_type", classes="mb-0", style=("output_card_style", "font-size: 0.8rem;")): with vuetify3.VCardTitle("Output Preferences", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2"): vuetify3.VCardSubtitle("Select Δt intervals for plotting output snapshots", classes="text-caption font-weight-bold mt-1", style="font-size: 0.75rem;") with vuetify3.VTooltip("Snapshot interval (Δt). Solver runs at fixed 0.1 s; frames are saved every Δt.", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField(v_bind="props", v_model=("dt_user", 0.1), label="Δt", type="number", step="0.1", density="compact", color="primary", classes="mt-1") vuetify3.VAlert(v_if="temporal_warning", type="warning", variant="tonal", density="compact", children=["{{ temporal_warning }}"], classes="mt-1") # IBM QPU monitor options (single field, single position ONLY) with vuetify3.VContainer(v_if="backend_type === 'QPU' && selected_qpu === 'IBM QPU'", classes="pa-0 mt-2"): vuetify3.VAlert( type="info", variant="tonal", density="compact", children=["IBM QPU: Only ONE field component and ONE monitor position supported."], classes="mb-2", ) with vuetify3.VRow(dense=True, classes="mb-1 align-center"): with vuetify3.VCol(cols=4, sm=3, md=3): vuetify3.VSelect( label="Field", v_model=("qpu_field_components", "Ez"), items=("ibm_qpu_field_options", ["Ez", "Hx", "Hy"]), density="compact", color="primary", hide_details=True, style="max-width: 160px;", ) with vuetify3.VCol(cols=8, sm=9, md=9): vuetify3.VTextField( label="Monitor position (x, y) in [0,1]", v_model=("qpu_monitor_samples", "(0.5, 0.5)"), density="compact", color="primary", hide_details=True, style="max-width: 320px;", hint="Single position only for IBM QPU", ) vuetify3.VAlert( v_if="qpu_monitor_sample_info", type="info", variant="tonal", density="compact", children=["{{ qpu_monitor_sample_info }}"], classes="mb-1", style="white-space: pre-line;", ) # Statevector Estimator monitor options (supports multiple fields and positions) with vuetify3.VContainer(v_if="backend_type === 'Simulator' && selected_simulator === 'Statevector Estimator'", classes="pa-0 mt-2"): with vuetify3.VRow(dense=True, classes="mb-1 align-center"): with vuetify3.VCol(cols=4, sm=3, md=3): vuetify3.VSelect( label="Field", v_model=("qpu_field_components", "Ez"), items=("qpu_field_options", ["All", "Ez", "Hx", "Hy"]), density="compact", color="primary", hide_details=True, style="max-width: 160px;", ) with vuetify3.VCol(cols=8, sm=9, md=9): vuetify3.VTextField( label="Sample position(s) (x, y) in [0,1]", v_model=("qpu_monitor_samples", "(0.5, 0.5)"), density="compact", color="primary", hide_details=True, style="max-width: 320px;", ) vuetify3.VAlert( v_if="qpu_monitor_sample_info", type="info", variant="tonal", density="compact", children=["{{ qpu_monitor_sample_info }}"], classes="mb-1", style="white-space: pre-line;", ) def _build_job_upload_card(): """Build the Job Result Upload card for loading previously saved QPU results.""" with vuetify3.VCard(classes="mb-1", style="font-size: 0.8rem;"): with vuetify3.VCardTitle("Upload Results", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2"): trame_html.Div( "Retrieve completed job results from IBM or IonQ using the Job ID", classes="text-caption text-medium-emphasis mb-2", ) # Platform selector with vuetify3.VTooltip(location="top"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VSelect( v_bind="props", label="Platform", v_model=("em_job_platform", "IBM"), items=("['IBM', 'IonQ']",), density="compact", hide_details=True, color="primary", classes="mb-2", prepend_icon="mdi-chip", ) trame_html.Span("Select the quantum hardware provider (IBM or IonQ)") # Job ID input with vuetify3.VTooltip(location="top"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", label="Job ID", v_model=("em_job_id", ""), density="compact", hide_details=True, color="primary", classes="mb-2", placeholder="e.g., 019b368e-6e22-7525-8512-fd16e0503673", prepend_icon="mdi-identifier", ) trame_html.Span("Enter the Job ID (UUID format from IBM or IonQ)") # Field / monitor / time parameters with vuetify3.VRow(dense=True, classes="mb-2"): with vuetify3.VCol(cols=6): with vuetify3.VTooltip(location="top"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VSelect( v_bind="props", label="Field Type", v_model=("em_job_field_type", "Ez"), items=("['Ez', 'Hx', 'Hy']",), density="compact", hide_details=True, color="primary", ) trame_html.Span("Select which field component to plot (Ez, Hx, Hy)") with vuetify3.VCol(cols=6): with vuetify3.VTooltip(location="top"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", label="Monitor Point (x, y)", v_model=("em_job_monitor_point", "(0, 0)"), density="compact", hide_details=True, color="primary", placeholder="(x, y)", ) trame_html.Span("Integer grid indices of the monitor point") with vuetify3.VRow(dense=True, classes="mb-2"): with vuetify3.VCol(cols=4): with vuetify3.VTooltip(location="top"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", label="Total Time (T)", v_model=("em_job_total_time", 1.0), type="number", density="compact", hide_details=True, color="primary", ) trame_html.Span("Total simulation time") with vuetify3.VCol(cols=4): with vuetify3.VTooltip(location="top"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", label="Snapshot Δt", v_model=("em_job_snapshot_dt", 0.1), type="number", density="compact", hide_details=True, color="primary", ) trame_html.Span("Time between snapshots") with vuetify3.VCol(cols=4): with vuetify3.VTooltip(location="top"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VTextField( v_bind="props", label="No. of Points per Direction", v_model=("em_job_nx", 4), type="number", density="compact", hide_details=True, color="primary", ) trame_html.Span("Grid dimension nx (grid is nx × nx)") vuetify3.VBtn( text="Retrieve & Generate Plot", color="secondary", variant="tonal", block=True, disabled=("!em_job_id || em_job_is_processing", True), loading=("em_job_is_processing", False), click=process_uploaded_em_job_result, prepend_icon="mdi-chart-box-outline", classes="mb-2", ) # Success message vuetify3.VAlert( v_if="em_job_upload_success", type="success", variant="tonal", density="compact", closable=True, children=["{{ em_job_upload_success }}"], classes="mt-2", ) # Error message vuetify3.VAlert( v_if="em_job_upload_error", type="error", variant="tonal", density="compact", closable=True, children=["{{ em_job_upload_error }}"], classes="mt-2", ) def _build_run_buttons(): """Build the Run and Stop buttons row.""" with vuetify3.VRow(dense=True, classes="mb-2"): with vuetify3.VCol(cols=9): with vuetify3.VTooltip("Starts the quantum simulation with the specified parameters.", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VBtn( v_bind="props", text=("run_button_text", "RUN!"), click=run_simulation_only, color="primary", block=True, disabled=("is_running || run_button_text === 'Successful!' || !geometry_selection || !dist_type || !!temporal_warning || nx === null || !backend_type", False), ) with vuetify3.VCol(cols=3): with vuetify3.VTooltip("Stop the running simulation", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VBtn( v_bind="props", text="Stop", click=stop_simulation_handler, color="error", block=True, disabled=("stop_button_disabled", True), ) def _build_reset_button(): """Build the Reset button.""" with trame_html.Div(style="flex: 0 0 auto;"): with vuetify3.VTooltip("Reset all parameters to their default values", location="bottom", color="primary"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VBtn( v_bind="props", text="Reset", click=reset_to_defaults, color="secondary", block=True, ) def _build_output_config_card(): """Build the Output Configuration card (appears after simulation, not for QPU).""" with vuetify3.VCard(v_if="simulation_has_run && backend_type !== 'QPU' && !(backend_type === 'Simulator' && selected_simulator === 'Statevector Estimator')", classes="mb-1", style="font-size: 0.8rem;"): with vuetify3.VCardSubtitle("Output Configuration", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px; font-weight: 600; color: #1A1A1A;"): with vuetify3.VCardText(classes="py-1 px-2", style="color: #1A1A1A;"): with vuetify3.VRadioGroup(v_model=("output_type", "Surface Plot"), row=True, density="compact", color="primary"): vuetify3.VRadio(label="Surface", value="Surface Plot", style="font-weight: 500;") vuetify3.VRadio(label="Time Series", value="Time Series Plot", style="font-weight: 500;") # Surface Plot options with vuetify3.VContainer(v_if="output_type === 'Surface Plot'", classes="pa-0"): vuetify3.VSelect(v_model=("surface_field", "Ez"), items=("surface_field_options", ["Ez", "Hx", "Hy"]), label="Field Component", density="compact", color="primary", style="font-weight: 500;") with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VBtn(v_bind="props", text="DOWNLOAD", color="primary", variant="tonal", block=True, classes="mt-1") with vuetify3.VList(density="compact"): vuetify3.VListSubheader("VTK") vuetify3.VListItem(title="Current frame (VTK)", prepend_icon="mdi-download", click=export_vtk) vuetify3.VListItem(title="All frames (VTK sequence)", prepend_icon="mdi-download-multiple", click=export_vtk_all_frames) vuetify3.VDivider() vuetify3.VListItem(title="Animation (MP4)", prepend_icon="mdi-movie", click=export_mp4) # Time Series options with vuetify3.VContainer(v_if="output_type === 'Time Series Plot'", classes="pa-0"): vuetify3.VSelect(v_model=("timeseries_field", "Ez"), items=("timeseries_field_options", ["All", "Ez", "Hx", "Hy"]), label="Field Component", density="compact", color="primary", style="font-weight: 500;") vuetify3.VTextarea( v_model=("timeseries_points", "(0.5, 0.5)"), label="Monitor Position(s) (x, y) in [0,1]", hint="e.g., (0.50, 0.50) or multiple comma-separated pairs", rows=2, auto_grow=True, color="primary", style="font-weight: 500;", ) vuetify3.VAlert( v_if="timeseries_point_info", type="info", variant="tonal", density="compact", children=["{{ timeseries_point_info }}"], classes="mt-1", style="white-space: pre-line;", ) with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end"): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VBtn(v_bind="props", text="DOWNLOAD", color="primary", variant="tonal", block=True, classes="mt-1") with vuetify3.VList(density="compact"): vuetify3.VListItem(title="Download CSV", prepend_icon="mdi-download", click=export_sim_timeseries_csv) vuetify3.VListItem(title="Download PNG", prepend_icon="mdi-image", click=export_sim_timeseries_png) vuetify3.VListItem(title="Download HTML", prepend_icon="mdi-file-html", click=export_sim_timeseries_html) def _build_main_plot_area(): """Build the main plot area (PyVista for Simulator).""" with vuetify3.VCard( v_if="geometry_selection && ((backend_type !== 'QPU' && !(backend_type === 'Simulator' && selected_simulator === 'Statevector Estimator')) || !qpu_ts_ready) && !(is_running && (backend_type === 'QPU' || (backend_type === 'Simulator' && selected_simulator === 'Statevector Estimator')))", classes="mb-1 flex-grow-1 d-flex flex-column", style="min-height: 0;", ): # Running indicator with vuetify3.VContainer(v_if="is_running", fluid=True, classes="fill-height d-flex flex-column align-center justify-center"): vuetify3.VProgressCircular(indeterminate=True, size=64, color="primary") vuetify3.VCardSubtitle("Running simulation...", classes="mt-4") # Geometry preview (no excitation selected yet) with vuetify3.VContainer( v_if="!is_running && geometry_selection && !dist_type", fluid=True, classes="pa-0 flex-grow-1 d-flex align-center justify-center", style="width: 100%; min-height: 560px;", ): geometry_preview_widget = plotly_widgets.Figure( figure=_build_geometry_placeholder("Select a geometry to preview."), responsive=True, style="width: 100%; height: 100%;", ) ctrl.geometry_preview_update = geometry_preview_widget.update # Excitation preview (before simulation runs) with vuetify3.VContainer( v_if="!is_running && dist_type && !simulation_has_run", fluid=True, classes="pa-0 flex-grow-1 d-flex align-center justify-center", style="width: 100%; min-height: 580px;", ): excitation_preview_widget = plotly_widgets.Figure( figure=_build_excitation_placeholder("Select an excitation to preview."), responsive=True, style="width: 100%; height: 100%;", ) ctrl.excitation_preview_update = excitation_preview_widget.update # Surface Plot: PyVista view with vuetify3.VContainer(v_if="!is_running && simulation_has_run && output_type === 'Surface Plot'", fluid=True, classes="pa-0", style=("pyvista_view_style", "aspect-ratio: 1 / 1; width: 100%;")): view = plotter_ui(plotter) ctrl.view_update = view.update # Time Series: Plotly figure with vuetify3.VContainer(v_if="!is_running && simulation_has_run && output_type === 'Time Series Plot'", fluid=True, classes="d-flex align-center justify-center pa-2", style="overflow: hidden;"): sim_ts = plotly_widgets.Figure( figure=go.Figure(layout=dict(width=900, height=660)), responsive=False, style="width: 900px; height: 660px;", ) ctrl.sim_ts_update = sim_ts.update # Time slider for surface plot with vuetify3.VContainer(v_if="simulation_has_run && output_type === 'Surface Plot' && backend_type !== 'QPU' && !(backend_type === 'Simulator' && selected_simulator === 'Statevector Estimator')", fluid=True, classes="pa-0 mt-2"): 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") def _build_qpu_plot_area(): """Build the QPU plot area (Plotly time series).""" with vuetify3.VCard(v_if="geometry_selection && (backend_type === 'QPU' || (backend_type === 'Simulator' && selected_simulator === 'Statevector Estimator')) && (is_running || qpu_ts_ready)", classes="flex-grow-1", style="min-height: 0;"): # Running indicator with vuetify3.VContainer(v_if="is_running", fluid=True, classes="fill-height d-flex flex-column align-center justify-center"): vuetify3.VProgressCircular(indeterminate=True, size=64, color="primary") vuetify3.VCardSubtitle("Running {{ backend_type === 'QPU' ? selected_qpu : selected_simulator }}...", classes="mt-4") # QPU timeseries with toolbar with vuetify3.VContainer(v_if="!is_running && qpu_ts_ready", fluid=True, classes="pa-2"): with vuetify3.VToolbar(density="compact", flat=True, color="transparent", classes="px-0"): vuetify3.VSelect( label="Component", v_model=("qpu_plot_filter", "All"), items=("qpu_plot_field_options", ["All"]), density="compact", color="primary", hide_details=True, style="max-width: 180px; margin-right: 8px;", disabled=("!qpu_ts_ready", True), update_modelValue=(ctrl.qpu_set_plot_filter, "[$event]") ) vuetify3.VSelect( label="Position", v_model=("qpu_plot_position_filter", "All positions"), items=("qpu_plot_position_options", ["All positions"]), density="compact", color="primary", hide_details=True, style="max-width: 200px; margin-right: 8px;", disabled=("!qpu_ts_ready", True), update_modelValue=(ctrl.qpu_set_plot_position_filter, "[$event]") ) vuetify3.VSpacer() vuetify3.VBtn( text="Clear Selection", color="secondary", variant="text", click=ctrl.on_qpu_ts_clear, disabled=("!qpu_ts_ready", True), ) with vuetify3.VMenu(open_on_hover=True, close_on_content_click=True, location="end", offset=8): with vuetify3.Template(v_slot_activator="{ props }"): vuetify3.VBtn( v_bind="props", text="DOWNLOAD", color="primary", variant="tonal", disabled=("!qpu_ts_ready", True), ) with vuetify3.VList(density="compact"): vuetify3.VListItem(title="Download CSV", prepend_icon="mdi-download", click=export_qpu_timeseries_csv, disabled=("!qpu_ts_ready", True)) vuetify3.VListItem(title="Download PNG", prepend_icon="mdi-image", click=export_qpu_timeseries_png, disabled=("!qpu_ts_ready", True)) vuetify3.VListItem(title="Download HTML", prepend_icon="mdi-file-html", click=export_qpu_timeseries_html, disabled=("!qpu_ts_ready", True)) trame_html.Div(style="height: 6px;") # Plotly figure for QPU timeseries qpu_ts_widget = plotly_widgets.Figure( figure=go.Figure(layout=dict(width=900, height=660)), responsive=False, style=("qpu_plot_style", "display: none; width: 900px; height: 660px; margin: 0 auto;"), click=ctrl.on_qpu_ts_click, ) ctrl.qpu_ts_update = qpu_ts_widget.update def _build_no_geometry_placeholder(): """Build placeholder when no geometry is selected.""" with vuetify3.VContainer(v_if="!geometry_selection", fluid=True, classes="flex-grow-1 d-flex align-center justify-center text-medium-emphasis"): vuetify3.VCardText("Select a geometry to display the preview and results.") def _build_status_console(): """Build the Status/Console card.""" with vuetify3.VCard(classes="mt-2", style="font-size: 0.8rem;"): with vuetify3.VCardTitle("Status", classes="text-subtitle-1 text-primary", style="font-size: 0.9rem; padding: 6px 10px;"): pass with vuetify3.VCardText(classes="py-1 px-2", style="height: 150px; overflow-y: auto; background-color: #f5f5f5; font-family: monospace;"): vuetify3.VTextarea( v_model=("console_output", ""), readonly=True, auto_grow=False, rows=6, variant="plain", hide_details=True, style="font-family: monospace; width: 100%; height: 100%;" ) def _build_status_window(): """Build the floating status window (bottom right).""" with vuetify3.VCard( v_if="status_visible", style="position: fixed; bottom: 16px; right: 16px; z-index: 1000; min-width: 320px; max-width: 450px;", elevation=8 ): with vuetify3.VCardTitle(classes="d-flex align-center", style="font-size: 0.95rem; padding: 8px 12px;"): vuetify3.VIcon("mdi-information-outline", size="small", classes="mr-2") trame_html.Span("Simulation Status") vuetify3.VSpacer() vuetify3.VBtn( icon="mdi-close", size="x-small", variant="text", click="status_visible = false" ) vuetify3.VDivider() with vuetify3.VCardText(classes="py-2 px-3"): vuetify3.VAlert( type=("status_type", "info"), variant="tonal", density="compact", children=["{{ status_message }}"] ) with vuetify3.VContainer(v_if="show_progress", classes="pa-0 mt-2"): vuetify3.VProgressLinear( model_value=("simulation_progress", 0), color="primary", height=6, striped=True ) trame_html.Div( "{{ simulation_progress }}% complete", classes="text-caption text-center mt-1", style="font-size: 0.75rem;" )