Spaces:
Paused
Paused
| """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;" | |
| ) | |