Skip to content
Merged
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
5 changes: 3 additions & 2 deletions docs/source/shape_editor_install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ https://blfauger.gitlabpages.inria.fr/nice/install.html.
cp Makefile.TEMPLATE Makefile
make -j
make nice_imas_inv_muscle3
make nice_imas_dir_muscle3

If this was successful, you should find the program ``nice_imas_inv_muscle3`` in the
``nice/run`` folder.
If this was successful, you should find the programs ``nice_imas_inv_muscle3`` and
``nice_imas_dir_muscle3`` in the ``nice/run`` folder.
65 changes: 46 additions & 19 deletions waveform_editor/gui/shape_editor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import importlib.resources
import logging
import xml.etree.ElementTree as ET

import imas
Expand All @@ -14,6 +15,8 @@
from waveform_editor.shape_editor.plasma_properties import PlasmaProperties
from waveform_editor.shape_editor.plasma_shape import PlasmaShape

logger = logging.getLogger(__name__)


def _reactive_title(title, is_valid):
return title if is_valid else f"{title} ⚠️"
Expand All @@ -37,40 +40,52 @@ def __init__(self):
self.plasma_properties = PlasmaProperties()
self.coil_currents = CoilCurrents()
self.nice_plotter = NicePlotter(
self.communicator, self.plasma_shape, self.plasma_properties
communicator=self.communicator,
plasma_shape=self.plasma_shape,
plasma_properties=self.plasma_properties,
)
self.nice_settings = settings.nice

self.xml_params = ET.fromstring(
self.xml_params_inv = ET.fromstring(
importlib.resources.files("waveform_editor.shape_editor.xml_param")
.joinpath("inverse_param.xml")
.read_text()
)
self.xml_params_dir = ET.fromstring(
importlib.resources.files("waveform_editor.shape_editor.xml_param")
.joinpath("param.xml")
.joinpath("direct_param.xml")
.read_text()
)

# UI Configuration
button_start = pn.widgets.Button(name="Run", on_click=self.submit)
button_start.disabled = (
self.plasma_shape.param.has_shape.rx.not_()
(
self.plasma_shape.param.has_shape.rx.not_()
& self.nice_settings.param.is_inverse_mode.rx()
)
| self.plasma_properties.param.has_properties.rx.not_()
| param.rx(self.nice_settings.required_params_filled).rx.not_()
| self.nice_settings.param.are_required_filled.rx.not_()
)
button_stop = pn.widgets.Button(name="Stop", on_click=self.stop_nice)
buttons = pn.Row(button_start, button_stop)
nice_mode_radio = pn.widgets.RadioBoxGroup.from_param(
self.nice_settings.param.mode, inline=True, margin=(15, 20, 0, 20)
)
buttons = pn.Row(button_start, button_stop, nice_mode_radio)

# Accordion does not allow dynamic titles, so use separate card for each option
options = pn.Column(
self._create_card(
self.nice_settings.panel,
"NICE Configuration",
is_valid=param.rx(self.nice_settings.required_params_filled),
),
self._create_card(
pn.Param(self.nice_plotter, show_name=False), "Plotting Parameters"
is_valid=self.nice_settings.param.are_required_filled.rx(),
),
self._create_card(self.nice_plotter, "Plotting Parameters"),
self._create_card(
self.plasma_shape,
"Plasma Shape",
is_valid=self.plasma_shape.param.has_shape,
visible=self.nice_settings.param.is_inverse_mode.rx(),
),
self._create_card(
pn.Column(self.plasma_properties, self.nice_plotter.profiles_pane),
Expand All @@ -91,14 +106,15 @@ def __init__(self):
),
)

def _create_card(self, panel_object, title, is_valid=None):
def _create_card(self, panel_object, title, is_valid=None, visible=True):
"""Create a collapsed card containing a panel object and a title.

Args:
panel_object: The panel object to place into the card.
title: The title to give the card.
is_valid: If supplied, binds the card title to update reactively using
`_reactive_title`.
visible: Whether the card is visible.
"""
if is_valid:
title = param.bind(_reactive_title, title=title, is_valid=is_valid)
Expand All @@ -107,6 +123,7 @@ def _create_card(self, panel_object, title, is_valid=None):
title=title,
sizing_mode="stretch_width",
collapsed=True,
visible=visible,
)
return card

Expand Down Expand Up @@ -169,9 +186,10 @@ def _create_equilibrium(self):
equilibrium.time_slice.resize(1)
equilibrium.vacuum_toroidal_field.b0.resize(1)

# Fill plasma shape
equilibrium.time_slice[0].boundary.outline.r = self.plasma_shape.outline_r
equilibrium.time_slice[0].boundary.outline.z = self.plasma_shape.outline_z
# Only fill plasma shape for NICE inverse mode
if self.nice_settings.is_inverse_mode:
equilibrium.time_slice[0].boundary.outline.r = self.plasma_shape.outline_r
equilibrium.time_slice[0].boundary.outline.z = self.plasma_shape.outline_z

# Fill plasma properties
equilibrium.vacuum_toroidal_field.r0 = self.plasma_properties.r0
Expand All @@ -194,14 +212,21 @@ async def submit(self, event=None):
description IDSs and an input equilibrium IDS."""

self.coil_currents.fill_pf_active(self.pf_active)
if self.nice_settings.is_direct_mode:
xml_params = self.xml_params_dir
else:
xml_params = self.xml_params_inv
self.coil_currents.update_fixed_coils_in_xml(xml_params)

# Update XML parameters:
self.coil_currents.update_fixed_coils_in_xml(self.xml_params)
self.xml_params.find("verbose").text = str(self.nice_settings.verbose)
xml_params.find("verbose").text = str(self.nice_settings.verbose)
equilibrium = self._create_equilibrium()
if not self.communicator.running:
await self.communicator.run()
await self.communicator.run(
is_direct_mode=self.nice_settings.is_direct_mode
)
await self.communicator.submit(
ET.tostring(self.xml_params, encoding="unicode"),
ET.tostring(xml_params, encoding="unicode"),
equilibrium.serialize(),
self.pf_active.serialize(),
self.pf_passive.serialize(),
Expand All @@ -210,7 +235,9 @@ async def submit(self, event=None):
)
self.coil_currents.sync_ui_with_pf_active(self.communicator.pf_active)

async def stop_nice(self, event):
@param.depends("nice_settings.mode", watch=True)
async def stop_nice(self, event=None):
logger.info("Stopping NICE...")
await self.communicator.close()

def __panel__(self):
Expand Down
75 changes: 56 additions & 19 deletions waveform_editor/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,23 @@


class NiceSettings(param.Parameterized):
REQUIRED = (
"executable",
INVERSE_MODE = "NICE Inverse"
DIRECT_MODE = "NICE Direct"

BASE_REQUIRED = (
"md_pf_active",
"md_pf_passive",
"md_wall",
"md_iron_core",
)
executable = param.String(
label="NICE executable path",
inv_executable = param.String(
label="NICE inverse executable path",
doc="Path to NICE inverse IMAS MUSCLE3 executable",
)
dir_executable = param.String(
label="NICE direct executable path",
doc="Path to NICE direct IMAS MUSCLE3 executable",
)
environment = param.Dict(
default={},
label="NICE environment variables",
Expand All @@ -37,10 +43,32 @@ class NiceSettings(param.Parameterized):
md_wall = param.String(label="'wall' machine description URI")
md_iron_core = param.String(label="'iron_core' machine description URI")
verbose = param.Integer(label="NICE verbosity (set to 1 for more verbose output)")
mode = param.Selector(
objects=[INVERSE_MODE, DIRECT_MODE], default=INVERSE_MODE, precedence=-1
)
are_required_filled = param.Boolean(precedence=-1)
is_direct_mode = param.Boolean(precedence=-1)
is_inverse_mode = param.Boolean(precedence=-1)

@param.depends("mode", watch=True, on_init=True)
def set_mode_flags(self):
self.is_direct_mode = self.mode == self.DIRECT_MODE
self.is_inverse_mode = self.mode == self.INVERSE_MODE

@param.depends(
*BASE_REQUIRED, "inv_executable", "dir_executable", "mode", watch=True
)
def check_required_params_filled(self):
base_ready = all(getattr(self, p) for p in self.BASE_REQUIRED)

@param.depends(*REQUIRED)
def required_params_filled(self):
return all(getattr(self, required) for required in self.REQUIRED)
if not base_ready:
self.are_required_filled = False
return

if self.mode == self.INVERSE_MODE:
self.are_required_filled = bool(self.inv_executable)
else:
self.are_required_filled = bool(self.dir_executable)

def apply_settings(self, params):
"""Update parameters from a dictionary, skipping unknown keys."""
Expand All @@ -51,24 +79,33 @@ def apply_settings(self, params):
self.param.update(**params)

def to_dict(self):
"""Returns a dictionary representation of current parameter values."""
return {p: getattr(self, p) for p in self.param if p != "name"}
"""Returns a dictionary representation of current parameter values, excluding
params with a precendence of -1."""
result = {}
for p in self.param:
param_obj = self.param[p]
if p != "name" and param_obj.precedence != -1:
result[p] = getattr(self, p)
return result

def panel(self):
items = []

for p in self.param:
if p == "name":
pass
elif p in self.REQUIRED:
items.append(
pn.Row(
pn.Param(self.param[p], show_name=False),
WarningIndicator(visible=self.param[p].rx.not_()),
)
)
else:
items.append(pn.Param(self.param[p], show_name=False))
continue

# Add warning indicator if required parameter is not filled
is_inv_required = p == "inv_executable" and self.is_inverse_mode
is_dir_required = p == "dir_executable" and self.is_direct_mode
is_base_required = p in self.BASE_REQUIRED

row_content = [pn.Param(self.param[p], show_name=False)]
if is_inv_required or is_dir_required or is_base_required:
warning = WarningIndicator(visible=self.param[p].rx.not_())
row_content.append(warning)

items.append(pn.Row(*row_content))

return pn.Column(*items)

Expand Down
18 changes: 11 additions & 7 deletions waveform_editor/shape_editor/coil_currents.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
import param
from panel.viewable import Viewer

from waveform_editor.settings import settings


class CoilCurrents(Viewer):
coil_ui = param.List(
doc="List of tuples containing the checkboxes and sliders for the coil currents"
)

def __init__(self):
super().__init__()
def __init__(self, **params):
super().__init__(**params)
self.nice_settings = settings.nice
self.sliders_ui = pn.Column(visible=self.param.coil_ui.rx.bool())
guide_message = pn.pane.Markdown(
"_To fix a coil to a specific current, enable the checkbox and provide "
Expand Down Expand Up @@ -44,13 +47,15 @@ def create_ui(self, pf_active):
new_coil_ui = []
for coil in pf_active.coil:
coil_current = coil.current
checkbox = pn.widgets.Checkbox(value=False, margin=(30, 10, 10, 10))
checkbox = pn.widgets.Checkbox(
margin=(30, 10, 10, 10),
disabled=self.nice_settings.param.is_direct_mode,
)
slider = pn.widgets.EditableFloatSlider(
name=f"{coil.name} Current [{coil_current.metadata.units}]",
value=coil_current.data[0] if coil_current.data.has_value else 0.0,
start=-5e4,
end=5e4,
disabled=checkbox.param.value.rx.not_(),
format="0",
width=450,
)
Expand All @@ -67,9 +72,8 @@ def fill_pf_active(self, pf_active):
pf_active: pf_active IDS to update the coil currents for.
"""
for i, coil_ui in enumerate(self.coil_ui):
checkbox, slider = coil_ui.objects
if checkbox.value:
pf_active.coil[i].current.data = np.array([slider.value])
_, slider = coil_ui.objects
pf_active.coil[i].current.data = np.array([slider.value])

def sync_ui_with_pf_active(self, pf_active):
"""Synchronize UI sliders with the current values from the pf_active IDS.
Expand Down
Loading
Loading