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
64 changes: 60 additions & 4 deletions waveform_editor/gui/shape_editor/plasma_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from waveform_editor.settings import NiceSettings, settings
from waveform_editor.shape_editor.plasma_properties_calc import (
compute_profiles_from_params,
tf_field_per_current,
)


Expand Down Expand Up @@ -57,9 +58,10 @@ class PropertyInput(Viewer):
changed = param.Event()
input_state = param.String(default="manual", precedence=-1)

def __init__(self, label, default_value, step=0.01, **params):
def __init__(self, label, default_value, step=0.01, extra=(), **params):
super().__init__(**params)
self._label = label
self._extra = extra
self.value = default_value
self.resolved_value = default_value

Expand Down Expand Up @@ -112,6 +114,7 @@ def __panel__(self):
return pn.Column(
header,
self._value_input,
*self._extra,
css_classes=["property-card"],
stylesheets=[CARD_CSS],
sizing_mode="stretch_width",
Expand All @@ -124,6 +127,7 @@ def __panel__(self):
"ip": -1.5e7,
"r0": 6.2,
"b0": -5.3,
"tf_current": -6.8e4,
"alpha": 1.0,
"beta": 0.65,
"gamma": 1.05,
Expand All @@ -132,6 +136,7 @@ def __panel__(self):
"ip": -4.0e5,
"r0": 2.42,
"b0": -3.76,
"tf_current": -1255.0,
"alpha": 1.0,
"beta": 0.85,
"gamma": 1.2,
Expand Down Expand Up @@ -356,6 +361,16 @@ class PlasmaProperties(Viewer):
doc="Triggered whenever the dpressure_dpsi and f_df_dpsi are updated."
)
has_properties = param.Boolean(doc="Whether the plasma properties are loaded.")
tf = param.Parameter(
default=None,
precedence=-1,
doc="The tf machine description, which the toroidal field is computed with.",
)
use_tf_current = param.Boolean(
default=True,
label="Set toroidal field by TF coil current",
doc="Compute the toroidal field from the TF coil current and the tf IDS.",
)

def __init__(self):
super().__init__()
Expand All @@ -368,9 +383,36 @@ def __init__(self):
"Reference major radius [m]",
default_value=DEFAULT_PROPERTIES["r0"],
)

def tf_checkbox():
return pn.Row(
pn.widgets.Checkbox.from_param(self.param.use_tf_current),
pn.widgets.TooltipIcon(
value=(
"Compute the toroidal field at R₀ from the current in the TF "
"coils, with B₀ = μ₀ N I / (2π R₀), where N is the total "
"number of turns of the TF coils in the tf machine description "
"and I the current in each turn."
),
align="center",
margin=(0, 0, 0, -10),
),
visible=self.param.tf.rx.is_not(None),
margin=(8, 0, 0, 0),
)

self._tf_field = pn.pane.HTML(margin=(8, 0, 0, 0))

self._b0 = PropertyInput(
"Toroidal magnetic field [T]",
default_value=DEFAULT_PROPERTIES["b0"],
extra=[tf_checkbox()],
)
self._tf_current = PropertyInput(
"TF coil current [A]",
default_value=DEFAULT_PROPERTIES["tf_current"],
step=100,
extra=[self._tf_field, tf_checkbox()],
)
self._profiles = PlasmaProfiles()

Expand All @@ -384,8 +426,9 @@ def __init__(self):
self.r0 = None
self.b0 = None

for widget in [self._ip, self._r0, self._b0, self._profiles]:
for widget in [self._ip, self._r0, self._b0, self._tf_current, self._profiles]:
widget.param.watch(self._load_plasma_properties, "changed")
self.param.watch(self._load_plasma_properties, ["tf", "use_tf_current"])

settings.nice.param.watch(self.apply_machine_properties, "machine_preset")
self.apply_machine_properties()
Expand All @@ -399,6 +442,7 @@ def apply_machine_properties(self, event=None):
self._ip.value = properties["ip"]
self._r0.value = properties["r0"]
self._b0.value = properties["b0"]
self._tf_current.value = properties["tf_current"]
self._profiles.param.update(
alpha=properties["alpha"],
beta=properties["beta"],
Expand Down Expand Up @@ -431,6 +475,10 @@ def _do_load(self, _event=None):
getattr(self, attr).load_value(float(eq[path]))
except Exception as e:
pn.state.notifications.error(f"Could not load {path}: {e}")
if self.tf is not None:
self._tf_current.load_value(
self._b0.value / tf_field_per_current(self.tf, self._r0.value)
)
except Exception as e:
pn.state.notifications.error(f"Could not open IDS {uri}: {e}")
return
Expand All @@ -439,7 +487,13 @@ def _do_load(self, _event=None):
def _load_plasma_properties(self, _event=None):
self.ip = self._ip.resolved_value
self.r0 = self._r0.resolved_value
self.b0 = self._b0.resolved_value
if self.tf is not None and self.use_tf_current:
self.b0 = self._tf_current.resolved_value * tf_field_per_current(
self.tf, self.r0
)
self._tf_field.object = f"Toroidal magnetic field at R₀: {self.b0:.4g} T"
else:
self.b0 = self._b0.resolved_value

self._profiles.r0 = self.r0
self.psi_norm = self._profiles.psi_norm
Expand All @@ -453,11 +507,13 @@ def _load_plasma_properties(self, _event=None):
self.param.trigger("profile_updated")

def __panel__(self):
from_tf = self.param.tf.rx.is_not(None) & self.param.use_tf_current.rx()
return pn.Column(
self._ids_source,
self._ip,
self._r0,
self._b0,
pn.Column(self._b0, visible=from_tf.rx.not_(), margin=0),
pn.Column(self._tf_current, visible=from_tf, margin=0),
self._profiles,
sizing_mode="stretch_width",
margin=(20, 20),
Expand Down
16 changes: 11 additions & 5 deletions waveform_editor/gui/shape_editor/settings_modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,21 +185,27 @@ def _build_modal(self):
preset_selector = pn.widgets.Select.from_param(
self.nice_settings.param.machine_preset, name="", width=self.PRESET_WIDTH
)
md_rows = []
md_rows = {True: [], False: []}
for md in self.nice_settings.machine_descriptions:
self._md_inputs[md] = pn.widgets.TextInput.from_param(md.param.uri, name="")
md_rows.append(
not_loaded = md.param.loaded.rx.not_()
if not md.required:
# An optional machine description may be left empty
not_loaded = not_loaded & md.param.uri.rx.bool()
md_rows[md.required].append(
self._form_row(
md.ids_name,
self._md_inputs[md],
WarningIndicator(margin=10, visible=md.param.loaded.rx.not_()),
WarningIndicator(margin=10, visible=not_loaded),
)
)
machine_preset_content = pn.Column(
self._section_label("Preset"),
preset_selector,
self._section_label("Machine Description URIs"),
self._settings_section(*md_rows),
self._section_label("Required Machine Descriptions"),
self._settings_section(*md_rows[True]),
self._section_label("Optional Machine Descriptions"),
self._settings_section(*md_rows[False]),
sizing_mode="stretch_width",
scroll=True,
)
Expand Down
56 changes: 26 additions & 30 deletions waveform_editor/gui/shape_editor/shape_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class ShapeEditor(Viewer):
pf_passive = param.ClassSelector(class_=IDSToplevel)
wall = param.ClassSelector(class_=IDSToplevel)
iron_core = param.ClassSelector(class_=IDSToplevel)
tf = param.ClassSelector(class_=IDSToplevel)

def __init__(self, main_gui):
super().__init__()
Expand Down Expand Up @@ -86,6 +87,9 @@ def __init__(self, main_gui):
)
self.run_select.param.watch(self._restore_run, "value")
self.nice_settings = settings.nice
for md in self.nice_settings.machine_descriptions:
md.param.watch(self._load_machine_description, "uri")
md.param.trigger("uri")

self.xml_text = (
importlib.resources.files("waveform_editor.shape_editor.xml_param")
Expand Down Expand Up @@ -353,36 +357,28 @@ def _restore_run(self, event):
self._update_metrics()
self._list_runs()

@param.depends("nice_settings.md_pf_active.uri", watch=True)
def _load_pf_active(self):
self.pf_active = self._load_slice(
self.nice_settings.md_pf_active.uri, "pf_active"
def _load_machine_description(self, event):
"""Load a machine description into the parameter of its IDS name, and into
that of the components that use it."""
md = event.obj
ids = self._load_slice(md.uri, md.ids_name)
self.param.update({md.ids_name: ids})
for component in (self.nice_plotter, self.plasma_properties):
if md.ids_name in component.param:
component.param.update({md.ids_name: ids})
if md.ids_name == "pf_active":
self.coil_currents.create_ui(ids)
md.loaded = ids is not None

def _iron_core_or_empty(self):
"""The iron_core to send to NICE, an empty one for a machine without."""
if self.iron_core is not None:
return self.iron_core
iron_core = self.factory.new("iron_core")
iron_core.ids_properties.homogeneous_time = (
imas.ids_defs.IDS_TIME_MODE_INDEPENDENT
)
self.nice_plotter.pf_active = self.pf_active
self.coil_currents.create_ui(self.pf_active)
self.nice_settings.md_pf_active.loaded = self.pf_active is not None

@param.depends("nice_settings.md_pf_passive.uri", watch=True)
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)
def _load_wall(self):
self.wall = self._load_slice(self.nice_settings.md_wall.uri, "wall")
self.nice_plotter.wall = self.wall
self.nice_settings.md_wall.loaded = self.wall is not None

@param.depends("nice_settings.md_iron_core.uri", watch=True)
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
return iron_core

def _create_equilibrium(self):
"""Create and initialize an equilibrium IDS.
Expand Down Expand Up @@ -556,7 +552,7 @@ async def _run_nice(self, auto):
self.pf_active.serialize(),
self.pf_passive.serialize(),
self.wall.serialize(),
self.iron_core.serialize(),
self._iron_core_or_empty().serialize(),
)
# NICE crashed without a result
if self.communicator.equilibrium is previous_equilibrium:
Expand Down
34 changes: 17 additions & 17 deletions waveform_editor/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ class MachineDescription(param.Parameterized):
uri = param.String()
loaded = param.Boolean(default=False, precedence=-1)

def __init__(self, ids_name: str, **params):
def __init__(self, ids_name: str, required=True, **params):
super().__init__(**params)
self.ids_name = ids_name
self.required = required
self.param.uri.label = f"'{ids_name}' machine description URI"
self.custom_uri = ""

Expand All @@ -40,7 +41,7 @@ class NiceSettings(param.Parameterized):
"pf_active": "imas:hdf5?path=/home/ITER/blokhus/public/imasdb/ITER/4/666666/3", # noqa E501
"pf_passive": "imas:hdf5?path=/home/ITER/blokhus/public/imasdb/ITER/4/666666/3", # noqa E501
"wall": "imas:hdf5?path=/home/ITER/blokhus/public/imasdb/ITER/4/666666/3",
"iron_core": "imas:hdf5?path=/home/ITER/blokhus/public/imasdb/ITER/4/666666/3", # noqa E501
"tf": "imas:hdf5?path=/home/ITER/blokhus/public/imasdb/ITER/4/111002/3",
},
PRESET_WEST: {
"pf_active": "imas:hdf5?path=/home/ITER/blokhus/public/imasdb/west_test_dd4", # noqa E501
Expand Down Expand Up @@ -84,7 +85,12 @@ class NiceSettings(param.Parameterized):
)
md_iron_core = param.ClassSelector(
class_=MachineDescription,
default=MachineDescription("iron_core"),
default=MachineDescription("iron_core", required=False),
precedence=-1,
)
md_tf = param.ClassSelector(
class_=MachineDescription,
default=MachineDescription("tf", required=False),
precedence=-1,
)

Expand Down Expand Up @@ -114,6 +120,7 @@ def __init__(self, **params):
self.md_pf_passive,
self.md_wall,
self.md_iron_core,
self.md_tf,
)

for md in self.machine_descriptions:
Expand All @@ -131,6 +138,8 @@ def _load_machine_description_presets(self):
for preset_name, preset in self.PRESET_URIS.items():
try:
for ids_name, uri in preset.items():
if not getattr(self, f"md_{ids_name}").required:
continue
with imas.DBEntry(uri, "r") as entry:
entry.get(ids_name, lazy=True)

Expand All @@ -154,22 +163,13 @@ def set_mode_flags(self):
@param.depends("machine_preset", watch=True)
def set_machine_preset(self):
preset = self._available_presets.get(self.machine_preset)

if preset is None:
uris = tuple(md.custom_uri for md in self.machine_descriptions)
else:
uris = (
preset["pf_active"],
preset["pf_passive"],
preset["wall"],
preset["iron_core"],
)

for md, uri in zip(self.machine_descriptions, uris, strict=True):
md.uri = uri
for md in self.machine_descriptions:
md.uri = md.custom_uri if preset is None else preset.get(md.ids_name, "")

def _check_required_params_filled(self, *events):
if not all(md.uri and md.loaded for md in self.machine_descriptions):
if not all(
md.uri and md.loaded for md in self.machine_descriptions if md.required
):
self.are_required_filled = False
return

Expand Down
15 changes: 15 additions & 0 deletions waveform_editor/shape_editor/plasma_properties_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,18 @@ def compute_profiles_from_params(r0, alpha, beta, gamma):
mu_0 = scipy.constants.mu_0
f_df_dpsi = (1 - beta) * mu_0 * r0 * (1 - psi_norm**alpha) ** gamma
return psi_norm, dpressure_dpsi, f_df_dpsi


def tf_field_per_current(tf, r):
"""The vacuum toroidal field at ``r`` per ampere of TF coil current,
B(R) / I = μ0 N / (2πR), with N the turns of all TF coils together.

Args:
tf: The tf machine description IDS.
r: The major radius of the field [m].
"""
if tf.is_periodic == 1:
turns = tf.coils_n * tf.coil[0].turns
else:
turns = sum(coil.turns for coil in tf.coil)
return scipy.constants.mu_0 * turns / (2 * np.pi * r)
Loading