Skip to content

Commit e8b953b

Browse files
authored
Merge pull request #124 from iterorganization/feature/nice-direct-mode
Implement NICE direct mode
2 parents 5f09fbd + 08e1188 commit e8b953b

File tree

9 files changed

+728
-394
lines changed

9 files changed

+728
-394
lines changed

docs/source/shape_editor_install.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ https://blfauger.gitlabpages.inria.fr/nice/install.html.
8787
cp Makefile.TEMPLATE Makefile
8888
make -j
8989
make nice_imas_inv_muscle3
90+
make nice_imas_dir_muscle3
9091
91-
If this was successful, you should find the program ``nice_imas_inv_muscle3`` in the
92-
``nice/run`` folder.
92+
If this was successful, you should find the programs ``nice_imas_inv_muscle3`` and
93+
``nice_imas_dir_muscle3`` in the ``nice/run`` folder.

waveform_editor/gui/shape_editor.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import importlib.resources
2+
import logging
23
import xml.etree.ElementTree as ET
34

45
import imas
@@ -14,6 +15,8 @@
1415
from waveform_editor.shape_editor.plasma_properties import PlasmaProperties
1516
from waveform_editor.shape_editor.plasma_shape import PlasmaShape
1617

18+
logger = logging.getLogger(__name__)
19+
1720

1821
def _reactive_title(title, is_valid):
1922
return title if is_valid else f"{title} ⚠️"
@@ -37,40 +40,52 @@ def __init__(self):
3740
self.plasma_properties = PlasmaProperties()
3841
self.coil_currents = CoilCurrents()
3942
self.nice_plotter = NicePlotter(
40-
self.communicator, self.plasma_shape, self.plasma_properties
43+
communicator=self.communicator,
44+
plasma_shape=self.plasma_shape,
45+
plasma_properties=self.plasma_properties,
4146
)
4247
self.nice_settings = settings.nice
4348

44-
self.xml_params = ET.fromstring(
49+
self.xml_params_inv = ET.fromstring(
50+
importlib.resources.files("waveform_editor.shape_editor.xml_param")
51+
.joinpath("inverse_param.xml")
52+
.read_text()
53+
)
54+
self.xml_params_dir = ET.fromstring(
4555
importlib.resources.files("waveform_editor.shape_editor.xml_param")
46-
.joinpath("param.xml")
56+
.joinpath("direct_param.xml")
4757
.read_text()
4858
)
4959

5060
# UI Configuration
5161
button_start = pn.widgets.Button(name="Run", on_click=self.submit)
5262
button_start.disabled = (
53-
self.plasma_shape.param.has_shape.rx.not_()
63+
(
64+
self.plasma_shape.param.has_shape.rx.not_()
65+
& self.nice_settings.param.is_inverse_mode.rx()
66+
)
5467
| self.plasma_properties.param.has_properties.rx.not_()
55-
| param.rx(self.nice_settings.required_params_filled).rx.not_()
68+
| self.nice_settings.param.are_required_filled.rx.not_()
5669
)
5770
button_stop = pn.widgets.Button(name="Stop", on_click=self.stop_nice)
58-
buttons = pn.Row(button_start, button_stop)
71+
nice_mode_radio = pn.widgets.RadioBoxGroup.from_param(
72+
self.nice_settings.param.mode, inline=True, margin=(15, 20, 0, 20)
73+
)
74+
buttons = pn.Row(button_start, button_stop, nice_mode_radio)
5975

6076
# Accordion does not allow dynamic titles, so use separate card for each option
6177
options = pn.Column(
6278
self._create_card(
6379
self.nice_settings.panel,
6480
"NICE Configuration",
65-
is_valid=param.rx(self.nice_settings.required_params_filled),
66-
),
67-
self._create_card(
68-
pn.Param(self.nice_plotter, show_name=False), "Plotting Parameters"
81+
is_valid=self.nice_settings.param.are_required_filled.rx(),
6982
),
83+
self._create_card(self.nice_plotter, "Plotting Parameters"),
7084
self._create_card(
7185
self.plasma_shape,
7286
"Plasma Shape",
7387
is_valid=self.plasma_shape.param.has_shape,
88+
visible=self.nice_settings.param.is_inverse_mode.rx(),
7489
),
7590
self._create_card(
7691
pn.Column(self.plasma_properties, self.nice_plotter.profiles_pane),
@@ -91,14 +106,15 @@ def __init__(self):
91106
),
92107
)
93108

94-
def _create_card(self, panel_object, title, is_valid=None):
109+
def _create_card(self, panel_object, title, is_valid=None, visible=True):
95110
"""Create a collapsed card containing a panel object and a title.
96111
97112
Args:
98113
panel_object: The panel object to place into the card.
99114
title: The title to give the card.
100115
is_valid: If supplied, binds the card title to update reactively using
101116
`_reactive_title`.
117+
visible: Whether the card is visible.
102118
"""
103119
if is_valid:
104120
title = param.bind(_reactive_title, title=title, is_valid=is_valid)
@@ -107,6 +123,7 @@ def _create_card(self, panel_object, title, is_valid=None):
107123
title=title,
108124
sizing_mode="stretch_width",
109125
collapsed=True,
126+
visible=visible,
110127
)
111128
return card
112129

@@ -169,9 +186,10 @@ def _create_equilibrium(self):
169186
equilibrium.time_slice.resize(1)
170187
equilibrium.vacuum_toroidal_field.b0.resize(1)
171188

172-
# Fill plasma shape
173-
equilibrium.time_slice[0].boundary.outline.r = self.plasma_shape.outline_r
174-
equilibrium.time_slice[0].boundary.outline.z = self.plasma_shape.outline_z
189+
# Only fill plasma shape for NICE inverse mode
190+
if self.nice_settings.is_inverse_mode:
191+
equilibrium.time_slice[0].boundary.outline.r = self.plasma_shape.outline_r
192+
equilibrium.time_slice[0].boundary.outline.z = self.plasma_shape.outline_z
175193

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

196214
self.coil_currents.fill_pf_active(self.pf_active)
215+
if self.nice_settings.is_direct_mode:
216+
xml_params = self.xml_params_dir
217+
else:
218+
xml_params = self.xml_params_inv
219+
self.coil_currents.update_fixed_coils_in_xml(xml_params)
220+
197221
# Update XML parameters:
198-
self.coil_currents.update_fixed_coils_in_xml(self.xml_params)
199-
self.xml_params.find("verbose").text = str(self.nice_settings.verbose)
222+
xml_params.find("verbose").text = str(self.nice_settings.verbose)
200223
equilibrium = self._create_equilibrium()
201224
if not self.communicator.running:
202-
await self.communicator.run()
225+
await self.communicator.run(
226+
is_direct_mode=self.nice_settings.is_direct_mode
227+
)
203228
await self.communicator.submit(
204-
ET.tostring(self.xml_params, encoding="unicode"),
229+
ET.tostring(xml_params, encoding="unicode"),
205230
equilibrium.serialize(),
206231
self.pf_active.serialize(),
207232
self.pf_passive.serialize(),
@@ -210,7 +235,9 @@ async def submit(self, event=None):
210235
)
211236
self.coil_currents.sync_ui_with_pf_active(self.communicator.pf_active)
212237

213-
async def stop_nice(self, event):
238+
@param.depends("nice_settings.mode", watch=True)
239+
async def stop_nice(self, event=None):
240+
logger.info("Stopping NICE...")
214241
await self.communicator.close()
215242

216243
def __panel__(self):

waveform_editor/settings.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,23 @@
1616

1717

1818
class NiceSettings(param.Parameterized):
19-
REQUIRED = (
20-
"executable",
19+
INVERSE_MODE = "NICE Inverse"
20+
DIRECT_MODE = "NICE Direct"
21+
22+
BASE_REQUIRED = (
2123
"md_pf_active",
2224
"md_pf_passive",
2325
"md_wall",
2426
"md_iron_core",
2527
)
26-
executable = param.String(
27-
label="NICE executable path",
28+
inv_executable = param.String(
29+
label="NICE inverse executable path",
2830
doc="Path to NICE inverse IMAS MUSCLE3 executable",
2931
)
32+
dir_executable = param.String(
33+
label="NICE direct executable path",
34+
doc="Path to NICE direct IMAS MUSCLE3 executable",
35+
)
3036
environment = param.Dict(
3137
default={},
3238
label="NICE environment variables",
@@ -37,10 +43,32 @@ class NiceSettings(param.Parameterized):
3743
md_wall = param.String(label="'wall' machine description URI")
3844
md_iron_core = param.String(label="'iron_core' machine description URI")
3945
verbose = param.Integer(label="NICE verbosity (set to 1 for more verbose output)")
46+
mode = param.Selector(
47+
objects=[INVERSE_MODE, DIRECT_MODE], default=INVERSE_MODE, precedence=-1
48+
)
49+
are_required_filled = param.Boolean(precedence=-1)
50+
is_direct_mode = param.Boolean(precedence=-1)
51+
is_inverse_mode = param.Boolean(precedence=-1)
52+
53+
@param.depends("mode", watch=True, on_init=True)
54+
def set_mode_flags(self):
55+
self.is_direct_mode = self.mode == self.DIRECT_MODE
56+
self.is_inverse_mode = self.mode == self.INVERSE_MODE
57+
58+
@param.depends(
59+
*BASE_REQUIRED, "inv_executable", "dir_executable", "mode", watch=True
60+
)
61+
def check_required_params_filled(self):
62+
base_ready = all(getattr(self, p) for p in self.BASE_REQUIRED)
4063

41-
@param.depends(*REQUIRED)
42-
def required_params_filled(self):
43-
return all(getattr(self, required) for required in self.REQUIRED)
64+
if not base_ready:
65+
self.are_required_filled = False
66+
return
67+
68+
if self.mode == self.INVERSE_MODE:
69+
self.are_required_filled = bool(self.inv_executable)
70+
else:
71+
self.are_required_filled = bool(self.dir_executable)
4472

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

5381
def to_dict(self):
54-
"""Returns a dictionary representation of current parameter values."""
55-
return {p: getattr(self, p) for p in self.param if p != "name"}
82+
"""Returns a dictionary representation of current parameter values, excluding
83+
params with a precendence of -1."""
84+
result = {}
85+
for p in self.param:
86+
param_obj = self.param[p]
87+
if p != "name" and param_obj.precedence != -1:
88+
result[p] = getattr(self, p)
89+
return result
5690

5791
def panel(self):
5892
items = []
5993

6094
for p in self.param:
6195
if p == "name":
62-
pass
63-
elif p in self.REQUIRED:
64-
items.append(
65-
pn.Row(
66-
pn.Param(self.param[p], show_name=False),
67-
WarningIndicator(visible=self.param[p].rx.not_()),
68-
)
69-
)
70-
else:
71-
items.append(pn.Param(self.param[p], show_name=False))
96+
continue
97+
98+
# Add warning indicator if required parameter is not filled
99+
is_inv_required = p == "inv_executable" and self.is_inverse_mode
100+
is_dir_required = p == "dir_executable" and self.is_direct_mode
101+
is_base_required = p in self.BASE_REQUIRED
102+
103+
row_content = [pn.Param(self.param[p], show_name=False)]
104+
if is_inv_required or is_dir_required or is_base_required:
105+
warning = WarningIndicator(visible=self.param[p].rx.not_())
106+
row_content.append(warning)
107+
108+
items.append(pn.Row(*row_content))
72109

73110
return pn.Column(*items)
74111

waveform_editor/shape_editor/coil_currents.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
import param
66
from panel.viewable import Viewer
77

8+
from waveform_editor.settings import settings
9+
810

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

14-
def __init__(self):
15-
super().__init__()
16+
def __init__(self, **params):
17+
super().__init__(**params)
18+
self.nice_settings = settings.nice
1619
self.sliders_ui = pn.Column(visible=self.param.coil_ui.rx.bool())
1720
guide_message = pn.pane.Markdown(
1821
"_To fix a coil to a specific current, enable the checkbox and provide "
@@ -44,13 +47,15 @@ def create_ui(self, pf_active):
4447
new_coil_ui = []
4548
for coil in pf_active.coil:
4649
coil_current = coil.current
47-
checkbox = pn.widgets.Checkbox(value=False, margin=(30, 10, 10, 10))
50+
checkbox = pn.widgets.Checkbox(
51+
margin=(30, 10, 10, 10),
52+
disabled=self.nice_settings.param.is_direct_mode,
53+
)
4854
slider = pn.widgets.EditableFloatSlider(
4955
name=f"{coil.name} Current [{coil_current.metadata.units}]",
5056
value=coil_current.data[0] if coil_current.data.has_value else 0.0,
5157
start=-5e4,
5258
end=5e4,
53-
disabled=checkbox.param.value.rx.not_(),
5459
format="0",
5560
width=450,
5661
)
@@ -67,9 +72,8 @@ def fill_pf_active(self, pf_active):
6772
pf_active: pf_active IDS to update the coil currents for.
6873
"""
6974
for i, coil_ui in enumerate(self.coil_ui):
70-
checkbox, slider = coil_ui.objects
71-
if checkbox.value:
72-
pf_active.coil[i].current.data = np.array([slider.value])
75+
_, slider = coil_ui.objects
76+
pf_active.coil[i].current.data = np.array([slider.value])
7377

7478
def sync_ui_with_pf_active(self, pf_active):
7579
"""Synchronize UI sliders with the current values from the pf_active IDS.

0 commit comments

Comments
 (0)