diff --git a/waveform_editor/gui/shape_editor/coil_currents.py b/waveform_editor/gui/shape_editor/coil_currents.py index 57884e30..8dbd9bdd 100644 --- a/waveform_editor/gui/shape_editor/coil_currents.py +++ b/waveform_editor/gui/shape_editor/coil_currents.py @@ -53,6 +53,12 @@ def exceeds_limit(self): class CoilCurrents(Viewer): coils = param.List(doc="List of CoilCurrentEntry for each coil") edited = param.Event(doc="Triggered whenever the user edits a coil.") + reference_ip = param.Number( + default=None, + precedence=-1, + doc="The plasma current the coil currents were computed for, None before " + "the first NICE run.", + ) # Table column names COIL_NAME = "coil_name" @@ -153,6 +159,7 @@ def create_ui(self, pf_active): Args: pf_active: pf_active IDS containing coils with current values. """ + self.reference_ip = None if not pf_active: self.coils = [] return @@ -264,16 +271,46 @@ def fill_pf_active(self, pf_active): if coil.current is not None: pf_active.coil[i].current.data = np.array([coil.current]) - def sync_ui_with_pf_active(self, pf_active): + def sync_ui_with_pf_active(self, pf_active, ip): """Synchronize UI with the current values from the pf_active IDS. Args: pf_active: pf_active IDS for which the coil currents are used. + ip: The plasma current the coil currents were computed for. """ for i, coil in enumerate(pf_active.coil): self.coils[i].current = coil.current.data[0] + self.reference_ip = ip self._update_table() + def scale_to_ip(self, ip): + """Scale the coil currents from the plasma current they were computed for to + a new one. The Grad-Shafranov equation is linear in the currents, so scaling + all of them by the same factor keeps the plasma shape. + + Args: + ip: The new plasma current [A]. + """ + if self.reference_ip is None: + pn.state.notifications.info( + "Run NICE first: the coil currents are scaled from the plasma " + "current of the last run." + ) + return + # There is no scale factor to or from a plasma current of 0 A + if ip == 0 or self.reference_ip == 0: + return + factor = ip / self.reference_ip + for coil in self.coils: + if coil.current is not None: + coil.current *= factor + self.reference_ip = ip + self._update_table() + pn.state.notifications.info( + f"Scaled the coil currents by {factor:.4g} with the plasma current." + ) + self._warn_exceeded() + def update_xml(self, xml_params: ET.Element): """Update XML parameters based on coil current table configuration. diff --git a/waveform_editor/gui/shape_editor/plasma_properties.py b/waveform_editor/gui/shape_editor/plasma_properties.py index 6bf706cd..2dc5d049 100644 --- a/waveform_editor/gui/shape_editor/plasma_properties.py +++ b/waveform_editor/gui/shape_editor/plasma_properties.py @@ -370,6 +370,13 @@ class PlasmaProperties(Viewer): label="Set toroidal field by TF coil current", doc="Compute the toroidal field from the TF coil current and the tf IDS.", ) + ip = param.Number(default=None, precedence=-1, doc="The plasma current [A].") + scale_coil_currents = param.Boolean( + default=False, + label="Scale coil currents with the plasma current", + doc="Scale the coil currents by the change of the plasma current, which keeps " + "the plasma shape for the same plasma profiles.", + ) def __init__(self): super().__init__() @@ -377,6 +384,23 @@ def __init__(self): "Plasma current [A]", default_value=DEFAULT_PROPERTIES["ip"], step=1e6, + extra=[ + pn.Row( + pn.widgets.Checkbox.from_param(self.param.scale_coil_currents), + pn.widgets.TooltipIcon( + value=( + "Multiply all coil currents by the ratio of the new to " + "the previous plasma current. For the same profiles, this " + "keeps the plasma shape, which is useful in direct mode. " + "The currents are scaled from the plasma current of the " + "last NICE run." + ), + align="center", + margin=(-3, 0, 0, -10), + ), + margin=(8, 0, 0, 0), + ) + ], ) self._r0 = PropertyInput( "Reference major radius [m]", diff --git a/waveform_editor/gui/shape_editor/shape_editor.py b/waveform_editor/gui/shape_editor/shape_editor.py index 1908aed4..80aa1115 100644 --- a/waveform_editor/gui/shape_editor/shape_editor.py +++ b/waveform_editor/gui/shape_editor/shape_editor.py @@ -68,6 +68,7 @@ def __init__(self, main_gui): self.plasma_shape = PlasmaShape() self.plasma_properties = PlasmaProperties() self.coil_currents = CoilCurrents() + self.plasma_properties.param.watch(self._scale_coil_currents, "ip") self.nice_plotter = NicePlotter( communicator=self.communicator, plasma_shape=self.plasma_shape, @@ -270,14 +271,20 @@ def _create_options_tabs(self, is_inverse_mode, has_shape, has_properties): sizing_mode="stretch_width", stylesheets=[".bk-tab { flex: 1; text-align: center; }"], ) - tabs.active = min(self._active_tab, log_tab) + # Without the plasma shape tab, the other tabs move one to the left + tabs.active = max(self._active_tab - (not is_inverse_mode), 0) tabs.param.watch(self._remember_active_tab, "active") self.terminal.visible = tabs.param.active.rx() == log_tab return tabs def _remember_active_tab(self, event): """Keep the open tab open when the tabs are rebuilt.""" - self._active_tab = event.new + self._active_tab = event.new + (not self.nice_settings.is_inverse_mode) + + def _scale_coil_currents(self, event): + """Scale the coil currents with the plasma current, when that is enabled.""" + if self.plasma_properties.scale_coil_currents: + self.coil_currents.scale_to_ip(event.new) def _load_slice(self, uri, ids_name, time=0): """Load an IDS slice and return it. @@ -353,7 +360,9 @@ def _restore_run(self, event): return self.communicator.equilibrium = equilibrium self.communicator.pf_active = pf_active - self.coil_currents.sync_ui_with_pf_active(pf_active) + self.coil_currents.sync_ui_with_pf_active( + pf_active, equilibrium.time_slice[0].global_quantities.ip + ) self._update_metrics() self._list_runs() @@ -557,7 +566,9 @@ async def _run_nice(self, auto): # NICE crashed without a result if self.communicator.equilibrium is previous_equilibrium: return - self.coil_currents.sync_ui_with_pf_active(self.communicator.pf_active) + self.coil_currents.sync_ui_with_pf_active( + self.communicator.pf_active, self.plasma_properties.ip + ) self._update_metrics() # Only a converged run is worth restoring if self.communicator.converged: