diff --git a/.gitignore b/.gitignore
index d45913ed6..2bea61af9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,3 +58,9 @@ cmake-build-*/
.DS_Store
.AppleDouble
.LSOverride
+
+#####################
+# Trame Dashboard (output files are temporary) #
+#####################
+src/python/impactx/diags*
+src/python/impactx/*.png
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 2b9aae57f..6ab1b75e0 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -68,6 +68,7 @@ Usage
usage/examples
usage/python
usage/parameters
+ usage/dashboard
usage/workflows
Data Analysis
diff --git a/docs/source/install/dependencies.rst b/docs/source/install/dependencies.rst
index d2ccf0a8d..9fff947c9 100644
--- a/docs/source/install/dependencies.rst
+++ b/docs/source/install/dependencies.rst
@@ -35,6 +35,9 @@ Optional dependencies include:
- `quantiphy `__
- `openPMD-api `__
- see our ``requirements.txt`` file for compatible versions
+ - web browser/Jupyter Dashboard: `trame `__
+
+ - see our ``src/python/impactx/dashboard/requirements.txt`` file for all packages
If you are on a high-performance computing (HPC) system, then :ref:`please see our separate HPC documentation `.
@@ -102,6 +105,12 @@ For OpenMP support, you will further need:
conda install -c conda-forge llvm-openmp
+For the ImpactX browser/Jupyter dashboard dependencies, install from the ImpactX source directory:
+
+.. code-block:: bash
+
+ python3 -m pip install -r src/python/impactx/dashboard/requirements.txt
+
Spack (Linux/macOS)
-------------------
@@ -144,6 +153,12 @@ Now install the WarpX/ImpactX dependencies in a new development environment:
spack install
python3 -m pip install jupyter matplotlib numpy openpmd-api openpmd-viewer pandas quantiphy scipy virtualenv yt
+For the ImpactX browser/Jupyter dashboard dependencies, install from the ImpactX source directory:
+
+.. code-block:: bash
+
+ python3 -m pip install -r src/python/impactx/dashboard/requirements.txt
+
In new terminal sessions, re-activate the environment with
.. code-block:: bash
diff --git a/docs/source/usage/dashboard.rst b/docs/source/usage/dashboard.rst
new file mode 100644
index 000000000..bcb4fd3e5
--- /dev/null
+++ b/docs/source/usage/dashboard.rst
@@ -0,0 +1,87 @@
+.. _usage-dashboard:
+
+Dashboard
+=========
+
+ImpactX Dashboard is a browser-based interface to ImpactX.
+It provides a graphical interface to a subset of ImpactX functionality.
+
+.. note::
+
+ ImpactX Dashboard is provided as a preview and continues to be developed.
+ Let us know in GitHub `discussions `__ and `issues `__ how it works for you and what kind of workflows you would like to run in it.
+
+
+Launching the Dashboard
+-----------------------
+
+The ImpactX Dashboard can be run in two modes, as a standalone browser application or inside a Jupyter notebook.
+
+1. **Standalone browser application:**
+ After installation of ImpactX including the Python modules, launch:
+
+ .. code-block:: bash
+
+ impactx-dashboard
+
+2. **JupyterLab:**
+ Start `JupyterLab `__, e.g., logging into a Jupyter service at an HPC center or locally on your computer using:
+
+ .. code-block:: bash
+
+ jupyter-lab
+
+ Inside JupyterLab, run the following Python code in a notebook to initialize and display the dashboard:
+
+ .. code-block:: python
+
+ from impactx.dashboard import JupyterApp
+
+ # Create new application instance
+ app = JupyterApp()
+
+ # Start the server and wait for the UI to be ready
+ await app.ui.ready
+
+ # Display the UI in the JupyterLab notebook
+ app.ui
+
+
+Navigation
+----------
+
+The user-friendly interface includes multiple tabs and menu options, intended to be navigated from top to bottom:
+
+- **Input Tab**: Allows to adjust simulation input parameters.
+- **Run Tab**: Enables to run simulations and monitor their progress.
+- **Analyze Tab**: Provides tools to visualize and analyze simulation results.
+
+.. figure:: https://gist.githubusercontent.com/ax3l/b56aa3c3261f9612e276f3198b34f771/raw/11bfe461a24e1daa7fd2d663c686b0fcc2b6e305/dashboard.png
+ :align: center
+ :width: 75%
+ :alt: phase space ellipse
+
+ Input section in the dashboard.
+
+
+Developers
+----------
+
+Additional Dependencies
+"""""""""""""""""""""""
+
+Additional dependencies to ImpactX for the dashboard are included relative ImpactX source directory:
+
+.. code-block:: bash
+
+ python -m pip install -r src/python/impactx/dashboard/requirements.txt
+
+Python Module
+"""""""""""""
+
+After installing only the ImpactX Python bindings, one can directly run the dashboard modules from the source tree during development, too.
+For this, navigate in the ImpactX source directory to the ``src/python/impactx`` directory and run:
+
+ .. code-block:: bash
+
+ python -m dashboard
diff --git a/setup.py b/setup.py
index 93a5a3aa6..e56ca982c 100644
--- a/setup.py
+++ b/setup.py
@@ -282,4 +282,9 @@ def build_extension(self, ext):
# new PEP 639 format
license="BSD-3-Clause-LBNL",
license_files=["LICENSE"],
+ entry_points={
+ "console_scripts": [
+ "impactx-dashboard=impactx.dashboard.__main__:main",
+ ],
+ },
)
diff --git a/src/python/impactx/dashboard/Analyze/__init__.py b/src/python/impactx/dashboard/Analyze/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/python/impactx/dashboard/Analyze/analyzeFunctions.py b/src/python/impactx/dashboard/Analyze/analyzeFunctions.py
new file mode 100644
index 000000000..7132ec344
--- /dev/null
+++ b/src/python/impactx/dashboard/Analyze/analyzeFunctions.py
@@ -0,0 +1,101 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+import pandas as pd
+
+from ..trame_setup import setup_server
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Content
+# -----------------------------------------------------------------------------
+
+
+class AnalyzeFunctions:
+ """
+ Helper functions for
+ preparing the contents for the 'Analyze' page
+ """
+
+ # -----------------------------------------------------------------------------
+ # Functions for Beam Characteristic and Ref particle data table
+ # -----------------------------------------------------------------------------
+
+ @staticmethod
+ def load_data(file_path):
+ """
+ Reads data from the provided file path.
+ :param file_path: The path to the file to be read.
+ :return: A DataFrame containing the data from the file.
+ """
+
+ df = pd.read_csv(file_path, sep=" ")
+ return df
+
+ @staticmethod
+ def convert_to_dict(combined_data):
+ """
+ Converts data to dictionary format
+ for Vuetify data table.
+ :param combined_data: The DataFrame to be converted.
+ :return: A tuple containing the data as a list of dictionaries and the headers.
+ """
+
+ dictionary = combined_data.to_dict(orient="records")
+ columns = combined_data.columns
+ headers = [
+ {"text": column.strip(), "value": column.strip()} for column in columns
+ ]
+ return dictionary, headers
+
+ @staticmethod
+ def combine_files(file1_name, file2_name):
+ """
+ Merges two files together.
+ :param file1_name: The name of the first file.
+ :param file2_name: The name of the second file.
+ :return: A DataFrame containing the merged data from the two files.
+ """
+
+ file1 = AnalyzeFunctions.load_data(file1_name)
+ file2 = AnalyzeFunctions.load_data(file2_name)
+ return pd.merge(file1, file2, how="outer")
+
+ @staticmethod
+ def filter_headers(allHeaders, selected_headers):
+ """
+ Retrieves only user-selected headers.
+ :param allHeaders: The list of all headers.
+ :param selected_headers: The list of headers selected by the user.
+ :return: A list of filtered headers based on user selection.
+ """
+
+ filtered_headers = []
+ for selectedHeader in allHeaders:
+ if selectedHeader["value"] in selected_headers:
+ filtered_headers.append(selectedHeader)
+ return filtered_headers
+
+ @staticmethod
+ def filter_data(allData, selected_headers):
+ """
+ Retrieves data for user-selected headers.
+ :param allData: The list of all data.
+ :param selected_headers: The list of headers selected by the user.
+ :return: A list of filtered data based on user selection.
+ """
+
+ filtered_data = []
+ for row in allData:
+ filtered_row = {}
+ for key, value in row.items():
+ if key in selected_headers:
+ filtered_row[key] = value
+ filtered_data.append(filtered_row)
+ return filtered_data
diff --git a/src/python/impactx/dashboard/Analyze/plot_ParameterEvolutionOverS/overS.py b/src/python/impactx/dashboard/Analyze/plot_ParameterEvolutionOverS/overS.py
new file mode 100644
index 000000000..eec4ccf9b
--- /dev/null
+++ b/src/python/impactx/dashboard/Analyze/plot_ParameterEvolutionOverS/overS.py
@@ -0,0 +1,44 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+import plotly.graph_objects as go
+
+
+def line_plot_1d(selected_headers, filtered_data):
+ """
+ Generates a 1D line plot using Plotly based on selected headers and filtered data.
+ """
+
+ x_axis = selected_headers[0] if len(selected_headers) > 1 else None
+ y_axis = selected_headers[1:] if len(selected_headers) > 2 else None
+
+ x = [row[x_axis] for row in filtered_data] if x_axis else []
+
+ figure_data = []
+ if y_axis:
+ for column in y_axis:
+ y = [row[column] for row in filtered_data]
+ trace = go.Scatter(
+ x=x,
+ y=y,
+ mode="lines+markers",
+ name=column,
+ line=dict(width=2),
+ marker=dict(size=8),
+ )
+ figure_data.append(trace)
+
+ return go.Figure(
+ data=figure_data,
+ layout=go.Layout(
+ title="Plot Over S",
+ xaxis=dict(title="s"),
+ yaxis=dict(title=""),
+ margin=dict(l=20, r=20, t=25, b=30),
+ ),
+ )
diff --git a/src/python/impactx/dashboard/Analyze/plot_PhaseSpaceProjections/phaseSpace.py b/src/python/impactx/dashboard/Analyze/plot_PhaseSpaceProjections/phaseSpace.py
new file mode 100644
index 000000000..ed027ca71
--- /dev/null
+++ b/src/python/impactx/dashboard/Analyze/plot_PhaseSpaceProjections/phaseSpace.py
@@ -0,0 +1,77 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from ...trame_setup import setup_server
+
+server, state, ctrl = setup_server()
+
+import base64
+import io
+
+from impactx import Config, ImpactX
+
+from ...Input.distributionParameters.distributionMain import distribution_parameters
+from ...Input.latticeConfiguration.latticeMain import lattice_elements
+from ..plot_PhaseSpaceProjections.phaseSpaceSettings import adjusted_settings_plot
+
+# Call MPI_Init and MPI_Finalize only once:
+if Config.have_mpi:
+ from mpi4py import MPI # noqa
+
+
+def fig_to_base64(fig):
+ """
+ Puts png in trame-compatible form
+ """
+ buf = io.BytesIO()
+ fig.savefig(buf, format="png")
+ buf.seek(0)
+ return base64.b64encode(buf.read()).decode("utf-8")
+
+
+def run_simulation():
+ """
+ This tests using ImpactX and Pandas Dataframes
+ """
+ sim = ImpactX()
+
+ sim.particle_shape = state.particle_shape
+ sim.space_charge = False
+ sim.slice_step_diagnostics = True
+ sim.init_grids()
+
+ # init particle beam
+ kin_energy_MeV = state.kin_energy_MeV
+ bunch_charge_C = state.bunch_charge_C
+ npart = state.npart
+
+ # reference particle
+ pc = sim.particle_container()
+ ref = pc.ref_particle()
+ ref.set_charge_qe(-1.0).set_mass_MeV(0.510998950).set_kin_energy_MeV(kin_energy_MeV)
+
+ distribution = distribution_parameters()
+ sim.add_particles(bunch_charge_C, distribution, npart)
+
+ lattice_configuration = lattice_elements()
+
+ sim.lattice.extend(lattice_configuration)
+
+ # simulate
+ sim.evolve()
+
+ fig = adjusted_settings_plot(pc)
+ fig_original = pc.plot_phasespace()
+
+ if fig_original is not None:
+ image_base64 = fig_to_base64(fig_original)
+ state.image_data = f"data:image/png;base64, {image_base64}"
+
+ sim.finalize()
+
+ return fig
diff --git a/src/python/impactx/dashboard/Analyze/plot_PhaseSpaceProjections/phaseSpaceSettings.py b/src/python/impactx/dashboard/Analyze/plot_PhaseSpaceProjections/phaseSpaceSettings.py
new file mode 100644
index 000000000..91c3b2b70
--- /dev/null
+++ b/src/python/impactx/dashboard/Analyze/plot_PhaseSpaceProjections/phaseSpaceSettings.py
@@ -0,0 +1,121 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+
+def adjusted_settings_plot(self, num_bins=50, root_rank=0):
+ """
+ Plot the longitudinal and transverse phase space projections with matplotlib.
+
+ Parameters
+ ----------
+ self : ImpactXParticleContainer_*
+ The particle container class in ImpactX
+ num_bins : int, default=50
+ The number of bins for spatial and momentum directions per plot axis.
+ root_rank : int, default=0
+ MPI root rank to reduce to in parallel runs.
+
+ Returns
+ -------
+ A matplotlib figure with containing the plot.
+ For MPI-parallel ranks, the figure is only created on the root_rank.
+ """
+ import matplotlib.pyplot as plt
+ import numpy as np
+ from scipy.stats import gaussian_kde
+
+ # Beam Characteristics
+ # rbc = self.reduced_beam_characteristics()
+ # update for plot unit system
+ m2mm = 1.0e3
+ rad2mrad = 1.0e3
+
+ # Data Extraction
+ df = self.to_df(local=True)
+
+ # Matplotlib canvas: figure and plottable axes areas
+ fig, axes = plt.subplots(1, 3, figsize=(12, 3))
+ (ax_xpx, ax_ypy, ax_tpt) = axes
+
+ # Plotting data points if df is not None
+ if df is not None:
+ # update for plot unit system
+ df["position_x"] = df["position_x"].multiply(m2mm)
+ df["position_y"] = df["position_y"].multiply(m2mm)
+ df["position_t"] = df["position_t"].multiply(m2mm)
+ df["momentum_x"] = df["momentum_x"].multiply(rad2mrad)
+ df["momentum_y"] = df["momentum_y"].multiply(rad2mrad)
+ df["momentum_t"] = df["momentum_t"].multiply(rad2mrad)
+
+ def scatter_density_plot(ax, x, y, xlabel, ylabel, title):
+ xy = np.vstack([x, y])
+ z = gaussian_kde(xy)(xy)
+ scatter = ax.scatter(x, y, c=z, cmap="viridis", alpha=0.5)
+ ax.set_xlabel(xlabel)
+ ax.set_ylabel(ylabel)
+ ax.set_title(title)
+ ax.axis("equal")
+ return scatter
+
+ scatter_xpx = scatter_density_plot(
+ ax_xpx,
+ df["position_x"],
+ df["momentum_x"],
+ "Δ x [mm]",
+ "Δ p_x [mrad]",
+ "",
+ )
+ scatter_ypy = scatter_density_plot(
+ ax_ypy,
+ df["position_y"],
+ df["momentum_y"],
+ "Δ y [mm]",
+ "Δ p_y [mrad]",
+ "",
+ )
+ scatter_tpt = scatter_density_plot(
+ ax_tpt,
+ df["position_t"],
+ df["momentum_t"],
+ "Δ ct [mm]",
+ "Delta p_t [p_0 . c]",
+ "",
+ )
+
+ fig.colorbar(scatter_xpx, ax=ax_xpx, fraction=0.046, pad=0.04)
+ fig.colorbar(scatter_ypy, ax=ax_ypy, fraction=0.046, pad=0.04)
+ fig.colorbar(scatter_tpt, ax=ax_tpt, fraction=0.046, pad=0.04)
+
+ fig.tight_layout()
+
+ else:
+ ax_xpx.text(
+ 0.5,
+ 0.5,
+ "No data available",
+ horizontalalignment="center",
+ verticalalignment="center",
+ )
+ ax_ypy.text(
+ 0.5,
+ 0.5,
+ "No data available",
+ horizontalalignment="center",
+ verticalalignment="center",
+ )
+ ax_tpt.text(
+ 0.5,
+ 0.5,
+ "No data available",
+ horizontalalignment="center",
+ verticalalignment="center",
+ )
+
+ fig.canvas.manager.set_window_title("Phase Space")
+
+ return fig
diff --git a/src/python/impactx/dashboard/Analyze/plotsMain.py b/src/python/impactx/dashboard/Analyze/plotsMain.py
new file mode 100644
index 000000000..4d71ffb86
--- /dev/null
+++ b/src/python/impactx/dashboard/Analyze/plotsMain.py
@@ -0,0 +1,252 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+import asyncio
+import contextlib
+import glob
+import io
+import os
+
+from trame.widgets import matplotlib, plotly, vuetify
+
+from ..trame_setup import setup_server
+from .analyzeFunctions import AnalyzeFunctions
+from .plot_ParameterEvolutionOverS.overS import line_plot_1d
+from .plot_PhaseSpaceProjections.phaseSpace import run_simulation
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Plotting
+# -----------------------------------------------------------------------------
+
+
+# Call plot_over_s
+def plot_over_s():
+ """
+ Generates a plot.
+ """
+
+ fig = line_plot_1d(state.selected_headers, state.filtered_data)
+ ctrl.plotly_figure_update(fig)
+
+
+PLOTS = {
+ "Plot Over S": plot_over_s,
+ "Phase Space Plots": None,
+}
+
+# -----------------------------------------------------------------------------
+# -----------------------------------------------------------------------------
+
+
+def available_plot_options(simulationClicked):
+ """
+ Displays plot_options for users based on status of simulation.
+ :param simulationClicked (bool): status of simulation status
+ :return: list of plot_options for users
+ """
+
+ if simulationClicked:
+ return list(PLOTS.keys())
+ else:
+ return ["Run Simulation To See Options"]
+
+
+def load_dataTable_data():
+ """
+ Loads and processes data from combined beam and reference particle files.
+ """
+
+ CURRENT_DIR = os.getcwd()
+ DIAGS_DIR = os.path.join(CURRENT_DIR, "diags")
+
+ base_path = DIAGS_DIR + "/"
+ REDUCED_BEAM_DATA = glob.glob(base_path + "reduced_beam_characteristics.*")[0]
+ REF_PARTICLE_DATA = glob.glob(base_path + "ref_particle.*")[0]
+
+ if not os.path.exists(REDUCED_BEAM_DATA) or not os.path.exists(REF_PARTICLE_DATA):
+ ctrl.terminal_print(
+ "Diagnostics files are missing. Please ensure they are in the correct directory."
+ )
+ return
+
+ combined_files = AnalyzeFunctions.combine_files(
+ REDUCED_BEAM_DATA, REF_PARTICLE_DATA
+ )
+ combined_files_data_converted_to_dictionary_format = (
+ AnalyzeFunctions.convert_to_dict(combined_files)
+ )
+ data, headers = combined_files_data_converted_to_dictionary_format
+ state.all_data = data
+ state.all_headers = headers
+ state.headers_without_step_or_s = state.all_headers[2:]
+
+
+# -----------------------------------------------------------------------------
+# Defaults
+# -----------------------------------------------------------------------------
+
+DEFAULT_HEADERS = ["s", "beta_x", "beta_y"]
+
+state.selected_headers = DEFAULT_HEADERS
+state.plot_options = available_plot_options(simulationClicked=False)
+state.show_table = False
+state.active_plot = None
+state.filtered_data = []
+state.all_data = []
+state.all_headers = []
+
+# -----------------------------------------------------------------------------
+# Functions to update table/plot
+# -----------------------------------------------------------------------------
+
+
+def update_data_table():
+ """
+ Combines reducedBeam and refParticle files
+ and updates data table upon column selection by user
+ """
+
+ load_dataTable_data()
+ state.filtered_data = AnalyzeFunctions.filter_data(
+ state.all_data, state.selected_headers
+ )
+ state.filtered_headers = AnalyzeFunctions.filter_headers(
+ state.all_headers, state.selected_headers
+ )
+
+
+def update_plot():
+ """
+ Performs actions to display correct information,
+ based on the plot optin selected by the user
+ """
+
+ if state.active_plot == "Plot Over S":
+ update_data_table()
+ plot_over_s()
+ state.show_table = True
+ elif state.active_plot == "Phase Space Plots":
+ state.show_table = False
+ ctrl.matplotlib_figure_update(state.simulation_data)
+
+
+def run_simulation_impactX():
+ buf = io.StringIO()
+
+ with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
+ state.simulation_data = run_simulation()
+
+ buf.seek(0)
+ lines = [line.strip() for line in buf]
+
+ # Use $nextTick to ensure the terminal is fully rendered before printing
+ async def print_lines():
+ for line in lines:
+ ctrl.terminal_print(line)
+ ctrl.terminal_print("Simulation complete.")
+
+ asyncio.create_task(print_lines())
+
+
+# -----------------------------------------------------------------------------
+# State changes
+# -----------------------------------------------------------------------------
+
+
+@state.change("selected_headers")
+def on_header_selection_change(selected_headers, **kwargs):
+ state.filtered_headers = AnalyzeFunctions.filter_headers(
+ state.all_headers, selected_headers
+ )
+ state.filtered_data = AnalyzeFunctions.filter_data(state.all_data, selected_headers)
+
+
+@state.change("filtered_data", "active_plot")
+def on_filtered_data_change(**kwargs):
+ update_plot()
+
+
+@ctrl.add("run_simulation")
+def run_simulation_and_store():
+ state.plot_options = available_plot_options(simulationClicked=True)
+ run_simulation_impactX()
+ update_plot()
+ load_dataTable_data()
+
+
+# -----------------------------------------------------------------------------
+# GUI
+# -----------------------------------------------------------------------------
+
+
+class AnalyzeSimulation:
+ """
+ Prepares contents for the 'Analyze' page.
+ """
+
+ @staticmethod
+ def card():
+ """
+ Displays any non-plot content for 'Analyze' page.
+ """
+
+ with vuetify.VContainer():
+ with vuetify.VCard(v_if=("show_table")):
+ with vuetify.VCol(style="width: 500px;"):
+ vuetify.VSelect(
+ v_model=("selected_headers",),
+ items=("headers_without_step_or_s",),
+ label="Select data to view",
+ multiple=True,
+ )
+ vuetify.VDivider()
+ vuetify.VDataTable(
+ headers=("filtered_headers",),
+ items=("filtered_data",),
+ header_class="centered-header",
+ dense=True,
+ height="325px",
+ )
+
+ @staticmethod
+ def plot():
+ """
+ Displays any plot content for 'Analyze' page.
+ """
+
+ with vuetify.VContainer(fluid=True):
+ with vuetify.VContainer(
+ v_if="active_plot === 'Phase Space Plots'", fluid=True
+ ):
+ with vuetify.VCard(style="height: 50vh; width: 150vh;"):
+ with vuetify.VTabs(v_model=("active_tab", 0)):
+ vuetify.VTab("Plot")
+ vuetify.VTab("Interact")
+ vuetify.VDivider()
+ with vuetify.VTabsItems(v_model="active_tab"):
+ with vuetify.VTabItem():
+ vuetify.VImg(v_if=("image_data",), src=("image_data",))
+
+ with vuetify.VTabItem():
+ with vuetify.VContainer(
+ style="height: 37vh; width: 147vh;"
+ ):
+ matplotlib_figure = matplotlib.Figure(
+ style="position: absolute"
+ )
+ ctrl.matplotlib_figure_update = matplotlib_figure.update
+
+ with vuetify.VContainer(
+ v_if="active_plot === 'Plot Over S'", style="height: 50vh; width: 90vh;"
+ ):
+ plotly_figure = plotly.Figure(
+ display_mode_bar="true", config={"responsive": True}
+ )
+ ctrl.plotly_figure_update = plotly_figure.update
diff --git a/src/python/impactx/dashboard/Input/__init__.py b/src/python/impactx/dashboard/Input/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/python/impactx/dashboard/Input/distributionParameters/__init__.py b/src/python/impactx/dashboard/Input/distributionParameters/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/python/impactx/dashboard/Input/distributionParameters/distributionMain.py b/src/python/impactx/dashboard/Input/distributionParameters/distributionMain.py
new file mode 100644
index 000000000..b57e16fe7
--- /dev/null
+++ b/src/python/impactx/dashboard/Input/distributionParameters/distributionMain.py
@@ -0,0 +1,212 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from trame.widgets import vuetify
+
+from impactx import distribution
+
+from ...trame_setup import setup_server
+from ..generalFunctions import generalFunctions
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Helpful
+# -----------------------------------------------------------------------------
+
+DISTRIBUTIONS_MODULE_NAME = distribution
+
+state.listOfDistributions = generalFunctions.select_classes(DISTRIBUTIONS_MODULE_NAME)
+state.listOfDistributionsAndParametersAndDefault = (
+ generalFunctions.class_parameters_with_defaults(DISTRIBUTIONS_MODULE_NAME)
+)
+
+# -----------------------------------------------------------------------------
+# Default
+# -----------------------------------------------------------------------------
+
+state.selectedDistribution = "Waterbag"
+state.selectedDistributionType = "Native"
+state.selectedDistributionParameters = []
+
+# -----------------------------------------------------------------------------
+# Main Functions
+# -----------------------------------------------------------------------------
+
+
+def populate_distribution_parameters(selectedDistribution):
+ """
+ Populates distribution parameters based on the selected distribution.
+ :param selectedDistribution (str): The name of the selected distribution
+ whos parameters need to be populated.
+ """
+
+ selectedDistributionParameters = (
+ state.listOfDistributionsAndParametersAndDefault.get(selectedDistribution, [])
+ )
+
+ state.selectedDistributionParameters = [
+ {
+ "parameter_name": parameter[0],
+ "parameter_default_value": parameter[1],
+ "parameter_type": parameter[2],
+ "parameter_error_message": generalFunctions.validate_against(
+ parameter[1], parameter[2]
+ ),
+ }
+ for parameter in selectedDistributionParameters
+ ]
+
+ generalFunctions.update_simulation_validation_status()
+ return selectedDistributionParameters
+
+
+def update_distribution_parameters(
+ parameterName, parameterValue, parameterErrorMessage
+):
+ """
+ Updates the value of a distribution parameter and its error message.
+
+ :param parameterName (str): The name of the parameter to update.
+ :param parameterValue: The new value for the parameter.
+ :param parameterErrorMessage: The error message related to the parameter's value.
+ """
+
+ for param in state.selectedDistributionParameters:
+ if param["parameter_name"] == parameterName:
+ param["parameter_default_value"] = parameterValue
+ param["parameter_error_message"] = parameterErrorMessage
+
+ generalFunctions.update_simulation_validation_status()
+ state.dirty("selectedDistributionParameters")
+
+
+# -----------------------------------------------------------------------------
+# Write to file functions
+# -----------------------------------------------------------------------------
+
+
+def parameter_input_checker():
+ """
+ Helper function to check if user input is valid.
+ :return: A dictionary with parameter names as keys and their validated values.
+ """
+
+ parameter_input = {}
+ for param in state.selectedDistributionParameters:
+ if param["parameter_error_message"] == []:
+ parameter_input[param["parameter_name"]] = float(
+ param["parameter_default_value"]
+ )
+ else:
+ parameter_input[param["parameter_name"]] = 0.0
+
+ return parameter_input
+
+
+def distribution_parameters():
+ """
+ Writes user input for distribution parameters in suitable format for simulation code.
+ :return: An instance of the selected distribution class, initialized with user-provided parameters.
+ """
+
+ distribution_name = state.selectedDistribution
+ parameters = parameter_input_checker()
+
+ distr = getattr(distribution, distribution_name)(**parameters)
+ return distr
+
+
+# -----------------------------------------------------------------------------
+# Callbacks
+# -----------------------------------------------------------------------------
+
+
+@state.change("selectedDistribution")
+def on_distribution_name_change(selectedDistribution, **kwargs):
+ populate_distribution_parameters(selectedDistribution)
+
+
+@state.change("selectedDistributionType")
+def on_distribution_type_change(**kwargs):
+ populate_distribution_parameters(state.selectedDistribution)
+
+
+@ctrl.add("updateDistributionParameters")
+def on_distribution_parameter_change(parameter_name, parameter_value, parameter_type):
+ parameter_value, input_type = generalFunctions.determine_input_type(parameter_value)
+ error_message = generalFunctions.validate_against(parameter_value, parameter_type)
+
+ update_distribution_parameters(parameter_name, parameter_value, error_message)
+
+
+# -----------------------------------------------------------------------------
+# Content
+# -----------------------------------------------------------------------------
+
+
+class DistributionParameters:
+ """
+ User-Input section for beam distribution.
+ """
+
+ @staticmethod
+ def card():
+ """
+ Creates UI content for beam distribution.
+ """
+
+ with vuetify.VCard(style="width: 340px; height: 300px"):
+ with vuetify.VCardTitle("Distribution Parameters"):
+ vuetify.VSpacer()
+ vuetify.VIcon(
+ "mdi-information",
+ style="color: #00313C;",
+ click=lambda: generalFunctions.documentation("BeamDistributions"),
+ )
+ vuetify.VDivider()
+ with vuetify.VCardText():
+ with vuetify.VRow():
+ with vuetify.VCol(cols=8):
+ vuetify.VCombobox(
+ label="Select Distribution",
+ v_model=("selectedDistribution",),
+ items=("listOfDistributions",),
+ dense=True,
+ )
+ with vuetify.VCol(cols=4):
+ vuetify.VSelect(
+ v_model=("selectedDistributionType",),
+ label="Type",
+ items=(["Native", "Twiss"],),
+ # change=(ctrl.kin_energy_unit_change, "[$event]"),
+ dense=True,
+ disabled=True,
+ )
+ with vuetify.VRow(classes="my-2"):
+ for i in range(3):
+ with vuetify.VCol(cols=4, classes="py-0"):
+ with vuetify.VRow(
+ v_for="(parameter, index) in selectedDistributionParameters"
+ ):
+ with vuetify.VCol(
+ v_if=f"index % 3 == {i}", classes="py-1"
+ ):
+ vuetify.VTextField(
+ label=("parameter.parameter_name",),
+ v_model=("parameter.parameter_default_value",),
+ change=(
+ ctrl.updateDistributionParameters,
+ "[parameter.parameter_name, $event, parameter.parameter_type]",
+ ),
+ error_messages=(
+ "parameter.parameter_error_message",
+ ),
+ type="number",
+ dense=True,
+ )
diff --git a/src/python/impactx/dashboard/Input/generalFunctions.py b/src/python/impactx/dashboard/Input/generalFunctions.py
new file mode 100644
index 000000000..60164bb62
--- /dev/null
+++ b/src/python/impactx/dashboard/Input/generalFunctions.py
@@ -0,0 +1,263 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+import inspect
+import os
+import re
+import subprocess
+import webbrowser
+
+from ..trame_setup import setup_server
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Code
+# -----------------------------------------------------------------------------
+
+
+class generalFunctions:
+
+ @staticmethod
+ def documentation(section_name):
+ """
+ Opens a tab to the specified section link in the documentation.
+ :param section_name (str): The name of the documentation section to open.
+ """
+
+ if section_name == "LatticeElements":
+ url = "https://impactx.readthedocs.io/en/latest/usage/python.html#lattice-elements"
+ elif section_name == "BeamDistributions":
+ url = "https://impactx.readthedocs.io/en/latest/usage/python.html#initial-beam-distributions"
+ elif section_name == "pythonParameters":
+ url = "https://impactx.readthedocs.io/en/latest/usage/python.html#general"
+ else:
+ raise ValueError(f"Invalid section name: {section_name}")
+
+ if "WSL_DISTRO_NAME" in os.environ:
+ subprocess.run(["explorer.exe", url])
+ else:
+ webbrowser.open_new_tab(url)
+
+ # -----------------------------------------------------------------------------
+ # Validation functions
+ # -----------------------------------------------------------------------------
+
+ @staticmethod
+ def determine_input_type(value):
+ """
+ Determines the type of the input value.
+ :param value: The input value whose type needs to be determined.
+ :return: A tuple containing the value converted to its determined type and the type itself.
+ """
+
+ try:
+ return int(value), int
+ except ValueError:
+ try:
+ return float(value), float
+ except ValueError:
+ return value, str
+
+ @staticmethod
+ def validate_against(input_value, value_type):
+ """
+ Returns an error message if the input value type does not match the desired type.
+ :param input_value: The value to validate.
+ :param value_type: The desired type ('int', 'float', 'str').
+ :return: A list of error messages. An empty list if there are no errors.
+ """
+
+ if value_type == "int":
+ if input_value is None:
+ return ["Must be an integer"]
+ try:
+ int(input_value)
+ return []
+ except ValueError:
+ return ["Must be an integer"]
+
+ elif value_type == "float":
+ if input_value is None:
+ return ["Must be a float"]
+ try:
+ float(input_value)
+ return []
+ except ValueError:
+ return ["Must be a float"]
+
+ elif value_type == "str":
+ if input_value is None:
+ return ["Must be a string"]
+ return []
+ else:
+ return ["Unknown type"]
+
+ @staticmethod
+ def update_simulation_validation_status():
+ """
+ Checks if any input fields are not provided with the correct input type.
+ Updates the state to enable or disable the run simulation button.
+ """
+
+ error_details = []
+
+ # Check for errors in distribution parameters
+ for param in state.selectedDistributionParameters:
+ if param["parameter_error_message"]:
+ error_details.append(
+ f"{param['parameter_name']}: {param['parameter_error_message']}"
+ )
+
+ # Check for errors in lattice parameters
+ for lattice in state.selectedLatticeList:
+ for param in lattice["parameters"]:
+ if param["parameter_error_message"]:
+ error_details.append(
+ f"Lattice {lattice['name']} - {param['parameter_name']}: {param['parameter_error_message']}"
+ )
+
+ # Check for errors in input card
+ if state.npart_validation:
+ error_details.append(f"Number of Particles: {state.npart_validation}")
+ if state.kin_energy_validation:
+ error_details.append(f"Kinetic Energy: {state.kin_energy_validation}")
+ if state.bunch_charge_C_validation:
+ error_details.append(f"Bunch Charge: {state.bunch_charge_C_validation}")
+ if state.selectedLatticeList == []:
+ error_details.append("LatticeListIsEmpty")
+
+ state.disableRunSimulationButton = bool(error_details)
+
+ # -----------------------------------------------------------------------------
+ # Class, parameter, default value, and default type retrievals
+ # -----------------------------------------------------------------------------
+
+ @staticmethod
+ def find_classes(module_name):
+ """
+ Returns a list of all classes in the given module.
+ :param module_name: The module to inspect.
+ :return: A list of tuples containing class names.
+ """
+
+ results = []
+ for name in dir(module_name):
+ attr = getattr(module_name, name)
+ if inspect.isclass(attr):
+ results.append((name, attr))
+ return results
+
+ @staticmethod
+ def find_init_docstring_for_classes(classes):
+ """
+ Retrieves the __init__ docstring of the given classes.
+ :param classes: A list of typles containing class names.
+ :return: A dictionary with class names as keys and their __init__ docstrings as values.
+ """
+
+ if not isinstance(classes, (list, tuple)):
+ raise TypeError("The 'classes' argument must be a list or tuple.")
+
+ docstrings = {}
+ for name, cls in classes:
+ init_method = getattr(cls, "__init__", None)
+ if init_method:
+ docstring = cls.__init__.__doc__
+ docstrings[name] = docstring
+ return docstrings
+
+ @staticmethod
+ def extract_parameters(docstring):
+ """
+ Parses specific information from docstrings.
+ Aimed to retrieve parameter names, values, and types.
+ :param docstring: The docstring to parse.
+ :return: A list of tuples containing parameter names, default values, and types.
+ """
+
+ parameters = []
+ docstring = re.search(r"\((.*?)\)", docstring).group(
+ 1
+ ) # Return class name and init signature
+ docstring = docstring.split(",")
+
+ for parameter in docstring:
+ if parameter.startswith("self"):
+ continue
+
+ name = parameter
+ default = None
+ parameter_type = "Any"
+
+ if ":" in parameter:
+ split_by_semicolon = parameter.split(":", 1)
+ name = split_by_semicolon[0].strip()
+ type_and_default = split_by_semicolon[1].strip()
+ if "=" in type_and_default:
+ split_by_equals = type_and_default.split("=", 1)
+ parameter_type = split_by_equals[0].strip()
+ default = split_by_equals[1].strip()
+ if default.startswith("'") and default.endswith("'"):
+ default = default[1:-1]
+ else:
+ parameter_type = type_and_default
+
+ parameters.append((name, default, parameter_type))
+
+ return parameters
+
+ @staticmethod
+ def class_parameters_with_defaults(module_name):
+ """
+ Given a module name, outputs a dictionary of class names and their parameters.
+ Keys are class names, and values are lists of parameter information (name, default value, type).
+ :param module_name: The module to inspect.
+ :return: A dictionary with class names as keys and parameter information as values.
+ """
+
+ classes = generalFunctions.find_classes(module_name)
+ docstrings = generalFunctions.find_init_docstring_for_classes(classes)
+
+ result = {}
+
+ for class_name, docstring in docstrings.items():
+ parameters = generalFunctions.extract_parameters(docstring)
+ result[class_name] = parameters
+
+ return result
+
+ @staticmethod
+ def select_classes(module_name):
+ """
+ Given a module name, outputs a list of all class names in the module.
+ :param module_name: The module to inspect.
+ :return: A list of class names.
+ """
+
+ return list(generalFunctions.class_parameters_with_defaults(module_name))
+
+ @staticmethod
+ def convert_to_correct_type(value, desired_type):
+ """
+ Converts the given value to the desired type.
+ :param value: The value to convert.
+ :param desired_type: The type to convert the value to ('int', 'float', 'str').
+ :return: The value converted to the desired type.
+ """
+
+ if value is None:
+ raise ValueError("Cannot convert to desired type")
+ if desired_type == "int":
+ return int(value)
+ elif desired_type == "float":
+ return float(value)
+ elif desired_type == "str":
+ return str(value)
+ else:
+ raise ValueError("Unknown type")
diff --git a/src/python/impactx/dashboard/Input/inputParameters/__init__.py b/src/python/impactx/dashboard/Input/inputParameters/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/python/impactx/dashboard/Input/inputParameters/inputFunctions.py b/src/python/impactx/dashboard/Input/inputParameters/inputFunctions.py
new file mode 100644
index 000000000..0624ef7b0
--- /dev/null
+++ b/src/python/impactx/dashboard/Input/inputParameters/inputFunctions.py
@@ -0,0 +1,66 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from ...trame_setup import setup_server
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Functions
+# -----------------------------------------------------------------------------
+
+CONVERSION_FACTORS = {
+ "meV": 1.0e-9,
+ "eV": 1.0e-6,
+ "keV": 1.0e-3,
+ "MeV": 1.0,
+ "GeV": 1.0e3,
+ "TeV": 1.0e6,
+}
+
+
+class InputFunctions:
+ """
+ Helper functions for the
+ User-Input section for beam properties.
+ """
+
+ @staticmethod
+ def value_of_kin_energy_MeV(kineticEnergyOnDisplayValue, OldUnit):
+ """
+ Converts the kinetic energy to MeV.
+ :param kineticEnergyOnDisplayValue: The kinetic energy value that is displayed in the UI.
+ :param OldUnit: The previous unit of kin_energy prior to change.
+ :return: The kinetic energy value converted to MeV.
+ """
+
+ state.kin_energy_MeV = (
+ kineticEnergyOnDisplayValue
+ * CONVERSION_FACTORS[OldUnit]
+ / CONVERSION_FACTORS["MeV"]
+ )
+ return state.kin_energy_MeV
+
+ @staticmethod
+ def update_kin_energy_on_display(old_unit, new_unit, kin_energy_value):
+ """
+ Updates the kinetic energy value in the UI.
+ :param old_unit: The previous unit of kin_energy prior to change.
+ :param new_unit: The new unit of kin_energy changed to by user.
+ :param kin_energy_value: The kinetic energy value in the current unit.
+ :return: The kinetic energy value converted to the new unit.
+ """
+
+ value_in_mev = InputFunctions.value_of_kin_energy_MeV(
+ kin_energy_value, old_unit
+ )
+ kin_energy_value_on_display = (
+ value_in_mev * CONVERSION_FACTORS["MeV"] / CONVERSION_FACTORS[new_unit]
+ )
+
+ return kin_energy_value_on_display
diff --git a/src/python/impactx/dashboard/Input/inputParameters/inputMain.py b/src/python/impactx/dashboard/Input/inputParameters/inputMain.py
new file mode 100644
index 000000000..52e7e63ba
--- /dev/null
+++ b/src/python/impactx/dashboard/Input/inputParameters/inputMain.py
@@ -0,0 +1,153 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from trame.widgets import vuetify
+
+from ...trame_setup import setup_server
+from ..generalFunctions import generalFunctions
+from .inputFunctions import InputFunctions
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Callbacks
+# -----------------------------------------------------------------------------
+
+
+@ctrl.add("on_input_change")
+def validate_and_convert_to_correct_type(
+ value, desired_type, state_name, validation_name
+):
+ validation_result = generalFunctions.validate_against(value, desired_type)
+ setattr(state, validation_name, validation_result)
+ generalFunctions.update_simulation_validation_status()
+
+ if validation_result == []:
+ converted_value = generalFunctions.convert_to_correct_type(value, desired_type)
+
+ if getattr(state, state_name) != converted_value:
+ setattr(state, state_name, converted_value)
+ if state_name == "kin_energy":
+ state.kin_energy_MeV = InputFunctions.value_of_kin_energy_MeV(
+ converted_value, state.kin_energy_unit
+ )
+
+
+@ctrl.add("kin_energy_unit_change")
+def on_convert_kin_energy_change(new_unit):
+ old_unit = state.old_kin_energy_unit
+ if old_unit != new_unit and float(state.kin_energy) > 0:
+ state.kin_energy = InputFunctions.update_kin_energy_on_display(
+ old_unit, new_unit, state.kin_energy
+ )
+ state.kin_energy_unit = new_unit
+ state.old_kin_energy_unit = new_unit
+ state.kin_energy_MeV = InputFunctions.value_of_kin_energy_MeV(
+ float(state.kin_energy), new_unit
+ )
+
+
+# -----------------------------------------------------------------------------
+# Content
+# -----------------------------------------------------------------------------
+
+
+class InputParameters:
+ """
+ User-Input section for beam properties.
+ """
+
+ def __init__(self):
+ state.particle_shape = 2
+ state.npart = 1000
+ state.kin_energy = 2.0e3
+ state.kin_energy_MeV = state.kin_energy
+ state.bunch_charge_C = 1.0e-9
+ state.kin_energy_unit = "MeV"
+ state.old_kin_energy_unit = "MeV"
+
+ state.npart_validation = []
+ state.kin_energy_validation = []
+ state.bunch_charge_C_validation = []
+
+ def card(self):
+ """
+ Creates UI content for beam properties.
+ """
+
+ with vuetify.VCard(style="width: 340px; height: 300px"):
+ with vuetify.VCardTitle("Input Parameters"):
+ vuetify.VSpacer()
+ vuetify.VIcon(
+ "mdi-information",
+ style="color: #00313C;",
+ click=lambda: generalFunctions.documentation("pythonParameters"),
+ )
+ vuetify.VDivider()
+ with vuetify.VCardText():
+ vuetify.VCombobox(
+ v_model=("particle_shape",),
+ label="Particle Shape",
+ items=([1, 2, 3],),
+ dense=True,
+ )
+ with vuetify.VRow(classes="my-0"):
+ with vuetify.VCol(cols=12, classes="py-0"):
+ vuetify.VTextField(
+ v_model=("npart",),
+ label="Number of Particles",
+ error_messages=("npart_validation",),
+ change=(
+ ctrl.on_input_change,
+ "[$event, 'int','npart','npart_validation']",
+ ),
+ type="number",
+ dense=True,
+ )
+ with vuetify.VRow(classes="my-2"):
+ with vuetify.VCol(cols=8, classes="py-0"):
+ vuetify.VTextField(
+ v_model=("kin_energy",),
+ label="Kinetic Energy",
+ error_messages=("kin_energy_validation",),
+ change=(
+ ctrl.on_input_change,
+ "[$event, 'float','kin_energy','kin_energy_validation']",
+ ),
+ type="number",
+ dense=True,
+ classes="mr-2",
+ )
+ with vuetify.VCol(cols=4, classes="py-0"):
+ vuetify.VSelect(
+ v_model=("kin_energy_unit",),
+ label="Unit",
+ items=(["meV", "eV", "keV", "MeV", "GeV", "TeV"],),
+ change=(ctrl.kin_energy_unit_change, "[$event]"),
+ dense=True,
+ )
+ with vuetify.VRow(classes="my-2"):
+ with vuetify.VCol(cols=8, classes="py-0"):
+ vuetify.VTextField(
+ label="Bunch Charge",
+ v_model=("bunch_charge_C",),
+ error_messages=("bunch_charge_C_validation",),
+ change=(
+ ctrl.on_input_change,
+ "[$event, 'float','bunch_charge_C','bunch_charge_C_validation']",
+ ),
+ type="number",
+ dense=True,
+ )
+ with vuetify.VCol(cols=4, classes="py-0"):
+ vuetify.VTextField(
+ label="Unit",
+ value="C",
+ dense=True,
+ disabled=True,
+ )
diff --git a/src/python/impactx/dashboard/Input/latticeConfiguration/__init__.py b/src/python/impactx/dashboard/Input/latticeConfiguration/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/python/impactx/dashboard/Input/latticeConfiguration/latticeMain.py b/src/python/impactx/dashboard/Input/latticeConfiguration/latticeMain.py
new file mode 100644
index 000000000..5371042c2
--- /dev/null
+++ b/src/python/impactx/dashboard/Input/latticeConfiguration/latticeMain.py
@@ -0,0 +1,433 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from trame.widgets import vuetify
+
+from impactx import elements
+
+from ...trame_setup import setup_server
+from ..generalFunctions import generalFunctions
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Helpful
+# -----------------------------------------------------------------------------
+
+LATTICE_ELEMENTS_MODULE_NAME = elements
+
+state.listOfLatticeElements = generalFunctions.select_classes(
+ LATTICE_ELEMENTS_MODULE_NAME
+)
+state.listOfLatticeElementParametersAndDefault = (
+ generalFunctions.class_parameters_with_defaults(LATTICE_ELEMENTS_MODULE_NAME)
+)
+
+# -----------------------------------------------------------------------------
+# Default
+# -----------------------------------------------------------------------------
+
+state.selectedLattice = None
+state.selectedLatticeList = []
+state.nsliceDefaultValue = None
+
+# -----------------------------------------------------------------------------
+# Main Functions
+# -----------------------------------------------------------------------------
+
+
+def add_lattice_element():
+ """
+ Adds the selected lattice element to the list of selected
+ lattice elements along with its default parameters.
+ :return: dictionary representing the added lattice element with its parameters.
+ """
+
+ selectedLattice = state.selectedLattice
+ selectedLatticeParameters = state.listOfLatticeElementParametersAndDefault.get(
+ selectedLattice, []
+ )
+
+ selectedLatticeElement = {
+ "name": selectedLattice,
+ "parameters": [
+ {
+ "parameter_name": parameter[0],
+ "parameter_default_value": parameter[1],
+ "parameter_type": parameter[2],
+ "parameter_error_message": generalFunctions.validate_against(
+ parameter[1], parameter[2]
+ ),
+ }
+ for parameter in selectedLatticeParameters
+ ],
+ }
+
+ state.selectedLatticeList.append(selectedLatticeElement)
+ generalFunctions.update_simulation_validation_status()
+ return selectedLatticeElement
+
+
+def update_latticeElement_parameters(
+ index, parameterName, parameterValue, parameterErrorMessage
+):
+ """
+ Updates parameter value and includes error message if user input is not valid
+ """
+
+ for param in state.selectedLatticeList[index]["parameters"]:
+ if param["parameter_name"] == parameterName:
+ param["parameter_default_value"] = parameterValue
+ param["parameter_error_message"] = parameterErrorMessage
+
+ generalFunctions.update_simulation_validation_status()
+ state.dirty("selectedLatticeList")
+
+
+# -----------------------------------------------------------------------------
+# Write to file functions
+# -----------------------------------------------------------------------------
+
+
+def parameter_input_checker_for_lattice(latticeElement):
+ """
+ Helper function to check if user input is valid.
+ :return: A dictionary with parameter names as keys and their validated values.
+ """
+
+ parameter_input = {}
+ for parameter in latticeElement["parameters"]:
+ if parameter["parameter_error_message"] == []:
+ if parameter["parameter_type"] == "str":
+ parameter_input[parameter["parameter_name"]] = (
+ f"'{parameter['parameter_default_value']}'"
+ )
+ else:
+ parameter_input[parameter["parameter_name"]] = parameter[
+ "parameter_default_value"
+ ]
+ else:
+ parameter_input[parameter["parameter_name"]] = 0
+
+ return parameter_input
+
+
+def lattice_elements():
+ """
+ Writes user input for lattice element parameters parameters in suitable format for simulation code.
+ :return: A list in the suitable format.
+ """
+
+ elements_list = []
+ for latticeElement in state.selectedLatticeList:
+ latticeElement_name = latticeElement["name"]
+ parameters = parameter_input_checker_for_lattice(latticeElement)
+
+ param_values = ", ".join(f"{value}" for value in parameters.values())
+ elements_list.append(eval(f"elements.{latticeElement_name}({param_values})"))
+
+ return elements_list
+
+
+# -----------------------------------------------------------------------------
+# Callbacks
+# -----------------------------------------------------------------------------
+
+
+@state.change("selectedLatticeList")
+def on_selectedLatticeList_change(selectedLatticeList, **kwargs):
+ if selectedLatticeList == []:
+ state.isSelectedLatticeListEmpty = "Please select a lattice element"
+ generalFunctions.update_simulation_validation_status()
+ else:
+ state.isSelectedLatticeListEmpty = ""
+
+
+@state.change("selectedLattice")
+def on_lattice_element_name_change(selectedLattice, **kwargs):
+ return
+
+
+@ctrl.add("add_latticeElement")
+def on_add_lattice_element_click():
+ selectedLattice = state.selectedLattice
+ if selectedLattice:
+ add_lattice_element()
+ state.dirty("selectedLatticeList")
+
+
+@ctrl.add("updateLatticeElementParameters")
+def on_lattice_element_parameter_change(
+ index, parameter_name, parameter_value, parameter_type
+):
+ parameter_value, input_type = generalFunctions.determine_input_type(parameter_value)
+ error_message = generalFunctions.validate_against(parameter_value, parameter_type)
+
+ update_latticeElement_parameters(
+ index, parameter_name, parameter_value, error_message
+ )
+
+
+@ctrl.add("clear_latticeElements")
+def on_clear_lattice_element_click():
+ state.selectedLatticeList = []
+
+
+@ctrl.add("deleteLatticeElement")
+def on_delete_LatticeElement_click(index):
+ state.selectedLatticeList.pop(index)
+ state.dirty("selectedLatticeList")
+
+
+@ctrl.add("move_latticeElementIndex_up")
+def on_move_latticeElementIndex_up_click(index):
+ if index > 0:
+ state.selectedLatticeList[index], state.selectedLatticeList[index - 1] = (
+ state.selectedLatticeList[index - 1],
+ state.selectedLatticeList[index],
+ )
+ state.dirty("selectedLatticeList")
+
+
+@ctrl.add("move_latticeElementIndex_down")
+def on_move_latticeElementIndex_down_click(index):
+ if index < len(state.selectedLatticeList) - 1:
+ state.selectedLatticeList[index], state.selectedLatticeList[index + 1] = (
+ state.selectedLatticeList[index + 1],
+ state.selectedLatticeList[index],
+ )
+ state.dirty("selectedLatticeList")
+
+
+@ctrl.add("nsliceDefaultChange")
+def update_default_value(parameter_name, new_value):
+ data = generalFunctions.class_parameters_with_defaults(elements)
+
+ for key, parameters in data.items():
+ for i, param in enumerate(parameters):
+ if param[0] == parameter_name:
+ parameters[i] = (param[0], new_value, param[2])
+
+ state.listOfLatticeElementParametersAndDefault = data
+
+
+# -----------------------------------------------------------------------------
+# ContentSetup
+# -----------------------------------------------------------------------------
+
+
+class LatticeConfiguration:
+ """
+ User-Input section for configuring lattice elements.
+ """
+
+ @staticmethod
+ def card():
+ """
+ Creates UI content for lattice configuration.
+ """
+
+ with vuetify.VDialog(v_model=("showDialog", False), width="1200px"):
+ LatticeConfiguration.dialog_lattice_elementList()
+
+ with vuetify.VDialog(v_model=("showDialog_settings", False), width="500px"):
+ LatticeConfiguration.dialog_lattice_settings()
+
+ with vuetify.VCard(style="width: 696px;"):
+ with vuetify.VCardTitle("Lattice Configuration"):
+ vuetify.VSpacer()
+ vuetify.VIcon(
+ "mdi-information",
+ classes="ml-2",
+ click=lambda: generalFunctions.documentation("LatticeElements"),
+ style="color: #00313C;",
+ )
+ vuetify.VDivider()
+ with vuetify.VCardText():
+ with vuetify.VRow(align="center", no_gutters=True):
+ with vuetify.VCol(cols=8):
+ vuetify.VCombobox(
+ label="Select Accelerator Lattice",
+ v_model=("selectedLattice", None),
+ items=("listOfLatticeElements",),
+ error_messages=("isSelectedLatticeListEmpty",),
+ dense=True,
+ classes="mr-2 pt-6",
+ )
+ with vuetify.VCol(cols="auto"):
+ vuetify.VBtn(
+ "ADD",
+ color="primary",
+ dense=True,
+ classes="mr-2",
+ click=ctrl.add_latticeElement,
+ )
+ with vuetify.VCol(cols="auto"):
+ vuetify.VBtn(
+ "CLEAR",
+ color="secondary",
+ dense=True,
+ classes="mr-2",
+ click=ctrl.clear_latticeElements,
+ )
+ with vuetify.VCol(cols="auto"):
+ vuetify.VIcon(
+ "mdi-cog",
+ click="showDialog_settings = true",
+ )
+ with vuetify.VRow():
+ with vuetify.VCol():
+ with vuetify.VCard(
+ style="height: 300px; width: 700px; overflow-y: auto;"
+ ):
+ with vuetify.VCardTitle(
+ "Elements", classes="text-subtitle-2 pa-3"
+ ):
+ vuetify.VSpacer()
+ vuetify.VIcon(
+ "mdi-arrow-expand",
+ color="primary",
+ click="showDialog = true",
+ )
+ vuetify.VDivider()
+ with vuetify.VContainer(fluid=True):
+ with vuetify.VRow(
+ v_for="(latticeElement, index) in selectedLatticeList",
+ align="center",
+ no_gutters=True,
+ style="min-width: 1500px;",
+ ):
+ with vuetify.VCol(cols="auto", classes="pa-2"):
+ vuetify.VIcon(
+ "mdi-menu-up",
+ click=(
+ ctrl.move_latticeElementIndex_up,
+ "[index]",
+ ),
+ )
+ vuetify.VIcon(
+ "mdi-menu-down",
+ click=(
+ ctrl.move_latticeElementIndex_down,
+ "[index]",
+ ),
+ )
+ vuetify.VIcon(
+ "mdi-delete",
+ click=(
+ ctrl.deleteLatticeElement,
+ "[index]",
+ ),
+ )
+ vuetify.VChip(
+ v_text=("latticeElement.name",),
+ dense=True,
+ classes="mr-2",
+ style="justify-content: center",
+ )
+ with vuetify.VCol(
+ v_for="(parameter, parameterIndex) in latticeElement.parameters",
+ cols="auto",
+ classes="pa-2",
+ ):
+ vuetify.VTextField(
+ label=("parameter.parameter_name",),
+ v_model=(
+ "parameter.parameter_default_value",
+ ),
+ change=(
+ ctrl.updateLatticeElementParameters,
+ "[index, parameter.parameter_name, $event, parameter.parameter_type]",
+ ),
+ error_messages=(
+ "parameter.parameter_error_message",
+ ),
+ dense=True,
+ style="width: 100px;",
+ )
+
+ @staticmethod
+ def dialog_lattice_elementList():
+ """
+ Displays the content shown on the dialog
+ box for lattice configuration.
+ """
+
+ with vuetify.VCard():
+ with vuetify.VCardTitle("Elements", classes="text-subtitle-2 pa-3"):
+ vuetify.VSpacer()
+ vuetify.VDivider()
+ with vuetify.VContainer(fluid=True):
+ with vuetify.VRow(
+ v_for="(latticeElement, index) in selectedLatticeList",
+ align="center",
+ no_gutters=True,
+ style="min-width: 1500px;",
+ ):
+ with vuetify.VCol(cols="auto", classes="pa-2"):
+ vuetify.VIcon(
+ "mdi-delete",
+ click=(ctrl.deleteLatticeElement, "[index]"),
+ )
+ vuetify.VChip(
+ v_text=("latticeElement.name",),
+ dense=True,
+ classes="mr-2",
+ style="justify-content: center",
+ )
+ with vuetify.VCol(
+ v_for="(parameter, parameterIndex) in latticeElement.parameters",
+ cols="auto",
+ classes="pa-2",
+ ):
+ vuetify.VTextField(
+ label=("parameter.parameter_name",),
+ v_model=("parameter.parameter_default_value",),
+ change=(
+ ctrl.updateLatticeElementParameters,
+ "[index, parameter.parameter_name, $event, parameter.parameter_type]",
+ ),
+ error_messages=("parameter.parameter_error_message",),
+ dense=True,
+ style="width: 100px;",
+ )
+
+ @staticmethod
+ def dialog_lattice_settings():
+ """
+ Creates UI content for lattice configuration
+ settings.
+ """
+
+ with vuetify.VCard():
+ with vuetify.VTabs(v_model=("tab", "Settings")):
+ vuetify.VTab("Settings")
+ # vuetify.VTab("Variable Referencing")
+ vuetify.VDivider()
+ with vuetify.VTabsItems(v_model="tab"):
+ with vuetify.VTabItem():
+ with vuetify.VContainer(fluid=True):
+ with vuetify.VRow(no_gutters=True, align="center"):
+ with vuetify.VCol(no_gutters=True, cols="auto"):
+ vuetify.VListItem(
+ "nslice", classes="ma-0 pl-0 font-weight-bold"
+ )
+ with vuetify.VCol(no_gutters=True):
+ vuetify.VTextField(
+ v_model=("nsliceDefaultValue",),
+ change=(
+ ctrl.nsliceDefaultChange,
+ "['nslice', $event]",
+ ),
+ placeholder="Value",
+ dense=True,
+ outlined=True,
+ hide_details=True,
+ style="max-width: 75px",
+ classes="ma-0 pa-0",
+ )
diff --git a/src/python/impactx/dashboard/Input/trameFunctions.py b/src/python/impactx/dashboard/Input/trameFunctions.py
new file mode 100644
index 000000000..599e00c03
--- /dev/null
+++ b/src/python/impactx/dashboard/Input/trameFunctions.py
@@ -0,0 +1,43 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from trame.widgets import vuetify
+
+from ..trame_setup import setup_server
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Code
+# -----------------------------------------------------------------------------
+
+
+class TrameFunctions:
+ """
+ Contains functions containing Vuetify
+ components.
+ """
+
+ @staticmethod
+ def create_route(route_title, mdi_icon):
+ """
+ Creates a route with a specified title and icon.
+ :param route_title: The title of the route.
+ :param mdi_icon: The icon to be used for the route.
+ """
+
+ state[route_title] = False # Does not display route by default
+
+ to = f"/{route_title}"
+ click = f"{route_title} = true"
+
+ with vuetify.VListItem(to=to, click=click):
+ with vuetify.VListItemIcon():
+ vuetify.VIcon(mdi_icon)
+ with vuetify.VListItemContent():
+ vuetify.VListItemTitle(route_title)
diff --git a/src/python/impactx/dashboard/Toolbar/__init__.py b/src/python/impactx/dashboard/Toolbar/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/python/impactx/dashboard/Toolbar/toolbarMain.py b/src/python/impactx/dashboard/Toolbar/toolbarMain.py
new file mode 100644
index 000000000..ff5894770
--- /dev/null
+++ b/src/python/impactx/dashboard/Toolbar/toolbarMain.py
@@ -0,0 +1,74 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from trame.widgets import vuetify
+
+from ..trame_setup import setup_server
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Common toolbar elements
+# -----------------------------------------------------------------------------
+
+
+class ToolbarElements:
+ """
+ Helper functions to create
+ Vuetify UI elements for toolbar.
+ """
+
+ @staticmethod
+ def plot_options():
+ vuetify.VSelect(
+ v_model=("active_plot", "1D plots over s"),
+ items=("plot_options",),
+ label="Select plot to view",
+ hide_details=True,
+ dense=True,
+ style="max-width: 250px",
+ disabled=("disableRunSimulationButton", True),
+ )
+
+ @staticmethod
+ def run_simulation_button():
+ vuetify.VBtn(
+ "Run Simulation",
+ style="background-color: #00313C; color: white; margin: 0 20px;",
+ click=ctrl.run_simulation,
+ disabled=("disableRunSimulationButton", True),
+ )
+
+
+# -----------------------------------------------------------------------------
+# Content
+# -----------------------------------------------------------------------------
+
+
+class Toolbars:
+ """
+ Builds section toolbars for various pages.
+ """
+
+ @staticmethod
+ def run_toolbar():
+ """
+ Builds toolbar for the 'Run' page.
+ """
+
+ vuetify.VSpacer(),
+ ToolbarElements.run_simulation_button(),
+
+ @staticmethod
+ def analyze_toolbar():
+ """
+ Builds toolbar for the 'Analyze' page.
+ """
+
+ vuetify.VSpacer()
+ ToolbarElements.plot_options()
diff --git a/src/python/impactx/dashboard/__init__.py b/src/python/impactx/dashboard/__init__.py
new file mode 100644
index 000000000..ba8d0fa04
--- /dev/null
+++ b/src/python/impactx/dashboard/__init__.py
@@ -0,0 +1,5 @@
+from .jupyterApplication import JupyterMainApplication as JupyterApp
+
+__all__ = [
+ "JupyterApp",
+]
diff --git a/src/python/impactx/dashboard/__main__.py b/src/python/impactx/dashboard/__main__.py
new file mode 100644
index 000000000..285c3fc18
--- /dev/null
+++ b/src/python/impactx/dashboard/__main__.py
@@ -0,0 +1,92 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+import sys
+
+from trame.ui.router import RouterViewLayout
+from trame.ui.vuetify import SinglePageWithDrawerLayout
+from trame.widgets import router, vuetify, xterm
+
+from .Analyze.plotsMain import AnalyzeSimulation
+from .Input.distributionParameters.distributionMain import DistributionParameters
+from .Input.inputParameters.inputMain import InputParameters
+from .Input.latticeConfiguration.latticeMain import LatticeConfiguration
+from .Input.trameFunctions import TrameFunctions
+from .start import main
+from .Toolbar.toolbarMain import Toolbars
+from .trame_setup import setup_server
+
+server, state, ctrl = setup_server()
+
+# -----------------------------------------------------------------------------
+# Router Views
+# -----------------------------------------------------------------------------
+
+inputParameters = InputParameters()
+
+with RouterViewLayout(server, "/Input"):
+ with vuetify.VContainer(fluid=True):
+ with vuetify.VRow():
+ with vuetify.VCol(cols="auto", classes="pa-2"):
+ with vuetify.VRow(no_gutters=True):
+ with vuetify.VCol(cols="auto", classes="pa-2"):
+ inputParameters.card()
+ with vuetify.VCol(cols="auto", classes="pa-2"):
+ DistributionParameters.card()
+ with vuetify.VRow(no_gutters=True):
+ with vuetify.VCol(cols="auto", classes="pa-2"):
+ LatticeConfiguration.card()
+
+with RouterViewLayout(server, "/Analyze"):
+ with vuetify.VContainer(fluid=True):
+ with vuetify.VRow(no_gutters=True, classes="fill-height"):
+ with vuetify.VCol(cols="auto", classes="pa-2 fill-height"):
+ AnalyzeSimulation.card()
+ with vuetify.VCol(cols="auto", classes="pa-2 fill-height"):
+ AnalyzeSimulation.plot()
+
+
+# -----------------------------------------------------------------------------
+# GUI
+# -----------------------------------------------------------------------------
+def init_terminal():
+ with xterm.XTerm(v_if="$route.path == '/Run'") as term:
+ ctrl.terminal_print = term.writeln
+
+
+def application():
+ init_terminal()
+ with SinglePageWithDrawerLayout(server) as layout:
+ layout.title.hide()
+ with layout.toolbar:
+ with vuetify.Template(v_if="$route.path == '/Analyze'"):
+ Toolbars.analyze_toolbar()
+ with vuetify.Template(v_if="$route.path == '/Run'"):
+ Toolbars.run_toolbar()
+
+ with layout.drawer as drawer:
+ drawer.width = 200
+ with vuetify.VList():
+ vuetify.VSubheader("Simulation")
+ TrameFunctions.create_route("Input", "mdi-file-edit")
+ TrameFunctions.create_route("Run", "mdi-play")
+ TrameFunctions.create_route("Analyze", "mdi-chart-box-multiple")
+
+ with layout.content:
+ router.RouterView()
+ init_terminal()
+ return layout
+
+
+application()
+# -----------------------------------------------------------------------------
+# Main
+# -----------------------------------------------------------------------------
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/src/python/impactx/dashboard/jupyterApplication.py b/src/python/impactx/dashboard/jupyterApplication.py
new file mode 100644
index 000000000..3d0fa3e8c
--- /dev/null
+++ b/src/python/impactx/dashboard/jupyterApplication.py
@@ -0,0 +1,22 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from .__main__ import application
+
+
+class JupyterMainApplication:
+ """
+ Creates specific components from
+ Trame Application for Jupyter Notebook.
+ """
+
+ def __init__(self):
+ self.ui = self.generate_ui()
+
+ def generate_ui(self):
+ return application()
diff --git a/src/python/impactx/dashboard/requirements.txt b/src/python/impactx/dashboard/requirements.txt
new file mode 100644
index 000000000..7eaac07dc
--- /dev/null
+++ b/src/python/impactx/dashboard/requirements.txt
@@ -0,0 +1,8 @@
+pandas>=2.2.0
+plotly>=5.23.0
+trame>=3.6.2
+trame-matplotlib>=2.0.3
+trame-plotly>=3.0.2
+trame-router>=2.2.0
+trame-vuetify>=2.6.2
+trame-xterm>=0.2.1
diff --git a/src/python/impactx/dashboard/start.py b/src/python/impactx/dashboard/start.py
new file mode 100644
index 000000000..54740cd9f
--- /dev/null
+++ b/src/python/impactx/dashboard/start.py
@@ -0,0 +1,24 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from .trame_setup import setup_server
+
+server, state, ctrl = setup_server()
+
+
+# -----------------------------------------------------------------------------
+# Main
+# -----------------------------------------------------------------------------
+
+
+def main():
+ """
+ Launches Trame application server
+ """
+ server.start()
+ return 0
diff --git a/src/python/impactx/dashboard/trame_setup.py b/src/python/impactx/dashboard/trame_setup.py
new file mode 100644
index 000000000..fee62e48f
--- /dev/null
+++ b/src/python/impactx/dashboard/trame_setup.py
@@ -0,0 +1,15 @@
+"""
+This file is part of ImpactX
+
+Copyright 2024 ImpactX contributors
+Authors: Parthib Roy, Axel Huebl
+License: BSD-3-Clause-LBNL
+"""
+
+from trame.app import get_server
+
+
+def setup_server(client_type="vue2"):
+ server = get_server(client_type=client_type)
+ state, ctrl = server.state, server.controller
+ return server, state, ctrl