From 557ee0ba21b1691331384462f6313fccc8ac4846 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Fri, 2 Oct 2026 15:17:23 +0200 Subject: [PATCH 1/4] add overlays for passive structures, iron core, and plasma facing components --- .../gui/shape_editor/nice_plotter.py | 124 +++++++++++++++++- .../gui/shape_editor/settings_modal.py | 3 + .../gui/shape_editor/shape_editor.py | 11 +- 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/waveform_editor/gui/shape_editor/nice_plotter.py b/waveform_editor/gui/shape_editor/nice_plotter.py index 387f99d1..16b1a764 100644 --- a/waveform_editor/gui/shape_editor/nice_plotter.py +++ b/waveform_editor/gui/shape_editor/nice_plotter.py @@ -36,6 +36,8 @@ class NicePlotter(Viewer): communicator = param.ClassSelector(class_=NiceIntegration, precedence=-1) wall = param.ClassSelector(class_=IDSToplevel, precedence=-1) pf_active = param.ClassSelector(class_=IDSToplevel, precedence=-1) + pf_passive = param.ClassSelector(class_=IDSToplevel, precedence=-1) + iron_core = param.ClassSelector(class_=IDSToplevel, precedence=-1) plasma_shape = param.ClassSelector(class_=PlasmaShape, precedence=-1) plasma_properties = param.ClassSelector(class_=PlasmaProperties, precedence=-1) nice_settings = param.ClassSelector(class_=NiceSettings, precedence=-1) @@ -50,6 +52,13 @@ class NicePlotter(Viewer): show_vacuum_vessel = param.Boolean( default=True, label="Show inner and outer vacuum vessel" ) + show_passive_structures = param.Boolean( + default=True, label="Show passive structures" + ) + show_iron_core = param.Boolean(default=True, label="Show iron core") + show_components = param.Boolean( + default=False, label="Show plasma facing components" + ) show_xo = param.Boolean(default=True, label="Show x-point and o-point") show_separatrix = param.Boolean(default=True, label="Show separatrix") show_desired_shape = param.Boolean(default=True, label="Show desired shape") @@ -103,7 +112,10 @@ def __init__(self, **params): hv.DynamicMap(self._plot_xo_points), hv.DynamicMap(self._plot_coil_rectangles), hv.DynamicMap(self._plot_wall), + hv.DynamicMap(self._plot_components), hv.DynamicMap(self._plot_vacuum_vessel), + hv.DynamicMap(self._plot_passive_structures), + hv.DynamicMap(self._plot_iron_core), hv.DynamicMap(self._plot_plasma_shape), self.editable_points, ] @@ -447,9 +459,16 @@ def _plot_vacuum_vessel(self): if self.show_vacuum_vessel and self.wall is not None: for unit in self.wall.description_2d[0].vessel.unit: name = str(unit.name) - r_vals = unit.annular.centreline.r - z_vals = unit.annular.centreline.z - paths.append((r_vals, z_vals, name)) + annular = unit.annular + if len(annular.centreline.r): + paths.append((annular.centreline.r, annular.centreline.z, name)) + else: + paths.append( + (annular.outline_inner.r, annular.outline_inner.z, name) + ) + paths.append( + (annular.outline_outer.r, annular.outline_outer.z, name) + ) return hv.Path(paths, vdims=["name"]).opts( color="black", line_width=2, @@ -476,6 +495,105 @@ def _plot_wall(self): hover_tooltips=[("", "@name")], ) + @pn.depends("wall", "show_components") + def _plot_components(self): + """Generates paths for the plasma facing components, which a machine + describes alongside the limiter it is computed with. + + Returns: + Holoviews path containing the geometry. + """ + paths = [] + if self.show_components and self.wall is not None: + # The first description is the limiter the equilibrium is computed with + for description in list(self.wall.description_2d)[1:]: + for unit in description.limiter.unit: + outline = unit.outline + paths.append( + (outline.r, outline.z, str(unit.description or unit.name)) + ) + return hv.Path(paths, vdims=["name"]).opts( + color="gray", + line_width=1, + hover_tooltips=[("", "@name")], + ) + + @pn.depends("pf_passive", "show_passive_structures") + def _plot_passive_structures(self): + """Generates paths for the passive conducting structures. + + Returns: + Holoviews path containing the geometry. + """ + paths = [] + if self.show_passive_structures and self.pf_passive is not None: + for loop in self.pf_passive.loop: + for element in loop.element: + outline = self._element_outline(element.geometry) + if outline is None: + logger.warning( + f"Passive structure {str(loop.name)!r} was skipped, as its " + "geometry is not an outline, a rectangle or an oblique" + ) + continue + paths.append((*outline, str(loop.name))) + return hv.Path(paths, vdims=["name"]).opts( + color="darkgray", + line_width=2, + hover_tooltips=[("", "@name")], + ) + + def _element_outline(self, geometry): + """The corners of an element of a machine, whichever way it is described. + + Args: + geometry: The geometry of an element. + + Returns: + Tuple of (r, z) of a closed outline + """ + if geometry.outline.has_value: + return geometry.outline.r, geometry.outline.z + rectangle = geometry.rectangle + if rectangle.has_value: + r, z = rectangle.r, rectangle.z + dr, dz = rectangle.width / 2, rectangle.height / 2 + return ( + np.array([r - dr, r + dr, r + dr, r - dr, r - dr]), + np.array([z - dz, z - dz, z + dz, z + dz, z - dz]), + ) + oblique = geometry.oblique + if oblique.has_value: + # Two sides from a corner, each at its own angle + dr_a = oblique.length_alpha * np.cos(oblique.alpha) + dz_a = oblique.length_alpha * np.sin(oblique.alpha) + dr_b = -oblique.length_beta * np.sin(oblique.beta) + dz_b = oblique.length_beta * np.cos(oblique.beta) + r, z = oblique.r, oblique.z + return ( + np.array([r, r + dr_a, r + dr_a + dr_b, r + dr_b, r]), + np.array([z, z + dz_a, z + dz_a + dz_b, z + dz_b, z]), + ) + return None + + @pn.depends("iron_core", "show_iron_core") + def _plot_iron_core(self): + """Generates paths for the iron core segments. + + Returns: + Holoviews path containing the geometry. + """ + paths = [] + if self.show_iron_core and self.iron_core is not None: + for segment in self.iron_core.segment: + outline = segment.geometry.outline + paths.append((outline.r, outline.z, str(segment.name))) + return hv.Path(paths, vdims=["name"]).opts( + color="saddlebrown", + line_width=2, + hover_tooltips=[("", "@name")], + ) + @pn.depends("communicator.equilibrium", "show_xo") def _plot_xo_points(self): """Plots X-points and O-points from the equilibrium. diff --git a/waveform_editor/gui/shape_editor/settings_modal.py b/waveform_editor/gui/shape_editor/settings_modal.py index f037ac01..021b6293 100644 --- a/waveform_editor/gui/shape_editor/settings_modal.py +++ b/waveform_editor/gui/shape_editor/settings_modal.py @@ -212,6 +212,9 @@ def _build_modal(self): "show_coils", "show_wall", "show_vacuum_vessel", + "show_passive_structures", + "show_iron_core", + "show_components", "show_xo", "show_separatrix", "show_desired_shape", diff --git a/waveform_editor/gui/shape_editor/shape_editor.py b/waveform_editor/gui/shape_editor/shape_editor.py index 92650a3a..2dd8de90 100644 --- a/waveform_editor/gui/shape_editor/shape_editor.py +++ b/waveform_editor/gui/shape_editor/shape_editor.py @@ -304,6 +304,7 @@ def _load_pf_passive(self): self.pf_passive = self._load_slice( self.nice_settings.md_pf_passive.uri, "pf_passive" ) + self.nice_plotter.pf_passive = self.pf_passive self.nice_settings.md_pf_passive.loaded = self.pf_passive is not None @param.depends("nice_settings.md_wall.uri", watch=True) @@ -317,6 +318,7 @@ def _load_iron_core(self): self.iron_core = self._load_slice( self.nice_settings.md_iron_core.uri, "iron_core" ) + self.nice_plotter.iron_core = self.iron_core self.nice_settings.md_iron_core.loaded = self.iron_core is not None def _create_equilibrium(self): @@ -484,7 +486,14 @@ def _update_metrics(self): self.metrics.Q95: float(global_quantities.q_95), } - @param.depends("nice_settings.mode", watch=True) + @param.depends( + "nice_settings.mode", + "nice_settings.md_pf_active.uri", + "nice_settings.md_pf_passive.uri", + "nice_settings.md_wall.uri", + "nice_settings.md_iron_core.uri", + watch=True, + ) async def stop_nice(self, event=None): logger.info("Stopping NICE...") await self.communicator.close() From 4b90d13ada3b0ad2b8cc8878f5e0212a4bc6376e Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Tue, 6 Oct 2026 10:52:18 +0200 Subject: [PATCH 2/4] create outline method, set plasma facing component to true --- .../gui/shape_editor/nice_plotter.py | 141 +++++++++--------- .../gui/shape_editor/settings_modal.py | 2 +- 2 files changed, 70 insertions(+), 73 deletions(-) diff --git a/waveform_editor/gui/shape_editor/nice_plotter.py b/waveform_editor/gui/shape_editor/nice_plotter.py index 16b1a764..27bf69d6 100644 --- a/waveform_editor/gui/shape_editor/nice_plotter.py +++ b/waveform_editor/gui/shape_editor/nice_plotter.py @@ -31,6 +31,56 @@ def hook(plot, _): return element.opts(hooks=[hook]) +def _element_outline(geometry): + """The corners of an element of a machine, whichever way it is described. + + Args: + geometry: The geometry of an element. + + Returns: + Tuple of (r, z) of a closed outline + """ + if geometry.outline.has_value: + return geometry.outline.r, geometry.outline.z + rectangle = geometry.rectangle + if rectangle.has_value: + r, z = rectangle.r, rectangle.z + dr, dz = rectangle.width / 2, rectangle.height / 2 + return ( + np.array([r - dr, r + dr, r + dr, r - dr, r - dr]), + np.array([z - dz, z - dz, z + dz, z + dz, z - dz]), + ) + oblique = geometry.oblique + if oblique.has_value: + # Two sides from a corner, each at its own angle + dr_a = oblique.length_alpha * np.cos(oblique.alpha) + dz_a = oblique.length_alpha * np.sin(oblique.alpha) + dr_b = -oblique.length_beta * np.sin(oblique.beta) + dz_b = oblique.length_beta * np.cos(oblique.beta) + r, z = oblique.r, oblique.z + return ( + np.array([r, r + dr_a, r + dr_a + dr_b, r + dr_b, r]), + np.array([z, z + dz_a, z + dz_a + dz_b, z + dz_b, z]), + ) + return None + + +def _geometry_paths(paths, color="black", line_width=2): + """Paths of the outlines of parts of a machine, named on hover. + + Args: + paths: List of (r, z, name) of each outline. + color: Colour of the lines. + line_width: Width of the lines. + """ + return hv.Path(paths, vdims=["name"]).opts( + color=color, + line_width=line_width, + show_legend=False, + hover_tooltips=[("", "@name")], + ) + + class NicePlotter(Viewer): # Input data, use negative precedence to hide from the UI communicator = param.ClassSelector(class_=NiceIntegration, precedence=-1) @@ -56,8 +106,8 @@ class NicePlotter(Viewer): default=True, label="Show passive structures" ) show_iron_core = param.Boolean(default=True, label="Show iron core") - show_components = param.Boolean( - default=False, label="Show plasma facing components" + show_plasma_facing_components = param.Boolean( + default=True, label="Show plasma facing components" ) show_xo = param.Boolean(default=True, label="Show x-point and o-point") show_separatrix = param.Boolean(default=True, label="Show separatrix") @@ -359,13 +409,7 @@ def _plot_coil_rectangles(self): show_legend=False, hover_tooltips=[("", "@name")], ) - paths = hv.Path(paths, vdims=["name"]).opts( - color="black", - line_width=1, - show_legend=False, - hover_tooltips=[("", "@name")], - ) - return rects * paths + return rects * _geometry_paths(paths, line_width=1) @pn.depends("communicator.equilibrium", "show_contour", "levels") def _plot_contours(self): @@ -469,11 +513,7 @@ def _plot_vacuum_vessel(self): paths.append( (annular.outline_outer.r, annular.outline_outer.z, name) ) - return hv.Path(paths, vdims=["name"]).opts( - color="black", - line_width=2, - hover_tooltips=[("", "@name")], - ) + return _geometry_paths(paths) @pn.depends("wall", "show_wall") def _plot_wall(self): @@ -489,13 +529,9 @@ def _plot_wall(self): r_vals = unit.outline.r z_vals = unit.outline.z paths.append((r_vals, z_vals, name)) - return hv.Path(paths, vdims=["name"]).opts( - color="black", - line_width=2, - hover_tooltips=[("", "@name")], - ) + return _geometry_paths(paths) - @pn.depends("wall", "show_components") + @pn.depends("wall", "show_plasma_facing_components") def _plot_components(self): """Generates paths for the plasma facing components, which a machine describes alongside the limiter it is computed with. @@ -504,7 +540,7 @@ def _plot_components(self): Holoviews path containing the geometry. """ paths = [] - if self.show_components and self.wall is not None: + if self.show_plasma_facing_components and self.wall is not None: # The first description is the limiter the equilibrium is computed with for description in list(self.wall.description_2d)[1:]: for unit in description.limiter.unit: @@ -512,11 +548,7 @@ def _plot_components(self): paths.append( (outline.r, outline.z, str(unit.description or unit.name)) ) - return hv.Path(paths, vdims=["name"]).opts( - color="gray", - line_width=1, - hover_tooltips=[("", "@name")], - ) + return _geometry_paths(paths, color="gray", line_width=1) @pn.depends("pf_passive", "show_passive_structures") def _plot_passive_structures(self): @@ -529,7 +561,7 @@ def _plot_passive_structures(self): if self.show_passive_structures and self.pf_passive is not None: for loop in self.pf_passive.loop: for element in loop.element: - outline = self._element_outline(element.geometry) + outline = _element_outline(element.geometry) if outline is None: logger.warning( f"Passive structure {str(loop.name)!r} was skipped, as its " @@ -537,44 +569,7 @@ def _plot_passive_structures(self): ) continue paths.append((*outline, str(loop.name))) - return hv.Path(paths, vdims=["name"]).opts( - color="darkgray", - line_width=2, - hover_tooltips=[("", "@name")], - ) - - def _element_outline(self, geometry): - """The corners of an element of a machine, whichever way it is described. - - Args: - geometry: The geometry of an element. - - Returns: - Tuple of (r, z) of a closed outline - """ - if geometry.outline.has_value: - return geometry.outline.r, geometry.outline.z - rectangle = geometry.rectangle - if rectangle.has_value: - r, z = rectangle.r, rectangle.z - dr, dz = rectangle.width / 2, rectangle.height / 2 - return ( - np.array([r - dr, r + dr, r + dr, r - dr, r - dr]), - np.array([z - dz, z - dz, z + dz, z + dz, z - dz]), - ) - oblique = geometry.oblique - if oblique.has_value: - # Two sides from a corner, each at its own angle - dr_a = oblique.length_alpha * np.cos(oblique.alpha) - dz_a = oblique.length_alpha * np.sin(oblique.alpha) - dr_b = -oblique.length_beta * np.sin(oblique.beta) - dz_b = oblique.length_beta * np.cos(oblique.beta) - r, z = oblique.r, oblique.z - return ( - np.array([r, r + dr_a, r + dr_a + dr_b, r + dr_b, r]), - np.array([z, z + dz_a, z + dz_a + dz_b, z + dz_b, z]), - ) - return None + return _geometry_paths(paths, color="darkgray") @pn.depends("iron_core", "show_iron_core") def _plot_iron_core(self): @@ -586,13 +581,15 @@ def _plot_iron_core(self): paths = [] if self.show_iron_core and self.iron_core is not None: for segment in self.iron_core.segment: - outline = segment.geometry.outline - paths.append((outline.r, outline.z, str(segment.name))) - return hv.Path(paths, vdims=["name"]).opts( - color="saddlebrown", - line_width=2, - hover_tooltips=[("", "@name")], - ) + outline = _element_outline(segment.geometry) + if outline is None: + logger.warning( + f"Iron core segment {str(segment.name)!r} was skipped, as its " + "geometry is not an outline, a rectangle or an oblique" + ) + continue + paths.append((*outline, str(segment.name))) + return _geometry_paths(paths, color="saddlebrown") @pn.depends("communicator.equilibrium", "show_xo") def _plot_xo_points(self): diff --git a/waveform_editor/gui/shape_editor/settings_modal.py b/waveform_editor/gui/shape_editor/settings_modal.py index 021b6293..c8be5f7b 100644 --- a/waveform_editor/gui/shape_editor/settings_modal.py +++ b/waveform_editor/gui/shape_editor/settings_modal.py @@ -214,7 +214,7 @@ def _build_modal(self): "show_vacuum_vessel", "show_passive_structures", "show_iron_core", - "show_components", + "show_plasma_facing_components", "show_xo", "show_separatrix", "show_desired_shape", From 20252b1e524f8d53ce8be9f3838fcd38aef97219 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Tue, 6 Oct 2026 16:45:18 +0200 Subject: [PATCH 3/4] deduplication --- .../gui/shape_editor/nice_plotter.py | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/waveform_editor/gui/shape_editor/nice_plotter.py b/waveform_editor/gui/shape_editor/nice_plotter.py index 27bf69d6..84fd5875 100644 --- a/waveform_editor/gui/shape_editor/nice_plotter.py +++ b/waveform_editor/gui/shape_editor/nice_plotter.py @@ -31,14 +31,15 @@ def hook(plot, _): return element.opts(hooks=[hook]) -def _element_outline(geometry): +def _element_outline(geometry, name): """The corners of an element of a machine, whichever way it is described. Args: geometry: The geometry of an element. + name: The name of the element, for the warning when it cannot be outlined. Returns: - Tuple of (r, z) of a closed outline + Tuple of (r, z) of a closed outline, which is empty if it cannot be outlined. """ if geometry.outline.has_value: return geometry.outline.r, geometry.outline.z @@ -62,7 +63,10 @@ def _element_outline(geometry): np.array([r, r + dr_a, r + dr_a + dr_b, r + dr_b, r]), np.array([z, z + dz_a, z + dz_a + dz_b, z + dz_b, z]), ) - return None + logger.warning( + f"{str(name)!r} is skipped, as it has no outline, rectangle or oblique" + ) + return [], [] def _geometry_paths(paths, color="black", line_width=2): @@ -559,16 +563,11 @@ def _plot_passive_structures(self): """ paths = [] if self.show_passive_structures and self.pf_passive is not None: - for loop in self.pf_passive.loop: - for element in loop.element: - outline = _element_outline(element.geometry) - if outline is None: - logger.warning( - f"Passive structure {str(loop.name)!r} was skipped, as its " - "geometry is not an outline, a rectangle or an oblique" - ) - continue - paths.append((*outline, str(loop.name))) + paths = [ + (*_element_outline(element.geometry, loop.name), str(loop.name)) + for loop in self.pf_passive.loop + for element in loop.element + ] return _geometry_paths(paths, color="darkgray") @pn.depends("iron_core", "show_iron_core") @@ -580,15 +579,10 @@ def _plot_iron_core(self): """ paths = [] if self.show_iron_core and self.iron_core is not None: - for segment in self.iron_core.segment: - outline = _element_outline(segment.geometry) - if outline is None: - logger.warning( - f"Iron core segment {str(segment.name)!r} was skipped, as its " - "geometry is not an outline, a rectangle or an oblique" - ) - continue - paths.append((*outline, str(segment.name))) + paths = [ + (*_element_outline(segment.geometry, segment.name), str(segment.name)) + for segment in self.iron_core.segment + ] return _geometry_paths(paths, color="saddlebrown") @pn.depends("communicator.equilibrium", "show_xo") From 7504c3b1326afe9c197b188d1a9985c6b4a8fac9 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Wed, 7 Oct 2026 13:58:20 +0200 Subject: [PATCH 4/4] update comment --- waveform_editor/gui/shape_editor/nice_plotter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/waveform_editor/gui/shape_editor/nice_plotter.py b/waveform_editor/gui/shape_editor/nice_plotter.py index 34d1780f..89e42389 100644 --- a/waveform_editor/gui/shape_editor/nice_plotter.py +++ b/waveform_editor/gui/shape_editor/nice_plotter.py @@ -36,7 +36,9 @@ def _element_outline(geometry, name): """The corners of an element of a machine, whichever way it is described. Args: - geometry: The geometry of an element. + geometry: An ``outline_2d_geometry_static`` structure, such as + ``pf_passive/loop/element/geometry`` or ``iron_core/segment/geometry``. + Its outline, rectangle or oblique is drawn. name: The name of the element, for the warning when it cannot be outlined. Returns: