Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
586 changes: 586 additions & 0 deletions tests/test_iter_gaps.py

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions waveform_editor/gui/shape_editor/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class Metrics(Viewer):
"""Chips row showing equilibrium metrics below the flux map."""

metrics = param.Dict(default={})
# Chips of the machine in use, as (symbol, unit, full name) by name
machine_metrics = param.Dict(default={})

ELONGATION = "elongation"
TRIANGULARITY = "triangularity"
Expand All @@ -35,15 +37,18 @@ class Metrics(Viewer):
def __init__(self, **params):
super().__init__(**params)
self._pane = pn.pane.HTML(
pn.bind(self._render, self.param.metrics),
pn.bind(self._render, self.param.metrics, self.param.machine_metrics),
sizing_mode="stretch_width",
max_width=NicePlotter.FRAME_WIDTH,
stylesheets=STYLES,
)

def _render(self, metrics=None):
def _render(self, metrics=None, machine_metrics=None):
chips = []
for key, (symbol, unit, tooltip) in self.METRICS.items():
for key, (symbol, unit, tooltip) in {
**self.METRICS,
**self.machine_metrics,
}.items():
val = metrics.get(key, "—") if metrics else "—"
if isinstance(val, float):
val = f"{val:.4g}"
Expand Down
143 changes: 143 additions & 0 deletions waveform_editor/gui/shape_editor/nice_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from waveform_editor.gui.shape_editor.plasma_properties import PlasmaProperties
from waveform_editor.gui.shape_editor.plasma_shape import PlasmaShape
from waveform_editor.settings import NiceSettings, settings
from waveform_editor.shape_editor import iter_gaps, west_gaps
from waveform_editor.shape_editor.nice_integration import NiceIntegration

matplotlib.use("Agg")
Expand Down Expand Up @@ -123,6 +124,9 @@ class NicePlotter(Viewer):
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")
show_gaps = param.Boolean(default=True, label="Show gaps")
show_desired_gaps = param.Boolean(default=True, label="Show desired shape gaps")
show_result_gaps = param.Boolean(default=False, label="Show resulting shape gaps")

# Renderer of the editable points, set once the plot is first rendered
_points_renderer = None
Expand Down Expand Up @@ -211,6 +215,7 @@ def __init__(self, **params):
hv.DynamicMap(self._plot_passive_structures),
hv.DynamicMap(self._plot_iron_core),
hv.DynamicMap(self._plot_plasma_shape),
hv.DynamicMap(self._plot_clearance_gaps),
self.editable_points,
]
# Lets the weighted points be added, dragged and deleted on the plot
Expand Down Expand Up @@ -736,5 +741,143 @@ def _plot_xo_points(self):
)
return o_scatter * x_scatter

@pn.depends(
"communicator.equilibrium",
"show_gaps",
"show_desired_gaps",
"show_result_gaps",
"nice_settings.machine_preset",
"plasma_shape.shape_updated",
"plasma_shape.input_mode",
)
def _plot_clearance_gaps(self):
"""Plots the clearance gaps for the active machine preset, showing both the
measurement reference points on the wall and the distance lines to the
plasma boundary.

Returns:
Holoviews overlay with points, target intercepts, dashed distance
segments, and text labels.
"""
if not self.show_gaps:
return self._empty_gaps_overlay()

preset = self.nice_settings.machine_preset
overlays = []

if (
self.show_desired_gaps
and not self.nice_settings.is_direct_mode
and self.plasma_shape.has_shape
):
r_des = self.plasma_shape.outline_r
z_des = self.plasma_shape.outline_z
if r_des is not None and len(r_des) > 0:
if preset == NiceSettings.PRESET_ITER:
des_items = iter_gaps.compute_gap_geometry(r_des, z_des)
elif preset == NiceSettings.PRESET_WEST:
des_items = west_gaps.compute_gap_geometry(
r_des, z_des, self.plasma_shape.desired_x_points
)
else:
des_items = []
if des_items:
overlays.append(self._build_gaps_overlay(des_items, color="blue"))

eq = self.communicator.equilibrium
if self.show_result_gaps and eq is not None and len(eq.time_slice) > 0:
ts = eq.time_slice[0]
if len(ts.boundary.outline.r) > 0:
if preset == NiceSettings.PRESET_ITER:
res_items = iter_gaps.compute_gap_geometry(
**iter_gaps.gap_inputs(ts)
)
elif preset == NiceSettings.PRESET_WEST:
res_items = west_gaps.compute_gap_geometry(
**west_gaps.gap_inputs(ts)
)
else:
res_items = []
if res_items:
overlays.append(self._build_gaps_overlay(res_items, color="red"))

if not overlays:
return self._empty_gaps_overlay()

combined = overlays[0]
for ov in overlays[1:]:
combined = combined * ov
return combined

def _build_gaps_overlay(self, items, color="blue"):
vdims = ["name", "symbol", "distance_str"]
pts = hv.Points(
[{"r": it["r_orig"], "z": it["z_orig"], **it} for it in items],
kdims=["r", "z"],
vdims=vdims,
).opts(
color=color,
size=8,
marker="diamond",
hover_tooltips=[
("Gap", "@name (@symbol)"),
("Location (R, Z)", "@r{0.000} m, @z{0.000} m"),
("Distance", "@distance_str"),
],
show_legend=False,
)
tgts = hv.Points(
[{"r": it["r_target"], "z": it["z_target"], **it} for it in items],
kdims=["r", "z"],
vdims=vdims,
).opts(
color=color,
size=5,
marker="circle",
hover_tooltips=[("Gap", "@name (@symbol)"), ("Distance", "@distance_str")],
show_legend=False,
)
segs = hv.Segments(
[
{
"r0": it["r_orig"],
"z0": it["z_orig"],
"r1": it["r_target"],
"z1": it["z_target"],
**it,
}
for it in items
],
kdims=["r0", "z0", "r1", "z1"],
vdims=vdims,
).opts(
color=color,
line_dash="dashed",
line_width=2,
hover_tooltips=[("Gap", "@name (@symbol)"), ("Distance", "@distance_str")],
show_legend=False,
)
lbls = _no_hover(
hv.Labels(
[
{"r": it["r_orig"], "z": it["z_orig"], "text": it["symbol"]}
for it in items
],
kdims=["r", "z"],
vdims=["text"],
).opts(
text_font_size="10pt",
text_color=color,
text_baseline="bottom",
text_align="center",
text_font_style="bold",
)
)
return pts * tgts * segs * lbls

def _empty_gaps_overlay(self):
"""Empty overlay for when gaps are disabled or cannot be computed."""
return self._build_gaps_overlay([])

def __panel__(self):
return self.panel_layout
Loading
Loading