diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py index 4e3e27e36..bff3c7b57 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py @@ -1,7 +1,9 @@ +from .dialogs import LatticeVisualizerDialogs as Dialogs from .statistics import LatticeVisualizerStatisticComponents as StatComponents from .statistics import LatticeVisualizerStatisticUtils as StatUtils __all__ = [ + "Dialogs", "StatComponents", "StatUtils", ] diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/dialogs.py b/src/python/impactx/dashboard/Input/visualization/lattice/dialogs.py new file mode 100644 index 000000000..813ce708a --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/dialogs.py @@ -0,0 +1,50 @@ +""" +This file is part of ImpactX + +Copyright 2025 ImpactX contributors +Authors: Parthib Roy +License: BSD-3-Clause-LBNL +""" + +from .... import vuetify +from ....Input.components.card import CardComponents + + +class LatticeVisualizerDialogs: + @staticmethod + def settings(): + """ + A button to open the settings dialog for the lattice visualizer. + """ + CardComponents.card_button( + "mdi-cog", + color="white", + click="lattice_visualizer_dialog_settings = true", + description="Settings", + ) + + @staticmethod + def element_colors_tab(): + """ + A tab inside of the settings dialog to manage element colors. + """ + + with vuetify.VCardText(): + with vuetify.VRow(): + with vuetify.VCol(cols=12): + vuetify.VCardSubtitle( + "Element Color Mapping", + ) + + @staticmethod + def general_settings_tab(): + """ + A tab inside of the settings dialog for general settings. + """ + + with vuetify.VCardText(): + with vuetify.VRow(): + with vuetify.VCol(cols=12): + vuetify.VCardSubtitle( + "Settings", + ) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py b/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py index 7735b3566..4319bece7 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py @@ -26,6 +26,7 @@ def _extract_parameter_values(parameter_name: str, value_type=float): """ Helper function to extract parameter values from the lattice list. + :param parameter_name: Name of the parameter to extract (case-insensitive) :param value_type: Type to convert values to (float, int, etc.) :return: List of extracted values @@ -42,6 +43,8 @@ def _extract_parameter_values(parameter_name: str, value_type=float): return values + return values + @staticmethod def update_length_statistics() -> None: """ @@ -67,11 +70,11 @@ def update_length_statistics() -> None: state.length_stats_content = [] @staticmethod - def update_element_counts() -> dict[str, int]: + def update_element_counts() -> list[tuple[str, int]]: """ Computes the element counts in the lattice list. - :return: Dictionary of element names and their counts, sorted by count descending. + :return: List of (element name, count) tuples, sorted by count descending. """ counts = {} for element in state.selected_lattice_list: diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index 6340530e2..452079e55 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -6,9 +6,13 @@ License: BSD-3-Clause-LBNL """ -from .... import html, state, vuetify -from ....Input.components import CardBase -from . import StatComponents, StatUtils +from trame.widgets import plotly + +from .... import ctrl, html, state, vuetify +from ....Input.components.card import CardBase +from ....Input.components.navigation import NavigationComponents +from . import Dialogs, StatComponents, StatUtils +from .visualization.plot import lattice_visualizer def _update_statistics() -> None: @@ -21,9 +25,17 @@ def _update_statistics() -> None: StatUtils.update_length_statistics() +def _update_lattice_visualization() -> None: + """ + Updates the plotly figure with an updated lattice visualization. + """ + ctrl.lattice_figure_update(lattice_visualizer()) + + @state.change("selected_lattice_list") def on_lattice_list_change(**kwargs): _update_statistics() + _update_lattice_visualization() class LatticeVisualizer(CardBase): @@ -38,6 +50,10 @@ def card_content(self): """ The content of the lattice visualizer. """ + with vuetify.VDialog( + v_model=("lattice_visualizer_dialog_settings", False), max_width="33.33vw" + ): + self.dialog_settings() with vuetify.VCard(): with vuetify.VCard( @@ -50,4 +66,24 @@ def card_content(self): with vuetify.VCardTitle(classes="d-flex align-center"): html.Div("Lattice Statistics") vuetify.VSpacer() + Dialogs.settings() StatComponents.statistics() + + with vuetify.VCard(color="#002949"): + with vuetify.VCardText(): + ctrl.lattice_figure_update = plotly.Figure( + display_mode_bar="true", style="width: 100%; height: 50vh" + ).update + + @staticmethod + def dialog_settings(): + dialog_name = "lattice_visualizer_dialog_tab_settings" + + with NavigationComponents.create_dialog_tabs( + dialog_name, 2, ["Element Colors", "General Settings"] + ): + with vuetify.VTabsWindow(v_model=(dialog_name, 0)): + with vuetify.VTabsWindowItem(): + Dialogs.element_colors_tab() + with vuetify.VTabsWindowItem(): + Dialogs.general_settings_tab() diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py new file mode 100644 index 000000000..b883a324e --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py @@ -0,0 +1,7 @@ +from .elements.elements import LatticeVisualizerElements +from .utils import LatticeVisualizerUtils + +__all__ = [ + "LatticeVisualizerElements", + "LatticeVisualizerUtils", +] diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/calculations.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/calculations.py new file mode 100644 index 000000000..b7900ea15 --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/calculations.py @@ -0,0 +1,58 @@ +""" +This file is part of ImpactX + +Copyright 2025 ImpactX contributors +Authors: Parthib Roy +License: BSD-3-Clause-LBNL + +Mathematical calculations for lattice element positioning and transformations. +""" + +import numpy as np + + +def transform(x, y, rotation_deg, dx): + """ + Transform coordinates based on angle and displacement. + """ + rotation_rad = np.radians(rotation_deg) + x_new = x + dx * np.cos(rotation_rad) + y_new = y + dx * np.sin(rotation_rad) + return x_new, y_new + + +def rotate_corners( + x: float, y: float, rotation_deg: float, ds: float = 1.0, width: float = 0.1 +) -> np.ndarray: + """ + Generates rectangle's corners after applying rotation matrix. + This is utilized to properly visualize a rotated lattice element in Plotly. + + :param x: starting x-coordinate before the rotation + :param y: starting y-coordinate before the rotation + :param rotation_deg: Rotation angle in degrees, counterclockwise. + :param ds: Length of the rectangle along the local X-axis (default is 1.0). + :param width: Half of the rectangle's height (default is 0.1). + :return: A NumPy array of shape (5, 2) with rotated (x, y) corner coordinates, closed for polygon plotting. + """ + rotation_rad = np.radians(rotation_deg) + + corners = np.array( + [ + [0, -width], + [ds, -width], + [ds, width], + [0, width], + [0, -width], # close polygon + ] + ) + + R = np.array( + [ + [np.cos(rotation_rad), -np.sin(rotation_rad)], + [np.sin(rotation_rad), np.cos(rotation_rad)], + ] + ) + + rotated = corners @ R.T + [x, y] + return rotated diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/elements.py new file mode 100644 index 000000000..c2a0502a2 --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/elements.py @@ -0,0 +1,357 @@ +""" +This file is part of ImpactX + +Copyright 2025 ImpactX contributors +Authors: Parthib Roy +License: BSD-3-Clause-LBNL +""" + +import numpy as np +import plotly.graph_objects as go + +from .calculations import rotate_corners, transform + +# Global color dictionary for quadrupole visual properties +QUAD_COLORS = { + "focusing_quadrupole": { + "legend_name": "Focusing Quadrupole", + "line_color": "darkblue", + "fill_color": "lightblue", + "line_width": 2, + }, + "defocusing_quadrupole": { + "legend_name": "Defocusing Quadrupole", + "line_color": "darkred", + "fill_color": "lightcoral", + "line_width": 2, + }, + "quadrupole": { + "legend_name": "Quadrupole", + "line_color": "darkgreen", + "fill_color": "lightgreen", + "line_width": 2, + }, +} + + +class LatticeVisualizerElements: + def __init__(self): + self.seen_elements = set() + self.show_labels = True # Track whether to show labels + + def reset_legend(self): + self.seen_elements.clear() + + def set_show_labels(self, show_labels): + """Set whether to show element labels on the plot.""" + self.show_labels = show_labels + + def _add_to_legend(self, element_type): + """ + Adds an element type to the legend if it hasn't been added already. + Returns True if this is the first time seeing this element type. + """ + + if element_type not in self.seen_elements: + self.seen_elements.add(element_type) + return True + return False + + def _add_annotation(self, fig, x, y, label, **kwargs): + """ + This is the part that adds the label to the plotly figure. + """ + kwargs.setdefault("text", label) + kwargs.setdefault("showarrow", False) + fig.add_annotation(x=x, y=y, **kwargs) + + def _add_trace( + self, + fig, + show_legend=False, + element_name=None, + legend_name=None, + legend_group=None, + **kwargs, + ): + """ + This is the function that actually draws on the plotly figure. + """ + kwargs.setdefault("showlegend", show_legend) + kwargs.setdefault("hoverinfo", "text") + + # Use element_name as default for legend_name and legend_group if not provided + if element_name: + legend_name = legend_name or element_name + legend_group = legend_group or element_name + + if show_legend and legend_name: + kwargs.setdefault("name", legend_name) + kwargs.setdefault("legendgroup", legend_group or legend_name) + elif legend_group: + kwargs.setdefault("legendgroup", legend_group) + kwargs.setdefault("showlegend", False) + + # Properly handle hover information + if "text" in kwargs: + kwargs["hovertemplate"] = kwargs["text"] + "" + else: + kwargs.setdefault("hoverinfo", "skip") + + trace = go.Scatter(**kwargs) + fig.add_trace(trace) + + def _generate_hover_text( + self, index, label, ds, dx, dy, rotation, **special_params + ): + """ + Generate standardized hover text for lattice elements. + + :param index: Element index in the lattice sequence + :param label: Element label/name + :param ds: Element length + :param dx: X displacement + :param dy: Y displacement + :param rotation: Rotation angle + :param special_params: Additional parameters specific to element type (k, rc, phi, etc.) + :return: Formatted hover text string + """ + text = f"Element #{index}
{label}
ds: {ds} m
" + + # Add special parameters if provided + for param_name, param_value in special_params.items(): + if param_name == "k": + text += f"k: {param_value} m⁻²
" + elif param_name == "rc": + text += f"rc: {param_value} m
" + elif param_name == "phi": + text += f"phi: {param_value}°
" + + text += f"dx: {dx} m
dy: {dy} m
rotation: {rotation}°" + + return text + + def drift(self, fig, x, y, ds, dx, dy, rotation, label, index, element_name): + show_legend = self._add_to_legend(element_name) + rotation_rad = np.radians(rotation) + thickness = 0.05 # line thickness (half-height for visual padding) + x += dx + y += dy + + rotated_corners = rotate_corners(x, y, rotation, ds, thickness) + xs, ys = rotated_corners[:, 0], rotated_corners[:, 1] + + self._add_trace( + fig, + x=xs, + y=ys, + mode="lines", + fill="toself", + line=dict(color="gray", width=1), + fillcolor="lightgray", + text=self._generate_hover_text(index, label, ds, dx, dy, rotation), + show_legend=show_legend, + element_name=element_name, + ) + + if self.show_labels: + self._add_annotation( + fig, + x=np.mean(xs), + y=np.mean(ys) + 0.3, + label=label, + font=dict(size=10), + ) + + x1 = x + ds * np.cos(rotation_rad) + y1 = y + ds * np.sin(rotation_rad) + return x1, y1, rotation + + def quad(self, fig, x, y, k, ds, dx, dy, rotation, label, index, element_name): + x1, y1 = transform(x, y, rotation, ds) + x += dx + y += dy + + # Determine quad type and get visual properties based on k value + if k > 0: + quad_type = "focusing_quadrupole" + elif k < 0: + quad_type = "defocusing_quadrupole" + else: + quad_type = "quadrupole" + + quad_colors = QUAD_COLORS[quad_type] + quad_legend_name = quad_colors["legend_name"] + + show_legend = self._add_to_legend(quad_legend_name) + rotated_corners = rotate_corners(x, y, rotation, ds, 0.2) + xs, ys = rotated_corners[:, 0], rotated_corners[:, 1] + + self._add_trace( + fig, + x=xs, + y=ys, + mode="lines", + fill="toself", + line=dict(color=quad_colors["line_color"], width=quad_colors["line_width"]), + fillcolor=quad_colors["fill_color"], + text=self._generate_hover_text(index, label, ds, dx, dy, rotation, k=k), + show_legend=show_legend, + legend_name=quad_legend_name, + legend_group=quad_legend_name, + ) + + if self.show_labels: + self._add_annotation(fig, x=(x + x1) / 2, y=y + 0.4, label=label) + return x1, y1, rotation + + def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label, index, element_name): + show_legend = self._add_to_legend(element_name) + """ + Draw a sector‐bend (SBEND) of length ds that has radius rc. + - rc is the radius of curvature (in meters). + - ds is the arc length (in meters). + - rotation is the incoming reference angle (in degrees). + - label is the magnet's name for hover/annotation. + """ + + # Apply any lateral offsets first: + x += dx + y += dy + + phi_rad = ds / rc # Bend angle in radians + rotation_rad = np.radians(rotation) + + # The circular‐arc center (in lab coords) is found by: + # cx = x - ρ sin(incoming_angle) + # cy = y + ρ cos(incoming_angle) + # because the bend is in the local x–z plane. + r = rc + cx = x - r * np.sin(rotation_rad) + cy = y + r * np.cos(rotation_rad) + + n_points = 50 + arc_thetas = np.linspace(0, phi_rad, n_points) + arc_x = cx + r * np.sin(rotation_rad + arc_thetas) + arc_y = cy - r * np.cos(rotation_rad + arc_thetas) + + self._add_trace( + fig, + x=arc_x, + y=arc_y, + mode="lines", + line=dict(color="blue", width=3), + text=self._generate_hover_text( + index, label, ds, dx, dy, rotation, rc=rc, phi=np.degrees(phi_rad) + ), + show_legend=show_legend, + element_name=element_name, + ) + + if self.show_labels: + self._add_annotation( + fig, + x=np.mean(arc_x), + y=np.mean(arc_y) + 0.3, + label=label, + font=dict(size=10), + ) + + # Compute exit point and exit angle (in degrees) + x_end = arc_x[-1] + y_end = arc_y[-1] + final_angle = rotation + np.degrees(phi_rad) + return x_end, y_end, final_angle + + def exactSBend( + self, + fig, + x, + y, + ds: float, + dx: float, + dy: float, + rotation_deg: float, + phi_deg: float, + label: str, + index: int, + element_name: str, + ): + show_legend = self._add_to_legend(element_name) + """ + Draws an ExactSBend lattice element on the lattice visualization. + """ + + phi_rad = np.radians(phi_deg) # phi is given in degrees in the input + rotation_rad = np.radians(rotation_deg) # may or may not be given + + # Ensure the bend starts at the proper x and y coordinates + x += dx + y += dy + + # Compute curvature radius and arc path + r = ds / phi_rad # radius of curvature + + # find coords of center of the circle + circle_center_x = x - r * np.sin(rotation_rad) + circle_center_y = y + r * np.cos(rotation_rad) + + # Generate arc points + n_points = 100 # determines smoothness of the arc + arc_thetas = np.linspace(0, phi_rad, n_points) + arc_x = circle_center_x + r * np.sin(rotation_rad + arc_thetas) + arc_y = circle_center_y - r * np.cos(rotation_rad + arc_thetas) + + self._add_trace( + fig, + x=arc_x, + y=arc_y, + mode="lines", + line=dict(color="blue", width=3), + text=self._generate_hover_text( + index, label, ds, dx, dy, rotation_deg, phi=phi_deg + ), + show_legend=show_legend, + element_name=element_name, + ) + + if self.show_labels: + self._add_annotation( + fig, + x=np.mean(arc_x), + y=np.mean(arc_y) + 0.3, + label=label, + font=dict(size=10), + ) + + # Compute new beamline exit point and angle + final_angle = rotation_deg + phi_deg + x_end = arc_x[-1] + y_end = arc_y[-1] + return x_end, y_end, final_angle + + def beam_monitor(self, fig, x, y, rotation, length, label, index, element_name): + show_legend = self._add_to_legend(element_name) + x1, y1 = transform(x, y, rotation, length) + fig.add_shape( + type="rect", + x0=x, + x1=x1, + y0=y - 0.15, + y1=y + 0.15, + line=dict(color="darkgray"), + fillcolor="lightgray", + ) + self._add_trace( + fig, + x=[(x + x1) / 2], + y=[y], + mode="markers", + marker=dict(size=15, color="rgba(0,0,0,0)"), + text=self._generate_hover_text(index, label, length, 0, 0, rotation), + show_legend=show_legend, + element_name=element_name, + ) + if self.show_labels: + self._add_annotation(fig, x=(x + x1) / 2, y=y + 0.3, label=label) + return x1, y1, rotation diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py new file mode 100644 index 000000000..0f16c595c --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py @@ -0,0 +1,204 @@ +""" +This file is part of ImpactX + +Copyright 2025 ImpactX contributors +Authors: Parthib Roy +License: BSD-3-Clause-LBNL +""" + +import plotly.graph_objects as go + +from ..... import setup_server +from . import LatticeVisualizerElements as DrawElements +from . import LatticeVisualizerUtils as Utils + +server, state, ctrl = setup_server() +draw = DrawElements() +utils = Utils() + + +def _error_plot(fig: go.Figure) -> go.Figure: + """ + The error plot to display when there is an issue with the lattice data. + + :return: A plotly figure with an error message. + """ + fig.add_annotation( + text="Error: No lattice elements found or invalid data.", + showarrow=False, + font=dict(size=16, color="red"), + align="center", + xref="paper", + yref="paper", + x=0.5, + y=0.5, + ) + fig.update_layout( + title="Lattice Visualization - Error", + xaxis=dict(title="X (m)", scaleanchor="y", scaleratio=1), + yaxis=dict(title="Y (m)"), + plot_bgcolor="white", + margin=dict(l=30, r=30, t=40, b=40), + ) + return fig + + +def lattice_visualizer(): + """ + Displays the lattice visualization using a plotly figure. + Called every time the lattice list is modified. + + The current parameters which affect the visualization are: + - `ds`: Length of the element (default 1.0 m) + - `dx`: X offset of the element (default 0.0 m) + - `dy`: Y offset of the element (default 0.0 m) + - `rotation`: Rotation of the element in degrees (default 0.0) + - `k`: Quadrupole strength (default 0.0) + - Defocusing quads are colored red, focusing quads are blue + - `phi`: Bend angle in radians (default 0.0) + + Shapes: + - `drift`: Straight line representing drift space + - `quadrupole`: Rectangle representing quadrupole magnets + - `bend`: Arc representing bending magnets + - `monitor`: Small rectangle representing beam monitors + + TO-DO: + - `rc`: Radius of curvature for bends (default 0.0) + - `name`: Name of the element (used for labeling) + - `color`: Color of the element (default based on type) + - `psi`: Pole face angle in radians (default 0.0) + - `g`: gap parameter in m + - more parameters as needed + """ + fig = go.Figure() + + if not state.selected_lattice_list: + return _error_plot(fig) + + try: + x, y, rotation = 0, 0, 0 + draw.reset_legend() + + # Check if we should show labels based on element count + element_count = len(state.selected_lattice_list) + draw.set_show_labels(element_count <= 20) + + for index, element in enumerate(state.selected_lattice_list, 1): + element_name = element.get( + "name", "" + ).lower() # This gets the actual element name + element_label = utils.get_element_name_param( + element + ) # This gets the name from parameters + ds = utils.get_element_param(element, "ds", 1.0) + dx = utils.get_element_param(element, "dx", 0.0) + dy = utils.get_element_param(element, "dy", 0.0) + element_rotation = utils.get_element_param(element, "rotation", 0.0) + rotation_total = rotation + element_rotation + + # Classify and draw element based on name + if "drift" in element_name: + x, y, rotation = draw.drift( + fig, + x, + y, + ds, + dx, + dy, + rotation_total, + element_label, + index, + element_name, + ) + elif "quad" in element_name: + k = utils.get_element_param(element, "k", 0.0) + x, y, rotation = draw.quad( + fig, + x, + y, + k, + ds, + dx, + dy, + rotation_total, + element_label, + index, + element_name, + ) + elif "bend" in element_name or "dipole" in element_name: + if element_name.startswith("sbend"): + rc = utils.get_element_param(element, "rc", 0.0) + x, y, rotation = draw.sBend( + fig, + x, + y, + ds, + dx, + dy, + rotation_total, + rc, + element_label, + index, + element_name, + ) + elif element_name.startswith("exactsbend"): + phi = utils.get_element_param(element, "phi", 0) + x, y, rotation = draw.exactSBend( + fig, + x, + y, + ds, + dx, + dy, + rotation_total, + phi, + element_label, + index, + element_name, + ) + else: + # Default bend handling + x, y, rotation = draw.drift( + fig, + x, + y, + ds, + dx, + dy, + rotation_total, + element_label, + index, + element_name, + ) + elif "monitor" in element_name or "bpm" in element_name: + x, y, rotation = draw.beam_monitor( + fig, x, y, rotation_total, ds, element_label, index, element_name + ) + else: + # Default fallback to drift + x, y, rotation = draw.drift( + fig, + x, + y, + ds, + dx, + dy, + rotation_total, + element_label, + index, + element_name, + ) + + except ValueError: + return _error_plot(fig) + + fig.update_layout( + title="Lattice Visualization", + xaxis=dict(title="X (m)", scaleanchor="y", scaleratio=1), + yaxis=dict(title="Y (m)"), + plot_bgcolor="white", + margin=dict(l=30, r=30, t=40, b=40), + ) + + return fig diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/utils.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/utils.py new file mode 100644 index 000000000..4bbb25891 --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/utils.py @@ -0,0 +1,34 @@ +""" +This file is part of ImpactX + +Copyright 2025 ImpactX contributors +Authors: Parthib Roy +License: BSD-3-Clause-LBNL +""" + + +class LatticeVisualizerUtils: + @staticmethod + def get_element_param(element, name, default=0.0): + for param in element.get("parameters", []): + if param.get("parameter_name", "").lower() == name.lower(): + try: + return float(param.get("sim_input", default)) + except (ValueError, TypeError): + raise ValueError( + f"Invalid value for {name}: {param.get('sim_input', default)}" + ) + return default + + @staticmethod + def get_element_name_param(element): + """Get the name parameter from the element's parameters list.""" + for param in element.get("parameters", []): + if param.get("parameter_name", "").lower() == "name": + name_value = param.get("sim_input", "") + if name_value and name_value.strip(): # Check if name is not empty + return name_value + + # If no name parameter found or it's empty, create a meaningful fallback + element_type = element.get("name", "") + return f"{element_type}" # Use the element type as fallback