From bf5ddea9e5f1fc8a7dee6aaaf9fcfc48a8468e4e Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Thu, 29 May 2025 23:47:52 -0700 Subject: [PATCH 01/35] add initial visualization all elements are same color, shape, act in the same way. just rounded squares at the moment that display left to right in order --- .../Input/visualization/lattice/ui.py | 39 +++++++++++++++++++ src/python/impactx/dashboard/__main__.py | 4 ++ 2 files changed, 43 insertions(+) create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/ui.py diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py new file mode 100644 index 000000000..ef4dbb5bd --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -0,0 +1,39 @@ +""" +This file is part of ImpactX + +Copyright 2025 ImpactX contributors +Authors: Parthib Roy +License: BSD-3-Clause-LBNL +""" + +from .... import setup_server, vuetify +from ... import CardBase, CardComponents +server, state, ctrl = setup_server() + + +@state.change("selected_lattice_list") +def on_lattice_list_change(**kwargs): + print(f"lattice list changed") + +class LatticeVisualizer(CardBase): + HEADER_NAME = "Lattice Visualizer" + + def __init__(self): + super().__init__() + + def card_content(self): + with vuetify.VCard(**self.card_props): + CardComponents.input_header(self.HEADER_NAME) + with vuetify.VCardText(): + with vuetify.VRow(): + with vuetify.VCol( + v_for="(element, index) in selected_lattice_list", + ): + with vuetify.VCard( + text=("element.name",), + class_="rounded-xl pa-2 text-center", + color="primary", + text_color="white", + elevation=2, + ): + pass \ No newline at end of file diff --git a/src/python/impactx/dashboard/__main__.py b/src/python/impactx/dashboard/__main__.py index 71829d592..a43f217c9 100644 --- a/src/python/impactx/dashboard/__main__.py +++ b/src/python/impactx/dashboard/__main__.py @@ -12,6 +12,7 @@ from trame.ui.vuetify3 import SinglePageWithDrawerLayout from trame.widgets import router, xterm +from .Input.visualization.lattice.ui import LatticeVisualizer from . import ( AnalyzeSimulation, DistributionParameters, @@ -25,6 +26,7 @@ setup_server, vuetify, ) + from .start import main from .Toolbar.sim_history.ui import load_my_js @@ -80,6 +82,8 @@ with vuetify.VRow(**card_row_padding): with vuetify.VCol(cols=12, **card_column_padding): lattice_config.card() + with vuetify.VCol(cols=12, md=6): + LatticeVisualizer().card() with RouterViewLayout(server, "/Analyze"): with vuetify.VContainer(fluid=True): From 2f846fc67746834f31051b8d89f5a44db52c6f0d Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Fri, 30 May 2025 00:02:11 -0700 Subject: [PATCH 02/35] add colors based off of element name small list for now --- .../Input/visualization/lattice/ui.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index ef4dbb5bd..2dda37006 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -10,10 +10,27 @@ from ... import CardBase, CardComponents server, state, ctrl = setup_server() +ELEMENT_COLOR_MAP = { + "drift": "blue lighten-2", + "quad": "red darken-1", + "monitor": "grey darken-2", +} + +def get_element_color(name: str) -> str: + """ + Determine Vuetify color for an element based on its name. + """ + clean_name = name.lower() + for element_key, color in ELEMENT_COLOR_MAP.items(): + if element_key in clean_name: + return color + return "grey lighten-1" + @state.change("selected_lattice_list") def on_lattice_list_change(**kwargs): - print(f"lattice list changed") + for element in state.selected_lattice_list: + element["color"] = get_element_color(element["name"]) class LatticeVisualizer(CardBase): HEADER_NAME = "Lattice Visualizer" @@ -31,8 +48,7 @@ def card_content(self): ): with vuetify.VCard( text=("element.name",), - class_="rounded-xl pa-2 text-center", - color="primary", + color=("element.color",), text_color="white", elevation=2, ): From ee49d10abe64ef2ea00e6a99d03d2d04553cb07c Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Fri, 30 May 2025 09:10:31 -0700 Subject: [PATCH 03/35] add settings and dialogs --- .../Input/visualization/lattice/__init__.py | 7 ++++ .../Input/visualization/lattice/dialogs.py | 31 +++++++++++++++++ .../Input/visualization/lattice/ui.py | 33 +++++++++++++++++-- .../Input/visualization/lattice/utils.py | 20 +++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/__init__.py create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/dialogs.py create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/utils.py diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py new file mode 100644 index 000000000..f72b25e3c --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py @@ -0,0 +1,7 @@ +from .dialogs import LatticeVisualizerDialogs +from .utils import LatticeVisualizerComponents + +__all__ = [ + "LatticeVisualizerComponents", + "LatticeVisualizerDialogs", +] 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..1568e270a --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/dialogs.py @@ -0,0 +1,31 @@ +""" +This file is part of ImpactX + +Copyright 2025 ImpactX contributors +Authors: Parthib Roy +License: BSD-3-Clause-LBNL +""" + +from .... import setup_server, vuetify + +server, state, ctrl = setup_server() + + +class LatticeVisualizerDialogs: + @staticmethod + def element_colors_tab(): + with vuetify.VCardText(): + with vuetify.VRow(): + with vuetify.VCol(cols=12): + vuetify.VCardSubtitle( + "Element Color Mapping", + ) + + @staticmethod + def general_settings_tab(): + with vuetify.VCardText(): + with vuetify.VRow(): + with vuetify.VCol(cols=12): + vuetify.VCardSubtitle( + "Settings", + ) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index 2dda37006..dfbc7095e 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -7,15 +7,19 @@ """ from .... import setup_server, vuetify -from ... import CardBase, CardComponents +from ... import CardBase, CardComponents, NavigationComponents +from . import LatticeVisualizerComponents, LatticeVisualizerDialogs + server, state, ctrl = setup_server() + ELEMENT_COLOR_MAP = { "drift": "blue lighten-2", "quad": "red darken-1", "monitor": "grey darken-2", } + def get_element_color(name: str) -> str: """ Determine Vuetify color for an element based on its name. @@ -32,15 +36,25 @@ def on_lattice_list_change(**kwargs): for element in state.selected_lattice_list: element["color"] = get_element_color(element["name"]) + class LatticeVisualizer(CardBase): HEADER_NAME = "Lattice Visualizer" + components = LatticeVisualizerComponents def __init__(self): super().__init__() def card_content(self): + with vuetify.VDialog( + v_model=("lattice_visualizer_dialog_settings", False), max_width="33.33vw" + ): + self.dialog_settings() + with vuetify.VCard(**self.card_props): - CardComponents.input_header(self.HEADER_NAME) + CardComponents.input_header( + self.HEADER_NAME, + additional_components={"end": self.components.settings}, + ) with vuetify.VCardText(): with vuetify.VRow(): with vuetify.VCol( @@ -52,4 +66,17 @@ def card_content(self): text_color="white", elevation=2, ): - pass \ No newline at end of file + pass + + @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(): + LatticeVisualizerDialogs.element_colors_tab() + with vuetify.VTabsWindowItem(): + LatticeVisualizerDialogs.general_settings_tab() diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py new file mode 100644 index 000000000..9f6272569 --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py @@ -0,0 +1,20 @@ +""" +This file is part of ImpactX + +Copyright 2025 ImpactX contributors +Authors: Parthib Roy +License: BSD-3-Clause-LBNL +""" + +from ... import CardComponents + + +class LatticeVisualizerComponents: + @staticmethod + def settings(): + CardComponents.card_button( + "mdi-cog", + color="grey-darken-2", + click="lattice_visualizer_dialog_settings = true", + documentation="Settings", + ) From b39b58e83efc37829f3c3d14114d330097fbeec9 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Fri, 30 May 2025 10:08:48 -0700 Subject: [PATCH 04/35] fix __main__.py --- src/python/impactx/dashboard/Input/visualization/lattice/ui.py | 3 +-- src/python/impactx/dashboard/__main__.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index dfbc7095e..77e3091c1 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -35,8 +35,7 @@ def get_element_color(name: str) -> str: def on_lattice_list_change(**kwargs): for element in state.selected_lattice_list: element["color"] = get_element_color(element["name"]) - - + class LatticeVisualizer(CardBase): HEADER_NAME = "Lattice Visualizer" components = LatticeVisualizerComponents diff --git a/src/python/impactx/dashboard/__main__.py b/src/python/impactx/dashboard/__main__.py index a43f217c9..2c46d4ba4 100644 --- a/src/python/impactx/dashboard/__main__.py +++ b/src/python/impactx/dashboard/__main__.py @@ -83,7 +83,8 @@ with vuetify.VCol(cols=12, **card_column_padding): lattice_config.card() with vuetify.VCol(cols=12, md=6): - LatticeVisualizer().card() + with vuetify.VRow(**card_row_padding): + LatticeVisualizer().card() with RouterViewLayout(server, "/Analyze"): with vuetify.VContainer(fluid=True): From e6b69b7bae3e69b659d601ba90d8553d17f27e97 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Fri, 30 May 2025 10:08:21 -0700 Subject: [PATCH 05/35] add: statistics --- .../Input/visualization/lattice/__init__.py | 3 +- .../Input/visualization/lattice/ui.py | 16 +- .../Input/visualization/lattice/utils.py | 151 ++++++++++++++++++ src/python/impactx/dashboard/__main__.py | 3 +- 4 files changed, 166 insertions(+), 7 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py index f72b25e3c..67e321d7b 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py @@ -1,7 +1,8 @@ from .dialogs import LatticeVisualizerDialogs -from .utils import LatticeVisualizerComponents +from .utils import LatticeVisualizerComponents, LatticeVisualizerUtils __all__ = [ "LatticeVisualizerComponents", "LatticeVisualizerDialogs", + "LatticeVisualizerUtils", ] diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index 77e3091c1..5bd4be161 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -8,10 +8,11 @@ from .... import setup_server, vuetify from ... import CardBase, CardComponents, NavigationComponents -from . import LatticeVisualizerComponents, LatticeVisualizerDialogs +from . import LatticeVisualizerComponents, LatticeVisualizerDialogs, LatticeVisualizerUtils server, state, ctrl = setup_server() +utils = LatticeVisualizerUtils ELEMENT_COLOR_MAP = { "drift": "blue lighten-2", @@ -30,19 +31,23 @@ def get_element_color(name: str) -> str: return color return "grey lighten-1" - @state.change("selected_lattice_list") def on_lattice_list_change(**kwargs): for element in state.selected_lattice_list: element["color"] = get_element_color(element["name"]) - + + state.total_elements = len(state.selected_lattice_list) + state.total_steps = utils.update_total_steps() + state.element_counts = utils.update_element_counts() + utils.update_length_statistics() + class LatticeVisualizer(CardBase): HEADER_NAME = "Lattice Visualizer" components = LatticeVisualizerComponents def __init__(self): super().__init__() - + def card_content(self): with vuetify.VDialog( v_model=("lattice_visualizer_dialog_settings", False), max_width="33.33vw" @@ -54,7 +59,8 @@ def card_content(self): self.HEADER_NAME, additional_components={"end": self.components.settings}, ) - with vuetify.VCardText(): + with vuetify.VCardText(): + self.components.statistics() with vuetify.VRow(): with vuetify.VCol( v_for="(element, index) in selected_lattice_list", diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py index 9f6272569..076a0d5f4 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py @@ -7,7 +7,86 @@ """ from ... import CardComponents +from .... import html, setup_server, vuetify +server, state, ctrl = setup_server() +state.total_elements = 0 +state.total_length = 0 +state.max_length = 0 +state.min_length = 0 +state.avg_length = 0 +state.total_steps = 0 +state.periods = 1 +state.element_counts = {} +state.length_stats_content = "" +state.lattice_list_empty = len(state.selected_lattice_list) == 0 + +class LatticeVisualizerUtils: + + @staticmethod + def update_length_statistics() -> None: + """ + Computes and return the total, min, max, and average length of the + lattice configuration. Sums all elements' 'ds' (length) parameters. + """ + lengths = [] + + for element in state.selected_lattice_list: + for param in element.get("parameters", []): + if param.get("parameter_name", "").lower() == "ds": + try: + lengths.append(float(param.get("sim_input", 0))) + except (ValueError, TypeError): + pass + + if lengths: + state.additional_length_stats_diabled = False + state.total_length = round(sum(lengths), 2) + state.min_length = round(min(lengths), 3) + state.max_length = round(max(lengths), 3) + state.avg_length = round(sum(lengths) / len(lengths), 3) + state.length_stats_content = [ + f"Longest: {state.max_length} m", + f"Shortest: {state.min_length} m", + f"Average: {state.avg_length} m" + ] + + @staticmethod + def update_element_counts() -> list[tuple[str, int]]: + """ + Computes the element counts in the lattice list. + + :return: List of (element name, count) tuples, sorted by count descending. + """ + counts = {} + for element in state.selected_lattice_list: + key = str(element["name"]).lower() + # can't do += 1 because key is not already initialized + counts[key] = counts.get(key, 0) + 1 + + state.lattice_list_empty = len(counts) == 0 + # sort from desc. so we see top elements left to right + return sorted(counts.items(), key=lambda item: item[1], reverse=True) + + @staticmethod + def update_total_steps() -> int: + """ + Computes the total number of steps by summing 'nslice' + across all lattice elements. + + :return: Total number of slices. + """ + total_steps = 0 + + for element in state.selected_lattice_list: + for param in element.get("parameters", []): + if param.get("parameter_name", "").lower() == "nslice": + try: + total_steps += int(param.get("sim_input", 0)) + except (ValueError, TypeError): + pass + + return total_steps class LatticeVisualizerComponents: @staticmethod @@ -18,3 +97,75 @@ def settings(): click="lattice_visualizer_dialog_settings = true", documentation="Settings", ) + + @staticmethod + def _stat(title: str) -> None: + """ + Displays a statistic block for the statistics section + in the lattice visualizer. + + :param title: The statistic name + """ + title_state_name = title.lower().replace(" ", "_") + + is_stat_length = "length" in title.lower() + suffix = " m" if is_stat_length else "" + + vuetify.VCardSubtitle(title) + + with vuetify.VCardTitle( + f"{{{{ {title_state_name} }}}}{suffix}", + classes="d-flex align-center justify-center" + ): + if is_stat_length: + LatticeVisualizerComponents._additional_length_stats() + + @staticmethod + def _additional_length_stats(): + with vuetify.VTooltip( + location="bottom", + ): + with vuetify.Template(v_slot_activator="{ props }"): + vuetify.VIcon( + "mdi-information", + size="x-small", + v_bind="props", + disabled=("lattice_list_empty",), + classes="ml-2", + ) + with vuetify.Template(v_for="line in length_stats_content"): + html.Div("{{ line }}") + + @staticmethod + def statistics(): + with vuetify.VCard( + title="Statistics", + elevation=2, + color="grey lighten-4", + style="margin-bottom: 20px;", + ): + with vuetify.VCardText(): + # row 1: numerical stats + with vuetify.VRow(classes="text-center"): + with vuetify.VCol(): + LatticeVisualizerComponents._stat("Total Elements") + with vuetify.VCol(): + LatticeVisualizerComponents._stat("Total Length") + with vuetify.VCol(): + LatticeVisualizerComponents._stat("Total Steps") + with vuetify.VCol(): + LatticeVisualizerComponents._stat("Periods") + + # row 2: element breakdown + with vuetify.VRow(): + with vuetify.VCol(cols=12): + vuetify.VCardSubtitle("Element Breakdown") + with vuetify.Template(v_if="lattice_list_empty"): + vuetify.VCardTitle("Lattice list is empty.") + with vuetify.Template(v_else=True): + with vuetify.VChipGroup(): + with vuetify.Template(v_for="[name, count] in element_counts", key="name"): + vuetify.VChip( + "{{ name.charAt(0).toUpperCase() + name.slice(1) }}: {{ count }}", + style="font-size: 0.75rem;", + ) diff --git a/src/python/impactx/dashboard/__main__.py b/src/python/impactx/dashboard/__main__.py index 2c46d4ba4..8eb6e96e9 100644 --- a/src/python/impactx/dashboard/__main__.py +++ b/src/python/impactx/dashboard/__main__.py @@ -84,7 +84,8 @@ lattice_config.card() with vuetify.VCol(cols=12, md=6): with vuetify.VRow(**card_row_padding): - LatticeVisualizer().card() + with vuetify.VCol(): + LatticeVisualizer().card() with RouterViewLayout(server, "/Analyze"): with vuetify.VContainer(fluid=True): From 62e8455355643d8878566c1231d650ddf33d1c78 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Fri, 30 May 2025 18:32:46 -0700 Subject: [PATCH 06/35] statistics: modify --- .../Input/visualization/lattice/__init__.py | 11 +++++----- .../Input/visualization/lattice/ui.py | 19 +++++++---------- .../Input/visualization/lattice/utils.py | 21 ++++++++++++------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py index 67e321d7b..ea3084fdc 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py @@ -1,8 +1,9 @@ -from .dialogs import LatticeVisualizerDialogs -from .utils import LatticeVisualizerComponents, LatticeVisualizerUtils +from .dialogs import LatticeVisualizerDialogs as Dialogs +from .utils import LatticeVisualizerComponents as Components +from .utils import LatticeVisualizerUtils as Utils __all__ = [ - "LatticeVisualizerComponents", - "LatticeVisualizerDialogs", - "LatticeVisualizerUtils", + "Dialogs", + "Components", + "Utils", ] diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index 5bd4be161..7b17b82b2 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -8,12 +8,10 @@ from .... import setup_server, vuetify from ... import CardBase, CardComponents, NavigationComponents -from . import LatticeVisualizerComponents, LatticeVisualizerDialogs, LatticeVisualizerUtils +from . import Components, Dialogs, Utils server, state, ctrl = setup_server() -utils = LatticeVisualizerUtils - ELEMENT_COLOR_MAP = { "drift": "blue lighten-2", "quad": "red darken-1", @@ -37,13 +35,12 @@ def on_lattice_list_change(**kwargs): element["color"] = get_element_color(element["name"]) state.total_elements = len(state.selected_lattice_list) - state.total_steps = utils.update_total_steps() - state.element_counts = utils.update_element_counts() - utils.update_length_statistics() + state.total_steps = Utils.update_total_steps() + state.element_counts = Utils.update_element_counts() + Utils.update_length_statistics() class LatticeVisualizer(CardBase): HEADER_NAME = "Lattice Visualizer" - components = LatticeVisualizerComponents def __init__(self): super().__init__() @@ -57,10 +54,10 @@ def card_content(self): with vuetify.VCard(**self.card_props): CardComponents.input_header( self.HEADER_NAME, - additional_components={"end": self.components.settings}, + additional_components={"end": Components.settings}, ) with vuetify.VCardText(): - self.components.statistics() + Components.statistics() with vuetify.VRow(): with vuetify.VCol( v_for="(element, index) in selected_lattice_list", @@ -82,6 +79,6 @@ def dialog_settings(): ): with vuetify.VTabsWindow(v_model=(dialog_name, 0)): with vuetify.VTabsWindowItem(): - LatticeVisualizerDialogs.element_colors_tab() + Dialogs.element_colors_tab() with vuetify.VTabsWindowItem(): - LatticeVisualizerDialogs.general_settings_tab() + Dialogs.general_settings_tab() diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py index 076a0d5f4..d363c6325 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py @@ -16,10 +16,10 @@ state.min_length = 0 state.avg_length = 0 state.total_steps = 0 -state.periods = 1 +state.periods = 1 if state.total_elements > 0 else 0 state.element_counts = {} state.length_stats_content = "" -state.lattice_list_empty = len(state.selected_lattice_list) == 0 +state.lattice_is_empty = len(state.selected_lattice_list) == 0 class LatticeVisualizerUtils: @@ -40,7 +40,6 @@ def update_length_statistics() -> None: pass if lengths: - state.additional_length_stats_diabled = False state.total_length = round(sum(lengths), 2) state.min_length = round(min(lengths), 3) state.max_length = round(max(lengths), 3) @@ -50,6 +49,12 @@ def update_length_statistics() -> None: f"Shortest: {state.min_length} m", f"Average: {state.avg_length} m" ] + else: + state.total_length = None + state.min_length = None + state.max_length = None + state.avg_length = None + state.length_stats_content = [] @staticmethod def update_element_counts() -> list[tuple[str, int]]: @@ -64,7 +69,7 @@ def update_element_counts() -> list[tuple[str, int]]: # can't do += 1 because key is not already initialized counts[key] = counts.get(key, 0) + 1 - state.lattice_list_empty = len(counts) == 0 + state.lattice_is_empty = len(counts) == 0 # sort from desc. so we see top elements left to right return sorted(counts.items(), key=lambda item: item[1], reverse=True) @@ -109,12 +114,12 @@ def _stat(title: str) -> None: title_state_name = title.lower().replace(" ", "_") is_stat_length = "length" in title.lower() - suffix = " m" if is_stat_length else "" + suffix = " m" if is_stat_length and state.total_elements > 0 else "" vuetify.VCardSubtitle(title) with vuetify.VCardTitle( - f"{{{{ {title_state_name} }}}}{suffix}", + f"{{{{ {title_state_name} || '-' }}}}{suffix}", classes="d-flex align-center justify-center" ): if is_stat_length: @@ -130,7 +135,7 @@ def _additional_length_stats(): "mdi-information", size="x-small", v_bind="props", - disabled=("lattice_list_empty",), + disabled=("lattice_is_empty",), classes="ml-2", ) with vuetify.Template(v_for="line in length_stats_content"): @@ -160,7 +165,7 @@ def statistics(): with vuetify.VRow(): with vuetify.VCol(cols=12): vuetify.VCardSubtitle("Element Breakdown") - with vuetify.Template(v_if="lattice_list_empty"): + with vuetify.Template(v_if="lattice_is_empty"): vuetify.VCardTitle("Lattice list is empty.") with vuetify.Template(v_else=True): with vuetify.VChipGroup(): From 79a70001af2b5ec6a8a3ef06cebe009156a25b06 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Fri, 30 May 2025 19:25:09 -0700 Subject: [PATCH 07/35] update state.periods --- .../Input/visualization/lattice/ui.py | 25 ++++--- .../Input/visualization/lattice/utils.py | 66 +++++++++---------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index 7b17b82b2..a6050764d 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -6,8 +6,8 @@ License: BSD-3-Clause-LBNL """ -from .... import setup_server, vuetify -from ... import CardBase, CardComponents, NavigationComponents +from .... import html, setup_server, vuetify +from ... import CardBase, NavigationComponents from . import Components, Dialogs, Utils server, state, ctrl = setup_server() @@ -35,12 +35,12 @@ def on_lattice_list_change(**kwargs): element["color"] = get_element_color(element["name"]) state.total_elements = len(state.selected_lattice_list) + state.periods = 1 if state.total_elements > 0 else 0 state.total_steps = Utils.update_total_steps() state.element_counts = Utils.update_element_counts() Utils.update_length_statistics() class LatticeVisualizer(CardBase): - HEADER_NAME = "Lattice Visualizer" def __init__(self): super().__init__() @@ -51,13 +51,20 @@ def card_content(self): ): self.dialog_settings() - with vuetify.VCard(**self.card_props): - CardComponents.input_header( - self.HEADER_NAME, - additional_components={"end": Components.settings}, - ) - with vuetify.VCardText(): + with vuetify.VCard(): + with vuetify.VCard( + classes="d-flex flex-column", + style="min-height: 3.75rem; margin-bottom: 20px;", + color="grey lighten-4", + elevation=2, + ): + # create custom header over using component in CardComponents + with vuetify.VCardTitle(classes="d-flex align-center"): + html.Div("Lattice Statistics") + vuetify.VSpacer() + Components.settings() Components.statistics() + with vuetify.VCardText(): with vuetify.VRow(): with vuetify.VCol( v_for="(element, index) in selected_lattice_list", diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py index d363c6325..e4364f873 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py @@ -16,7 +16,7 @@ state.min_length = 0 state.avg_length = 0 state.total_steps = 0 -state.periods = 1 if state.total_elements > 0 else 0 +state.periods = 0 state.element_counts = {} state.length_stats_content = "" state.lattice_is_empty = len(state.selected_lattice_list) == 0 @@ -98,9 +98,9 @@ class LatticeVisualizerComponents: def settings(): CardComponents.card_button( "mdi-cog", - color="grey-darken-2", + color="white", click="lattice_visualizer_dialog_settings = true", - documentation="Settings", + description="Settings", ) @staticmethod @@ -116,11 +116,11 @@ def _stat(title: str) -> None: is_stat_length = "length" in title.lower() suffix = " m" if is_stat_length and state.total_elements > 0 else "" - vuetify.VCardSubtitle(title) + vuetify.VCardSubtitle(title, classes="pb-0 mb-0") with vuetify.VCardTitle( f"{{{{ {title_state_name} || '-' }}}}{suffix}", - classes="d-flex align-center justify-center" + classes="d-flex align-center justify-center my-0 py-0" ): if is_stat_length: LatticeVisualizerComponents._additional_length_stats() @@ -143,34 +143,28 @@ def _additional_length_stats(): @staticmethod def statistics(): - with vuetify.VCard( - title="Statistics", - elevation=2, - color="grey lighten-4", - style="margin-bottom: 20px;", - ): - with vuetify.VCardText(): - # row 1: numerical stats - with vuetify.VRow(classes="text-center"): - with vuetify.VCol(): - LatticeVisualizerComponents._stat("Total Elements") - with vuetify.VCol(): - LatticeVisualizerComponents._stat("Total Length") - with vuetify.VCol(): - LatticeVisualizerComponents._stat("Total Steps") - with vuetify.VCol(): - LatticeVisualizerComponents._stat("Periods") - - # row 2: element breakdown - with vuetify.VRow(): - with vuetify.VCol(cols=12): - vuetify.VCardSubtitle("Element Breakdown") - with vuetify.Template(v_if="lattice_is_empty"): - vuetify.VCardTitle("Lattice list is empty.") - with vuetify.Template(v_else=True): - with vuetify.VChipGroup(): - with vuetify.Template(v_for="[name, count] in element_counts", key="name"): - vuetify.VChip( - "{{ name.charAt(0).toUpperCase() + name.slice(1) }}: {{ count }}", - style="font-size: 0.75rem;", - ) + with vuetify.VCardText(): + # row 1: numerical stats + with vuetify.VRow(classes="text-center"): + with vuetify.VCol(): + LatticeVisualizerComponents._stat("Total Elements") + with vuetify.VCol(): + LatticeVisualizerComponents._stat("Total Length") + with vuetify.VCol(): + LatticeVisualizerComponents._stat("Total Steps") + with vuetify.VCol(): + LatticeVisualizerComponents._stat("Periods") + + # row 2: element breakdown + with vuetify.VRow(classes="pt-0 mt-0"): + with vuetify.VCol(cols=12): + vuetify.VCardSubtitle("Element Breakdown") + with vuetify.Template(v_if="lattice_is_empty"): + vuetify.VCardTitle("Lattice list is empty.") + with vuetify.Template(v_else=True): + with vuetify.VChipGroup(): + with vuetify.Template(v_for="[name, count] in element_counts", key="name"): + vuetify.VChip( + "{{ name.charAt(0).toUpperCase() + name.slice(1) }}: {{ count }}", + style="font-size: 0.75rem;", + ) From e4d0ac204e8e95a6990217081e58f8f8612e8625 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Wed, 4 Jun 2025 12:16:36 -0700 Subject: [PATCH 08/35] rename classes --- .../Input/visualization/lattice/__init__.py | 8 ++++---- .../dashboard/Input/visualization/lattice/ui.py | 13 +++++++------ .../dashboard/Input/visualization/lattice/utils.py | 14 +++++++------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py index ea3084fdc..3e8da6e2b 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py @@ -1,9 +1,9 @@ from .dialogs import LatticeVisualizerDialogs as Dialogs -from .utils import LatticeVisualizerComponents as Components -from .utils import LatticeVisualizerUtils as Utils +from .utils import LatticeVisualizerStatisticComponents as StatComponents +from .utils import LatticeVisualizerStatisticUtils as StatUtils __all__ = [ "Dialogs", - "Components", - "Utils", + "StatComponents", + "StatUtils", ] diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index a6050764d..b4e5c1b21 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -8,7 +8,7 @@ from .... import html, setup_server, vuetify from ... import CardBase, NavigationComponents -from . import Components, Dialogs, Utils +from . import Dialogs, StatComponents, StatUtils server, state, ctrl = setup_server() @@ -36,9 +36,9 @@ def on_lattice_list_change(**kwargs): state.total_elements = len(state.selected_lattice_list) state.periods = 1 if state.total_elements > 0 else 0 - state.total_steps = Utils.update_total_steps() - state.element_counts = Utils.update_element_counts() - Utils.update_length_statistics() + state.total_steps = StatUtils.update_total_steps() + state.element_counts = StatUtils.update_element_counts() + StatUtils.update_length_statistics() class LatticeVisualizer(CardBase): @@ -62,8 +62,9 @@ def card_content(self): with vuetify.VCardTitle(classes="d-flex align-center"): html.Div("Lattice Statistics") vuetify.VSpacer() - Components.settings() - Components.statistics() + StatComponents.settings() + StatComponents.statistics() + with vuetify.VCardText(): with vuetify.VRow(): with vuetify.VCol( diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py index e4364f873..fe9f48188 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/utils.py @@ -21,7 +21,7 @@ state.length_stats_content = "" state.lattice_is_empty = len(state.selected_lattice_list) == 0 -class LatticeVisualizerUtils: +class LatticeVisualizerStatisticUtils: @staticmethod def update_length_statistics() -> None: @@ -93,7 +93,7 @@ def update_total_steps() -> int: return total_steps -class LatticeVisualizerComponents: +class LatticeVisualizerStatisticComponents: @staticmethod def settings(): CardComponents.card_button( @@ -123,7 +123,7 @@ def _stat(title: str) -> None: classes="d-flex align-center justify-center my-0 py-0" ): if is_stat_length: - LatticeVisualizerComponents._additional_length_stats() + LatticeVisualizerStatisticComponents._additional_length_stats() @staticmethod def _additional_length_stats(): @@ -147,13 +147,13 @@ def statistics(): # row 1: numerical stats with vuetify.VRow(classes="text-center"): with vuetify.VCol(): - LatticeVisualizerComponents._stat("Total Elements") + LatticeVisualizerStatisticComponents._stat("Total Elements") with vuetify.VCol(): - LatticeVisualizerComponents._stat("Total Length") + LatticeVisualizerStatisticComponents._stat("Total Length") with vuetify.VCol(): - LatticeVisualizerComponents._stat("Total Steps") + LatticeVisualizerStatisticComponents._stat("Total Steps") with vuetify.VCol(): - LatticeVisualizerComponents._stat("Periods") + LatticeVisualizerStatisticComponents._stat("Periods") # row 2: element breakdown with vuetify.VRow(classes="pt-0 mt-0"): From 8fa965ffcf6eae934ea9fe0cee0d733a1c95eba5 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Wed, 4 Jun 2025 12:24:15 -0700 Subject: [PATCH 09/35] rename utils -> statistic_utils.py --- .../impactx/dashboard/Input/visualization/lattice/__init__.py | 4 ++-- .../visualization/lattice/{utils.py => statistic_utils.py} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/python/impactx/dashboard/Input/visualization/lattice/{utils.py => statistic_utils.py} (100%) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py index 3e8da6e2b..799771715 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py @@ -1,6 +1,6 @@ from .dialogs import LatticeVisualizerDialogs as Dialogs -from .utils import LatticeVisualizerStatisticComponents as StatComponents -from .utils import LatticeVisualizerStatisticUtils as StatUtils +from .statistic_utils import LatticeVisualizerStatisticComponents as StatComponents +from .statistic_utils import LatticeVisualizerStatisticUtils as StatUtils __all__ = [ "Dialogs", diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/utils.py b/src/python/impactx/dashboard/Input/visualization/lattice/statistic_utils.py similarity index 100% rename from src/python/impactx/dashboard/Input/visualization/lattice/utils.py rename to src/python/impactx/dashboard/Input/visualization/lattice/statistic_utils.py From 6b09e8c6bcca7aa516be416cb8da791c08425455 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Wed, 4 Jun 2025 15:14:30 -0700 Subject: [PATCH 10/35] move statistic updates to own helper function --- .../dashboard/Input/visualization/lattice/ui.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index b4e5c1b21..f59e28e5e 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -29,8 +29,10 @@ def get_element_color(name: str) -> str: return color return "grey lighten-1" -@state.change("selected_lattice_list") -def on_lattice_list_change(**kwargs): +def _update_statistics(): + """ + Update statistics based on the current selected lattice elements. + """ for element in state.selected_lattice_list: element["color"] = get_element_color(element["name"]) @@ -40,6 +42,10 @@ def on_lattice_list_change(**kwargs): state.element_counts = StatUtils.update_element_counts() StatUtils.update_length_statistics() +@state.change("selected_lattice_list") +def on_lattice_list_change(**kwargs): + _update_statistics() + class LatticeVisualizer(CardBase): def __init__(self): From 17c61a57eb842cefd69c8613ea716a5ab7b99010 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Wed, 4 Jun 2025 19:43:13 -0700 Subject: [PATCH 11/35] change colors --- src/python/impactx/dashboard/Input/visualization/lattice/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index f59e28e5e..2d7573812 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -61,7 +61,7 @@ def card_content(self): with vuetify.VCard( classes="d-flex flex-column", style="min-height: 3.75rem; margin-bottom: 20px;", - color="grey lighten-4", + color="#002949", elevation=2, ): # create custom header over using component in CardComponents From 2b0eff905443e318840e5467c35cbf07db223c05 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 3 Jun 2025 18:15:46 -0700 Subject: [PATCH 12/35] draft - lattice visualizer --- .../Input/visualization/lattice/__init__.py | 2 +- .../Input/visualization/lattice/elements.py | 280 ++++++++++++++++++ .../Input/visualization/lattice/plot.py | 145 +++++++++ .../Input/visualization/lattice/ui.py | 17 +- .../impactx/dashboard/Toolbar/controls.py | 3 +- 5 files changed, 444 insertions(+), 3 deletions(-) create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/elements.py create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/plot.py diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py index 799771715..fa374a96a 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py @@ -4,6 +4,6 @@ __all__ = [ "Dialogs", - "StatComponents", + "StatComponents", "StatUtils", ] diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py new file mode 100644 index 000000000..77572d04f --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -0,0 +1,280 @@ +""" +This file is part of ImpactX + +Copyright 2025 ImpactX contributors +Authors: Parthib Roy +License: BSD-3-Clause-LBNL +""" + +import plotly.graph_objects as go +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 + +import numpy as np + +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 +import numpy as np + + +class LatticeVisualizerElements: + + def __init__(self): + pass + + def _add_trace(self, fig, **kwargs): + """ + This is the function that actually draws on the plotly figure. + """ + kwargs.setdefault("showlegend", False) + kwargs.setdefault("hoverinfo", "text") + trace = go.Scatter(**kwargs) + fig.add_trace(trace) + + 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 drift(self, fig, x, y, ds, dx, dy, rotation, label): + 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", + hovertext=f"{label}
Length: {ds:.2f} m" + ) + + + 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): + x1, y1 = transform(x, y, rotation, ds) + x += dx + y += dy + + match k: + case _ if k > 0: + line_color = "darkblue" + fill_color = "lightblue" + case _ if k < 0: + line_color = "darkred" + fill_color = "lightcoral" + case _: + line_color = "darkgreen" + fill_color = "lightgreen" + + 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=line_color, width=2), + fillcolor=fill_color, + hovertext=f"{label}
Length: {ds:.2f} m" + ) + + 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): + """ + 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), + hovertext=( + f"{label}
" + f"Length: {ds:.2f} m
" + f"Radius of curvature: {rc:.2f} m
" + f"dx: {dx:.3f} m
" + f"dy: {dy:.3f} m
" + f"Bend Angle: {np.degrees(phi_rad):.2f}°" + ) + ) + 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): + """ + 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), + hovertext=( + f"{label}
" + f"Length: {ds:.2f} m
" + f"Bend Angle: {phi_deg:.1f}°
" + f"dx: {dx:.3f} m
" + f"dy: {dy:.3f} m" + ) + ) + + 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): + 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)'), + hovertext=f"{label}
Length: {length:.2f} m" + ) + 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/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py new file mode 100644 index 000000000..74767e9e1 --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py @@ -0,0 +1,145 @@ +""" +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 .elements import LatticeVisualizerElements as DrawElements + +from .... import setup_server + +server, state, ctrl = setup_server() +draw = DrawElements() + +# ----------------------------------------------------------------------------- +# utils +# ----------------------------------------------------------------------------- + +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 + +def classify_element(element_name, element=None): + name = element_name.lower() + match name: + case _ if "drift" in name: + return "drift" + case _ if "quad" in name: + return "quadrupole" + case _ if "bend" in name or "dipole" in name: + return "bend" + case _ if "monitor" in name or "bpm" in name: + return "monitor" + case _: + return "drift" # default fallback + +# ----------------------------------------------------------------------------- +# Lattice Visualizer +# ----------------------------------------------------------------------------- + +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 + + for element in state.selected_lattice_list: + element_name = element.get("name", "") + element_type = classify_element(element_name, element) + ds = get_element_param(element, "ds", 1.0) + dx = get_element_param(element, "dx", 0.0) + dy = get_element_param(element, "dy", 0.0) + element_rotation = get_element_param(element, "rotation", 0.0) + rotation_total = rotation + element_rotation + + match element_type: + case "drift": + x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_name) + case "quadrupole": + k = get_element_param(element, "k", 0.0) + x, y, rotation = draw.quad(fig, x, y, k, ds, dx, dy, rotation_total, element_name) + case "bend": + if element_name.lower().startswith("sbend"): + rc = get_element_param(element, "rc", 0.0) + x, y, rotation = draw.sBend(fig, x, y, ds, dx, dy, rotation_total, rc, element_name) + elif element_name.lower().startswith("exactsbend"): + phi = get_element_param(element, "phi", 0) + x, y, rotation = draw.exactSBend(fig, x, y, ds, dx, dy, rotation_total, phi, element_name) + case "monitor": + x, y, rotation = draw.beam_monitor(fig, x, y, rotation_total, ds, element_name) + case _: + x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, 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 \ No newline at end of file diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index 2d7573812..0b7f69372 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -7,14 +7,17 @@ """ from .... import html, setup_server, vuetify +from trame.widgets import plotly + from ... import CardBase, NavigationComponents from . import Dialogs, StatComponents, StatUtils +from .plot import lattice_visualizer server, state, ctrl = setup_server() ELEMENT_COLOR_MAP = { "drift": "blue lighten-2", - "quad": "red darken-1", + "quad": "green darken-1", "monitor": "grey darken-2", } @@ -42,9 +45,14 @@ def _update_statistics(): state.element_counts = StatUtils.update_element_counts() StatUtils.update_length_statistics() +def _update_lattice_visualization(): + # Update plotly figure with new 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): @@ -70,6 +78,13 @@ def card_content(self): vuetify.VSpacer() StatComponents.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 with vuetify.VCardText(): with vuetify.VRow(): diff --git a/src/python/impactx/dashboard/Toolbar/controls.py b/src/python/impactx/dashboard/Toolbar/controls.py index 506fc28de..55e0d43c9 100644 --- a/src/python/impactx/dashboard/Toolbar/controls.py +++ b/src/python/impactx/dashboard/Toolbar/controls.py @@ -36,7 +36,8 @@ def on_import_file_change(import_file, **kwargs): state.importing_file = True DashboardParser.file_details(import_file) DashboardParser.populate_impactx_simulation_file_to_ui(import_file) - except Exception: + except Exception as e: + print(f"Error parsing import file: {e}") state.import_file_error = True state.import_file_error_message = "Unable to parse" finally: From f658acd0fcb08dc3229563da33ea32a78e3f35d0 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Thu, 5 Jun 2025 22:34:46 -0700 Subject: [PATCH 13/35] add legend for each unique element --- .../Input/visualization/lattice/elements.py | 57 ++++++++++++++++--- .../Input/visualization/lattice/plot.py | 1 + 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index 77572d04f..06467567c 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -56,13 +56,30 @@ def rotate_corners(x: float, y: float, rotation_deg: float, ds: float = 1.0, wid class LatticeVisualizerElements: def __init__(self): - pass + self.seen_elements = set() + self.show_labels = True # Track whether to show labels - def _add_trace(self, fig, **kwargs): + 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. + """ + if element_type not in self.seen_elements: + self.seen_elements.add(element_type) + + def _add_trace(self, fig, show_legend=False, legend_name=None, **kwargs): """ This is the function that actually draws on the plotly figure. """ - kwargs.setdefault("showlegend", False) + kwargs.setdefault("showlegend", show_legend) + if show_legend and legend_name: + kwargs.setdefault("name", legend_name) kwargs.setdefault("hoverinfo", "text") trace = go.Scatter(**kwargs) fig.add_trace(trace) @@ -76,6 +93,7 @@ def _add_annotation(self, fig, x, y, label, **kwargs): fig.add_annotation(x=x, y=y, **kwargs) def drift(self, fig, x, y, ds, dx, dy, rotation, label): + show_legend = self._add_to_legend("drift") rotation_rad = np.radians(rotation) thickness = 0.05 # line thickness (half-height for visual padding) x += dx @@ -92,7 +110,9 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label): fill="toself", line=dict(color="gray", width=1), fillcolor="lightgray", - hovertext=f"{label}
Length: {ds:.2f} m" + hovertext=f"{label}
Length: {ds:.2f} m", + show_legend=self.should_show_legend("drift"), + legend_name="Drift" ) @@ -110,18 +130,25 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label): def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): + show_legend = self._add_to_legend(quad_type) x1, y1 = transform(x, y, rotation, ds) x += dx y += dy - + match k: case _ if k > 0: + quad_type = "focusing_quad" + legend_name = "Focusing Quadrupole" line_color = "darkblue" fill_color = "lightblue" case _ if k < 0: + quad_type = "defocusing_quad" + legend_name = "Defocusing Quadrupole" line_color = "darkred" fill_color = "lightcoral" case _: + quad_type = "quadrupole" + legend_name = "Quadrupole" line_color = "darkgreen" fill_color = "lightgreen" @@ -136,7 +163,9 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): fill="toself", line=dict(color=line_color, width=2), fillcolor=fill_color, - hovertext=f"{label}
Length: {ds:.2f} m" + hovertext=f"{label}
Length: {ds:.2f} m", + show_legend=show_legend, + legend_name=legend_name ) self._add_annotation(fig, x=(x + x1)/2, y=y+0.4, label=label) @@ -145,6 +174,7 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): + show_legend = self._add_to_legend("sbend") """ Draw a sector‐bend (SBEND) of length ds that has radius rc. - rc is the radius of curvature (in meters). @@ -186,8 +216,11 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): f"dx: {dx:.3f} m
" f"dy: {dy:.3f} m
" f"Bend Angle: {np.degrees(phi_rad):.2f}°" - ) + ), + show_legend=self.should_show_legend("sbend"), + legend_name="Sector Bend" ) + self._add_annotation( fig, x=np.mean(arc_x), @@ -204,6 +237,7 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: float, phi_deg: float, label: str): + show_legend = self._add_to_legend("exactsbend") """ Draws an ExactSBend lattice element on the lattice visualization. """ @@ -240,7 +274,9 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f f"Bend Angle: {phi_deg:.1f}°
" f"dx: {dx:.3f} m
" f"dy: {dy:.3f} m" - ) + ), + show_legend= self.should_show_legend("exactsbend"), + legend_name="Exact Sector Bend" ) self._add_annotation( @@ -260,6 +296,7 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f def beam_monitor(self, fig, x, y, rotation, length, label): + show_legend = self._add_to_legend("monitor") x1, y1 = transform(x, y, rotation, length) fig.add_shape( type="rect", @@ -273,7 +310,9 @@ def beam_monitor(self, fig, x, y, rotation, length, label): x=[(x + x1)/2], y=[y], mode="markers", marker=dict(size=15, color='rgba(0,0,0,0)'), - hovertext=f"{label}
Length: {length:.2f} m" + hovertext=f"{label}
Length: {length:.2f} m", + show_legend=self.should_show_legend("monitor"), + legend_name="Beam Monitor" ) 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/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py index 74767e9e1..a4d808ecd 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py @@ -104,6 +104,7 @@ def lattice_visualizer(): try: x, y, rotation = 0, 0, 0 + draw.reset_legend() for element in state.selected_lattice_list: element_name = element.get("name", "") From b7a82eff79a05bed3113c518ea81279423e2fbd8 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Thu, 5 Jun 2025 22:39:39 -0700 Subject: [PATCH 14/35] group up indexes in legend --- .../impactx/dashboard/Input/visualization/lattice/elements.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index 06467567c..4edf46d7d 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -80,6 +80,10 @@ def _add_trace(self, fig, show_legend=False, legend_name=None, **kwargs): kwargs.setdefault("showlegend", show_legend) if show_legend and legend_name: kwargs.setdefault("name", legend_name) + kwargs.setdefault("legendgroup", legend_name) + elif legend_name: + kwargs.setdefault("legendgroup", legend_name) + kwargs.setdefault("showlegend", False) kwargs.setdefault("hoverinfo", "text") trace = go.Scatter(**kwargs) fig.add_trace(trace) From 7f963c51bab822e19939c2d781186e91698203e8 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Thu, 5 Jun 2025 22:43:33 -0700 Subject: [PATCH 15/35] get element parameters name --- .../Input/visualization/lattice/elements.py | 31 +++++++++++-------- .../Input/visualization/lattice/plot.py | 22 ++++++++----- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index 4edf46d7d..481f53d7b 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -73,16 +73,16 @@ def _add_to_legend(self, element_type): if element_type not in self.seen_elements: self.seen_elements.add(element_type) - def _add_trace(self, fig, show_legend=False, legend_name=None, **kwargs): + def _add_trace(self, fig, show_legend=False, legend_name=None, legend_group=None, **kwargs): """ This is the function that actually draws on the plotly figure. """ kwargs.setdefault("showlegend", show_legend) if show_legend and legend_name: kwargs.setdefault("name", legend_name) - kwargs.setdefault("legendgroup", legend_name) - elif legend_name: - kwargs.setdefault("legendgroup", legend_name) + kwargs.setdefault("legendgroup", legend_group or legend_name) + elif legend_group: + kwargs.setdefault("legendgroup", legend_group) kwargs.setdefault("showlegend", False) kwargs.setdefault("hoverinfo", "text") trace = go.Scatter(**kwargs) @@ -115,8 +115,9 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label): line=dict(color="gray", width=1), fillcolor="lightgray", hovertext=f"{label}
Length: {ds:.2f} m", - show_legend=self.should_show_legend("drift"), - legend_name="Drift" + show_legend=show_legend, + legend_name="Drift", + legend_group="Drift" ) @@ -169,7 +170,8 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): fillcolor=fill_color, hovertext=f"{label}
Length: {ds:.2f} m", show_legend=show_legend, - legend_name=legend_name + legend_name=legend_name, + legend_group=legend_name ) self._add_annotation(fig, x=(x + x1)/2, y=y+0.4, label=label) @@ -221,8 +223,9 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): f"dy: {dy:.3f} m
" f"Bend Angle: {np.degrees(phi_rad):.2f}°" ), - show_legend=self.should_show_legend("sbend"), - legend_name="Sector Bend" + show_legend=show_legend, + legend_name="Sector Bend", + legend_group="Sector Bend" ) self._add_annotation( @@ -279,8 +282,9 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f f"dx: {dx:.3f} m
" f"dy: {dy:.3f} m" ), - show_legend= self.should_show_legend("exactsbend"), - legend_name="Exact Sector Bend" + show_legend=show_legend, + legend_name="Exact Sector Bend", + legend_group="Exact Sector Bend" ) self._add_annotation( @@ -315,8 +319,9 @@ def beam_monitor(self, fig, x, y, rotation, length, label): mode="markers", marker=dict(size=15, color='rgba(0,0,0,0)'), hovertext=f"{label}
Length: {length:.2f} m", - show_legend=self.should_show_legend("monitor"), - legend_name="Beam Monitor" + show_legend=show_legend, + legend_name="Beam Monitor", + legend_group="Beam Monitor" ) 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/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py index a4d808ecd..157a3e102 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py @@ -27,6 +27,13 @@ def get_element_param(element, name, default=0.0): raise ValueError(f"Invalid value for {name}: {param.get('sim_input', default)}") return default +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": + return param.get("sim_input", "") + return element.get("name", "") # fallback to element name if no name parameter + def classify_element(element_name, element=None): name = element_name.lower() match name: @@ -107,7 +114,8 @@ def lattice_visualizer(): draw.reset_legend() for element in state.selected_lattice_list: - element_name = element.get("name", "") + element_name = element.get("name", "") # This gets the actual element name + element_label = get_element_name_param(element) # This gets the name from parameters element_type = classify_element(element_name, element) ds = get_element_param(element, "ds", 1.0) dx = get_element_param(element, "dx", 0.0) @@ -117,21 +125,21 @@ def lattice_visualizer(): match element_type: case "drift": - x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_name) + x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label) case "quadrupole": k = get_element_param(element, "k", 0.0) - x, y, rotation = draw.quad(fig, x, y, k, ds, dx, dy, rotation_total, element_name) + x, y, rotation = draw.quad(fig, x, y, k, ds, dx, dy, rotation_total, element_label) case "bend": if element_name.lower().startswith("sbend"): rc = get_element_param(element, "rc", 0.0) - x, y, rotation = draw.sBend(fig, x, y, ds, dx, dy, rotation_total, rc, element_name) + x, y, rotation = draw.sBend(fig, x, y, ds, dx, dy, rotation_total, rc, element_label) elif element_name.lower().startswith("exactsbend"): phi = get_element_param(element, "phi", 0) - x, y, rotation = draw.exactSBend(fig, x, y, ds, dx, dy, rotation_total, phi, element_name) + x, y, rotation = draw.exactSBend(fig, x, y, ds, dx, dy, rotation_total, phi, element_label) case "monitor": - x, y, rotation = draw.beam_monitor(fig, x, y, rotation_total, ds, element_name) + x, y, rotation = draw.beam_monitor(fig, x, y, rotation_total, ds, element_label) case _: - x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_name) + x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label) except ValueError: return _error_plot(fig) From a2eec42d0e9a1358f21fb9cc338fcb571f0d124c Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Thu, 5 Jun 2025 22:49:29 -0700 Subject: [PATCH 16/35] do not display labels past 20 elements --- .../Input/visualization/lattice/elements.py | 54 ++++++++++--------- .../Input/visualization/lattice/plot.py | 4 ++ 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index 481f53d7b..c7b32dd64 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -120,14 +120,14 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label): legend_group="Drift" ) - - self._add_annotation( - fig, - x=np.mean(xs), - y=np.mean(ys) + 0.3, - label=label, - font=dict(size=10), - ) + 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) @@ -174,11 +174,10 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): legend_group=legend_name ) - self._add_annotation(fig, x=(x + x1)/2, y=y+0.4, label=label) + 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): show_legend = self._add_to_legend("sbend") """ @@ -228,13 +227,14 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): legend_group="Sector Bend" ) - self._add_annotation( - fig, - x=np.mean(arc_x), - y=np.mean(arc_y) + 0.3, - label=label, - font=dict(size=10), - ) + 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] @@ -287,13 +287,14 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f legend_group="Exact Sector Bend" ) - self._add_annotation( - fig, - x=np.mean(arc_x), - y=np.mean(arc_y) + 0.3, - label=label, - font=dict(size=10), - ) + 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 @@ -323,6 +324,7 @@ def beam_monitor(self, fig, x, y, rotation, length, label): legend_name="Beam Monitor", legend_group="Beam Monitor" ) - self._add_annotation(fig, x=(x + x1)/2, y=y+0.3, label=label) + 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/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py index 157a3e102..eb91fffd0 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py @@ -112,6 +112,10 @@ def lattice_visualizer(): 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 element in state.selected_lattice_list: element_name = element.get("name", "") # This gets the actual element name From 90d5a721f7e4ac77e37c8a8d5aaa61e5fb7d6c83 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Thu, 5 Jun 2025 22:58:46 -0700 Subject: [PATCH 17/35] fix hover annotations --- .../Input/visualization/lattice/elements.py | 59 ++++++++++++++----- .../Input/visualization/lattice/plot.py | 9 ++- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index c7b32dd64..790fc1425 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -84,7 +84,14 @@ def _add_trace(self, fig, show_legend=False, legend_name=None, legend_group=None elif legend_group: kwargs.setdefault("legendgroup", legend_group) kwargs.setdefault("showlegend", False) - kwargs.setdefault("hoverinfo", "text") + + # Properly handle hover information + if 'hovertext' in kwargs: + kwargs['hovertemplate'] = kwargs.pop('hovertext') + '' + kwargs['hoverinfo'] = 'text' + else: + kwargs.setdefault("hoverinfo", "skip") + trace = go.Scatter(**kwargs) fig.add_trace(trace) @@ -114,7 +121,14 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label): fill="toself", line=dict(color="gray", width=1), fillcolor="lightgray", - hovertext=f"{label}
Length: {ds:.2f} m", + text=( + f"{label}
" + f"ds: {ds} m
" + f"dx: {dx} m
" + f"dy: {dy} m
" + f"rotation: {rotation}°" + ), + hoverinfo="text", show_legend=show_legend, legend_name="Drift", legend_group="Drift" @@ -168,7 +182,15 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): fill="toself", line=dict(color=line_color, width=2), fillcolor=fill_color, - hovertext=f"{label}
Length: {ds:.2f} m", + text=( + f"{label}
" + f"ds: {ds} m
" + f"k: {k} m⁻²
" + f"dx: {dx} m
" + f"dy: {dy} m
" + f"rotation: {rotation}°" + ), + hoverinfo="text", show_legend=show_legend, legend_name=legend_name, legend_group=legend_name @@ -214,14 +236,15 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): y=arc_y, mode="lines", line=dict(color="blue", width=3), - hovertext=( + text=( f"{label}
" - f"Length: {ds:.2f} m
" - f"Radius of curvature: {rc:.2f} m
" - f"dx: {dx:.3f} m
" - f"dy: {dy:.3f} m
" - f"Bend Angle: {np.degrees(phi_rad):.2f}°" + f"ds: {ds} m
" + f"rc: {rc} m
" + f"dx: {dx} m
" + f"dy: {dy} m
" + f"phi: {np.degrees(phi_rad)}°" ), + hoverinfo="text", show_legend=show_legend, legend_name="Sector Bend", legend_group="Sector Bend" @@ -275,13 +298,14 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f y=arc_y, mode="lines", line=dict(color="blue", width=3), - hovertext=( + text=( f"{label}
" - f"Length: {ds:.2f} m
" - f"Bend Angle: {phi_deg:.1f}°
" - f"dx: {dx:.3f} m
" - f"dy: {dy:.3f} m" + f"ds: {ds} m
" + f"phi: {phi_deg}°
" + f"dx: {dx} m
" + f"dy: {dy} m" ), + hoverinfo="text", show_legend=show_legend, legend_name="Exact Sector Bend", legend_group="Exact Sector Bend" @@ -319,7 +343,12 @@ def beam_monitor(self, fig, x, y, rotation, length, label): x=[(x + x1)/2], y=[y], mode="markers", marker=dict(size=15, color='rgba(0,0,0,0)'), - hovertext=f"{label}
Length: {length:.2f} m", + text=( + f"{label}
" + f"ds: {length} m
" + f"rotation: {rotation}°" + ), + hoverinfo="text", show_legend=show_legend, legend_name="Beam Monitor", legend_group="Beam Monitor" diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py index eb91fffd0..4d20fe671 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py @@ -31,8 +31,13 @@ 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": - return param.get("sim_input", "") - return element.get("name", "") # fallback to element name if no name parameter + 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 def classify_element(element_name, element=None): name = element_name.lower() From 5e249539e432bd8416907d01aeba90531e20ba6e Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Mon, 9 Jun 2025 18:55:21 -0700 Subject: [PATCH 18/35] refactor: element hover text --- .../Input/visualization/lattice/elements.py | 95 ++++++++----------- 1 file changed, 42 insertions(+), 53 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index 790fc1425..ac4d22019 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -78,6 +78,8 @@ def _add_trace(self, fig, show_legend=False, legend_name=None, legend_group=None This is the function that actually draws on the plotly figure. """ kwargs.setdefault("showlegend", show_legend) + kwargs.setdefault("hoverinfo", "text") + if show_legend and legend_name: kwargs.setdefault("name", legend_name) kwargs.setdefault("legendgroup", legend_group or legend_name) @@ -86,22 +88,47 @@ def _add_trace(self, fig, show_legend=False, legend_name=None, legend_group=None kwargs.setdefault("showlegend", False) # Properly handle hover information - if 'hovertext' in kwargs: - kwargs['hovertemplate'] = kwargs.pop('hovertext') + '' - kwargs['hoverinfo'] = 'text' + if 'text' in kwargs: + kwargs['hovertemplate'] = kwargs['text'] + '' else: kwargs.setdefault("hoverinfo", "skip") trace = go.Scatter(**kwargs) fig.add_trace(trace) - def _add_annotation(self, fig, x, y, label, **kwargs): + def _generate_hover_text(self, label, ds, dx, dy, rotation, **special_params): """ - This is the part that adds the label to the plotly figure. + Generate standardized hover text for lattice elements. + + :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 """ - kwargs.setdefault("text", label) - kwargs.setdefault("showarrow", False) - fig.add_annotation(x=x, y=y, **kwargs) + text = ( + f"{label}
" + f"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
" + f"dy: {dy} m
" + f"rotation: {rotation}°" + ) + + return text def drift(self, fig, x, y, ds, dx, dy, rotation, label): show_legend = self._add_to_legend("drift") @@ -121,14 +148,7 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label): fill="toself", line=dict(color="gray", width=1), fillcolor="lightgray", - text=( - f"{label}
" - f"ds: {ds} m
" - f"dx: {dx} m
" - f"dy: {dy} m
" - f"rotation: {rotation}°" - ), - hoverinfo="text", + text=self._generate_hover_text(label, ds, dx, dy, rotation), show_legend=show_legend, legend_name="Drift", legend_group="Drift" @@ -149,7 +169,6 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label): def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): - show_legend = self._add_to_legend(quad_type) x1, y1 = transform(x, y, rotation, ds) x += dx y += dy @@ -171,6 +190,7 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): line_color = "darkgreen" fill_color = "lightgreen" + show_legend = self._add_to_legend(quad_type) rotated_corners = rotate_corners(x, y, rotation, ds, 0.2) xs, ys = rotated_corners[:, 0], rotated_corners[:, 1] @@ -182,15 +202,7 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): fill="toself", line=dict(color=line_color, width=2), fillcolor=fill_color, - text=( - f"{label}
" - f"ds: {ds} m
" - f"k: {k} m⁻²
" - f"dx: {dx} m
" - f"dy: {dy} m
" - f"rotation: {rotation}°" - ), - hoverinfo="text", + text=self._generate_hover_text(label, ds, dx, dy, rotation, k=k), show_legend=show_legend, legend_name=legend_name, legend_group=legend_name @@ -207,7 +219,7 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): - 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. + - label is the magnet's name for hover/annotation. """ # Apply any lateral offsets first: @@ -236,15 +248,7 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): y=arc_y, mode="lines", line=dict(color="blue", width=3), - text=( - f"{label}
" - f"ds: {ds} m
" - f"rc: {rc} m
" - f"dx: {dx} m
" - f"dy: {dy} m
" - f"phi: {np.degrees(phi_rad)}°" - ), - hoverinfo="text", + text=self._generate_hover_text(label, ds, dx, dy, rotation, rc=rc, phi=np.degrees(phi_rad)), show_legend=show_legend, legend_name="Sector Bend", legend_group="Sector Bend" @@ -265,7 +269,6 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): 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): show_legend = self._add_to_legend("exactsbend") """ @@ -298,14 +301,7 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f y=arc_y, mode="lines", line=dict(color="blue", width=3), - text=( - f"{label}
" - f"ds: {ds} m
" - f"phi: {phi_deg}°
" - f"dx: {dx} m
" - f"dy: {dy} m" - ), - hoverinfo="text", + text=self._generate_hover_text(label, ds, dx, dy, rotation_deg, phi=phi_deg), show_legend=show_legend, legend_name="Exact Sector Bend", legend_group="Exact Sector Bend" @@ -326,8 +322,6 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f y_end = arc_y[-1] return x_end, y_end, final_angle - - def beam_monitor(self, fig, x, y, rotation, length, label): show_legend = self._add_to_legend("monitor") x1, y1 = transform(x, y, rotation, length) @@ -343,12 +337,7 @@ def beam_monitor(self, fig, x, y, rotation, length, label): x=[(x + x1)/2], y=[y], mode="markers", marker=dict(size=15, color='rgba(0,0,0,0)'), - text=( - f"{label}
" - f"ds: {length} m
" - f"rotation: {rotation}°" - ), - hoverinfo="text", + text=self._generate_hover_text(label, length, 0, 0, rotation), show_legend=show_legend, legend_name="Beam Monitor", legend_group="Beam Monitor" From 098788489ada1decb1fe073cbf326460660169fd Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Mon, 9 Jun 2025 18:58:17 -0700 Subject: [PATCH 19/35] display the element index on hover --- .../Input/visualization/lattice/elements.py | 24 ++++++++++--------- .../Input/visualization/lattice/plot.py | 14 +++++------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index ac4d22019..26f10fa8e 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -96,10 +96,11 @@ def _add_trace(self, fig, show_legend=False, legend_name=None, legend_group=None trace = go.Scatter(**kwargs) fig.add_trace(trace) - def _generate_hover_text(self, label, ds, dx, dy, rotation, **special_params): + 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 @@ -109,6 +110,7 @@ def _generate_hover_text(self, label, ds, dx, dy, rotation, **special_params): :return: Formatted hover text string """ text = ( + f"Element #{index}
" f"{label}
" f"ds: {ds} m
" ) @@ -130,7 +132,7 @@ def _generate_hover_text(self, label, ds, dx, dy, rotation, **special_params): return text - def drift(self, fig, x, y, ds, dx, dy, rotation, label): + def drift(self, fig, x, y, ds, dx, dy, rotation, label, index): show_legend = self._add_to_legend("drift") rotation_rad = np.radians(rotation) thickness = 0.05 # line thickness (half-height for visual padding) @@ -148,7 +150,7 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label): fill="toself", line=dict(color="gray", width=1), fillcolor="lightgray", - text=self._generate_hover_text(label, ds, dx, dy, rotation), + text=self._generate_hover_text(index, label, ds, dx, dy, rotation), show_legend=show_legend, legend_name="Drift", legend_group="Drift" @@ -168,7 +170,7 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label): return x1, y1, rotation - def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): + def quad(self, fig, x, y, k, ds, dx, dy, rotation, label, index): x1, y1 = transform(x, y, rotation, ds) x += dx y += dy @@ -202,7 +204,7 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): fill="toself", line=dict(color=line_color, width=2), fillcolor=fill_color, - text=self._generate_hover_text(label, ds, dx, dy, rotation, k=k), + text=self._generate_hover_text(index, label, ds, dx, dy, rotation, k=k), show_legend=show_legend, legend_name=legend_name, legend_group=legend_name @@ -212,7 +214,7 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label): 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): + def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label, index): show_legend = self._add_to_legend("sbend") """ Draw a sector‐bend (SBEND) of length ds that has radius rc. @@ -248,7 +250,7 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): y=arc_y, mode="lines", line=dict(color="blue", width=3), - text=self._generate_hover_text(label, ds, dx, dy, rotation, rc=rc, phi=np.degrees(phi_rad)), + text=self._generate_hover_text(index, label, ds, dx, dy, rotation, rc=rc, phi=np.degrees(phi_rad)), show_legend=show_legend, legend_name="Sector Bend", legend_group="Sector Bend" @@ -269,7 +271,7 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label): 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): + def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: float, phi_deg: float, label: str, index: int): show_legend = self._add_to_legend("exactsbend") """ Draws an ExactSBend lattice element on the lattice visualization. @@ -301,7 +303,7 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f y=arc_y, mode="lines", line=dict(color="blue", width=3), - text=self._generate_hover_text(label, ds, dx, dy, rotation_deg, phi=phi_deg), + text=self._generate_hover_text(index, label, ds, dx, dy, rotation_deg, phi=phi_deg), show_legend=show_legend, legend_name="Exact Sector Bend", legend_group="Exact Sector Bend" @@ -322,7 +324,7 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f y_end = arc_y[-1] return x_end, y_end, final_angle - def beam_monitor(self, fig, x, y, rotation, length, label): + def beam_monitor(self, fig, x, y, rotation, length, label, index): show_legend = self._add_to_legend("monitor") x1, y1 = transform(x, y, rotation, length) fig.add_shape( @@ -337,7 +339,7 @@ def beam_monitor(self, fig, x, y, rotation, length, label): x=[(x + x1)/2], y=[y], mode="markers", marker=dict(size=15, color='rgba(0,0,0,0)'), - text=self._generate_hover_text(label, length, 0, 0, rotation), + text=self._generate_hover_text(index, label, length, 0, 0, rotation), show_legend=show_legend, legend_name="Beam Monitor", legend_group="Beam Monitor" diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py index 4d20fe671..865d7af37 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py @@ -122,7 +122,7 @@ def lattice_visualizer(): element_count = len(state.selected_lattice_list) draw.set_show_labels(element_count <= 20) - for element in state.selected_lattice_list: + for index, element in enumerate(state.selected_lattice_list, 1): element_name = element.get("name", "") # This gets the actual element name element_label = get_element_name_param(element) # This gets the name from parameters element_type = classify_element(element_name, element) @@ -134,21 +134,21 @@ def lattice_visualizer(): match element_type: case "drift": - x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label) + x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label, index) case "quadrupole": k = get_element_param(element, "k", 0.0) - x, y, rotation = draw.quad(fig, x, y, k, ds, dx, dy, rotation_total, element_label) + x, y, rotation = draw.quad(fig, x, y, k, ds, dx, dy, rotation_total, element_label, index) case "bend": if element_name.lower().startswith("sbend"): rc = get_element_param(element, "rc", 0.0) - x, y, rotation = draw.sBend(fig, x, y, ds, dx, dy, rotation_total, rc, element_label) + x, y, rotation = draw.sBend(fig, x, y, ds, dx, dy, rotation_total, rc, element_label, index) elif element_name.lower().startswith("exactsbend"): phi = get_element_param(element, "phi", 0) - x, y, rotation = draw.exactSBend(fig, x, y, ds, dx, dy, rotation_total, phi, element_label) + x, y, rotation = draw.exactSBend(fig, x, y, ds, dx, dy, rotation_total, phi, element_label, index) case "monitor": - x, y, rotation = draw.beam_monitor(fig, x, y, rotation_total, ds, element_label) + x, y, rotation = draw.beam_monitor(fig, x, y, rotation_total, ds, element_label, index) case _: - x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label) + x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label, index) except ValueError: return _error_plot(fig) From e055af2a31bfbc26f0c8a06f2029ab961f48de40 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Mon, 9 Jun 2025 19:03:51 -0700 Subject: [PATCH 20/35] simplify: remove call to 'classify_elements' --- .../Input/visualization/lattice/plot.py | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py index 865d7af37..04a8a25bd 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py @@ -39,20 +39,6 @@ def get_element_name_param(element): element_type = element.get("name", "") return f"{element_type}" # Use the element type as fallback -def classify_element(element_name, element=None): - name = element_name.lower() - match name: - case _ if "drift" in name: - return "drift" - case _ if "quad" in name: - return "quadrupole" - case _ if "bend" in name or "dipole" in name: - return "bend" - case _ if "monitor" in name or "bpm" in name: - return "monitor" - case _: - return "drift" # default fallback - # ----------------------------------------------------------------------------- # Lattice Visualizer # ----------------------------------------------------------------------------- @@ -123,32 +109,36 @@ def lattice_visualizer(): draw.set_show_labels(element_count <= 20) for index, element in enumerate(state.selected_lattice_list, 1): - element_name = element.get("name", "") # This gets the actual element name + element_name = element.get("name", "").lower() # This gets the actual element name element_label = get_element_name_param(element) # This gets the name from parameters - element_type = classify_element(element_name, element) ds = get_element_param(element, "ds", 1.0) dx = get_element_param(element, "dx", 0.0) dy = get_element_param(element, "dy", 0.0) element_rotation = get_element_param(element, "rotation", 0.0) rotation_total = rotation + element_rotation - match element_type: - case "drift": - x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label, index) - case "quadrupole": - k = get_element_param(element, "k", 0.0) - x, y, rotation = draw.quad(fig, x, y, k, ds, dx, dy, rotation_total, element_label, index) - case "bend": - if element_name.lower().startswith("sbend"): - rc = get_element_param(element, "rc", 0.0) - x, y, rotation = draw.sBend(fig, x, y, ds, dx, dy, rotation_total, rc, element_label, index) - elif element_name.lower().startswith("exactsbend"): - phi = get_element_param(element, "phi", 0) - x, y, rotation = draw.exactSBend(fig, x, y, ds, dx, dy, rotation_total, phi, element_label, index) - case "monitor": - x, y, rotation = draw.beam_monitor(fig, x, y, rotation_total, ds, element_label, index) - case _: + # 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) + elif "quad" in element_name: + k = get_element_param(element, "k", 0.0) + x, y, rotation = draw.quad(fig, x, y, k, ds, dx, dy, rotation_total, element_label, index) + elif "bend" in element_name or "dipole" in element_name: + if element_name.startswith("sbend"): + rc = get_element_param(element, "rc", 0.0) + x, y, rotation = draw.sBend(fig, x, y, ds, dx, dy, rotation_total, rc, element_label, index) + elif element_name.startswith("exactsbend"): + phi = get_element_param(element, "phi", 0) + x, y, rotation = draw.exactSBend(fig, x, y, ds, dx, dy, rotation_total, phi, element_label, index) + else: + # Default bend handling x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label, index) + 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) + else: + # Default fallback to drift + x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label, index) + except ValueError: return _error_plot(fig) From 027f443db0ec3787c55bbc391f5e13f7c0a92680 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Mon, 9 Jun 2025 19:12:56 -0700 Subject: [PATCH 21/35] add back removed annotation method somewhere deleted in older commit --- .../dashboard/Input/visualization/lattice/elements.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index 26f10fa8e..35f72c92e 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -59,6 +59,7 @@ def __init__(self): self.seen_elements = set() self.show_labels = True # Track whether to show labels + def reset_legend(self): self.seen_elements.clear() @@ -73,6 +74,14 @@ def _add_to_legend(self, element_type): if element_type not in self.seen_elements: self.seen_elements.add(element_type) + 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, legend_name=None, legend_group=None, **kwargs): """ This is the function that actually draws on the plotly figure. From d712fcaa35ebbda8b400c41455d7da2034846fa3 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Mon, 9 Jun 2025 19:26:01 -0700 Subject: [PATCH 22/35] fix unique elements showing on legend --- .../Input/visualization/lattice/elements.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index 35f72c92e..0c946f69f 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -70,9 +70,13 @@ def set_show_labels(self, 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): """ @@ -162,7 +166,7 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label, index): text=self._generate_hover_text(index, label, ds, dx, dy, rotation), show_legend=show_legend, legend_name="Drift", - legend_group="Drift" + legend_group="drift" ) if self.show_labels: @@ -216,7 +220,7 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label, index): text=self._generate_hover_text(index, label, ds, dx, dy, rotation, k=k), show_legend=show_legend, legend_name=legend_name, - legend_group=legend_name + legend_group=quad_type ) if self.show_labels: @@ -262,7 +266,7 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label, index): text=self._generate_hover_text(index, label, ds, dx, dy, rotation, rc=rc, phi=np.degrees(phi_rad)), show_legend=show_legend, legend_name="Sector Bend", - legend_group="Sector Bend" + legend_group="sbend" ) if self.show_labels: @@ -315,7 +319,7 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f text=self._generate_hover_text(index, label, ds, dx, dy, rotation_deg, phi=phi_deg), show_legend=show_legend, legend_name="Exact Sector Bend", - legend_group="Exact Sector Bend" + legend_group="exactsbend" ) if self.show_labels: @@ -351,7 +355,7 @@ def beam_monitor(self, fig, x, y, rotation, length, label, index): text=self._generate_hover_text(index, label, length, 0, 0, rotation), show_legend=show_legend, legend_name="Beam Monitor", - legend_group="Beam Monitor" + legend_group="monitor" ) if self.show_labels: self._add_annotation(fig, x=(x + x1)/2, y=y+0.3, label=label) From 1fce53c7af8f92434c2625507adbf8cd64e55383 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Mon, 9 Jun 2025 19:33:51 -0700 Subject: [PATCH 23/35] add more defaults to _add_trace --- .../Input/visualization/lattice/elements.py | 74 +++++++++---------- .../Input/visualization/lattice/plot.py | 14 ++-- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py index 0c946f69f..bc6a9cf98 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/elements.py @@ -86,13 +86,18 @@ def _add_annotation(self, fig, x, y, label, **kwargs): kwargs.setdefault("showarrow", False) fig.add_annotation(x=x, y=y, **kwargs) - def _add_trace(self, fig, show_legend=False, legend_name=None, legend_group=None, **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) @@ -145,8 +150,8 @@ def _generate_hover_text(self, index, label, ds, dx, dy, rotation, **special_par return text - def drift(self, fig, x, y, ds, dx, dy, rotation, label, index): - show_legend = self._add_to_legend("drift") + 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 @@ -165,8 +170,7 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label, index): fillcolor="lightgray", text=self._generate_hover_text(index, label, ds, dx, dy, rotation), show_legend=show_legend, - legend_name="Drift", - legend_group="drift" + element_name=element_name ) if self.show_labels: @@ -183,29 +187,26 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label, index): return x1, y1, rotation - def quad(self, fig, x, y, k, ds, dx, dy, rotation, label, index): + 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 - match k: - case _ if k > 0: - quad_type = "focusing_quad" - legend_name = "Focusing Quadrupole" - line_color = "darkblue" - fill_color = "lightblue" - case _ if k < 0: - quad_type = "defocusing_quad" - legend_name = "Defocusing Quadrupole" - line_color = "darkred" - fill_color = "lightcoral" - case _: - quad_type = "quadrupole" - legend_name = "Quadrupole" - line_color = "darkgreen" - fill_color = "lightgreen" - - show_legend = self._add_to_legend(quad_type) + # Determine quad type and visual properties based on k value + if k > 0: + quad_legend_name = "Focusing Quadrupole" + line_color = "darkblue" + fill_color = "lightblue" + elif k < 0: + quad_legend_name = "Defocusing Quadrupole" + line_color = "darkred" + fill_color = "lightcoral" + else: + quad_legend_name = "Quadrupole" + line_color = "darkgreen" + fill_color = "lightgreen" + + 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] @@ -219,16 +220,16 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label, index): fillcolor=fill_color, text=self._generate_hover_text(index, label, ds, dx, dy, rotation, k=k), show_legend=show_legend, - legend_name=legend_name, - legend_group=quad_type + 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): - show_legend = self._add_to_legend("sbend") + 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). @@ -265,8 +266,7 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label, index): 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, - legend_name="Sector Bend", - legend_group="sbend" + element_name=element_name ) if self.show_labels: @@ -284,8 +284,8 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label, index): 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): - show_legend = self._add_to_legend("exactsbend") + 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. """ @@ -318,8 +318,7 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f 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, - legend_name="Exact Sector Bend", - legend_group="exactsbend" + element_name=element_name ) if self.show_labels: @@ -337,8 +336,8 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f y_end = arc_y[-1] return x_end, y_end, final_angle - def beam_monitor(self, fig, x, y, rotation, length, label, index): - show_legend = self._add_to_legend("monitor") + 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", @@ -354,8 +353,7 @@ def beam_monitor(self, fig, x, y, rotation, length, label, index): 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, - legend_name="Beam Monitor", - legend_group="monitor" + element_name=element_name ) if self.show_labels: self._add_annotation(fig, x=(x + x1)/2, y=y+0.3, label=label) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py index 04a8a25bd..9672649df 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/plot.py @@ -119,25 +119,25 @@ def lattice_visualizer(): # 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) + x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label, index, element_name) elif "quad" in element_name: k = get_element_param(element, "k", 0.0) - x, y, rotation = draw.quad(fig, x, y, k, ds, dx, dy, rotation_total, element_label, index) + 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 = get_element_param(element, "rc", 0.0) - x, y, rotation = draw.sBend(fig, x, y, ds, dx, dy, rotation_total, rc, element_label, index) + 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 = get_element_param(element, "phi", 0) - x, y, rotation = draw.exactSBend(fig, x, y, ds, dx, dy, rotation_total, phi, element_label, index) + 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) + 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) + 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) + x, y, rotation = draw.drift(fig, x, y, ds, dx, dy, rotation_total, element_label, index, element_name) except ValueError: return _error_plot(fig) From 2bab9aa8ad6b5e25662d350f4cad34a60452d5ba Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 08:25:43 -0700 Subject: [PATCH 24/35] move visualization files to own folder --- src/python/impactx/dashboard/Input/visualization/lattice/ui.py | 2 +- .../Input/visualization/lattice/{ => visualization}/elements.py | 0 .../Input/visualization/lattice/{ => visualization}/plot.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/python/impactx/dashboard/Input/visualization/lattice/{ => visualization}/elements.py (100%) rename src/python/impactx/dashboard/Input/visualization/lattice/{ => visualization}/plot.py (97%) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index 0b7f69372..6a051be89 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -11,7 +11,7 @@ from ... import CardBase, NavigationComponents from . import Dialogs, StatComponents, StatUtils -from .plot import lattice_visualizer +from .visualization.plot import lattice_visualizer server, state, ctrl = setup_server() diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py similarity index 100% rename from src/python/impactx/dashboard/Input/visualization/lattice/elements.py rename to src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py similarity index 97% rename from src/python/impactx/dashboard/Input/visualization/lattice/plot.py rename to src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py index 9672649df..919f3867e 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py @@ -9,7 +9,7 @@ import plotly.graph_objects as go from .elements import LatticeVisualizerElements as DrawElements -from .... import setup_server +from ..... import setup_server server, state, ctrl = setup_server() draw = DrawElements() From d1a9b27312e4fbee80eb1543abcea666ee8fb11d Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 08:32:29 -0700 Subject: [PATCH 25/35] create utilities file for the visualizer --- .../lattice/visualization/__init__.py | 7 +++ .../lattice/visualization/plot.py | 50 ++++--------------- .../lattice/visualization/utils.py | 32 ++++++++++++ 3 files changed, 50 insertions(+), 39 deletions(-) create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/visualization/utils.py 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..743f9d9d9 --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py @@ -0,0 +1,7 @@ +from .elements import LatticeVisualizerElements +from .utils import LatticeVisualizerUtils + +__all__ = [ + "LatticeVisualizerElements", + "LatticeVisualizerUtils", +] diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py index 919f3867e..d0f2c59d6 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py @@ -7,41 +7,13 @@ """ import plotly.graph_objects as go -from .elements import LatticeVisualizerElements as DrawElements - +from . import LatticeVisualizerElements as DrawElements +from . import LatticeVisualizerUtils as Utils from ..... import setup_server server, state, ctrl = setup_server() draw = DrawElements() - -# ----------------------------------------------------------------------------- -# utils -# ----------------------------------------------------------------------------- - -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 - -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 - -# ----------------------------------------------------------------------------- -# Lattice Visualizer -# ----------------------------------------------------------------------------- +utils = Utils() def _error_plot(fig: go.Figure) -> go.Figure: """ @@ -110,25 +82,25 @@ def lattice_visualizer(): for index, element in enumerate(state.selected_lattice_list, 1): element_name = element.get("name", "").lower() # This gets the actual element name - element_label = get_element_name_param(element) # This gets the name from parameters - ds = get_element_param(element, "ds", 1.0) - dx = get_element_param(element, "dx", 0.0) - dy = get_element_param(element, "dy", 0.0) - element_rotation = get_element_param(element, "rotation", 0.0) + 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 = get_element_param(element, "k", 0.0) + 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 = get_element_param(element, "rc", 0.0) + 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 = get_element_param(element, "phi", 0) + 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 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..b32630b35 --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/utils.py @@ -0,0 +1,32 @@ +""" +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 From c2045eb69fd4a7efa2355c1f29d8ab8e1d3897d4 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 08:40:15 -0700 Subject: [PATCH 26/35] update: dialogs.py --- .../Input/visualization/lattice/dialogs.py | 22 +++++++++++++++++++ .../visualization/lattice/statistic_utils.py | 10 --------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/dialogs.py b/src/python/impactx/dashboard/Input/visualization/lattice/dialogs.py index 1568e270a..b6e1a5ee6 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/dialogs.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/dialogs.py @@ -6,14 +6,32 @@ License: BSD-3-Clause-LBNL """ +from ... import CardComponents from .... import setup_server, vuetify server, state, ctrl = setup_server() 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): @@ -23,6 +41,10 @@ def element_colors_tab(): @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): diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/statistic_utils.py b/src/python/impactx/dashboard/Input/visualization/lattice/statistic_utils.py index fe9f48188..169228750 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/statistic_utils.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/statistic_utils.py @@ -6,7 +6,6 @@ License: BSD-3-Clause-LBNL """ -from ... import CardComponents from .... import html, setup_server, vuetify server, state, ctrl = setup_server() @@ -94,15 +93,6 @@ def update_total_steps() -> int: return total_steps class LatticeVisualizerStatisticComponents: - @staticmethod - def settings(): - CardComponents.card_button( - "mdi-cog", - color="white", - click="lattice_visualizer_dialog_settings = true", - description="Settings", - ) - @staticmethod def _stat(title: str) -> None: """ From db34eb4c93747a17610d647c89ceadc9e0bd5562 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 08:43:41 -0700 Subject: [PATCH 27/35] update ui.py --- .../Input/visualization/lattice/ui.py | 40 +++---------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index 6a051be89..675909600 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -15,38 +15,21 @@ server, state, ctrl = setup_server() -ELEMENT_COLOR_MAP = { - "drift": "blue lighten-2", - "quad": "green darken-1", - "monitor": "grey darken-2", -} - -def get_element_color(name: str) -> str: - """ - Determine Vuetify color for an element based on its name. - """ - clean_name = name.lower() - for element_key, color in ELEMENT_COLOR_MAP.items(): - if element_key in clean_name: - return color - return "grey lighten-1" - -def _update_statistics(): +def _update_statistics() -> None: """ Update statistics based on the current selected lattice elements. """ - for element in state.selected_lattice_list: - element["color"] = get_element_color(element["name"]) - state.total_elements = len(state.selected_lattice_list) state.periods = 1 if state.total_elements > 0 else 0 state.total_steps = StatUtils.update_total_steps() state.element_counts = StatUtils.update_element_counts() StatUtils.update_length_statistics() -def _update_lattice_visualization(): - # Update plotly figure with new lattice visualization +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") @@ -86,19 +69,6 @@ def card_content(self): style="width: 100%; height: 50vh" ).update - with vuetify.VCardText(): - with vuetify.VRow(): - with vuetify.VCol( - v_for="(element, index) in selected_lattice_list", - ): - with vuetify.VCard( - text=("element.name",), - color=("element.color",), - text_color="white", - elevation=2, - ): - pass - @staticmethod def dialog_settings(): dialog_name = "lattice_visualizer_dialog_tab_settings" From 88b789e29f4d5fbfd416e241c3ff2cf8f485b7f6 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 08:45:30 -0700 Subject: [PATCH 28/35] rename statistics_utils -> statistics --- .../dashboard/Input/visualization/lattice/__init__.py | 4 ++-- .../lattice/{statistic_utils.py => statistics.py} | 0 .../impactx/dashboard/Input/visualization/lattice/ui.py | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) rename src/python/impactx/dashboard/Input/visualization/lattice/{statistic_utils.py => statistics.py} (100%) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py index fa374a96a..7b0566e32 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/__init__.py @@ -1,6 +1,6 @@ from .dialogs import LatticeVisualizerDialogs as Dialogs -from .statistic_utils import LatticeVisualizerStatisticComponents as StatComponents -from .statistic_utils import LatticeVisualizerStatisticUtils as StatUtils +from .statistics import LatticeVisualizerStatisticComponents as StatComponents +from .statistics import LatticeVisualizerStatisticUtils as StatUtils __all__ = [ "Dialogs", diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/statistic_utils.py b/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py similarity index 100% rename from src/python/impactx/dashboard/Input/visualization/lattice/statistic_utils.py rename to src/python/impactx/dashboard/Input/visualization/lattice/statistics.py diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index 675909600..dfde353a4 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -38,6 +38,9 @@ def on_lattice_list_change(**kwargs): _update_lattice_visualization() class LatticeVisualizer(CardBase): + """ + Displays the lattice visualizer section on the inputs page of the dashboard. + """ def __init__(self): super().__init__() @@ -59,7 +62,7 @@ def card_content(self): with vuetify.VCardTitle(classes="d-flex align-center"): html.Div("Lattice Statistics") vuetify.VSpacer() - StatComponents.settings() + Dialogs.settings() StatComponents.statistics() with vuetify.VCard(color="#002949"): From b186001ca685f894ecfcff4745873111b417b148 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 08:48:41 -0700 Subject: [PATCH 29/35] update ui.py --- src/python/impactx/dashboard/Input/visualization/lattice/ui.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index dfde353a4..cd8c48e48 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -46,6 +46,9 @@ def __init__(self): super().__init__() def card_content(self): + """ + The content of the lattice visualizer. + """ with vuetify.VDialog( v_model=("lattice_visualizer_dialog_settings", False), max_width="33.33vw" ): From af5dd228c8c0bcbd8556807c2d4903313ada5296 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 08:53:40 -0700 Subject: [PATCH 30/35] use helper function to retrieve lattice values --- .../Input/visualization/lattice/statistics.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py b/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py index 169228750..628a35e2d 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py @@ -23,20 +23,33 @@ class LatticeVisualizerStatisticUtils: @staticmethod - def update_length_statistics() -> None: + def _extract_parameter_values(parameter_name: str, value_type=float): """ - Computes and return the total, min, max, and average length of the - lattice configuration. Sums all elements' 'ds' (length) parameters. + 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 """ - lengths = [] - + values = [] + for element in state.selected_lattice_list: for param in element.get("parameters", []): - if param.get("parameter_name", "").lower() == "ds": + if param.get("parameter_name", "").lower() == parameter_name.lower(): try: - lengths.append(float(param.get("sim_input", 0))) + values.append(value_type(param.get("sim_input", 0))) except (ValueError, TypeError): pass + + return values + + @staticmethod + def update_length_statistics() -> None: + """ + Computes and return the total, min, max, and average length of the + lattice configuration. Sums all elements' 'ds' (length) parameters. + """ + lengths = LatticeVisualizerStatisticUtils._extract_parameter_values("ds", float) if lengths: state.total_length = round(sum(lengths), 2) @@ -80,17 +93,8 @@ def update_total_steps() -> int: :return: Total number of slices. """ - total_steps = 0 - - for element in state.selected_lattice_list: - for param in element.get("parameters", []): - if param.get("parameter_name", "").lower() == "nslice": - try: - total_steps += int(param.get("sim_input", 0)) - except (ValueError, TypeError): - pass - - return total_steps + steps = LatticeVisualizerStatisticUtils._extract_parameter_values("nslice", int) + return sum(steps) class LatticeVisualizerStatisticComponents: @staticmethod From 4ffcf1f38c23b29d90756fb01fad056ddfa18cbd Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 08:57:12 -0700 Subject: [PATCH 31/35] add: calculation.py for the visualizer separate logic --- .../lattice/visualization/calculations.py | 52 +++++++++++++++++++ .../lattice/visualization/elements.py | 44 +--------------- 2 files changed, 53 insertions(+), 43 deletions(-) create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/visualization/calculations.py diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/calculations.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/calculations.py new file mode 100644 index 000000000..e8a735f16 --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/calculations.py @@ -0,0 +1,52 @@ +""" +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.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py index bc6a9cf98..7c0774b81 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py @@ -8,49 +8,7 @@ import plotly.graph_objects as go 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 - -import numpy as np - -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 -import numpy as np +from .calculations import transform, rotate_corners class LatticeVisualizerElements: From 44fc94040e57dcbed98fb4e15aa1efcbe25c5f31 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 09:00:46 -0700 Subject: [PATCH 32/35] store quad colors in global dict --- .../lattice/visualization/elements.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py index 7c0774b81..c31e03fd9 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py @@ -10,6 +10,28 @@ import numpy as np from .calculations import transform, rotate_corners +# 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: @@ -150,19 +172,16 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label, index, element_name): x += dx y += dy - # Determine quad type and visual properties based on k value + # Determine quad type and get visual properties based on k value if k > 0: - quad_legend_name = "Focusing Quadrupole" - line_color = "darkblue" - fill_color = "lightblue" + quad_type = 'focusing_quadrupole' elif k < 0: - quad_legend_name = "Defocusing Quadrupole" - line_color = "darkred" - fill_color = "lightcoral" + quad_type = 'defocusing_quadrupole' else: - quad_legend_name = "Quadrupole" - line_color = "darkgreen" - fill_color = "lightgreen" + 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) @@ -174,8 +193,8 @@ def quad(self, fig, x, y, k, ds, dx, dy, rotation, label, index, element_name): y=ys, mode="lines", fill="toself", - line=dict(color=line_color, width=2), - fillcolor=fill_color, + 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, From 473e348c974c7d46ff95c54392d5836f7f80d115 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 09:05:29 -0700 Subject: [PATCH 33/35] create elements folder inside of lattice/visualization --- .../Input/visualization/lattice/visualization/__init__.py | 4 ++-- .../lattice/visualization/{ => elements}/calculations.py | 0 .../lattice/visualization/{ => elements}/elements.py | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/python/impactx/dashboard/Input/visualization/lattice/visualization/{ => elements}/calculations.py (100%) rename src/python/impactx/dashboard/Input/visualization/lattice/visualization/{ => elements}/elements.py (100%) diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py index 743f9d9d9..26c8c01e0 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py @@ -1,5 +1,5 @@ -from .elements import LatticeVisualizerElements -from .utils import LatticeVisualizerUtils +from .elements.elements import LatticeVisualizerElements +from . import LatticeVisualizerUtils __all__ = [ "LatticeVisualizerElements", diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/calculations.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/calculations.py similarity index 100% rename from src/python/impactx/dashboard/Input/visualization/lattice/visualization/calculations.py rename to src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/calculations.py diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/elements.py similarity index 100% rename from src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements.py rename to src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/elements.py From d69d6642c95a7e67c66536d89681b0db000dff86 Mon Sep 17 00:00:00 2001 From: Parthib Roy Date: Tue, 10 Jun 2025 14:37:02 -0700 Subject: [PATCH 34/35] fix import and state.periods for stats --- .../impactx/dashboard/Input/visualization/lattice/statistics.py | 1 - src/python/impactx/dashboard/Input/visualization/lattice/ui.py | 1 - .../Input/visualization/lattice/visualization/__init__.py | 2 +- .../visualization/lattice/visualization/elements/__init__.py | 1 + 4 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/__init__.py diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py b/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py index 628a35e2d..04c4b4023 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/statistics.py @@ -15,7 +15,6 @@ state.min_length = 0 state.avg_length = 0 state.total_steps = 0 -state.periods = 0 state.element_counts = {} state.length_stats_content = "" state.lattice_is_empty = len(state.selected_lattice_list) == 0 diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py index cd8c48e48..6c5c41360 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/ui.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/ui.py @@ -21,7 +21,6 @@ def _update_statistics() -> None: Update statistics based on the current selected lattice elements. """ state.total_elements = len(state.selected_lattice_list) - state.periods = 1 if state.total_elements > 0 else 0 state.total_steps = StatUtils.update_total_steps() state.element_counts = StatUtils.update_element_counts() StatUtils.update_length_statistics() diff --git a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py index 26c8c01e0..b883a324e 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/__init__.py @@ -1,5 +1,5 @@ from .elements.elements import LatticeVisualizerElements -from . import LatticeVisualizerUtils +from .utils import LatticeVisualizerUtils __all__ = [ "LatticeVisualizerElements", 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..d3f5a12fa --- /dev/null +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/__init__.py @@ -0,0 +1 @@ + From d35d9f4c7e971576551efbde967bb1425b907216 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:33:57 +0000 Subject: [PATCH 35/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../visualization/elements/__init__.py | 1 - .../visualization/elements/calculations.py | 32 ++-- .../visualization/elements/elements.py | 175 ++++++++++-------- .../lattice/visualization/plot.py | 117 ++++++++++-- .../lattice/visualization/utils.py | 8 +- 5 files changed, 219 insertions(+), 114 deletions(-) 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 index d3f5a12fa..e69de29bb 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/__init__.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/__init__.py @@ -1 +0,0 @@ - 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 index e8a735f16..b7900ea15 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/calculations.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/calculations.py @@ -21,7 +21,9 @@ def transform(x, y, rotation_deg, dx): 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: +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. @@ -35,18 +37,22 @@ def rotate_corners(x: float, y: float, rotation_deg: float, ds: float = 1.0, wid """ 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)], - ]) + 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 index c31e03fd9..c2a0502a2 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/elements.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/elements/elements.py @@ -6,40 +6,39 @@ License: BSD-3-Clause-LBNL """ -import plotly.graph_objects as go import numpy as np -from .calculations import transform, rotate_corners +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 + "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 + "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, }, - '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() @@ -52,7 +51,7 @@ 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 @@ -66,68 +65,70 @@ def _add_annotation(self, fig, x, y, label, **kwargs): 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): + 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'] + '' + 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): + 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 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}
" - f"{label}
" - f"ds: {ds} m
" - ) - + 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': + if param_name == "k": text += f"k: {param_value} m⁻²
" - elif param_name == 'rc': + elif param_name == "rc": text += f"rc: {param_value} m
" - elif param_name == 'phi': + elif param_name == "phi": text += f"phi: {param_value}°
" - - text += ( - f"dx: {dx} m
" - f"dy: {dy} m
" - f"rotation: {rotation}°" - ) - + + 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): @@ -150,7 +151,7 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label, index, element_name): fillcolor="lightgray", text=self._generate_hover_text(index, label, ds, dx, dy, rotation), show_legend=show_legend, - element_name=element_name + element_name=element_name, ) if self.show_labels: @@ -166,43 +167,42 @@ def drift(self, fig, x, y, ds, dx, dy, rotation, label, index, element_name): 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' + quad_type = "focusing_quadrupole" elif k < 0: - quad_type = 'defocusing_quadrupole' + quad_type = "defocusing_quadrupole" else: - quad_type = 'quadrupole' - + quad_type = "quadrupole" + quad_colors = QUAD_COLORS[quad_type] - quad_legend_name = quad_colors['legend_name'] + 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'], + 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 + legend_group=quad_legend_name, ) if self.show_labels: - self._add_annotation(fig, x=(x + x1)/2, y=y+0.4, label=label) + 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): @@ -219,10 +219,10 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label, index, element_name) x += dx y += dy - phi_rad = ds / rc # Bend angle in radians + phi_rad = ds / rc # Bend angle in radians rotation_rad = np.radians(rotation) - # The circular‐arc center (in lab coords) is found by: + # 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. @@ -241,11 +241,13 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label, index, element_name) 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)), + 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 + element_name=element_name, ) - + if self.show_labels: self._add_annotation( fig, @@ -261,14 +263,27 @@ def sBend(self, fig, x, y, ds, dx, dy, rotation, rc, label, index, element_name) 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): + 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 + 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 @@ -282,7 +297,7 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f circle_center_y = y + r * np.cos(rotation_rad) # Generate arc points - n_points = 100 # determines smoothness of the arc + 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) @@ -293,9 +308,11 @@ def exactSBend(self, fig, x, y, ds: float, dx: float, dy: float, rotation_deg: f 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), + text=self._generate_hover_text( + index, label, ds, dx, dy, rotation_deg, phi=phi_deg + ), show_legend=show_legend, - element_name=element_name + element_name=element_name, ) if self.show_labels: @@ -318,21 +335,23 @@ def beam_monitor(self, fig, x, y, rotation, length, label, index, 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, + 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], + x=[(x + x1) / 2], + y=[y], mode="markers", - marker=dict(size=15, color='rgba(0,0,0,0)'), + 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 + element_name=element_name, ) if self.show_labels: - self._add_annotation(fig, x=(x + x1)/2, y=y+0.3, label=label) + 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 index d0f2c59d6..0f16c595c 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/plot.py @@ -7,14 +7,16 @@ """ import plotly.graph_objects as go + +from ..... import setup_server from . import LatticeVisualizerElements as DrawElements from . import LatticeVisualizerUtils as Utils -from ..... import setup_server 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. @@ -26,8 +28,10 @@ def _error_plot(fig: go.Figure) -> go.Figure: showarrow=False, font=dict(size=16, color="red"), align="center", - xref="paper", yref="paper", - x=0.5, y=0.5 + xref="paper", + yref="paper", + x=0.5, + y=0.5, ) fig.update_layout( title="Lattice Visualization - Error", @@ -43,7 +47,7 @@ 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) @@ -68,21 +72,25 @@ def lattice_visualizer(): - 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 + 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) @@ -91,26 +99,97 @@ def lattice_visualizer(): # 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) + 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) + 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) + 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) + 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) + 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) + 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) - + x, y, rotation = draw.drift( + fig, + x, + y, + ds, + dx, + dy, + rotation_total, + element_label, + index, + element_name, + ) + except ValueError: return _error_plot(fig) @@ -121,5 +200,5 @@ def lattice_visualizer(): plot_bgcolor="white", margin=dict(l=30, r=30, t=40, b=40), ) - - return fig \ No newline at end of file + + 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 index b32630b35..4bbb25891 100644 --- a/src/python/impactx/dashboard/Input/visualization/lattice/visualization/utils.py +++ b/src/python/impactx/dashboard/Input/visualization/lattice/visualization/utils.py @@ -6,8 +6,8 @@ License: BSD-3-Clause-LBNL """ -class LatticeVisualizerUtils: +class LatticeVisualizerUtils: @staticmethod def get_element_param(element, name, default=0.0): for param in element.get("parameters", []): @@ -15,7 +15,9 @@ def get_element_param(element, name, default=0.0): try: return float(param.get("sim_input", default)) except (ValueError, TypeError): - raise ValueError(f"Invalid value for {name}: {param.get('sim_input', default)}") + raise ValueError( + f"Invalid value for {name}: {param.get('sim_input', default)}" + ) return default @staticmethod @@ -26,7 +28,7 @@ def get_element_name_param(element): 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