From cb47065527fa4248b5eeb4b5b131ae598b56b196 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 26 Mar 2026 17:27:13 +0100 Subject: [PATCH 01/35] added separatrix, contour center, and magnetic axes with clockable option --- idstools/compute/equilibrium.py | 94 +++++++++++++++++++++ idstools/scripts/bin/plotequilibrium | 31 ++++--- idstools/view/equilibrium.py | 118 ++++++++++++++++++++++++--- 3 files changed, 223 insertions(+), 20 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index d103121b..dfea0dc4 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -301,6 +301,100 @@ def get_ip(self) -> list: for time_index in range(len(self.ids.time_slice)) ] + def get_separatrix(self, time_slice: int) -> Union[dict, None]: + """Return the closed separatrix outline for a given time slice. + + Reads ``boundary.outline.r/z``, validates that the arrays are + non-empty and contain at least one finite, non-fill value, then + closes the polygon by appending the first point. + + Args: + time_slice (int): Index into ``time_slice``. + + Returns: + dict with keys ``"r"`` and ``"z"`` (1-D ndarrays, polygon + closed), or ``None`` if the data are absent or invalid. + """ + try: + bnd = self.ids.time_slice[time_slice].boundary + r = np.asarray(bnd.outline.r, dtype=float) + z = np.asarray(bnd.outline.z, dtype=float) + except Exception as exc: + logger.debug(f"get_separatrix: could not read boundary.outline – {exc}") + return None + + def _valid(arr): + return arr.size > 0 and np.any(np.isfinite(arr) & (np.abs(arr) < 1.0e20)) + + if not (_valid(r) and _valid(z)): + logger.debug("get_separatrix: boundary.outline contains no valid data") + return None + + # close the polygon + r = np.append(r, r[0]) + z = np.append(z, z[0]) + return {"r": r, "z": z} + + def get_magnetic_axis(self, time_slice: int) -> Union[dict, None]: + """Return the magnetic axis position for a given time slice. + + Reads ``global_quantities.magnetic_axis.r/z`` and validates the + scalar values. + + Args: + time_slice (int): Index into ``time_slice``. + + Returns: + dict with scalar keys ``"r"`` and ``"z"`` (floats), or + ``None`` if the data are absent or invalid. + """ + try: + mag_ax = self.ids.time_slice[time_slice].global_quantities.magnetic_axis + r = float(mag_ax.r) + z = float(mag_ax.z) + except Exception as exc: + logger.debug(f"get_magnetic_axis: could not read magnetic_axis – {exc}") + return None + + def _valid(val): + return np.isfinite(val) and abs(val) < 1.0e20 + + if not (_valid(r) and _valid(z)): + logger.debug("get_magnetic_axis: magnetic_axis contains no valid data") + return None + + return {"r": r, "z": z} + + def get_current_centre(self, time_slice: int) -> Union[dict, None]: + """Return the current centroid position for a given time slice. + + Reads ``global_quantities.current_centre.r/z`` and validates the + scalar values. + + Args: + time_slice (int): Index into ``time_slice``. + + Returns: + dict with scalar keys ``"r"`` and ``"z"`` (floats), or + ``None`` if the data are absent or invalid. + """ + try: + cc = self.ids.time_slice[time_slice].global_quantities.current_centre + r = float(cc.r) + z = float(cc.z) + except Exception as exc: + logger.debug(f"get_current_centre: could not read current_centre – {exc}") + return None + + def _valid(val): + return np.isfinite(val) and abs(val) < 1.0e20 + + if not (_valid(r) and _valid(z)): + logger.debug("get_current_centre: current_centre contains no valid data") + return None + + return {"r": r, "z": z} + def get_top_view(self, time_slice: int) -> dict: """ The function returns data for plotting the top view of a 2D shape. diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index f4a3f03e..ba90a93d 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -48,11 +48,6 @@ if __name__ == "__main__": formatter_class=RichHelpFormatter, ) parser.add_argument("-t", "--time", help="Time (default=middle)", type=float, default=-99.0) - parser.add_argument( - "--rho", - help="Show rho overlay on the plot", - action="store_true", - ) parser.add_argument( "-p", "--plots", @@ -190,13 +185,28 @@ if __name__ == "__main__": ids_data = get_md_data(mduris, args.dd_update) plot_machine_description(ax1, ids_data) - c_psi, c_rho = view_object.view_magnetic_poloidal_flux(ax1, time_slice, plot_rho=args.rho) + c_psi, c_rho, rho_collections = view_object.view_magnetic_poloidal_flux(ax1, time_slice) + + # rho — created second, stacks further left of psi (pad=0.02 keeps a small gap) + if c_rho and rho_collections: + # cbar_rho = canvas.fig.colorbar(c_rho, ax=ax1, location="left", pad=0.004, fraction=0.04) + cbar_rho = canvas.fig.colorbar(c_rho, ax=ax1, location="left") + cbar_rho.set_label(r"$\rho$") + cbar_rho.ax.set_visible(False) # hidden by default, synced with rho overlay + + def _sync_rho_cbar(event): + # fires after the view's on_legend_click already toggled visibility + visible = rho_collections[0].get_visible() + if cbar_rho.ax.get_visible() != visible: + cbar_rho.ax.set_visible(visible) + canvas.fig.canvas.draw_idle() + + ax1.figure.canvas.mpl_connect("pick_event", _sync_rho_cbar) + # psi — created first, sits directly adjacent to the axes (pad=0.01) if c_psi: - cbar_psi = canvas.fig.colorbar(c_psi, ax=ax1, orientation="horizontal", pad=0.08, fraction=0.03) + # cbar_psi = canvas.fig.colorbar(c_psi, ax=ax1, location="left", pad=0.002, fraction=0.04) + cbar_psi = canvas.fig.colorbar(c_psi, ax=ax1, location="left") cbar_psi.set_label(r"$\psi$ [Wb]") - if c_rho: - cbar_rho = canvas.fig.colorbar(c_rho, ax=ax1, orientation="horizontal", pad=0.08, fraction=0.03) - cbar_rho.set_label(r"$\rho$ [Wb]") ax1.set_title(title) xmin, xmax = ax1.get_xlim() @@ -216,6 +226,7 @@ if __name__ == "__main__": canvas.fig.suptitle(get_title(args, "Equilibrium", time_value)) canvas.fig.set_size_inches(14, 8) + # left=0.15 reserves room for up to two vertical colorbars on the left canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.024, right=0.988, hspace=0.221, wspace=0.25) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) if args.save: diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 2416967e..81c0d314 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -13,6 +13,7 @@ except ImportError: import imas import matplotlib.pyplot as plt +from matplotlib.lines import Line2D as ProxyLine import numpy as np from idstools.compute.equilibrium import EquilibriumCompute @@ -40,7 +41,9 @@ def view_magnetic_poloidal_flux( ax: plt.axes, time_slice: int, profiles2d_index: int = 0, - plot_rho: bool = False, + plot_separatrix: bool = True, + plot_magnetic_axis: bool = True, + plot_current_centre: bool = True, ): """ This function plots the magnetic poloidal flux contours on a 2D Cartesian grid. @@ -80,6 +83,7 @@ def view_magnetic_poloidal_flux( :meth:`plotIP` """ contour_lines_psi = contour_lines_rho = None + _rho_collections = [] cartestion_grid = self.compute_obj.get2d_cartesian_grid(time_slice, profiles2d_index) if cartestion_grid is not None: levels = 50 @@ -95,19 +99,113 @@ def view_magnetic_poloidal_flux( # # fmt="%.2e", # inline_spacing=1, # ) - if plot_rho: - rho2d = self.compute_obj.get_rho2d(time_slice) - if rho2d is not None: - contour_lines_rho = ax.contour( - cartestion_grid["r2d"], cartestion_grid["z2d"], rho2d, levels=levels, cmap="YlOrBr" - ) + + # rho overlay: always draw when data is available; + # capture artists with before/after snapshot (works across all matplotlib versions) + rho2d = self.compute_obj.get_rho2d(time_slice) + if rho2d is not None: + _before = set(ax.collections) + contour_lines_rho = ax.contour( + cartestion_grid["r2d"], cartestion_grid["z2d"], rho2d, levels=levels, cmap="YlOrBr" + ) + _rho_collections = [c for c in ax.collections if c not in _before] + # hidden by default; user can toggle via legend click + for _c in _rho_collections: + _c.set_visible(False) + else: + _rho_collections = [] ax.set_aspect("equal", adjustable="box") ax.set_xlabel("$R$ [m]") ax.set_ylabel("$Z$ [m]") - # ax.set_xlim(3.4, cartestionGrid["r2d"].max()) - # ax.set_ylim(cartestionGrid["z2d"].min() * 0.7, cartestionGrid["z2d"].max() * 0.7) - return contour_lines_psi, contour_lines_rho + + # --- overlays: draw all available, store (proxy, artists_list) tuples --- + # Each entry: (proxy Line2D for legend, list of data artists to toggle) + overlay_entries = [] + + # rho contour (starts hidden by default) + if contour_lines_rho is not None and _rho_collections: + proxy_rho = ProxyLine( + [0], [0], color="darkorange", linewidth=1.5, label="\u03c1 contours", alpha=0.3 + ) # dimmed to match hidden state + overlay_entries.append((proxy_rho, _rho_collections)) + + if plot_separatrix: + sep = self.compute_obj.get_separatrix(time_slice) + if sep is not None: + (line,) = ax.plot(sep["r"], sep["z"], color="red", linewidth=2.0, label="separatrix", zorder=5) + overlay_entries.append((line, [line])) + + if plot_magnetic_axis: + mag_ax = self.compute_obj.get_magnetic_axis(time_slice) + if mag_ax is not None: + (marker,) = ax.plot( + mag_ax["r"], + mag_ax["z"], + marker="^", + color="yellow", + markersize=14, + markeredgecolor="black", + markeredgewidth=0.8, + linestyle="None", + label="magnetic axis", + zorder=6, + ) + overlay_entries.append((marker, [marker])) + + if plot_current_centre: + cc = self.compute_obj.get_current_centre(time_slice) + if cc is not None: + (marker,) = ax.plot( + cc["r"], + cc["z"], + marker="+", + color="cyan", + markersize=12, + markeredgewidth=2.0, + linestyle="None", + label="current centre", + zorder=6, + ) + overlay_entries.append((marker, [marker])) + + # --- clickable legend: click a legend entry to toggle its overlay ---- + if overlay_entries: + handles = [proxy for proxy, _ in overlay_entries] + legend = ax.legend( + handles=handles, + loc="upper left", + bbox_to_anchor=(1.15, 1), + fancybox=True, + fontsize=10, + labelspacing=1.2, + title="Overlays\n(click to toggle)", + ) + legend.get_title().set_fontsize(10) + legend.get_title().set_fontstyle("italic") + legend.get_title().set_color("gray") + + leg_map = {} + for legline, (_, artists) in zip(legend.get_lines(), overlay_entries): + legline.set_picker(8) + leg_map[legline] = artists + + def on_legend_click(event): + legline = event.artist + if legline not in leg_map: + return + artists = leg_map[legline] + if not artists: + return + visible = not artists[0].get_visible() + for a in artists: + a.set_visible(visible) + legline.set_alpha(1.0 if visible else 0.3) + ax.figure.canvas.draw_idle() + + ax.figure.canvas.mpl_connect("pick_event", on_legend_click) + + return contour_lines_psi, contour_lines_rho, _rho_collections def view_pulse_info(self, ax: plt.axes, title: str, hostdir: str, shot: int, run: int, t: float): self.database_info(ax, title, hostdir, shot, run, t) From 7bc37fa6323abf0864fe668be257e644f6e14ba6 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 27 Mar 2026 12:35:24 +0100 Subject: [PATCH 02/35] added quantities and spacing between plots --- idstools/compute/equilibrium.py | 54 ++++++++++++++++ idstools/scripts/bin/plotequilibrium | 39 ++++++------ idstools/view/equilibrium.py | 95 +++++++++++++++++++++++----- 3 files changed, 155 insertions(+), 33 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index dfea0dc4..1a52f8d8 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -395,6 +395,60 @@ def _valid(val): return {"r": r, "z": z} + def get_scalar_annotation_quantities(self, time_slice: int) -> list: + """Return validated scalar global/boundary quantities for annotation display. + + Reads a fixed set of scalar fields from ``global_quantities`` and + ``boundary``, validates each value (finite and < 1e20), and returns + only those with valid data. + + Args: + time_slice (int): Index into ``time_slice``. + + Returns: + list of dicts, each with ``"label"`` (LaTeX str) and ``"text"`` + (formatted value + unit str). Empty list if nothing is valid. + """ + + def _valid(val): + try: + v = float(val) + return np.isfinite(v) and abs(v) < 1.0e20 + except Exception: + return False + + items = [] + ts = self.ids.time_slice[time_slice] + gq = ts.global_quantities + bnd = ts.boundary + + _specs = [ + (lambda: float(gq.ip), lambda v: {"label": "$I_p$", "text": f"{v/1e6:.3f} MA"}), + ( + lambda: float( + getattr( + gq.magnetic_axis, "b_field_phi" if hasattr(gq.magnetic_axis, "b_field_phi") else "b_field_tor" + ) + ), + lambda v: {"label": r"$B_\phi$(axis)", "text": f"{v:.3f} T"}, + ), + (lambda: float(gq.psi_axis), lambda v: {"label": r"$\psi_{\rm axis}$", "text": f"{v:.4g} Wb"}), + (lambda: float(gq.psi_boundary), lambda v: {"label": r"$\psi_{\rm bnd}$", "text": f"{v:.4g} Wb"}), + (lambda: float(gq.q_axis), lambda v: {"label": "$q_0$", "text": f"{v:.3f}"}), + (lambda: float(gq.q_95), lambda v: {"label": "$q_{95}$", "text": f"{v:.3f}"}), + (lambda: float(bnd.minor_radius), lambda v: {"label": "$a$", "text": f"{v:.3f} m"}), + (lambda: float(bnd.elongation), lambda v: {"label": r"$\kappa$", "text": f"{v:.3f}"}), + (lambda: float(bnd.triangularity), lambda v: {"label": r"$\delta$", "text": f"{v:.3f}"}), + ] + for getter, formatter in _specs: + try: + val = getter() + if _valid(val): + items.append(formatter(val)) + except Exception: + pass + return items + def get_top_view(self, time_slice: int) -> dict: """ The function returns data for plotting the top view of a 2D shape. diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index ba90a93d..bfbe9112 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -124,14 +124,15 @@ if __name__ == "__main__": database_text = "" if args.plots: compute_obj = EquilibriumCompute(ids_obj_equilibrium) - profiles_1d_quantities = compute_obj.get_profiles_1d_quantities(time_slice, ["pressure", "q", "beta_pol"]) + profiles_1d_quantities = compute_obj.get_profiles_1d_quantities( + time_slice, ["pressure", "q", "beta_pol"] + ) p1dcounter = sum(1 for value in profiles_1d_quantities.values() if value.has_value) global_quantities = compute_obj.get_global_quantities( time_slice, ["q_min.value", "q_95", "li_3", "beta_tor", "energy_mhd"] ) gcounter = sum(1 for value in global_quantities.values() if value["has_value"]) - total_plots = p1dcounter + gcounter if total_plots % 2 == 1: @@ -140,12 +141,13 @@ if __name__ == "__main__": col_size = int(total_plots / 2) col_size = col_size + 1 - canvas = PlotCanvas(2, col_size) + + canvas = PlotCanvas(2, col_size + 1) ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0, rowspan=2) axes_list1 = [] axes_list2 = [] plotting_counter = 0 - for col in range(1, col_size): + for col in range(2, col_size + 1): for row in [0, 1]: if plotting_counter < p1dcounter: axes_list1.append(canvas.add_axes(title="", xlabel="", row=row, col=col)) @@ -187,26 +189,23 @@ if __name__ == "__main__": c_psi, c_rho, rho_collections = view_object.view_magnetic_poloidal_flux(ax1, time_slice) - # rho — created second, stacks further left of psi (pad=0.02 keeps a small gap) + if c_psi: + cbar_psi = canvas.fig.colorbar(c_psi, ax=ax1, orientation="horizontal", pad=0.08, fraction=0.03) + cbar_psi.set_label(r"$\psi$ [Wb]") + if c_rho and rho_collections: - # cbar_rho = canvas.fig.colorbar(c_rho, ax=ax1, location="left", pad=0.004, fraction=0.04) - cbar_rho = canvas.fig.colorbar(c_rho, ax=ax1, location="left") + cbar_rho = canvas.fig.colorbar(c_rho, ax=ax1, orientation="horizontal", pad=0.08, fraction=0.03) cbar_rho.set_label(r"$\rho$") - cbar_rho.ax.set_visible(False) # hidden by default, synced with rho overlay + cbar_rho.ax.set_visible(False) def _sync_rho_cbar(event): - # fires after the view's on_legend_click already toggled visibility visible = rho_collections[0].get_visible() if cbar_rho.ax.get_visible() != visible: cbar_rho.ax.set_visible(visible) canvas.fig.canvas.draw_idle() - ax1.figure.canvas.mpl_connect("pick_event", _sync_rho_cbar) - # psi — created first, sits directly adjacent to the axes (pad=0.01) - if c_psi: - # cbar_psi = canvas.fig.colorbar(c_psi, ax=ax1, location="left", pad=0.002, fraction=0.04) - cbar_psi = canvas.fig.colorbar(c_psi, ax=ax1, location="left") - cbar_psi.set_label(r"$\psi$ [Wb]") + canvas.fig.canvas.mpl_connect("pick_event", _sync_rho_cbar) + ax1.set_title(title) xmin, xmax = ax1.get_xlim() @@ -220,14 +219,18 @@ if __name__ == "__main__": rotation="vertical", fontsize=7, ) + if args.plots: view_object.plot_profiles_1d_quantities(axes_list1, time_slice) view_object.plot_global_quantities(axes_list2, time_value) canvas.fig.suptitle(get_title(args, "Equilibrium", time_value)) - canvas.fig.set_size_inches(14, 8) - # left=0.15 reserves room for up to two vertical colorbars on the left - canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.024, right=0.988, hspace=0.221, wspace=0.25) + if args.plots: + canvas.fig.set_size_inches(10 + col_size * 1.6, 8) + canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.024, right=0.988, hspace=0.221, wspace=0.20) + else: + canvas.fig.set_size_inches(14, 8) + canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.024, right=0.988, hspace=0.221, wspace=0.25) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) if args.save: fname = get_file_name(args, f"{os.path.basename(__file__)}_Equilibrium", time_value) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 81c0d314..5c4d0dea 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -133,8 +133,10 @@ def view_magnetic_poloidal_flux( if plot_separatrix: sep = self.compute_obj.get_separatrix(time_slice) if sep is not None: - (line,) = ax.plot(sep["r"], sep["z"], color="red", linewidth=2.0, label="separatrix", zorder=5) - overlay_entries.append((line, [line])) + (line,) = ax.plot(sep["r"], sep["z"], color="red", linewidth=2.0, zorder=5) + line.set_visible(False) + proxy_sep = ProxyLine([0], [0], color="red", linewidth=2.0, label="separatrix") + overlay_entries.append((proxy_sep, [line])) if plot_magnetic_axis: mag_ax = self.compute_obj.get_magnetic_axis(time_slice) @@ -142,16 +144,25 @@ def view_magnetic_poloidal_flux( (marker,) = ax.plot( mag_ax["r"], mag_ax["z"], - marker="^", - color="yellow", - markersize=14, - markeredgecolor="black", - markeredgewidth=0.8, + marker="x", + color="saddlebrown", + markersize=6, + markeredgewidth=1.5, linestyle="None", - label="magnetic axis", zorder=6, ) - overlay_entries.append((marker, [marker])) + marker.set_visible(False) + proxy_mag = ProxyLine( + [0], + [0], + color="saddlebrown", + marker="x", + markersize=6, + markeredgewidth=1.5, + linestyle="None", + label="magnetic axis", + ) + overlay_entries.append((proxy_mag, [marker])) if plot_current_centre: cc = self.compute_obj.get_current_centre(time_slice) @@ -160,16 +171,32 @@ def view_magnetic_poloidal_flux( cc["r"], cc["z"], marker="+", - color="cyan", - markersize=12, + color="deeppink", + markersize=8, markeredgewidth=2.0, linestyle="None", - label="current centre", zorder=6, ) - overlay_entries.append((marker, [marker])) + marker.set_visible(False) + proxy_cc = ProxyLine( + [0], + [0], + color="deeppink", + marker="+", + markersize=8, + markeredgewidth=2.0, + linestyle="None", + label="current centre", + ) + overlay_entries.append((proxy_cc, [marker])) + + # annotation text box (quantities summary below the axes) + ann_txt = self.view_global_quantities_annotation(ax, time_slice) + if ann_txt is not None: + proxy_ann = ProxyLine([0], [0], color="steelblue", linewidth=3, label="quantities") + overlay_entries.append((proxy_ann, [ann_txt])) - # --- clickable legend: click a legend entry to toggle its overlay ---- + # --- clickable legend if overlay_entries: handles = [proxy for proxy, _ in overlay_entries] legend = ax.legend( @@ -183,13 +210,19 @@ def view_magnetic_poloidal_flux( ) legend.get_title().set_fontsize(10) legend.get_title().set_fontstyle("italic") - legend.get_title().set_color("gray") + + legend.get_title().set_ha("center") + for text in legend.get_texts(): + text.set_ha("center") leg_map = {} for legline, (_, artists) in zip(legend.get_lines(), overlay_entries): legline.set_picker(8) leg_map[legline] = artists + if artists and not artists[0].get_visible(): + legline.set_alpha(0.3) + def on_legend_click(event): legline = event.artist if legline not in leg_map: @@ -210,6 +243,38 @@ def on_legend_click(event): def view_pulse_info(self, ax: plt.axes, title: str, hostdir: str, shot: int, run: int, t: float): self.database_info(ax, title, hostdir, shot, run, t) + def view_global_quantities_annotation(self, ax: plt.axes, time_slice: int): + """Draw a scalar global-quantities text box below the axes. + + Reads validated scalars via + :meth:`idstools.compute.equilibrium.EquilibriumCompute.get_scalar_annotation_quantities` + and renders them as a styled text box just below the axes. + + Args: + ax: matplotlib axes. + time_slice (int): time-slice index. + + Returns: + matplotlib ``Text`` artist, or ``None`` if no valid data. + """ + items = self.compute_obj.get_scalar_annotation_quantities(time_slice) + if not items: + return None + + textstr = "\n".join(f"{d['label']} = {d['text']}" for d in items) + txt = ax.text( + 1.20, + 0.5, + textstr, + transform=ax.transAxes, + fontsize=8, + horizontalalignment="left", + verticalalignment="center", + clip_on=False, + bbox=dict(boxstyle="round,pad=0.5", facecolor="white", alpha=0.85, edgecolor="steelblue"), + ) + return txt + def plot_ip(self, ax): """ This function plots the plasma current over time on a given axis. From 8759cdb66df57224a0e85678310de69358fbc91a Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 27 Mar 2026 13:55:11 +0100 Subject: [PATCH 03/35] fixed integration with machine description --- idstools/view/equilibrium.py | 46 +++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 5c4d0dea..12b86fb4 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -119,8 +119,13 @@ def view_magnetic_poloidal_flux( ax.set_xlabel("$R$ [m]") ax.set_ylabel("$Z$ [m]") - # --- overlays: draw all available, store (proxy, artists_list) tuples --- - # Each entry: (proxy Line2D for legend, list of data artists to toggle) + # Get any handles already in the axes legend (e.g. from machine description). + _existing_legend = ax.get_legend() + if _existing_legend is not None: + _md_handles = list(_existing_legend.legend_handles) + _md_labels = [t.get_text() for t in _existing_legend.get_texts()] + else: + _md_handles, _md_labels = [], [] overlay_entries = [] # rho contour (starts hidden by default) @@ -133,9 +138,8 @@ def view_magnetic_poloidal_flux( if plot_separatrix: sep = self.compute_obj.get_separatrix(time_slice) if sep is not None: - (line,) = ax.plot(sep["r"], sep["z"], color="red", linewidth=2.0, zorder=5) - line.set_visible(False) - proxy_sep = ProxyLine([0], [0], color="red", linewidth=2.0, label="separatrix") + (line,) = ax.plot(sep["r"], sep["z"], color="saddlebrown", linewidth=2.0, linestyle="--", zorder=5) + proxy_sep = ProxyLine([0], [0], color="saddlebrown", linewidth=2.0, linestyle="--", label="separatrix") overlay_entries.append((proxy_sep, [line])) if plot_magnetic_axis: @@ -151,7 +155,6 @@ def view_magnetic_poloidal_flux( linestyle="None", zorder=6, ) - marker.set_visible(False) proxy_mag = ProxyLine( [0], [0], @@ -177,7 +180,6 @@ def view_magnetic_poloidal_flux( linestyle="None", zorder=6, ) - marker.set_visible(False) proxy_cc = ProxyLine( [0], [0], @@ -197,10 +199,15 @@ def view_magnetic_poloidal_flux( overlay_entries.append((proxy_ann, [ann_txt])) # --- clickable legend - if overlay_entries: - handles = [proxy for proxy, _ in overlay_entries] + if overlay_entries or _md_handles: + overlay_proxies = [proxy for proxy, _ in overlay_entries] + + all_handles = _md_handles + overlay_proxies + all_labels = _md_labels + [p.get_label() for p in overlay_proxies] + legend = ax.legend( - handles=handles, + handles=all_handles, + labels=all_labels, loc="upper left", bbox_to_anchor=(1.15, 1), fancybox=True, @@ -210,18 +217,23 @@ def view_magnetic_poloidal_flux( ) legend.get_title().set_fontsize(10) legend.get_title().set_fontstyle("italic") - legend.get_title().set_ha("center") for text in legend.get_texts(): text.set_ha("center") leg_map = {} - for legline, (_, artists) in zip(legend.get_lines(), overlay_entries): - legline.set_picker(8) - leg_map[legline] = artists - + n_md = len(_md_handles) + for i, orig_artist in enumerate(_md_handles): + leg_h = legend.legend_handles[i] + leg_h.set_picker(8) + leg_map[leg_h] = [orig_artist] + + for i, (_, artists) in enumerate(overlay_entries): + leg_h = legend.legend_handles[n_md + i] + leg_h.set_picker(8) + leg_map[leg_h] = artists if artists and not artists[0].get_visible(): - legline.set_alpha(0.3) + leg_h.set_alpha(0.3) def on_legend_click(event): legline = event.artist @@ -264,7 +276,7 @@ def view_global_quantities_annotation(self, ax: plt.axes, time_slice: int): textstr = "\n".join(f"{d['label']} = {d['text']}" for d in items) txt = ax.text( 1.20, - 0.5, + 0.1, textstr, transform=ax.transAxes, fontsize=8, From 1b927c97bbbc7d5d4cb84aba9c5d27be528b5645 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 27 Mar 2026 15:04:43 +0100 Subject: [PATCH 04/35] fixed colorbars position --- idstools/scripts/bin/plotequilibrium | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index bfbe9112..1e3a90d9 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -190,12 +190,16 @@ if __name__ == "__main__": c_psi, c_rho, rho_collections = view_object.view_magnetic_poloidal_flux(ax1, time_slice) if c_psi: - cbar_psi = canvas.fig.colorbar(c_psi, ax=ax1, orientation="horizontal", pad=0.08, fraction=0.03) - cbar_psi.set_label(r"$\psi$ [Wb]") + cax_psi = ax1.inset_axes([-0.20, 0.05, 0.05, 0.88]) # [x, y, w, h] in axes coords + cbar_psi = canvas.fig.colorbar(c_psi, cax=cax_psi) + cbar_psi.ax.set_title(r"$\psi$ [Wb]", fontsize=7, pad=4) + cbar_psi.ax.tick_params(labelsize=7) if c_rho and rho_collections: - cbar_rho = canvas.fig.colorbar(c_rho, ax=ax1, orientation="horizontal", pad=0.08, fraction=0.03) - cbar_rho.set_label(r"$\rho$") + cax_rho = ax1.inset_axes([-0.35, 0.05, 0.05, 0.88]) # just to the right of psi bar + cbar_rho = canvas.fig.colorbar(c_rho, cax=cax_rho) + cbar_rho.ax.set_title(r"$\rho$", fontsize=7, pad=4) + cbar_rho.ax.tick_params(labelsize=7) cbar_rho.ax.set_visible(False) def _sync_rho_cbar(event): From 75381549c70acf5d3c08623286f1a61f3e09e7e5 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 27 Mar 2026 15:34:39 +0100 Subject: [PATCH 05/35] fix color of separatrix --- idstools/view/equilibrium.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 12b86fb4..d0dd3dc4 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -138,8 +138,8 @@ def view_magnetic_poloidal_flux( if plot_separatrix: sep = self.compute_obj.get_separatrix(time_slice) if sep is not None: - (line,) = ax.plot(sep["r"], sep["z"], color="saddlebrown", linewidth=2.0, linestyle="--", zorder=5) - proxy_sep = ProxyLine([0], [0], color="saddlebrown", linewidth=2.0, linestyle="--", label="separatrix") + (line,) = ax.plot(sep["r"], sep["z"], color="red", linewidth=2.0, linestyle="--", zorder=5) + proxy_sep = ProxyLine([0], [0], color="red", linewidth=2.0, linestyle="--", label="separatrix") overlay_entries.append((proxy_sep, [line])) if plot_magnetic_axis: @@ -279,7 +279,7 @@ def view_global_quantities_annotation(self, ax: plt.axes, time_slice: int): 0.1, textstr, transform=ax.transAxes, - fontsize=8, + fontsize=9, horizontalalignment="left", verticalalignment="center", clip_on=False, From 5d0d18d003e6e39eb9190db53a7f3633f21806b5 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 27 Mar 2026 16:42:32 +0100 Subject: [PATCH 06/35] remove legend for quantities --- idstools/view/equilibrium.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index d0dd3dc4..6d63dea2 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -193,10 +193,7 @@ def view_magnetic_poloidal_flux( overlay_entries.append((proxy_cc, [marker])) # annotation text box (quantities summary below the axes) - ann_txt = self.view_global_quantities_annotation(ax, time_slice) - if ann_txt is not None: - proxy_ann = ProxyLine([0], [0], color="steelblue", linewidth=3, label="quantities") - overlay_entries.append((proxy_ann, [ann_txt])) + self.view_global_quantities_annotation(ax, time_slice) # --- clickable legend if overlay_entries or _md_handles: From cd82599072fa338bd2336f8970b7408c0fe8de0c Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 27 Mar 2026 16:58:02 +0100 Subject: [PATCH 07/35] fixed formatting --- idstools/view/equilibrium.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 6d63dea2..63202170 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -100,8 +100,7 @@ def view_magnetic_poloidal_flux( # inline_spacing=1, # ) - # rho overlay: always draw when data is available; - # capture artists with before/after snapshot (works across all matplotlib versions) + # rho overlay rho2d = self.compute_obj.get_rho2d(time_slice) if rho2d is not None: _before = set(ax.collections) @@ -109,7 +108,6 @@ def view_magnetic_poloidal_flux( cartestion_grid["r2d"], cartestion_grid["z2d"], rho2d, levels=levels, cmap="YlOrBr" ) _rho_collections = [c for c in ax.collections if c not in _before] - # hidden by default; user can toggle via legend click for _c in _rho_collections: _c.set_visible(False) else: @@ -128,11 +126,9 @@ def view_magnetic_poloidal_flux( _md_handles, _md_labels = [], [] overlay_entries = [] - # rho contour (starts hidden by default) + # rho contour if contour_lines_rho is not None and _rho_collections: - proxy_rho = ProxyLine( - [0], [0], color="darkorange", linewidth=1.5, label="\u03c1 contours", alpha=0.3 - ) # dimmed to match hidden state + proxy_rho = ProxyLine([0], [0], color="darkorange", linewidth=1.5, label="\u03c1 contours", alpha=0.3) overlay_entries.append((proxy_rho, _rho_collections)) if plot_separatrix: @@ -192,7 +188,6 @@ def view_magnetic_poloidal_flux( ) overlay_entries.append((proxy_cc, [marker])) - # annotation text box (quantities summary below the axes) self.view_global_quantities_annotation(ax, time_slice) # --- clickable legend From 847b356bbd8951f3d3a5a3df12ad68d792698178 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 27 Mar 2026 17:26:37 +0100 Subject: [PATCH 08/35] added documetation about tkinter installation --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed99be6d..ccf62dca 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,20 @@ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=IT ## Requirements -- Python ≥ 3.8 -- IMAS Python Access Layer (`imas-python`) +- Python ≥ 3.10 + +### Installed automatically via pip - NumPy, Matplotlib, Pandas - Rich (for enhanced terminal output) +### Requires separate installation +- **Tkinter** — usually bundled with Python but may require system packages: + - Linux (Debian/Ubuntu): `sudo apt install python3-tk` + - Linux (RHEL/CentOS/Rocky): `sudo dnf install python3-tkinter` + - Windows: included in the [python.org](https://www.python.org/downloads/) installer ("tcl/tk and IDLE" component, enabled by default) + - macOS (python.org installer): included by default + - macOS (Homebrew): `brew install python-tk` (or `brew install python-tk@3.x` for a specific version) + ## Documentation Full documentation is available at the project repository. Each tool includes built-in help: From c5a32990eb3c10ced2cad5c9dcecc81e6c34d296 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 30 Mar 2026 16:13:18 +0200 Subject: [PATCH 09/35] Add boundary overlays (outline, separatrix, x-points, strike-points, geom axis) --- idstools/compute/equilibrium.py | 138 ++++++++++++++++++---- idstools/scripts/bin/plotequilibrium | 6 +- idstools/view/equilibrium.py | 168 ++++++++++++++++++++++++--- 3 files changed, 272 insertions(+), 40 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index 1a52f8d8..a9e4e423 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -301,39 +301,133 @@ def get_ip(self) -> list: for time_index in range(len(self.ids.time_slice)) ] - def get_separatrix(self, time_slice: int) -> Union[dict, None]: - """Return the closed separatrix outline for a given time slice. + def get_boundary_data(self, time_slice: int) -> dict: + """Return boundary and boundary_separatrix data for a given time slice. - Reads ``boundary.outline.r/z``, validates that the arrays are - non-empty and contain at least one finite, non-fill value, then - closes the polygon by appending the first point. + Reads: + + * ``boundary/outline/r|z`` + * ``boundary/type`` (0=limiter, 1=diverted) + * ``boundary/psi_norm`` + * ``boundary/geometric_axis/r|z`` + * ``boundary_separatrix/outline/r|z`` + * ``boundary_separatrix/x_point[i]/r|z`` + * ``boundary_separatrix/strike_point[i]/r|z`` Args: time_slice (int): Index into ``time_slice``. Returns: - dict with keys ``"r"`` and ``"z"`` (1-D ndarrays, polygon - closed), or ``None`` if the data are absent or invalid. + dict with keys: + + * ``"bnd_r"``, ``"bnd_z"`` boundary outline (closed), or ``None`` + * ``"bnd_type"`` int or ``None`` + * ``"bnd_psi_norm"`` float or ``None`` + * ``"bnd_geom_r"``, ``"bnd_geom_z"`` geometric axis scalars or ``None`` + * ``"sep_r"``, ``"sep_z"`` separatrix outline (closed), or ``None`` + * ``"sep_xpoints"`` list of (r, z) tuples + * ``"sep_strikepoints"`` list of (r, z) tuples """ + + _FILL = imas.ids_defs.EMPTY_FLOAT + + def _valid_arr(arr): + a = np.asarray(arr, dtype=float) + return a.size > 0 and np.any(np.isfinite(a) & (np.abs(a) < 1.0e20)) + + def _valid_scalar(val): + try: + v = float(val) + return np.isfinite(v) and abs(v) < 1.0e20 + except Exception: + return False + + def _clean(arr): + a = np.asarray(arr, dtype=float) + a[(~np.isfinite(a)) | (np.abs(a) >= 1.0e20)] = np.nan + return a + + def _read_outline(node): + try: + r = np.asarray(node.outline.r, dtype=float) + z = np.asarray(node.outline.z, dtype=float) + except Exception: + return None, None + if not (_valid_arr(r) and _valid_arr(z)): + return None, None + r, z = _clean(r), _clean(z) + return np.append(r, r[0]), np.append(z, z[0]) + + def _read_points(node, attr): + pts = [] + try: + arr = getattr(node, attr) + except AttributeError: + return pts + for pt in arr: + try: + r, z = float(pt.r), float(pt.z) + except Exception: + continue + if _valid_scalar(r) and _valid_scalar(z): + pts.append((r, z)) + return pts + + result = { + "bnd_r": None, + "bnd_z": None, + "bnd_type": None, + "bnd_psi_norm": None, + "bnd_geom_r": None, + "bnd_geom_z": None, + "sep_r": None, + "sep_z": None, + "sep_xpoints": [], + "sep_strikepoints": [], + } + try: - bnd = self.ids.time_slice[time_slice].boundary - r = np.asarray(bnd.outline.r, dtype=float) - z = np.asarray(bnd.outline.z, dtype=float) - except Exception as exc: - logger.debug(f"get_separatrix: could not read boundary.outline – {exc}") - return None + ts = self.ids.time_slice[time_slice] + except Exception: + return result - def _valid(arr): - return arr.size > 0 and np.any(np.isfinite(arr) & (np.abs(arr) < 1.0e20)) + # boundary + try: + bnd = ts.boundary + result["bnd_r"], result["bnd_z"] = _read_outline(bnd) - if not (_valid(r) and _valid(z)): - logger.debug("get_separatrix: boundary.outline contains no valid data") - return None + bnd_type = int(bnd.type) + if _valid_scalar(bnd_type): + result["bnd_type"] = bnd_type + except Exception: + pass - # close the polygon - r = np.append(r, r[0]) - z = np.append(z, z[0]) - return {"r": r, "z": z} + try: + psi_norm = float(ts.boundary.psi_norm) + if _valid_scalar(psi_norm): + result["bnd_psi_norm"] = psi_norm + except Exception: + pass + + try: + gax_r = float(ts.boundary.geometric_axis.r) + gax_z = float(ts.boundary.geometric_axis.z) + if _valid_scalar(gax_r) and _valid_scalar(gax_z): + result["bnd_geom_r"] = gax_r + result["bnd_geom_z"] = gax_z + except Exception: + pass + + # boundary_separatrix + try: + sep = ts.boundary_separatrix + result["sep_r"], result["sep_z"] = _read_outline(sep) + result["sep_xpoints"] = _read_points(sep, "x_point") + result["sep_strikepoints"] = _read_points(sep, "strike_point") + except Exception: + pass + + return result def get_magnetic_axis(self, time_slice: int) -> Union[dict, None]: """Return the magnetic axis position for a given time slice. diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 1e3a90d9..bc8ad95e 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -114,7 +114,7 @@ if __name__ == "__main__": ids_obj_equilibrium = connection.get("equilibrium", occurrence=occurrence, autoconvert=False) ids_obj_equilibrium = imas.convert_ids(ids_obj_equilibrium, connection.factory.version) else: - ids_obj_equilibrium = connection.get("equilibrium", occurrence=occurrence, lazy=True, autoconvert=False) + ids_obj_equilibrium = connection.get("equilibrium", occurrence=occurrence, autoconvert=False) if ids_obj_equilibrium.time is not None: time_slice, time_value = get_nearest_time(ids_obj_equilibrium.time, args.time) @@ -124,9 +124,7 @@ if __name__ == "__main__": database_text = "" if args.plots: compute_obj = EquilibriumCompute(ids_obj_equilibrium) - profiles_1d_quantities = compute_obj.get_profiles_1d_quantities( - time_slice, ["pressure", "q", "beta_pol"] - ) + profiles_1d_quantities = compute_obj.get_profiles_1d_quantities(time_slice, ["pressure", "q", "beta_pol"]) p1dcounter = sum(1 for value in profiles_1d_quantities.values() if value.has_value) global_quantities = compute_obj.get_global_quantities( diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 63202170..62cdff77 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -41,9 +41,9 @@ def view_magnetic_poloidal_flux( ax: plt.axes, time_slice: int, profiles2d_index: int = 0, - plot_separatrix: bool = True, plot_magnetic_axis: bool = True, plot_current_centre: bool = True, + plot_boundary_data: bool = True, ): """ This function plots the magnetic poloidal flux contours on a 2D Cartesian grid. @@ -131,23 +131,17 @@ def view_magnetic_poloidal_flux( proxy_rho = ProxyLine([0], [0], color="darkorange", linewidth=1.5, label="\u03c1 contours", alpha=0.3) overlay_entries.append((proxy_rho, _rho_collections)) - if plot_separatrix: - sep = self.compute_obj.get_separatrix(time_slice) - if sep is not None: - (line,) = ax.plot(sep["r"], sep["z"], color="red", linewidth=2.0, linestyle="--", zorder=5) - proxy_sep = ProxyLine([0], [0], color="red", linewidth=2.0, linestyle="--", label="separatrix") - overlay_entries.append((proxy_sep, [line])) - if plot_magnetic_axis: mag_ax = self.compute_obj.get_magnetic_axis(time_slice) if mag_ax is not None: (marker,) = ax.plot( mag_ax["r"], mag_ax["z"], - marker="x", + marker="o", color="saddlebrown", - markersize=6, - markeredgewidth=1.5, + markerfacecolor="saddlebrown", + markeredgecolor="saddlebrown", + markersize=7, linestyle="None", zorder=6, ) @@ -155,9 +149,10 @@ def view_magnetic_poloidal_flux( [0], [0], color="saddlebrown", - marker="x", - markersize=6, - markeredgewidth=1.5, + marker="o", + markerfacecolor="saddlebrown", + markeredgecolor="saddlebrown", + markersize=7, linestyle="None", label="magnetic axis", ) @@ -188,6 +183,151 @@ def view_magnetic_poloidal_flux( ) overlay_entries.append((proxy_cc, [marker])) + if plot_boundary_data: + + bd = self.compute_obj.get_boundary_data(time_slice) + + # boundary outline + if bd["bnd_r"] is not None and bd["bnd_z"] is not None: + bnd_type_str = {0: "limiter", 1: "diverted"}.get(bd["bnd_type"], "") + psi_label = f" (\u03c8_n={bd['bnd_psi_norm']:.4f})" if bd["bnd_psi_norm"] is not None else "" + bnd_label = f"boundary{psi_label}" + (f" [{bnd_type_str}]" if bnd_type_str else "") + (bnd_line,) = ax.plot( + bd["bnd_r"], + bd["bnd_z"], + color="#1f77b4", + linewidth=2.0, + linestyle="-", + zorder=4, + ) + proxy_bnd = ProxyLine([0], [0], color="#1f77b4", linewidth=2.0, linestyle="-", label=bnd_label) + overlay_entries.append((proxy_bnd, [bnd_line])) + + # boundary_separatrix outline + if bd["sep_r"] is not None and bd["sep_z"] is not None: + (sep_line,) = ax.plot( + bd["sep_r"], + bd["sep_z"], + color="#d62728", + linewidth=2.0, + linestyle="--", + zorder=4, + ) + proxy_sep_bnd = ProxyLine( + [0], [0], color="#d62728", linewidth=2.0, linestyle="--", label="boundary_separatrix" + ) + overlay_entries.append((proxy_sep_bnd, [sep_line])) + + # geometric axis + if bd["bnd_geom_r"] is not None and bd["bnd_geom_z"] is not None: + (gax_marker,) = ax.plot( + bd["bnd_geom_r"], + bd["bnd_geom_z"], + marker="D", + color="cyan", + markersize=7, + markeredgecolor="black", + markeredgewidth=0.8, + linestyle="None", + zorder=6, + ) + proxy_gax = ProxyLine( + [0], + [0], + color="cyan", + marker="D", + markersize=7, + linestyle="None", + label=f"geom. axis (R={bd['bnd_geom_r']:.3f}, Z={bd['bnd_geom_z']:.3f} m)", + ) + overlay_entries.append((proxy_gax, [gax_marker])) + + # x-points (boundary_separatrix) + _xp_groups = [ + (bd["sep_xpoints"], "darkgreen", "x_point (sep)"), + ] + for xp_list, xp_color, xp_label in _xp_groups: + _xp_artists = [] + for xp_idx, (xr, xz) in enumerate(xp_list): + (mk,) = ax.plot( + xr, + xz, + marker="x", + color=xp_color, + markersize=7, + markeredgewidth=2, + linestyle="None", + zorder=7, + ) + ann = ax.annotate( + f"X{xp_idx}", + xy=(xr, xz), + xytext=(-6, 6), + textcoords="offset points", + fontsize=8, + ha="right", + color=xp_color, + fontweight="bold", + zorder=8, + ) + _xp_artists.append(mk) + _xp_artists.append(ann) + if _xp_artists: + proxy_xp = ProxyLine( + [0], + [0], + color=xp_color, + marker="x", + markersize=7, + markeredgewidth=2, + linestyle="None", + label=xp_label, + ) + overlay_entries.append((proxy_xp, _xp_artists)) + + # strike-points (boundary_separatrix) + _sp_groups = [ + (bd["sep_strikepoints"], "darkorange", "strike_point (sep)"), + ] + for sp_list, sp_color, sp_label in _sp_groups: + _sp_artists = [] + for sp_idx, (sr, sz) in enumerate(sp_list): + (mk,) = ax.plot( + sr, + sz, + marker="+", + color=sp_color, + markersize=7, + markeredgewidth=2.0, + linestyle="None", + zorder=7, + ) + ann = ax.annotate( + f"S{sp_idx}", + xy=(sr, sz), + xytext=(-6, 6), + textcoords="offset points", + fontsize=8, + ha="right", + color=sp_color, + fontweight="bold", + zorder=8, + ) + _sp_artists.append(mk) + _sp_artists.append(ann) + if _sp_artists: + proxy_sp = ProxyLine( + [0], + [0], + color=sp_color, + marker="+", + markersize=7, + markeredgewidth=2.0, + linestyle="None", + label=sp_label, + ) + overlay_entries.append((proxy_sp, _sp_artists)) + self.view_global_quantities_annotation(ax, time_slice) # --- clickable legend From 09ea0b84323eb0a19e9872c698a879eafc68de89 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 30 Mar 2026 18:01:42 +0200 Subject: [PATCH 10/35] do not join strike points and rename labels --- idstools/compute/equilibrium.py | 10 +++++++++- idstools/view/equilibrium.py | 11 ++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index a9e4e423..2d8c989b 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -356,7 +356,15 @@ def _read_outline(node): if not (_valid_arr(r) and _valid_arr(z)): return None, None r, z = _clean(r), _clean(z) - return np.append(r, r[0]), np.append(z, z[0]) + # Insert NaN at large jumps so disconnected arcs are not joined + dist = np.sqrt(np.diff(r) ** 2 + np.diff(z) ** 2) + median_dist = np.nanmedian(dist) + if median_dist > 0: + breaks = np.where(dist > 20.0 * median_dist)[0] + 1 + if len(breaks): + r = np.insert(r, breaks, np.nan) + z = np.insert(z, breaks, np.nan) + return r, z def _read_points(node, attr): pts = [] diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 62cdff77..e8434865 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -189,9 +189,8 @@ def view_magnetic_poloidal_flux( # boundary outline if bd["bnd_r"] is not None and bd["bnd_z"] is not None: - bnd_type_str = {0: "limiter", 1: "diverted"}.get(bd["bnd_type"], "") psi_label = f" (\u03c8_n={bd['bnd_psi_norm']:.4f})" if bd["bnd_psi_norm"] is not None else "" - bnd_label = f"boundary{psi_label}" + (f" [{bnd_type_str}]" if bnd_type_str else "") + bnd_label = f"boundary{psi_label}" (bnd_line,) = ax.plot( bd["bnd_r"], bd["bnd_z"], @@ -213,9 +212,7 @@ def view_magnetic_poloidal_flux( linestyle="--", zorder=4, ) - proxy_sep_bnd = ProxyLine( - [0], [0], color="#d62728", linewidth=2.0, linestyle="--", label="boundary_separatrix" - ) + proxy_sep_bnd = ProxyLine([0], [0], color="#d62728", linewidth=2.0, linestyle="--", label="separatrix") overlay_entries.append((proxy_sep_bnd, [sep_line])) # geometric axis @@ -244,7 +241,7 @@ def view_magnetic_poloidal_flux( # x-points (boundary_separatrix) _xp_groups = [ - (bd["sep_xpoints"], "darkgreen", "x_point (sep)"), + (bd["sep_xpoints"], "darkgreen", "x_point"), ] for xp_list, xp_color, xp_label in _xp_groups: _xp_artists = [] @@ -287,7 +284,7 @@ def view_magnetic_poloidal_flux( # strike-points (boundary_separatrix) _sp_groups = [ - (bd["sep_strikepoints"], "darkorange", "strike_point (sep)"), + (bd["sep_strikepoints"], "darkorange", "strike_point"), ] for sp_list, sp_color, sp_label in _sp_groups: _sp_artists = [] From 038177307ceed6830e011684b0b09077c7033aa5 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 30 Mar 2026 18:17:55 +0200 Subject: [PATCH 11/35] changed the marker for current center --- idstools/view/equilibrium.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index e8434865..52ab0c09 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -164,10 +164,10 @@ def view_magnetic_poloidal_flux( (marker,) = ax.plot( cc["r"], cc["z"], - marker="+", + marker="*", color="deeppink", - markersize=8, - markeredgewidth=2.0, + markersize=10, + markeredgewidth=1.0, linestyle="None", zorder=6, ) @@ -175,9 +175,9 @@ def view_magnetic_poloidal_flux( [0], [0], color="deeppink", - marker="+", - markersize=8, - markeredgewidth=2.0, + marker="*", + markersize=10, + markeredgewidth=1.0, linestyle="None", label="current centre", ) From dfa98ea44d7eedd3b9a5d42670537b3a67d97e13 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Tue, 31 Mar 2026 10:11:09 +0200 Subject: [PATCH 12/35] reverted rho option and use of IMAS constants --- idstools/compute/equilibrium.py | 19 ++++++++++--------- idstools/scripts/bin/plotequilibrium | 20 ++++++++------------ idstools/view/equilibrium.py | 26 ++++++++------------------ 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index 2d8c989b..de6411f8 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -16,6 +16,8 @@ from idstools.database import DBMaster +_IDS_VALID_THRESHOLD = abs(imas.ids_defs.EMPTY_FLOAT) + logger = logging.getLogger("module") @@ -329,22 +331,20 @@ def get_boundary_data(self, time_slice: int) -> dict: * ``"sep_strikepoints"`` list of (r, z) tuples """ - _FILL = imas.ids_defs.EMPTY_FLOAT - def _valid_arr(arr): a = np.asarray(arr, dtype=float) - return a.size > 0 and np.any(np.isfinite(a) & (np.abs(a) < 1.0e20)) + return a.size > 0 and np.any(np.isfinite(a) & (np.abs(a) < _IDS_VALID_THRESHOLD)) def _valid_scalar(val): try: v = float(val) - return np.isfinite(v) and abs(v) < 1.0e20 + return np.isfinite(v) and abs(v) < _IDS_VALID_THRESHOLD except Exception: return False def _clean(arr): a = np.asarray(arr, dtype=float) - a[(~np.isfinite(a)) | (np.abs(a) >= 1.0e20)] = np.nan + a[(~np.isfinite(a)) | (np.abs(a) >= _IDS_VALID_THRESHOLD)] = np.nan return a def _read_outline(node): @@ -459,7 +459,7 @@ def get_magnetic_axis(self, time_slice: int) -> Union[dict, None]: return None def _valid(val): - return np.isfinite(val) and abs(val) < 1.0e20 + return np.isfinite(val) and abs(val) < _IDS_VALID_THRESHOLD if not (_valid(r) and _valid(z)): logger.debug("get_magnetic_axis: magnetic_axis contains no valid data") @@ -489,7 +489,7 @@ def get_current_centre(self, time_slice: int) -> Union[dict, None]: return None def _valid(val): - return np.isfinite(val) and abs(val) < 1.0e20 + return np.isfinite(val) and abs(val) < _IDS_VALID_THRESHOLD if not (_valid(r) and _valid(z)): logger.debug("get_current_centre: current_centre contains no valid data") @@ -501,7 +501,8 @@ def get_scalar_annotation_quantities(self, time_slice: int) -> list: """Return validated scalar global/boundary quantities for annotation display. Reads a fixed set of scalar fields from ``global_quantities`` and - ``boundary``, validates each value (finite and < 1e20), and returns + ``boundary``, validates each value (finite and below the IDS fill + value threshold), and returns only those with valid data. Args: @@ -515,7 +516,7 @@ def get_scalar_annotation_quantities(self, time_slice: int) -> list: def _valid(val): try: v = float(val) - return np.isfinite(v) and abs(v) < 1.0e20 + return np.isfinite(v) and abs(v) < _IDS_VALID_THRESHOLD except Exception: return False diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index bc8ad95e..9e64ac4e 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -48,6 +48,11 @@ if __name__ == "__main__": formatter_class=RichHelpFormatter, ) parser.add_argument("-t", "--time", help="Time (default=middle)", type=float, default=-99.0) + parser.add_argument( + "--rho", + help="Show rho overlay on the plot", + action="store_true", + ) parser.add_argument( "-p", "--plots", @@ -185,7 +190,7 @@ if __name__ == "__main__": ids_data = get_md_data(mduris, args.dd_update) plot_machine_description(ax1, ids_data) - c_psi, c_rho, rho_collections = view_object.view_magnetic_poloidal_flux(ax1, time_slice) + c_psi, c_rho = view_object.view_magnetic_poloidal_flux(ax1, time_slice, plot_rho=args.rho) if c_psi: cax_psi = ax1.inset_axes([-0.20, 0.05, 0.05, 0.88]) # [x, y, w, h] in axes coords @@ -193,20 +198,11 @@ if __name__ == "__main__": cbar_psi.ax.set_title(r"$\psi$ [Wb]", fontsize=7, pad=4) cbar_psi.ax.tick_params(labelsize=7) - if c_rho and rho_collections: - cax_rho = ax1.inset_axes([-0.35, 0.05, 0.05, 0.88]) # just to the right of psi bar + if c_rho: + cax_rho = ax1.inset_axes([-0.35, 0.05, 0.05, 0.88]) # just to the left of psi bar cbar_rho = canvas.fig.colorbar(c_rho, cax=cax_rho) cbar_rho.ax.set_title(r"$\rho$", fontsize=7, pad=4) cbar_rho.ax.tick_params(labelsize=7) - cbar_rho.ax.set_visible(False) - - def _sync_rho_cbar(event): - visible = rho_collections[0].get_visible() - if cbar_rho.ax.get_visible() != visible: - cbar_rho.ax.set_visible(visible) - canvas.fig.canvas.draw_idle() - - canvas.fig.canvas.mpl_connect("pick_event", _sync_rho_cbar) ax1.set_title(title) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 52ab0c09..049b178a 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -44,6 +44,7 @@ def view_magnetic_poloidal_flux( plot_magnetic_axis: bool = True, plot_current_centre: bool = True, plot_boundary_data: bool = True, + plot_rho: bool = False, ): """ This function plots the magnetic poloidal flux contours on a 2D Cartesian grid. @@ -83,7 +84,6 @@ def view_magnetic_poloidal_flux( :meth:`plotIP` """ contour_lines_psi = contour_lines_rho = None - _rho_collections = [] cartestion_grid = self.compute_obj.get2d_cartesian_grid(time_slice, profiles2d_index) if cartestion_grid is not None: levels = 50 @@ -101,17 +101,12 @@ def view_magnetic_poloidal_flux( # ) # rho overlay - rho2d = self.compute_obj.get_rho2d(time_slice) - if rho2d is not None: - _before = set(ax.collections) - contour_lines_rho = ax.contour( - cartestion_grid["r2d"], cartestion_grid["z2d"], rho2d, levels=levels, cmap="YlOrBr" - ) - _rho_collections = [c for c in ax.collections if c not in _before] - for _c in _rho_collections: - _c.set_visible(False) - else: - _rho_collections = [] + if plot_rho: + rho2d = self.compute_obj.get_rho2d(time_slice) + if rho2d is not None: + contour_lines_rho = ax.contour( + cartestion_grid["r2d"], cartestion_grid["z2d"], rho2d, levels=levels, cmap="YlOrBr" + ) ax.set_aspect("equal", adjustable="box") ax.set_xlabel("$R$ [m]") @@ -126,11 +121,6 @@ def view_magnetic_poloidal_flux( _md_handles, _md_labels = [], [] overlay_entries = [] - # rho contour - if contour_lines_rho is not None and _rho_collections: - proxy_rho = ProxyLine([0], [0], color="darkorange", linewidth=1.5, label="\u03c1 contours", alpha=0.3) - overlay_entries.append((proxy_rho, _rho_collections)) - if plot_magnetic_axis: mag_ax = self.compute_obj.get_magnetic_axis(time_slice) if mag_ax is not None: @@ -379,7 +369,7 @@ def on_legend_click(event): ax.figure.canvas.mpl_connect("pick_event", on_legend_click) - return contour_lines_psi, contour_lines_rho, _rho_collections + return contour_lines_psi, contour_lines_rho def view_pulse_info(self, ax: plt.axes, title: str, hostdir: str, shot: int, run: int, t: float): self.database_info(ax, title, hostdir, shot, run, t) From e332fcbcf0ec7a24a23b5f2e649775bc40735bd7 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Tue, 31 Mar 2026 10:25:37 +0200 Subject: [PATCH 13/35] fixed formatting issue --- idstools/compute/equilibrium.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index de6411f8..ca492152 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -526,7 +526,7 @@ def _valid(val): bnd = ts.boundary _specs = [ - (lambda: float(gq.ip), lambda v: {"label": "$I_p$", "text": f"{v/1e6:.3f} MA"}), + (lambda: float(gq.ip), lambda v: {"label": "$I_p$", "text": f"{v / 1e6:.3f} MA"}), ( lambda: float( getattr( From 0ba5b0ec859af666e0bfedecea9d8355a0b19cbc Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Tue, 31 Mar 2026 17:09:36 +0200 Subject: [PATCH 14/35] contour_tree implementation DD4 --- idstools/compute/equilibrium.py | 90 ++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index ca492152..7ac6abdc 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -312,10 +312,21 @@ def get_boundary_data(self, time_slice: int) -> dict: * ``boundary/type`` (0=limiter, 1=diverted) * ``boundary/psi_norm`` * ``boundary/geometric_axis/r|z`` + + if boundary_separatrix is available: + * ``boundary_separatrix/outline/r|z`` * ``boundary_separatrix/x_point[i]/r|z`` * ``boundary_separatrix/strike_point[i]/r|z`` + if contour_tree is available: + + * ``contour_tree/node[i]/critical_type`` + * ``contour_tree/node[i]/r|z`` for X-points (``critical_type == 1``) + * ``contour_tree/node[i]/levelset/r|z`` for separatrix outline + * ``constraints/strike_point[i]/position_reconstructed/r|z`` for strike points + (fallback to ``position_measured/r|z`` when needed) + Args: time_slice (int): Index into ``time_slice``. @@ -381,6 +392,55 @@ def _read_points(node, attr): pts.append((r, z)) return pts + def _read_contour_tree(ts_node): + """Read separatrix/X-point data from ``time_slice.contour_tree.node``. + + * ``node.critical_type == 1`` for X-points (saddle points) + * first valid X-point ``node.levelset.r/z`` as separatrix contour + """ + sep_r = sep_z = None + xpoints = [] + + try: + nodes = ts_node.contour_tree.node + except Exception: + return sep_r, sep_z, xpoints + + for node in nodes: + try: + critical_type = int(node.critical_type) + except Exception: + continue + + if critical_type != 1: # 1 = saddle / X-point + continue + + try: + xr = float(node.r) + xz = float(node.z) + except Exception: + xr = xz = None + + if xr is not None and _valid_scalar(xr) and xz is not None and _valid_scalar(xz): + xpoints.append((xr, xz)) + + if sep_r is not None and sep_z is not None: + continue + + try: + r = np.asarray(node.levelset.r, dtype=float) + z = np.asarray(node.levelset.z, dtype=float) + except Exception: + continue + + if not (_valid_arr(r) and _valid_arr(z)): + continue + + sep_r = _clean(r) + sep_z = _clean(z) + + return sep_r, sep_z, xpoints + result = { "bnd_r": None, "bnd_z": None, @@ -426,14 +486,30 @@ def _read_points(node, attr): except Exception: pass - # boundary_separatrix - try: + # boundary_separatrix (DD3 ) + if hasattr(ts, "boundary_separatrix"): sep = ts.boundary_separatrix - result["sep_r"], result["sep_z"] = _read_outline(sep) - result["sep_xpoints"] = _read_points(sep, "x_point") - result["sep_strikepoints"] = _read_points(sep, "strike_point") - except Exception: - pass + try: + result["sep_r"], result["sep_z"] = _read_outline(sep) + result["sep_xpoints"] = _read_points(sep, "x_point") + result["sep_strikepoints"] = _read_points(sep, "strike_point") + except Exception: + pass + + # contour_tree.node (DD4) + if hasattr(ts, "contour_tree") and hasattr(ts.contour_tree, "node"): + contour_sep_r, contour_sep_z, contour_xpoints = _read_contour_tree(ts) + + if ( + (result["sep_r"] is None or result["sep_z"] is None) + and contour_sep_r is not None + and contour_sep_z is not None + ): + result["sep_r"] = contour_sep_r + result["sep_z"] = contour_sep_z + + if not result["sep_xpoints"] and contour_xpoints: + result["sep_xpoints"] = contour_xpoints return result From 26bd780fe38d61601ed083c53f5e0e93c0c94002 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Tue, 31 Mar 2026 17:16:57 +0200 Subject: [PATCH 15/35] removed boundary and made separatrix black --- idstools/view/equilibrium.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 049b178a..1bf5b23b 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -177,32 +177,17 @@ def view_magnetic_poloidal_flux( bd = self.compute_obj.get_boundary_data(time_slice) - # boundary outline - if bd["bnd_r"] is not None and bd["bnd_z"] is not None: - psi_label = f" (\u03c8_n={bd['bnd_psi_norm']:.4f})" if bd["bnd_psi_norm"] is not None else "" - bnd_label = f"boundary{psi_label}" - (bnd_line,) = ax.plot( - bd["bnd_r"], - bd["bnd_z"], - color="#1f77b4", - linewidth=2.0, - linestyle="-", - zorder=4, - ) - proxy_bnd = ProxyLine([0], [0], color="#1f77b4", linewidth=2.0, linestyle="-", label=bnd_label) - overlay_entries.append((proxy_bnd, [bnd_line])) - # boundary_separatrix outline if bd["sep_r"] is not None and bd["sep_z"] is not None: (sep_line,) = ax.plot( bd["sep_r"], bd["sep_z"], - color="#d62728", + color="#000000", linewidth=2.0, linestyle="--", zorder=4, ) - proxy_sep_bnd = ProxyLine([0], [0], color="#d62728", linewidth=2.0, linestyle="--", label="separatrix") + proxy_sep_bnd = ProxyLine([0], [0], color="#000000", linewidth=2.0, linestyle="--", label="separatrix") overlay_entries.append((proxy_sep_bnd, [sep_line])) # geometric axis @@ -231,7 +216,7 @@ def view_magnetic_poloidal_flux( # x-points (boundary_separatrix) _xp_groups = [ - (bd["sep_xpoints"], "darkgreen", "x_point"), + (bd["sep_xpoints"], "red", "x_point"), ] for xp_list, xp_color, xp_label in _xp_groups: _xp_artists = [] From 58b63529b95481aa150137f549cd0d4cc0c53df2 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 11 Jun 2026 12:51:42 +0200 Subject: [PATCH 16/35] added lazy=True and fixed bug in reading data --- idstools/compute/equilibrium.py | 96 ++++++++++++++++++++++------ idstools/scripts/bin/plotequilibrium | 10 ++- 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index 82d667e9..3d31003c 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -334,11 +334,12 @@ def _valid_scalar(val): try: v = float(val) return np.isfinite(v) and abs(v) < _IDS_VALID_THRESHOLD - except Exception: + except Exception as exc: + logger.debug(f"get_boundary_data: invalid scalar {val!r} ({exc})") return False def _clean(arr): - a = np.asarray(arr, dtype=float) + a = np.array(arr, dtype=float, copy=True) a[(~np.isfinite(a)) | (np.abs(a) >= _IDS_VALID_THRESHOLD)] = np.nan return a @@ -346,9 +347,11 @@ def _read_outline(node): try: r = np.asarray(node.outline.r, dtype=float) z = np.asarray(node.outline.z, dtype=float) - except Exception: + except Exception as exc: + logger.debug(f"get_boundary_data: could not read outline from {node!r}: {exc}") return None, None if not (_valid_arr(r) and _valid_arr(z)): + logger.debug("get_boundary_data: outline has no valid data " f"(r.size={r.size}, z.size={z.size})") return None, None r, z = _clean(r), _clean(z) # Insert NaN at large jumps so disconnected arcs are not joined @@ -366,14 +369,27 @@ def _read_points(node, attr): try: arr = getattr(node, attr) except AttributeError: + logger.debug(f"get_boundary_data: {attr} is not available on {node!r}") + return pts + except Exception as exc: + logger.debug(f"get_boundary_data: could not access {attr} on {node!r}: {exc}") return pts - for pt in arr: + try: + n_points = len(arr) + except Exception as exc: + logger.debug(f"get_boundary_data: could not get length of {attr}: {exc}") + n_points = None + for pt_index, pt in enumerate(arr): try: r, z = float(pt.r), float(pt.z) - except Exception: + except Exception as exc: + logger.debug(f"get_boundary_data: could not read {attr}[{pt_index}].r/z: {exc}") continue if _valid_scalar(r) and _valid_scalar(z): pts.append((r, z)) + else: + logger.debug(f"get_boundary_data: {attr}[{pt_index}] contains invalid r/z ({r}, {z})") + logger.debug(f"get_boundary_data: read {len(pts)} valid {attr} points out of {n_points}") return pts def _read_contour_tree(ts_node): @@ -387,26 +403,43 @@ def _read_contour_tree(ts_node): try: nodes = ts_node.contour_tree.node - except Exception: + except Exception as exc: + logger.debug(f"get_boundary_data: could not access contour_tree.node: {exc}") return sep_r, sep_z, xpoints - for node in nodes: + try: + n_nodes = len(nodes) + except Exception as exc: + logger.debug(f"get_boundary_data: could not get length of contour_tree.node: {exc}") + n_nodes = None + + n_saddles = 0 + for node_index, node in enumerate(nodes): try: critical_type = int(node.critical_type) - except Exception: + except Exception as exc: + logger.debug( + f"get_boundary_data: could not read contour_tree.node[{node_index}].critical_type: {exc}" + ) continue if critical_type != 1: # 1 = saddle / X-point continue + n_saddles += 1 try: xr = float(node.r) xz = float(node.z) - except Exception: + except Exception as exc: + logger.debug(f"get_boundary_data: could not read contour_tree.node[{node_index}].r/z: {exc}") xr = xz = None if xr is not None and _valid_scalar(xr) and xz is not None and _valid_scalar(xz): xpoints.append((xr, xz)) + else: + logger.debug( + f"get_boundary_data: contour_tree.node[{node_index}] saddle has invalid r/z " f"({xr}, {xz})" + ) if sep_r is not None and sep_z is not None: continue @@ -414,15 +447,27 @@ def _read_contour_tree(ts_node): try: r = np.asarray(node.levelset.r, dtype=float) z = np.asarray(node.levelset.z, dtype=float) - except Exception: + except Exception as exc: + logger.debug( + f"get_boundary_data: could not read contour_tree.node[{node_index}].levelset.r/z: {exc}" + ) continue if not (_valid_arr(r) and _valid_arr(z)): + logger.debug( + f"get_boundary_data: contour_tree.node[{node_index}].levelset has no valid data " + f"(r.size={r.size}, z.size={z.size})" + ) continue sep_r = _clean(r) sep_z = _clean(z) + logger.debug( + "get_boundary_data: contour_tree summary " + f"(nodes={n_nodes}, saddles={n_saddles}, xpoints={len(xpoints)}, " + f"has_separatrix={sep_r is not None and sep_z is not None})" + ) return sep_r, sep_z, xpoints result = { @@ -440,7 +485,8 @@ def _read_contour_tree(ts_node): try: ts = self.ids.time_slice[time_slice] - except Exception: + except Exception as exc: + logger.debug(f"get_boundary_data: could not access time_slice[{time_slice}]: {exc}") return result # boundary @@ -451,15 +497,15 @@ def _read_contour_tree(ts_node): bnd_type = int(bnd.type) if _valid_scalar(bnd_type): result["bnd_type"] = bnd_type - except Exception: - pass + except Exception as exc: + logger.debug(f"get_boundary_data: could not read boundary data: {exc}") try: psi_norm = float(ts.boundary.psi_norm) if _valid_scalar(psi_norm): result["bnd_psi_norm"] = psi_norm - except Exception: - pass + except Exception as exc: + logger.debug(f"get_boundary_data: could not read boundary.psi_norm: {exc}") try: gax_r = float(ts.boundary.geometric_axis.r) @@ -467,8 +513,8 @@ def _read_contour_tree(ts_node): if _valid_scalar(gax_r) and _valid_scalar(gax_z): result["bnd_geom_r"] = gax_r result["bnd_geom_z"] = gax_z - except Exception: - pass + except Exception as exc: + logger.debug(f"get_boundary_data: could not read boundary.geometric_axis.r/z: {exc}") # boundary_separatrix (DD3 ) if hasattr(ts, "boundary_separatrix"): @@ -477,8 +523,13 @@ def _read_contour_tree(ts_node): result["sep_r"], result["sep_z"] = _read_outline(sep) result["sep_xpoints"] = _read_points(sep, "x_point") result["sep_strikepoints"] = _read_points(sep, "strike_point") - except Exception: - pass + logger.debug( + "get_boundary_data: boundary_separatrix summary " + f"(has_outline={result['sep_r'] is not None and result['sep_z'] is not None}, " + f"xpoints={len(result['sep_xpoints'])}, strikepoints={len(result['sep_strikepoints'])})" + ) + except Exception as exc: + logger.debug(f"get_boundary_data: could not read boundary_separatrix data: {exc}") # contour_tree.node (DD4) if hasattr(ts, "contour_tree") and hasattr(ts.contour_tree, "node"): @@ -495,6 +546,13 @@ def _read_contour_tree(ts_node): if not result["sep_xpoints"] and contour_xpoints: result["sep_xpoints"] = contour_xpoints + logger.debug( + "get_boundary_data: final summary " + f"(has_boundary={result['bnd_r'] is not None and result['bnd_z'] is not None}, " + f"has_separatrix={result['sep_r'] is not None and result['sep_z'] is not None}, " + f"xpoints={len(result['sep_xpoints'])}, strikepoints={len(result['sep_strikepoints'])})" + ) + return result def get_magnetic_axis(self, time_slice: int) -> Union[dict, None]: diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 9e64ac4e..1eec67c7 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -5,7 +5,6 @@ # "imas:mdsplus?user=public;shot=111001;run=102;database=ITER_MD;version=3#pf_active" import argparse -import copy import logging import os @@ -79,6 +78,11 @@ if __name__ == "__main__": help="Show labels", action="store_true", ) + parser.add_argument( + "--debug", + help="Show diagnostic logging", + action="store_true", + ) parser.add_argument( "--save", help="Save figure at default location", @@ -104,7 +108,7 @@ if __name__ == "__main__": splitted_ids_path_fragment = ids_path_fragment.split("/", 1) occurrence = int(splitted_ids_path_fragment[0]) - logger = setup_logger("module", stdout_level=logging.INFO) + logger = setup_logger("module", stdout_level=logging.DEBUG if args.debug else logging.INFO) connection = DBMaster.get_connection(args) if connection is None: @@ -119,7 +123,7 @@ if __name__ == "__main__": ids_obj_equilibrium = connection.get("equilibrium", occurrence=occurrence, autoconvert=False) ids_obj_equilibrium = imas.convert_ids(ids_obj_equilibrium, connection.factory.version) else: - ids_obj_equilibrium = connection.get("equilibrium", occurrence=occurrence, autoconvert=False) + ids_obj_equilibrium = connection.get("equilibrium", occurrence=occurrence, lazy=True, autoconvert=False) if ids_obj_equilibrium.time is not None: time_slice, time_value = get_nearest_time(ids_obj_equilibrium.time, args.time) From 094c40fad9079197985454aa83cda6f435377a29 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 11 Jun 2026 13:47:04 +0200 Subject: [PATCH 17/35] magnetic_axis +, current center + , geometric axis x and separatrix with reddish color --- idstools/view/equilibrium.py | 48 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index db0257fc..60ae43dd 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -135,11 +135,10 @@ def view_magnetic_poloidal_flux( (marker,) = ax.plot( mag_ax["r"], mag_ax["z"], - marker="o", + marker="+", color="saddlebrown", - markerfacecolor="saddlebrown", - markeredgecolor="saddlebrown", - markersize=7, + markersize=6, + markeredgewidth=1.4, linestyle="None", zorder=6, ) @@ -147,10 +146,9 @@ def view_magnetic_poloidal_flux( [0], [0], color="saddlebrown", - marker="o", - markerfacecolor="saddlebrown", - markeredgecolor="saddlebrown", - markersize=7, + marker="+", + markersize=6, + markeredgewidth=1.4, linestyle="None", label="magnetic axis", ) @@ -162,10 +160,10 @@ def view_magnetic_poloidal_flux( (marker,) = ax.plot( cc["r"], cc["z"], - marker="*", + marker="+", color="deeppink", - markersize=10, - markeredgewidth=1.0, + markersize=6, + markeredgewidth=1.4, linestyle="None", zorder=6, ) @@ -173,9 +171,9 @@ def view_magnetic_poloidal_flux( [0], [0], color="deeppink", - marker="*", - markersize=10, - markeredgewidth=1.0, + marker="+", + markersize=6, + markeredgewidth=1.4, linestyle="None", label="current centre", ) @@ -190,12 +188,14 @@ def view_magnetic_poloidal_flux( (sep_line,) = ax.plot( bd["sep_r"], bd["sep_z"], - color="#000000", + color="firebrick", linewidth=2.0, linestyle="--", zorder=4, ) - proxy_sep_bnd = ProxyLine([0], [0], color="#000000", linewidth=2.0, linestyle="--", label="separatrix") + proxy_sep_bnd = ProxyLine( + [0], [0], color="firebrick", linewidth=2.0, linestyle="--", label="separatrix" + ) overlay_entries.append((proxy_sep_bnd, [sep_line])) # geometric axis @@ -203,20 +203,20 @@ def view_magnetic_poloidal_flux( (gax_marker,) = ax.plot( bd["bnd_geom_r"], bd["bnd_geom_z"], - marker="D", - color="cyan", - markersize=7, - markeredgecolor="black", - markeredgewidth=0.8, + marker="x", + color="darkcyan", + markersize=6, + markeredgewidth=1.4, linestyle="None", zorder=6, ) proxy_gax = ProxyLine( [0], [0], - color="cyan", - marker="D", - markersize=7, + color="darkcyan", + marker="x", + markersize=6, + markeredgewidth=1.4, linestyle="None", label=f"geom. axis (R={bd['bnd_geom_r']:.3f}, Z={bd['bnd_geom_z']:.3f} m)", ) From 0cd9c11c115f9f7c9cc8f6303bace11fce01a2aa Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 11 Jun 2026 14:21:48 +0200 Subject: [PATCH 18/35] for DD3 read values from boundary x_point and strike_point --- idstools/compute/equilibrium.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index 3d31003c..2d6e8db6 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -493,6 +493,13 @@ def _read_contour_tree(ts_node): try: bnd = ts.boundary result["bnd_r"], result["bnd_z"] = _read_outline(bnd) + result["sep_xpoints"] = _read_points(bnd, "x_point") + result["sep_strikepoints"] = _read_points(bnd, "strike_point") + logger.debug( + "get_boundary_data: boundary summary " + f"(has_outline={result['bnd_r'] is not None and result['bnd_z'] is not None}, " + f"xpoints={len(result['sep_xpoints'])}, strikepoints={len(result['sep_strikepoints'])})" + ) bnd_type = int(bnd.type) if _valid_scalar(bnd_type): @@ -521,12 +528,16 @@ def _read_contour_tree(ts_node): sep = ts.boundary_separatrix try: result["sep_r"], result["sep_z"] = _read_outline(sep) - result["sep_xpoints"] = _read_points(sep, "x_point") - result["sep_strikepoints"] = _read_points(sep, "strike_point") + sep_xpoints = _read_points(sep, "x_point") + sep_strikepoints = _read_points(sep, "strike_point") + if sep_xpoints: + result["sep_xpoints"] = sep_xpoints + if sep_strikepoints: + result["sep_strikepoints"] = sep_strikepoints logger.debug( "get_boundary_data: boundary_separatrix summary " f"(has_outline={result['sep_r'] is not None and result['sep_z'] is not None}, " - f"xpoints={len(result['sep_xpoints'])}, strikepoints={len(result['sep_strikepoints'])})" + f"xpoints={len(sep_xpoints)}, strikepoints={len(sep_strikepoints)})" ) except Exception as exc: logger.debug(f"get_boundary_data: could not read boundary_separatrix data: {exc}") From e46a8f2108134994df657a79c70c67dc5a6f3c53 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 12 Jun 2026 08:56:49 +0200 Subject: [PATCH 19/35] plot rho2d with transpose --- idstools/view/equilibrium.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 60ae43dd..83700609 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -113,7 +113,7 @@ def view_magnetic_poloidal_flux( rho2d = self.compute_obj.get_rho2d(time_slice) if rho2d is not None: contour_lines_rho = ax.contour( - cartestion_grid["r2d"], cartestion_grid["z2d"], rho2d, levels=levels, cmap="YlOrBr" + cartestion_grid["r2d"], cartestion_grid["z2d"], rho2d.T, levels=levels, cmap="YlOrBr" ) ax.set_aspect("equal", adjustable="box") From 9c650a2736eed86bc003542411583d4fc895453d Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 12 Jun 2026 09:48:21 +0200 Subject: [PATCH 20/35] show URI on top left corner --- idstools/scripts/bin/plotequilibrium | 15 ++++----------- idstools/view/domain/mdplot.py | 13 ++----------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 1eec67c7..60425845 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -210,17 +210,10 @@ if __name__ == "__main__": ax1.set_title(title) - xmin, xmax = ax1.get_xlim() - ymin, ymax = ax1.get_ylim() - ax1.text( - xmax + 0.01 * abs(xmax), - ymin + 0.5 * abs(ymax - ymin), - f"{get_database_path(args, time_value=time_value)}\n{database_text}", - horizontalalignment="left", - verticalalignment="center", - rotation="vertical", - fontsize=7, - ) + database_label = get_database_path(args, time_value=time_value) + if database_text: + database_label += f"\n{database_text}" + canvas.set_text(y=0.985, text=database_label, fontsize=6) if args.plots: view_object.plot_profiles_1d_quantities(axes_list1, time_slice) diff --git a/idstools/view/domain/mdplot.py b/idstools/view/domain/mdplot.py index e93fc1ed..77feaf01 100644 --- a/idstools/view/domain/mdplot.py +++ b/idstools/view/domain/mdplot.py @@ -151,14 +151,5 @@ def plot_machine_description(ax, ids_data): # ax.callbacks.connect("ylim_changed", update_labels) ax.plot() - xmin, xmax = ax.get_xlim() - ymin, ymax = ax.get_ylim() - ax.text( - xmax + 0.01 * abs(xmax), - ymin + 0.5 * abs(ymax - ymin), - f"{database_path}", - horizontalalignment="left", - verticalalignment="center", - rotation="vertical", - fontsize=7, - ) + if database_path: + ax.figure.text(0.001, 0.965, database_path, ha="left", va="top", fontsize=6) From 863e09b1e88645d01e889eafdf8756c483c36151 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 12 Jun 2026 10:46:54 +0200 Subject: [PATCH 21/35] Do not show quantities at the start and make it visible/invisible based on legend --- idstools/scripts/bin/plotequilibrium | 2 +- idstools/view/domain/mdplot.py | 2 +- idstools/view/equilibrium.py | 28 +++++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 60425845..1e55f84d 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -213,7 +213,7 @@ if __name__ == "__main__": database_label = get_database_path(args, time_value=time_value) if database_text: database_label += f"\n{database_text}" - canvas.set_text(y=0.985, text=database_label, fontsize=6) + canvas.set_text(y=0.985, text=database_label, fontsize=7) if args.plots: view_object.plot_profiles_1d_quantities(axes_list1, time_slice) diff --git a/idstools/view/domain/mdplot.py b/idstools/view/domain/mdplot.py index 77feaf01..4997be39 100644 --- a/idstools/view/domain/mdplot.py +++ b/idstools/view/domain/mdplot.py @@ -152,4 +152,4 @@ def plot_machine_description(ax, ids_data): ax.plot() if database_path: - ax.figure.text(0.001, 0.965, database_path, ha="left", va="top", fontsize=6) + ax.figure.text(0.001, 0.965, database_path, ha="left", va="top", fontsize=7) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 83700609..18697eff 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -308,7 +308,20 @@ def view_magnetic_poloidal_flux( ) overlay_entries.append((proxy_sp, _sp_artists)) - self.view_global_quantities_annotation(ax, time_slice) + quantity_annotation = self.view_global_quantities_annotation(ax, time_slice) + if quantity_annotation is not None: + quantity_annotation.set_visible(False) + proxy_quantities = ProxyLine( + [0], + [0], + color="steelblue", + marker="*", + markerfacecolor="white", + markersize=7, + linestyle="None", + label="quantities", + ) + overlay_entries.append((proxy_quantities, [quantity_annotation])) # --- clickable legend if overlay_entries or _md_handles: @@ -334,18 +347,25 @@ def view_magnetic_poloidal_flux( text.set_ha("center") leg_map = {} + legend_texts = legend.get_texts() n_md = len(_md_handles) for i, orig_artist in enumerate(_md_handles): leg_h = legend.legend_handles[i] leg_h.set_picker(8) leg_map[leg_h] = [orig_artist] + legend_texts[i].set_picker(True) + leg_map[legend_texts[i]] = [orig_artist] for i, (_, artists) in enumerate(overlay_entries): leg_h = legend.legend_handles[n_md + i] + leg_text = legend_texts[n_md + i] leg_h.set_picker(8) leg_map[leg_h] = artists + leg_text.set_picker(True) + leg_map[leg_text] = artists if artists and not artists[0].get_visible(): leg_h.set_alpha(0.3) + leg_text.set_alpha(0.3) def on_legend_click(event): legline = event.artist @@ -358,6 +378,12 @@ def on_legend_click(event): for a in artists: a.set_visible(visible) legline.set_alpha(1.0 if visible else 0.3) + if legline in legend.legend_handles: + leg_index = legend.legend_handles.index(legline) + legend_texts[leg_index].set_alpha(1.0 if visible else 0.3) + elif legline in legend_texts: + leg_index = legend_texts.index(legline) + legend.legend_handles[leg_index].set_alpha(1.0 if visible else 0.3) ax.figure.canvas.draw_idle() ax.figure.canvas.mpl_connect("pick_event", on_legend_click) From d3246f9666ac3d8ec319e61758dcf8941308164d Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 12 Jun 2026 12:03:35 +0200 Subject: [PATCH 22/35] add buttons to clear overlay show legends inside to save space and based on user requiremenst --- idstools/scripts/bin/plotequilibrium | 74 +++++++++++++++++++++++++--- idstools/view/equilibrium.py | 35 ++++++++----- 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 1e55f84d..8a7a87d9 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -29,6 +29,7 @@ from idstools.utils.idslogger import setup_logger from idstools.view.common import PlotCanvas from idstools.view.domain.mdplot import plot_machine_description from idstools.view.equilibrium import EquilibriumView +from matplotlib.widgets import Button class MdAction(argparse.Action): @@ -149,12 +150,12 @@ if __name__ == "__main__": col_size = col_size + 1 - canvas = PlotCanvas(2, col_size + 1) + canvas = PlotCanvas(2, col_size) ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0, rowspan=2) axes_list1 = [] axes_list2 = [] plotting_counter = 0 - for col in range(2, col_size + 1): + for col in range(1, col_size): for row in [0, 1]: if plotting_counter < p1dcounter: axes_list1.append(canvas.add_axes(title="", xlabel="", row=row, col=col)) @@ -167,7 +168,10 @@ if __name__ == "__main__": ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0) canvas.update_style(args.rc) - if args.md: + + def plot_md_overlay(): + if not args.md: + return idses = "" mduris = [] for mduri in args.md: @@ -194,7 +198,15 @@ if __name__ == "__main__": ids_data = get_md_data(mduris, args.dd_update) plot_machine_description(ax1, ids_data) - c_psi, c_rho = view_object.view_magnetic_poloidal_flux(ax1, time_slice, plot_rho=args.rho) + c_psi, c_rho = view_object.view_magnetic_poloidal_flux( + ax1, + time_slice, + plot_magnetic_axis=False, + plot_current_centre=False, + plot_boundary_data=False, + plot_rho=args.rho, + plot_annotations=False, + ) if c_psi: cax_psi = ax1.inset_axes([-0.20, 0.05, 0.05, 0.88]) # [x, y, w, h] in axes coords @@ -222,10 +234,60 @@ if __name__ == "__main__": canvas.fig.suptitle(get_title(args, "Equilibrium", time_value)) if args.plots: canvas.fig.set_size_inches(10 + col_size * 1.6, 8) - canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.024, right=0.988, hspace=0.221, wspace=0.20) + canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.048, right=0.988, hspace=0.221, wspace=0.20) else: canvas.fig.set_size_inches(14, 8) - canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.024, right=0.988, hspace=0.221, wspace=0.25) + canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.048, right=0.988, hspace=0.221, wspace=0.25) + + if not args.save: + overlay_state = {"created": False, "artists": []} + clear_button_ax = canvas.fig.add_axes([0.74, 0.94, 0.125, 0.04]) + clear_button = Button(clear_button_ax, "Clear overlays") + button_ax = canvas.fig.add_axes([0.875, 0.94, 0.095, 0.04]) + overlay_button = Button(button_ax, "Show legend") + + def on_overlay_button_clicked(_event): + if not overlay_state["created"]: + before_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) + plot_md_overlay() + view_object.view_magnetic_poloidal_flux( + ax1, + time_slice, + plot_magnetic_axis=True, + plot_current_centre=True, + plot_boundary_data=True, + plot_rho=False, + plot_annotations=True, + plot_psi=False, + ) + after_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) + overlay_state["artists"] = list(after_artists - before_artists) + overlay_state["created"] = True + overlay_button.label.set_text("Hide legend") + else: + legend = ax1.get_legend() + if legend is not None: + visible = not legend.get_visible() + legend.set_visible(visible) + overlay_button.label.set_text("Hide legend" if visible else "Show legend") + canvas.fig.canvas.draw_idle() + + def on_clear_overlay_clicked(_event): + for artist in overlay_state["artists"]: + try: + artist.remove() + except (NotImplementedError, ValueError): + artist.set_visible(False) + overlay_state["artists"] = [] + overlay_state["created"] = False + overlay_button.label.set_text("Show legend") + canvas.fig.canvas.draw_idle() + + overlay_button.on_clicked(on_overlay_button_clicked) + clear_button.on_clicked(on_clear_overlay_clicked) + canvas.fig._idstools_overlay_button = overlay_button + canvas.fig._idstools_clear_overlay_button = clear_button + canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) if args.save: fname = get_file_name(args, f"{os.path.basename(__file__)}_Equilibrium", time_value) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 18697eff..3107f825 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -45,6 +45,8 @@ def view_magnetic_poloidal_flux( plot_current_centre: bool = True, plot_boundary_data: bool = True, plot_rho: bool = False, + plot_annotations: bool = True, + plot_psi: bool = True, ): """ This function plots the magnetic poloidal flux contours on a 2D Cartesian grid. @@ -84,9 +86,11 @@ def view_magnetic_poloidal_flux( :meth:`plotIP` """ contour_lines_psi = contour_lines_rho = None - cartestion_grid = self.compute_obj.get2d_cartesian_grid(time_slice, profiles2d_index) - if cartestion_grid is not None: - levels = 50 + levels = 50 + cartestion_grid = None + if plot_psi or plot_rho: + cartestion_grid = self.compute_obj.get2d_cartesian_grid(time_slice, profiles2d_index) + if cartestion_grid is not None and plot_psi: # As per IMAS data dictionary psi is stored as [R, Z] with shape (N_R, N_Z). # Check this reference : @@ -308,9 +312,8 @@ def view_magnetic_poloidal_flux( ) overlay_entries.append((proxy_sp, _sp_artists)) - quantity_annotation = self.view_global_quantities_annotation(ax, time_slice) + quantity_annotation = self.view_global_quantities_annotation(ax, time_slice) if plot_annotations else None if quantity_annotation is not None: - quantity_annotation.set_visible(False) proxy_quantities = ProxyLine( [0], [0], @@ -333,13 +336,19 @@ def view_magnetic_poloidal_flux( legend = ax.legend( handles=all_handles, labels=all_labels, - loc="upper left", - bbox_to_anchor=(1.15, 1), + loc="upper right", fancybox=True, + frameon=True, + framealpha=1.0, + facecolor="white", + edgecolor="black", fontsize=10, labelspacing=1.2, - title="Overlays\n(click to toggle)", + title="Overlays (click to toggle)", ) + legend.get_frame().set_alpha(1.0) + legend.get_frame().set_facecolor("white") + legend.set_zorder(1000) legend.get_title().set_fontsize(10) legend.get_title().set_fontstyle("italic") legend.get_title().set_ha("center") @@ -413,14 +422,14 @@ def view_global_quantities_annotation(self, ax: plt.axes, time_slice: int): textstr = "\n".join(f"{d['label']} = {d['text']}" for d in items) txt = ax.text( - 1.20, - 0.1, + 0.98, + 0.02, textstr, transform=ax.transAxes, fontsize=9, - horizontalalignment="left", - verticalalignment="center", - clip_on=False, + horizontalalignment="right", + verticalalignment="bottom", + clip_on=True, bbox=dict(boxstyle="round,pad=0.5", facecolor="white", alpha=0.85, edgecolor="steelblue"), ) return txt From 2be598a5fd088cd40c19628769cabfee823cb47b Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 12 Jun 2026 15:11:34 +0200 Subject: [PATCH 23/35] added overlay CLI argument --- idstools/scripts/bin/plotequilibrium | 59 ++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 8a7a87d9..dea5c559 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -53,6 +53,11 @@ if __name__ == "__main__": help="Show rho overlay on the plot", action="store_true", ) + parser.add_argument( + "--overlay", + help="Show equilibrium overlays", + action="store_true", + ) parser.add_argument( "-p", "--plots", @@ -130,7 +135,6 @@ if __name__ == "__main__": time_slice, time_value = get_nearest_time(ids_obj_equilibrium.time, args.time) view_object = EquilibriumView(ids_obj_equilibrium) - title = f"2D Equilibrium at time {time_value:.3f}" database_text = "" if args.plots: compute_obj = EquilibriumCompute(ids_obj_equilibrium) @@ -168,10 +172,11 @@ if __name__ == "__main__": ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0) canvas.update_style(args.rc) + md_overlay_state = {"created": False} def plot_md_overlay(): - if not args.md: - return + if not args.md or md_overlay_state["created"]: + return False idses = "" mduris = [] for mduri in args.md: @@ -197,6 +202,11 @@ if __name__ == "__main__": else: ids_data = get_md_data(mduris, args.dd_update) plot_machine_description(ax1, ids_data) + md_overlay_state["created"] = True + return True + + if args.save: + plot_md_overlay() c_psi, c_rho = view_object.view_magnetic_poloidal_flux( ax1, @@ -207,6 +217,9 @@ if __name__ == "__main__": plot_rho=args.rho, plot_annotations=False, ) + legend = ax1.get_legend() + if legend is not None: + legend.set_visible(False) if c_psi: cax_psi = ax1.inset_axes([-0.20, 0.05, 0.05, 0.88]) # [x, y, w, h] in axes coords @@ -220,8 +233,6 @@ if __name__ == "__main__": cbar_rho.ax.set_title(r"$\rho$", fontsize=7, pad=4) cbar_rho.ax.tick_params(labelsize=7) - ax1.set_title(title) - database_label = get_database_path(args, time_value=time_value) if database_text: database_label += f"\n{database_text}" @@ -239,8 +250,31 @@ if __name__ == "__main__": canvas.fig.set_size_inches(14, 8) canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.048, right=0.988, hspace=0.221, wspace=0.25) + def create_overlays(show_legend=True): + plot_md_overlay() + view_object.view_magnetic_poloidal_flux( + ax1, + time_slice, + plot_magnetic_axis=True, + plot_current_centre=True, + plot_boundary_data=True, + plot_rho=False, + plot_annotations=True, + plot_psi=False, + ) + legend = ax1.get_legend() + if legend is not None: + legend.set_visible(show_legend) + + startup_overlay_artists = [] + if args.overlay: + before_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) + create_overlays(show_legend=args.save) + after_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) + startup_overlay_artists = list(after_artists - before_artists) + if not args.save: - overlay_state = {"created": False, "artists": []} + overlay_state = {"created": args.overlay, "artists": startup_overlay_artists} clear_button_ax = canvas.fig.add_axes([0.74, 0.94, 0.125, 0.04]) clear_button = Button(clear_button_ax, "Clear overlays") button_ax = canvas.fig.add_axes([0.875, 0.94, 0.095, 0.04]) @@ -249,17 +283,7 @@ if __name__ == "__main__": def on_overlay_button_clicked(_event): if not overlay_state["created"]: before_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) - plot_md_overlay() - view_object.view_magnetic_poloidal_flux( - ax1, - time_slice, - plot_magnetic_axis=True, - plot_current_centre=True, - plot_boundary_data=True, - plot_rho=False, - plot_annotations=True, - plot_psi=False, - ) + create_overlays(show_legend=True) after_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) overlay_state["artists"] = list(after_artists - before_artists) overlay_state["created"] = True @@ -280,6 +304,7 @@ if __name__ == "__main__": artist.set_visible(False) overlay_state["artists"] = [] overlay_state["created"] = False + md_overlay_state["created"] = False overlay_button.label.set_text("Show legend") canvas.fig.canvas.draw_idle() From 1c13e23e6c11429cdbc780d4dc8e2ef2f8e21d74 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 12 Jun 2026 17:55:07 +0200 Subject: [PATCH 24/35] fixed comment --- idstools/scripts/bin/plotequilibrium | 68 ++++++---------------------- idstools/view/domain/mdplot.py | 4 +- idstools/view/equilibrium.py | 38 +++++----------- 3 files changed, 28 insertions(+), 82 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index dea5c559..c0e94350 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -22,14 +22,12 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger from idstools.view.common import PlotCanvas from idstools.view.domain.mdplot import plot_machine_description from idstools.view.equilibrium import EquilibriumView -from matplotlib.widgets import Button class MdAction(argparse.Action): @@ -84,6 +82,11 @@ if __name__ == "__main__": help="Show labels", action="store_true", ) + parser.add_argument( + "--provenance", + help="Show equilibrium and machine-description URIs on the figure", + action="store_true", + ) parser.add_argument( "--debug", help="Show diagnostic logging", @@ -201,7 +204,8 @@ if __name__ == "__main__": ids_data = get_md_data(mduris, args.dd_update, idses=idses) else: ids_data = get_md_data(mduris, args.dd_update) - plot_machine_description(ax1, ids_data) + plot_machine_description(ax1, ids_data, show_provenance=args.provenance) + ax1.set_title("") md_overlay_state["created"] = True return True @@ -233,16 +237,16 @@ if __name__ == "__main__": cbar_rho.ax.set_title(r"$\rho$", fontsize=7, pad=4) cbar_rho.ax.tick_params(labelsize=7) - database_label = get_database_path(args, time_value=time_value) - if database_text: - database_label += f"\n{database_text}" - canvas.set_text(y=0.985, text=database_label, fontsize=7) + if args.provenance: + database_label = get_database_path(args, time_value=time_value) + if database_text: + database_label += f"\n{database_text}" + canvas.set_text(y=0.985, text=database_label, fontsize=7) if args.plots: view_object.plot_profiles_1d_quantities(axes_list1, time_slice) view_object.plot_global_quantities(axes_list2, time_value) - canvas.fig.suptitle(get_title(args, "Equilibrium", time_value)) if args.plots: canvas.fig.set_size_inches(10 + col_size * 1.6, 8) canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.048, right=0.988, hspace=0.221, wspace=0.20) @@ -259,59 +263,15 @@ if __name__ == "__main__": plot_current_centre=True, plot_boundary_data=True, plot_rho=False, - plot_annotations=True, + plot_annotations=not args.plots, plot_psi=False, ) legend = ax1.get_legend() if legend is not None: legend.set_visible(show_legend) - startup_overlay_artists = [] if args.overlay: - before_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) - create_overlays(show_legend=args.save) - after_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) - startup_overlay_artists = list(after_artists - before_artists) - - if not args.save: - overlay_state = {"created": args.overlay, "artists": startup_overlay_artists} - clear_button_ax = canvas.fig.add_axes([0.74, 0.94, 0.125, 0.04]) - clear_button = Button(clear_button_ax, "Clear overlays") - button_ax = canvas.fig.add_axes([0.875, 0.94, 0.095, 0.04]) - overlay_button = Button(button_ax, "Show legend") - - def on_overlay_button_clicked(_event): - if not overlay_state["created"]: - before_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) - create_overlays(show_legend=True) - after_artists = set(ax1.get_children()) | set(canvas.fig.get_children()) - overlay_state["artists"] = list(after_artists - before_artists) - overlay_state["created"] = True - overlay_button.label.set_text("Hide legend") - else: - legend = ax1.get_legend() - if legend is not None: - visible = not legend.get_visible() - legend.set_visible(visible) - overlay_button.label.set_text("Hide legend" if visible else "Show legend") - canvas.fig.canvas.draw_idle() - - def on_clear_overlay_clicked(_event): - for artist in overlay_state["artists"]: - try: - artist.remove() - except (NotImplementedError, ValueError): - artist.set_visible(False) - overlay_state["artists"] = [] - overlay_state["created"] = False - md_overlay_state["created"] = False - overlay_button.label.set_text("Show legend") - canvas.fig.canvas.draw_idle() - - overlay_button.on_clicked(on_overlay_button_clicked) - clear_button.on_clicked(on_clear_overlay_clicked) - canvas.fig._idstools_overlay_button = overlay_button - canvas.fig._idstools_clear_overlay_button = clear_button + create_overlays(show_legend=not args.plots) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) if args.save: diff --git a/idstools/view/domain/mdplot.py b/idstools/view/domain/mdplot.py index 4997be39..7bd0f3c8 100644 --- a/idstools/view/domain/mdplot.py +++ b/idstools/view/domain/mdplot.py @@ -36,7 +36,7 @@ def update_labels(ax): ax.figure.canvas.draw_idle() -def plot_machine_description(ax, ids_data): +def plot_machine_description(ax, ids_data, show_provenance=True): """ The `plotMachineDescription` method is responsible for plotting the machine description based on the provided pulse list. @@ -151,5 +151,5 @@ def plot_machine_description(ax, ids_data): # ax.callbacks.connect("ylim_changed", update_labels) ax.plot() - if database_path: + if show_provenance and database_path: ax.figure.text(0.001, 0.965, database_path, ha="left", va="top", fontsize=7) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 3107f825..2ec913dd 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -222,7 +222,7 @@ def view_magnetic_poloidal_flux( markersize=6, markeredgewidth=1.4, linestyle="None", - label=f"geom. axis (R={bd['bnd_geom_r']:.3f}, Z={bd['bnd_geom_z']:.3f} m)", + label="Geom axis", ) overlay_entries.append((proxy_gax, [gax_marker])) @@ -271,7 +271,7 @@ def view_magnetic_poloidal_flux( # strike-points (boundary_separatrix) _sp_groups = [ - (bd["sep_strikepoints"], "darkorange", "strike_point"), + (bd["sep_strikepoints"], "red", "strike_point"), ] for sp_list, sp_color, sp_label in _sp_groups: _sp_artists = [] @@ -312,19 +312,8 @@ def view_magnetic_poloidal_flux( ) overlay_entries.append((proxy_sp, _sp_artists)) - quantity_annotation = self.view_global_quantities_annotation(ax, time_slice) if plot_annotations else None - if quantity_annotation is not None: - proxy_quantities = ProxyLine( - [0], - [0], - color="steelblue", - marker="*", - markerfacecolor="white", - markersize=7, - linestyle="None", - label="quantities", - ) - overlay_entries.append((proxy_quantities, [quantity_annotation])) + if plot_annotations: + self.view_global_quantities_annotation(ax, time_slice) # --- clickable legend if overlay_entries or _md_handles: @@ -336,7 +325,8 @@ def view_magnetic_poloidal_flux( legend = ax.legend( handles=all_handles, labels=all_labels, - loc="upper right", + loc="upper left", + bbox_to_anchor=(1.15, 1), fancybox=True, frameon=True, framealpha=1.0, @@ -344,14 +334,10 @@ def view_magnetic_poloidal_flux( edgecolor="black", fontsize=10, labelspacing=1.2, - title="Overlays (click to toggle)", ) legend.get_frame().set_alpha(1.0) legend.get_frame().set_facecolor("white") legend.set_zorder(1000) - legend.get_title().set_fontsize(10) - legend.get_title().set_fontstyle("italic") - legend.get_title().set_ha("center") for text in legend.get_texts(): text.set_ha("center") @@ -422,15 +408,15 @@ def view_global_quantities_annotation(self, ax: plt.axes, time_slice: int): textstr = "\n".join(f"{d['label']} = {d['text']}" for d in items) txt = ax.text( - 0.98, - 0.02, + 1.15, + 0.55, textstr, transform=ax.transAxes, fontsize=9, - horizontalalignment="right", - verticalalignment="bottom", - clip_on=True, - bbox=dict(boxstyle="round,pad=0.5", facecolor="white", alpha=0.85, edgecolor="steelblue"), + horizontalalignment="left", + verticalalignment="top", + clip_on=False, + bbox=dict(boxstyle="round,pad=0.5", facecolor="white", alpha=1.0, edgecolor="steelblue"), ) return txt From 692ead85528584591f79ff90e43cf03de7101c53 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 12 Jun 2026 18:09:17 +0200 Subject: [PATCH 25/35] fixed provenance --- idstools/scripts/bin/plotequilibrium | 2 +- idstools/view/domain/mdplot.py | 2 +- idstools/view/equilibrium.py | 37 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index c0e94350..7e54c6bb 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -241,7 +241,7 @@ if __name__ == "__main__": database_label = get_database_path(args, time_value=time_value) if database_text: database_label += f"\n{database_text}" - canvas.set_text(y=0.985, text=database_label, fontsize=7) + canvas.set_text(y=0.985, text=database_label, fontsize=8) if args.plots: view_object.plot_profiles_1d_quantities(axes_list1, time_slice) diff --git a/idstools/view/domain/mdplot.py b/idstools/view/domain/mdplot.py index 7bd0f3c8..875c7480 100644 --- a/idstools/view/domain/mdplot.py +++ b/idstools/view/domain/mdplot.py @@ -152,4 +152,4 @@ def plot_machine_description(ax, ids_data, show_provenance=True): ax.plot() if show_provenance and database_path: - ax.figure.text(0.001, 0.965, database_path, ha="left", va="top", fontsize=7) + ax.figure.text(0.001, 0.965, database_path, ha="left", va="top", fontsize=8) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 2ec913dd..236996bc 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -227,6 +227,10 @@ def view_magnetic_poloidal_flux( overlay_entries.append((proxy_gax, [gax_marker])) # x-points (boundary_separatrix) + point_marker_size = 7 + point_marker_edgewidth = 2.0 + point_label_fontsize = 8 + _xp_groups = [ (bd["sep_xpoints"], "red", "x_point"), ] @@ -238,8 +242,8 @@ def view_magnetic_poloidal_flux( xz, marker="x", color=xp_color, - markersize=7, - markeredgewidth=2, + markersize=point_marker_size, + markeredgewidth=point_marker_edgewidth, linestyle="None", zorder=7, ) @@ -248,7 +252,7 @@ def view_magnetic_poloidal_flux( xy=(xr, xz), xytext=(-6, 6), textcoords="offset points", - fontsize=8, + fontsize=point_label_fontsize, ha="right", color=xp_color, fontweight="bold", @@ -262,8 +266,8 @@ def view_magnetic_poloidal_flux( [0], color=xp_color, marker="x", - markersize=7, - markeredgewidth=2, + markersize=point_marker_size, + markeredgewidth=point_marker_edgewidth, linestyle="None", label=xp_label, ) @@ -281,8 +285,8 @@ def view_magnetic_poloidal_flux( sz, marker="+", color=sp_color, - markersize=7, - markeredgewidth=2.0, + markersize=point_marker_size, + markeredgewidth=point_marker_edgewidth, linestyle="None", zorder=7, ) @@ -291,7 +295,7 @@ def view_magnetic_poloidal_flux( xy=(sr, sz), xytext=(-6, 6), textcoords="offset points", - fontsize=8, + fontsize=point_label_fontsize, ha="right", color=sp_color, fontweight="bold", @@ -305,8 +309,8 @@ def view_magnetic_poloidal_flux( [0], color=sp_color, marker="+", - markersize=7, - markeredgewidth=2.0, + markersize=point_marker_size, + markeredgewidth=point_marker_edgewidth, linestyle="None", label=sp_label, ) @@ -328,18 +332,15 @@ def view_magnetic_poloidal_flux( loc="upper left", bbox_to_anchor=(1.15, 1), fancybox=True, - frameon=True, + frameon=False, framealpha=1.0, facecolor="white", - edgecolor="black", fontsize=10, labelspacing=1.2, ) - legend.get_frame().set_alpha(1.0) - legend.get_frame().set_facecolor("white") legend.set_zorder(1000) for text in legend.get_texts(): - text.set_ha("center") + text.set_ha("left") leg_map = {} legend_texts = legend.get_texts() @@ -409,14 +410,14 @@ def view_global_quantities_annotation(self, ax: plt.axes, time_slice: int): textstr = "\n".join(f"{d['label']} = {d['text']}" for d in items) txt = ax.text( 1.15, - 0.55, + 0.0, textstr, transform=ax.transAxes, fontsize=9, horizontalalignment="left", - verticalalignment="top", + verticalalignment="bottom", clip_on=False, - bbox=dict(boxstyle="round,pad=0.5", facecolor="white", alpha=1.0, edgecolor="steelblue"), + bbox=dict(boxstyle="round,pad=0.5", facecolor="white", alpha=1.0, edgecolor="none"), ) return txt From 10229bb46e351c6070783d0231bf101597d5492a Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 15 Jun 2026 15:13:29 +0200 Subject: [PATCH 26/35] provenance if passed in center. do not show same uri if --md passed. added --no-overlay option. default it will show overlays. added provennace as title --- idstools/scripts/bin/plotequilibrium | 39 +++++++++++---------- idstools/scripts/bin/plotmachinedescription | 7 ++-- idstools/utils/clihelper.py | 5 ++- idstools/view/domain/mdplot.py | 29 +++++++++------ 4 files changed, 46 insertions(+), 34 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 7e54c6bb..f93810d6 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -52,9 +52,11 @@ if __name__ == "__main__": action="store_true", ) parser.add_argument( - "--overlay", - help="Show equilibrium overlays", - action="store_true", + "--no-overlay", + dest="overlay", + help="Hide equilibrium overlays", + action="store_false", + default=True, ) parser.add_argument( "-p", @@ -77,11 +79,6 @@ if __name__ == "__main__": "testpulse.nc" """, ) - parser.add_argument( - "--show-labels", - help="Show labels", - action="store_true", - ) parser.add_argument( "--provenance", help="Show equilibrium and machine-description URIs on the figure", @@ -175,11 +172,11 @@ if __name__ == "__main__": ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0) canvas.update_style(args.rc) - md_overlay_state = {"created": False} + md_overlay_state = {"created": False, "provenance_lines": []} def plot_md_overlay(): if not args.md or md_overlay_state["created"]: - return False + return md_overlay_state["provenance_lines"] idses = "" mduris = [] for mduri in args.md: @@ -204,7 +201,9 @@ if __name__ == "__main__": ids_data = get_md_data(mduris, args.dd_update, idses=idses) else: ids_data = get_md_data(mduris, args.dd_update) - plot_machine_description(ax1, ids_data, show_provenance=args.provenance) + md_provenance = plot_machine_description(ax1, ids_data, main_uri=get_database_path(args).strip()) + if md_provenance: + md_overlay_state["provenance_lines"] = md_provenance.splitlines() ax1.set_title("") md_overlay_state["created"] = True return True @@ -237,22 +236,16 @@ if __name__ == "__main__": cbar_rho.ax.set_title(r"$\rho$", fontsize=7, pad=4) cbar_rho.ax.tick_params(labelsize=7) - if args.provenance: - database_label = get_database_path(args, time_value=time_value) - if database_text: - database_label += f"\n{database_text}" - canvas.set_text(y=0.985, text=database_label, fontsize=8) - if args.plots: view_object.plot_profiles_1d_quantities(axes_list1, time_slice) view_object.plot_global_quantities(axes_list2, time_value) if args.plots: canvas.fig.set_size_inches(10 + col_size * 1.6, 8) - canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.048, right=0.988, hspace=0.221, wspace=0.20) + canvas.fig.subplots_adjust(top=0.933, bottom=0.100, left=0.048, right=0.988, hspace=0.221, wspace=0.20) else: canvas.fig.set_size_inches(14, 8) - canvas.fig.subplots_adjust(top=0.933, bottom=0.05, left=0.048, right=0.988, hspace=0.221, wspace=0.25) + canvas.fig.subplots_adjust(top=0.933, bottom=0.100, left=0.048, right=0.988, hspace=0.221, wspace=0.25) def create_overlays(show_legend=True): plot_md_overlay() @@ -273,6 +266,14 @@ if __name__ == "__main__": if args.overlay: create_overlays(show_legend=not args.plots) + if args.provenance: + database_label = get_database_path(args, time_value=time_value) + if md_overlay_state["provenance_lines"]: + database_label += "\n" + "\n".join(md_overlay_state["provenance_lines"]) + if database_text: + database_label += f"\n{database_text}" + canvas.set_sup_title(database_label, fontsize=8, y=0.985) + canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) if args.save: fname = get_file_name(args, f"{os.path.basename(__file__)}_Equilibrium", time_value) diff --git a/idstools/scripts/bin/plotmachinedescription b/idstools/scripts/bin/plotmachinedescription index cc5c3d2b..992c9254 100644 --- a/idstools/scripts/bin/plotmachinedescription +++ b/idstools/scripts/bin/plotmachinedescription @@ -68,10 +68,13 @@ if __name__ == "__main__": mdcanvas = PlotCanvas(1, 1, figsize=(10, 10)) mdcanvas.update_style(args.rc) ax = mdcanvas.add_axes(title="", xlabel="R (m)", ylabel="Z (m)", row=0, col=0) - plot_machine_description(ax, ids_data) + md_provenance = plot_machine_description(ax, ids_data) mdcanvas.fig.subplots_adjust(top=0.916, bottom=0.09, left=0.044, right=0.953, hspace=0.287, wspace=0.2) mdcanvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - mdcanvas.fig.suptitle("Machine Description") + suptitle = "Machine Description" + if md_provenance: + suptitle += "\n" + md_provenance + mdcanvas.fig.suptitle(suptitle, fontsize=8) if args.save: fname = os.path.basename(__file__) + "_machine_description.png" if args.directory: diff --git a/idstools/utils/clihelper.py b/idstools/utils/clihelper.py index c7b4cc19..9cf5652f 100644 --- a/idstools/utils/clihelper.py +++ b/idstools/utils/clihelper.py @@ -241,11 +241,10 @@ def get_database_path(imasargs, time_value=None) -> str: database_absolute_path = database_absolute_path[:-2] time_string = "" if time_value: - time_string = f"time:{time_value:.3f})" - hostdir = f"{socket.gethostname()}:{database_absolute_path} " + time_string = f"time:{time_value:.3f}" + hostdir = f"{database_absolute_path} " if pulse_info: hostdir += f"({pulse_info})" if time_string: hostdir += f"#{time_string}" - # return hostdir diff --git a/idstools/view/domain/mdplot.py b/idstools/view/domain/mdplot.py index 875c7480..a46642ab 100644 --- a/idstools/view/domain/mdplot.py +++ b/idstools/view/domain/mdplot.py @@ -36,15 +36,25 @@ def update_labels(ax): ax.figure.canvas.draw_idle() -def plot_machine_description(ax, ids_data, show_provenance=True): +def plot_machine_description(ax, ids_data, main_uri=None): """ The `plotMachineDescription` method is responsible for plotting the machine description based on the provided pulse list. + Args: + main_uri: If provided, machine description entries with the same URI are omitted + from the provenance text (to avoid duplication when MD and main data share a URI). """ database_path = "" + def _provenance_line(ids_name_label, connection_args): + """Return a provenance line for this IDS, or empty string if URI matches main_uri.""" + uri = get_database_path(connection_args).strip() + if main_uri is not None and uri.strip() == main_uri.strip(): + return "" + return f"{ids_name_label} = {uri}\n" + mdlegends = [] mdlabels = [] for idsinfo, ids_data_and_config in ids_data.items(): @@ -70,7 +80,7 @@ def plot_machine_description(ax, ids_data, show_provenance=True): if _legend: mdlegends.append(_legend) mdlabels.append(f"pf_active:{idsocc}/coil[{select}]") - database_path += "pf_active = " + get_database_path(ids_data_and_config["connectionArgs"]) + "\n" + database_path += _provenance_line("pf_active", ids_data_and_config["connectionArgs"]) elif ids_name == "tf": select2 = ":" if len(matches) == 2: @@ -81,7 +91,7 @@ def plot_machine_description(ax, ids_data, show_provenance=True): if _legend: mdlegends.append(_legend) mdlabels.append(f"tf:{idsocc}/coil[{select}]/conductor[{select2}]") - database_path += "tf = " + get_database_path(ids_data_and_config["connectionArgs"]) + "\n" + database_path += _provenance_line("tf", ids_data_and_config["connectionArgs"]) elif ids_name == "pf_passive": pfpassiveview = PFPassiveView(ids_data_and_config["idsData"]) if "loop" in idsfield or idsfield == "": @@ -90,7 +100,7 @@ def plot_machine_description(ax, ids_data, show_provenance=True): mdlegends.append(_legend) mdlabels.append(f"pf_passive:{idsocc}/loop[{select}]") - database_path += "pf_passive = " + get_database_path(ids_data_and_config["connectionArgs"]) + "\n" + database_path += _provenance_line("pf_passive", ids_data_and_config["connectionArgs"]) elif ids_name == "wall": wallview = WallView(ids_data_and_config["idsData"]) select2 = ":" @@ -100,7 +110,7 @@ def plot_machine_description(ax, ids_data, show_provenance=True): wallview.view_wall_vessel(ax, select_description2d=select, select_unit=select2) if "limiter" in idsfield or idsfield == "": wallview.view_wall_limiter(ax, select_description2d=select, select_unit=select2) - database_path += "wall = " + get_database_path(ids_data_and_config["connectionArgs"]) + "\n" + database_path += _provenance_line("wall", ids_data_and_config["connectionArgs"]) elif ids_name == "magnetics": magnetics_view = MagneticsView(ids_data_and_config["idsData"]) if "b_field_phi_probe" in idsfield or idsfield == "": @@ -128,10 +138,10 @@ def plot_machine_description(ax, ids_data, show_provenance=True): if _legend: mdlegends.append(_legend) mdlabels.append(f"magnetics:{idsocc}/shunt[{select}]") - database_path += "magnetics = " + get_database_path(ids_data_and_config["connectionArgs"]) + "\n" + database_path += _provenance_line("magnetics", ids_data_and_config["connectionArgs"]) else: - database_path += ( - f"{ids_name} = " + get_database_path(ids_data_and_config["connectionArgs"]) + "No visualization yet\n" + database_path += _provenance_line(ids_name, ids_data_and_config["connectionArgs"]).replace( + "\n", " No visualization yet\n", 1 ) logger.info(f"Visualization is not implemented yet for machine description {ids_name}") @@ -151,5 +161,4 @@ def plot_machine_description(ax, ids_data, show_provenance=True): # ax.callbacks.connect("ylim_changed", update_labels) ax.plot() - if show_provenance and database_path: - ax.figure.text(0.001, 0.965, database_path, ha="left", va="top", fontsize=8) + return database_path.strip() From 612add85e36408ee4fdfc625fc8bb8536b6fb124 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 15 Jun 2026 15:24:50 +0200 Subject: [PATCH 27/35] removed unused import --- idstools/utils/clihelper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/idstools/utils/clihelper.py b/idstools/utils/clihelper.py index 9cf5652f..3151f239 100644 --- a/idstools/utils/clihelper.py +++ b/idstools/utils/clihelper.py @@ -1,7 +1,6 @@ import argparse import os import re -import socket try: import imaspy as imas From afdc00bedefb94d0de109faa8afe15c7c28ad0b0 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 15 Jun 2026 16:23:58 +0200 Subject: [PATCH 28/35] read boundary/outline --- idstools/compute/equilibrium.py | 114 +++++++++++++++++--------------- 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index 2d6e8db6..46610bbd 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -288,42 +288,17 @@ def get_ip(self) -> list: ] def get_boundary_data(self, time_slice: int) -> dict: - """Return boundary and boundary_separatrix data for a given time slice. + """Return boundary data for a given time slice. - Reads: + Reads ``boundary/outline``, ``boundary_separatrix`` (DD3), or + ``contour_tree`` (DD4) for the separatrix outline, X-points, and + strike-points. If the separatrix is still missing, falls back to + ``boundary/outline`` for diverted plasmas (``type==1``) or + ``boundary/lcfs`` for limiter/unknown. - * ``boundary/outline/r|z`` - * ``boundary/type`` (0=limiter, 1=diverted) - * ``boundary/psi_norm`` - * ``boundary/geometric_axis/r|z`` - - if boundary_separatrix is available: - - * ``boundary_separatrix/outline/r|z`` - * ``boundary_separatrix/x_point[i]/r|z`` - * ``boundary_separatrix/strike_point[i]/r|z`` - - if contour_tree is available: - - * ``contour_tree/node[i]/critical_type`` - * ``contour_tree/node[i]/r|z`` for X-points (``critical_type == 1``) - * ``contour_tree/node[i]/levelset/r|z`` for separatrix outline - * ``constraints/strike_point[i]/position_reconstructed/r|z`` for strike points - (fallback to ``position_measured/r|z`` when needed) - - Args: - time_slice (int): Index into ``time_slice``. - - Returns: - dict with keys: - - * ``"bnd_r"``, ``"bnd_z"`` boundary outline (closed), or ``None`` - * ``"bnd_type"`` int or ``None`` - * ``"bnd_psi_norm"`` float or ``None`` - * ``"bnd_geom_r"``, ``"bnd_geom_z"`` geometric axis scalars or ``None`` - * ``"sep_r"``, ``"sep_z"`` separatrix outline (closed), or ``None`` - * ``"sep_xpoints"`` list of (r, z) tuples - * ``"sep_strikepoints"`` list of (r, z) tuples + Returns a dict with keys ``bnd_r``, ``bnd_z``, ``bnd_type``, + ``bnd_psi_norm``, ``bnd_geom_r``, ``bnd_geom_z``, ``sep_r``, + ``sep_z``, ``sep_xpoints``, ``sep_strikepoints``. """ def _valid_arr(arr): @@ -364,32 +339,32 @@ def _read_outline(node): z = np.insert(z, breaks, np.nan) return r, z - def _read_points(node, attr): + def _read_points(node, attr, ids_path): pts = [] try: arr = getattr(node, attr) except AttributeError: - logger.debug(f"get_boundary_data: {attr} is not available on {node!r}") + logger.debug(f"get_boundary_data: {ids_path}/{attr} is not available") return pts except Exception as exc: - logger.debug(f"get_boundary_data: could not access {attr} on {node!r}: {exc}") + logger.debug(f"get_boundary_data: could not access {ids_path}/{attr}: {exc}") return pts try: n_points = len(arr) except Exception as exc: - logger.debug(f"get_boundary_data: could not get length of {attr}: {exc}") + logger.debug(f"get_boundary_data: could not get length of {ids_path}/{attr}: {exc}") n_points = None for pt_index, pt in enumerate(arr): try: r, z = float(pt.r), float(pt.z) except Exception as exc: - logger.debug(f"get_boundary_data: could not read {attr}[{pt_index}].r/z: {exc}") + logger.debug(f"get_boundary_data: could not read {ids_path}/{attr}[{pt_index}]/r|z: {exc}") continue if _valid_scalar(r) and _valid_scalar(z): pts.append((r, z)) else: - logger.debug(f"get_boundary_data: {attr}[{pt_index}] contains invalid r/z ({r}, {z})") - logger.debug(f"get_boundary_data: read {len(pts)} valid {attr} points out of {n_points}") + logger.debug(f"get_boundary_data: {ids_path}/{attr}[{pt_index}]/r|z invalid ({r}, {z})") + logger.debug(f"get_boundary_data: {ids_path}/{attr} — read {len(pts)} valid points out of {n_points}") return pts def _read_contour_tree(ts_node): @@ -493,10 +468,10 @@ def _read_contour_tree(ts_node): try: bnd = ts.boundary result["bnd_r"], result["bnd_z"] = _read_outline(bnd) - result["sep_xpoints"] = _read_points(bnd, "x_point") - result["sep_strikepoints"] = _read_points(bnd, "strike_point") + result["sep_xpoints"] = _read_points(bnd, "x_point", f"time_slice[{time_slice}]/boundary") + result["sep_strikepoints"] = _read_points(bnd, "strike_point", f"time_slice[{time_slice}]/boundary") logger.debug( - "get_boundary_data: boundary summary " + f"get_boundary_data: time_slice[{time_slice}]/boundary summary " f"(has_outline={result['bnd_r'] is not None and result['bnd_z'] is not None}, " f"xpoints={len(result['sep_xpoints'])}, strikepoints={len(result['sep_strikepoints'])})" ) @@ -505,14 +480,14 @@ def _read_contour_tree(ts_node): if _valid_scalar(bnd_type): result["bnd_type"] = bnd_type except Exception as exc: - logger.debug(f"get_boundary_data: could not read boundary data: {exc}") + logger.debug(f"get_boundary_data: could not read time_slice[{time_slice}]/boundary: {exc}") try: psi_norm = float(ts.boundary.psi_norm) if _valid_scalar(psi_norm): result["bnd_psi_norm"] = psi_norm except Exception as exc: - logger.debug(f"get_boundary_data: could not read boundary.psi_norm: {exc}") + logger.debug(f"get_boundary_data: could not read time_slice[{time_slice}]/boundary/psi_norm: {exc}") try: gax_r = float(ts.boundary.geometric_axis.r) @@ -521,26 +496,28 @@ def _read_contour_tree(ts_node): result["bnd_geom_r"] = gax_r result["bnd_geom_z"] = gax_z except Exception as exc: - logger.debug(f"get_boundary_data: could not read boundary.geometric_axis.r/z: {exc}") + logger.debug( + f"get_boundary_data: could not read time_slice[{time_slice}]/boundary/geometric_axis/r|z: {exc}" + ) # boundary_separatrix (DD3 ) if hasattr(ts, "boundary_separatrix"): sep = ts.boundary_separatrix try: result["sep_r"], result["sep_z"] = _read_outline(sep) - sep_xpoints = _read_points(sep, "x_point") - sep_strikepoints = _read_points(sep, "strike_point") + sep_xpoints = _read_points(sep, "x_point", f"time_slice[{time_slice}]/boundary_separatrix") + sep_strikepoints = _read_points(sep, "strike_point", f"time_slice[{time_slice}]/boundary_separatrix") if sep_xpoints: result["sep_xpoints"] = sep_xpoints if sep_strikepoints: result["sep_strikepoints"] = sep_strikepoints logger.debug( - "get_boundary_data: boundary_separatrix summary " + f"get_boundary_data: time_slice[{time_slice}]/boundary_separatrix summary " f"(has_outline={result['sep_r'] is not None and result['sep_z'] is not None}, " f"xpoints={len(sep_xpoints)}, strikepoints={len(sep_strikepoints)})" ) except Exception as exc: - logger.debug(f"get_boundary_data: could not read boundary_separatrix data: {exc}") + logger.debug(f"get_boundary_data: could not read time_slice[{time_slice}]/boundary_separatrix: {exc}") # contour_tree.node (DD4) if hasattr(ts, "contour_tree") and hasattr(ts.contour_tree, "node"): @@ -557,6 +534,35 @@ def _read_contour_tree(ts_node): if not result["sep_xpoints"] and contour_xpoints: result["sep_xpoints"] = contour_xpoints + # Separatrix fallback when boundary_separatrix / contour_tree provided nothing. + if result["sep_r"] is None or result["sep_z"] is None: + if result["bnd_type"] == 1: + # type=1 (diverted): boundary/outline IS the separatrix — reuse directly. + if result["bnd_r"] is not None and result["bnd_z"] is not None: + result["sep_r"] = result["bnd_r"] + result["sep_z"] = result["bnd_z"] + logger.debug( + f"get_boundary_data: time_slice[{time_slice}]/boundary/outline/r|z " + f"— sep outline reused (type=1 diverted, {result['sep_r'].size} pts)" + ) + else: + # type=0 (limiter) or unknown: outline is the limiter contour, not the LCFS. + # Fall back to boundary/lcfs + try: + r_raw = np.asarray(ts.boundary.lcfs.r, dtype=float) + z_raw = np.asarray(ts.boundary.lcfs.z, dtype=float) + mask = r_raw > 0 + r_raw, z_raw = _clean(r_raw[mask]), _clean(z_raw[mask]) + if r_raw.size > 0: + result["sep_r"] = r_raw + result["sep_z"] = z_raw + logger.debug( + f"get_boundary_data: time_slice[{time_slice}]/boundary/lcfs/r|z " + f"— sep outline filled ({r_raw.size} pts)" + ) + except Exception as exc: + logger.debug(f"get_boundary_data: could not read time_slice[{time_slice}]/boundary/lcfs/r|z: {exc}") + logger.debug( "get_boundary_data: final summary " f"(has_boundary={result['bnd_r'] is not None and result['bnd_z'] is not None}, " @@ -614,14 +620,16 @@ def get_current_centre(self, time_slice: int) -> Union[dict, None]: r = float(cc.r) z = float(cc.z) except Exception as exc: - logger.debug(f"get_current_centre: could not read current_centre – {exc}") + path = f"time_slice[{time_slice}]/global_quantities/current_centre/r|z" + logger.debug(f"get_current_centre: could not read {path} – {exc}") return None def _valid(val): return np.isfinite(val) and abs(val) < _IDS_VALID_THRESHOLD if not (_valid(r) and _valid(z)): - logger.debug("get_current_centre: current_centre contains no valid data") + path = f"time_slice[{time_slice}]/global_quantities/current_centre/r|z" + logger.debug(f"get_current_centre: {path} contains no valid data") return None return {"r": r, "z": z} From d45066d5af10b5412232c38f4772756a445fadc3 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 15 Jun 2026 22:05:12 +0200 Subject: [PATCH 29/35] added plotting script provennace as title --- idstools/domain/ecstray.py | 11 +++-- idstools/scripts/bin/plotcoresources | 5 +-- idstools/scripts/bin/plotcoretransport | 4 +- idstools/scripts/bin/ploteccomposition | 4 +- idstools/scripts/bin/plotecray | 28 ++++++------ idstools/scripts/bin/plotecstrayradiation | 48 ++++++++++++++------- idstools/scripts/bin/plotedgeprofiles | 5 +-- idstools/scripts/bin/plotequilibrium | 8 ++-- idstools/scripts/bin/plothcd | 11 +---- idstools/scripts/bin/plothcddistributions | 5 +-- idstools/scripts/bin/plothcdwaves | 5 +-- idstools/scripts/bin/plotkineticprofiles | 5 +-- idstools/scripts/bin/plotmachinedescription | 2 +- idstools/scripts/bin/plotneutron | 5 +-- idstools/scripts/bin/plotpressure | 7 +-- idstools/scripts/bin/plotrotation | 5 +-- idstools/scripts/bin/plotscenario | 10 +---- idstools/scripts/bin/plotspectrometry | 7 +-- idstools/view/domain/ecstray.py | 21 ++++++--- idstools/view/equilibrium.py | 2 +- 20 files changed, 90 insertions(+), 108 deletions(-) diff --git a/idstools/domain/ecstray.py b/idstools/domain/ecstray.py index 0dec54fd..0a55e2e5 100644 --- a/idstools/domain/ecstray.py +++ b/idstools/domain/ecstray.py @@ -21,7 +21,7 @@ def __init__(self, equilibrium_ids: object, core_profiles_ids: object, waves_ids # self.coreProfilesCompute = coreProfilesIds self.waves_compute = WavesCompute(waves_ids) - def get_resonance_layer(self, coherent_wave_index, time_slice, n_harm=None): + def get_resonance_layer(self, coherent_wave_index, time_slice, n_harm=None, equilibrium_time_slice=None): """This function calculates and returns a dictionary (Resonance Layer) containing r and z values corresponding to the resonance points based on the provided nHarm values, b_resonance, and b_total arrays. @@ -56,11 +56,14 @@ def get_resonance_layer(self, coherent_wave_index, time_slice, n_harm=None): """ if n_harm is None: n_harm = [1, 2, 3, 4] + if equilibrium_time_slice is None: + equilibrium_time_slice = time_slice + b_resonance = self.waves_compute.get_b_resonance(coherent_wave_index, time_slice, harmonic_frequencies=n_harm) - profile2d_index, b_total = self.equilibrium_compute.get_b_total(time_slice) + profile2d_index, b_total = self.equilibrium_compute.get_b_total(equilibrium_time_slice) if profile2d_index != -99: - r = self.equilibrium_compute.ids.time_slice[time_slice].profiles_2d[profile2d_index].grid.dim1 - z = self.equilibrium_compute.ids.time_slice[time_slice].profiles_2d[profile2d_index].grid.dim2 + r = self.equilibrium_compute.ids.time_slice[equilibrium_time_slice].profiles_2d[profile2d_index].grid.dim1 + z = self.equilibrium_compute.ids.time_slice[equilibrium_time_slice].profiles_2d[profile2d_index].grid.dim2 [nr, nz] = np.shape(b_total) b_err = 10 / nr diff --git a/idstools/scripts/bin/plotcoresources b/idstools/scripts/bin/plotcoresources index c8f7a4b2..9c0e2499 100644 --- a/idstools/scripts/bin/plotcoresources +++ b/idstools/scripts/bin/plotcoresources @@ -17,7 +17,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -98,11 +97,9 @@ if __name__ == "__main__": ax_torque_waveform = canvas.add_axes(row=1, col=3) ret = core_source_view.view_torque_waveform(ax_torque_waveform, time_slice) - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas.fig.subplots_adjust(top=0.916, bottom=0.09, left=0.044, right=0.953, hspace=0.287, wspace=0.2) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(args, "Core sources", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) if args.save: fname = get_file_name(args, os.path.basename(__file__) + "_core_sources", time_value) if args.directory: diff --git a/idstools/scripts/bin/plotcoretransport b/idstools/scripts/bin/plotcoretransport index a66560b1..2a692b31 100644 --- a/idstools/scripts/bin/plotcoretransport +++ b/idstools/scripts/bin/plotcoretransport @@ -24,7 +24,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -175,8 +174,7 @@ if __name__ == "__main__": model_index, logscale=args.logscale, ) - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas.fig.suptitle(get_title(args, "Core transport", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) canvas.fig.subplots_adjust(top=0.9, bottom=0.094, left=0.035, right=0.948, hspace=0.417, wspace=0.117) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) canvas.remove_empty_axes() diff --git a/idstools/scripts/bin/ploteccomposition b/idstools/scripts/bin/ploteccomposition index 26684b76..9f243e5d 100644 --- a/idstools/scripts/bin/ploteccomposition +++ b/idstools/scripts/bin/ploteccomposition @@ -16,7 +16,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -113,8 +112,7 @@ if __name__ == "__main__": waves_view.plot_ecrh_waveform(ax3, time_slice) waves_view.plot_e_c_c_d_waveform(ax4, time_slice) - canvas.set_text(text=f"{get_database_path(args, time_value)}") - canvas.fig.suptitle(get_title(args, "EC Composition", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) canvas.fig.subplots_adjust(top=0.941, bottom=0.122, left=0.052, right=0.925, hspace=0.2, wspace=0.2) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) diff --git a/idstools/scripts/bin/plotecray b/idstools/scripts/bin/plotecray index 7dfeb4a3..eeae1085 100644 --- a/idstools/scripts/bin/plotecray +++ b/idstools/scripts/bin/plotecray @@ -141,9 +141,8 @@ if __name__ == "__main__": exit(1) # Search for adequate time slice for display - time_array = ids_waves.time - ntime = len(ids_waves.time) - time_slice, time_value = get_nearest_time(time_array, args.time) + time_index_waves, time_value = get_nearest_time(ids_waves.time, args.time) + time_index_equilibrium, _ = get_nearest_time(ids_equilibrium.time, time_value) if len(ids_waves.code.name) > 0: logger.info(f"Code name = {ids_waves.code.name.upper()}") @@ -159,13 +158,18 @@ if __name__ == "__main__": wave_view = WavesView(ids_waves) wave_compute = WavesCompute(ids_waves) - beam_tracing_dict = wave_compute.get_beam_tracing(time_slice) + beam_tracing_dict = wave_compute.get_beam_tracing(time_index_waves) logger.info( f"There are {beam_tracing_dict['active_beams_count']} active beam(s)" f"and each beam has {beam_tracing_dict['max_total_beams']} ray(s)" ) - ecstra_view.plot_poloidal_view(ax_polview, coherent_wave_index=0, time_slice=time_slice) + ecstra_view.plot_poloidal_view( + ax_polview, + coherent_wave_index=0, + time_slice=time_index_waves, + equilibrium_time_slice=time_index_equilibrium, + ) if args.md is True: args.md = ["wall", "pf_active"] @@ -242,22 +246,20 @@ if __name__ == "__main__": wave_view.plot_pol_view_traces( ax_polview, - time_slice, + time_index_waves, color=color, style=style, ) - equi_view.plot_topplotequilibrium(ax_topview, time_slice) - wave_view.plot_top_view_traces(ax_topview, time_slice, color=color, style=style, label=label_code) - - wave_view.plot_electron_power(ax_powview, time_slice, color=color, style=style) - wave_view.plot_power_flow_normal(ax_powparview, time_slice, color=color, style=style) + equi_view.plot_topplotequilibrium(ax_topview, time_index_equilibrium) + wave_view.plot_top_view_traces(ax_topview, time_index_waves, color=color, style=style, label=label_code) - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") + wave_view.plot_electron_power(ax_powview, time_index_waves, color=color, style=style) + wave_view.plot_power_flow_normal(ax_powparview, time_index_waves, color=color, style=style) canvas.fig.subplots_adjust(top=0.95, bottom=0.097, left=0, right=0.948, hspace=0.2, wspace=0.108) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(args, "EC rays", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) if args.save: fname = get_file_name(args, os.path.basename(__file__) + "_EC_rays", time_value) diff --git a/idstools/scripts/bin/plotecstrayradiation b/idstools/scripts/bin/plotecstrayradiation index 07080d87..6b7f8841 100644 --- a/idstools/scripts/bin/plotecstrayradiation +++ b/idstools/scripts/bin/plotecstrayradiation @@ -4,6 +4,7 @@ import argparse import logging import os +import sys from rich_argparse import RichHelpFormatter @@ -19,7 +20,6 @@ from idstools.input_processing import ( from idstools.utils.clihelper import ( get_database_path, get_file_name, - get_title, rcparam_parser, dbentry_parser, ) @@ -32,6 +32,15 @@ from idstools.view.polygon import PolygonView from idstools.view.waves import WavesView logger = setup_logger("module", stdout_level=logging.INFO) + + +def _first_existing_path(*paths): + for path in paths: + if os.path.exists(path): + return path + return paths[-1] + + if __name__ == "__main__": parser = argparse.ArgumentParser( description="---- Shows electron cyclotron stray radiation information by showing different plots", @@ -61,20 +70,28 @@ if __name__ == "__main__": time_index_waves = 0 current_file_path = os.path.dirname(os.path.abspath(__file__)) + source_tree_root = os.path.abspath(os.path.join(current_file_path, "../../..")) - scenario_file = os.path.join(current_file_path, "../resources/input/scenario.yaml") - wallfile = os.path.join(current_file_path, "../resources/input/wall2d.txt") - filelaunchers = os.path.join(current_file_path, "../resources/input/ec_waveforms.yaml") - path_result = os.path.join(current_file_path, "../resources/results/") - - if not os.path.exists(scenario_file): - scenario_file = os.path.join(current_file_path, "input/scenario.yaml") - if not os.path.exists(wallfile): - wallfile = os.path.join(current_file_path, "input/wall2d.txt") - if not os.path.exists(filelaunchers): - filelaunchers = os.path.join(current_file_path, "input/ec_waveforms.yaml") - if not os.path.exists(path_result): - path_result = os.path.join(current_file_path, "results/") + scenario_file = _first_existing_path( + os.path.join(source_tree_root, "resources/input/scenario.yaml"), + os.path.join(sys.prefix, "bin/input/scenario.yaml"), + os.path.join(current_file_path, "input/scenario.yaml"), + ) + wallfile = _first_existing_path( + os.path.join(source_tree_root, "resources/input/wall2d.txt"), + os.path.join(sys.prefix, "bin/input/wall2d.txt"), + os.path.join(current_file_path, "input/wall2d.txt"), + ) + filelaunchers = _first_existing_path( + os.path.join(source_tree_root, "resources/input/ec_waveforms.yaml"), + os.path.join(sys.prefix, "bin/input/ec_waveforms.yaml"), + os.path.join(current_file_path, "input/ec_waveforms.yaml"), + ) + path_result = _first_existing_path( + os.path.join(source_tree_root, "resources/results"), + os.path.join(sys.prefix, "bin/results"), + os.path.join(current_file_path, "results"), + ) wall2d = read_wall(wallfile) @@ -166,7 +183,6 @@ if __name__ == "__main__": ax_polygon, wall2d, beam_wall, coherent_wave_index, time_index_waves, time_index_waves ) - canvas.set_text(text=f"{get_database_path(args, time_value=time_value_equilibrium)}") canvas.fig.subplots_adjust( top=0.88, bottom=0.11, @@ -175,7 +191,7 @@ if __name__ == "__main__": hspace=0.458, wspace=0.234, ) - canvas.fig.suptitle(get_title(args, "EC Stray Radiation", time_value_equilibrium)) + canvas.set_sup_title(get_database_path(args, time_value=time_value_equilibrium)) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) if args.save: fname = get_file_name(args, f"{os.path.basename(__file__)}_Equilibrium", time_value_equilibrium) diff --git a/idstools/scripts/bin/plotedgeprofiles b/idstools/scripts/bin/plotedgeprofiles index 4d484430..aceeb5ce 100644 --- a/idstools/scripts/bin/plotedgeprofiles +++ b/idstools/scripts/bin/plotedgeprofiles @@ -20,7 +20,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -186,11 +185,9 @@ if __name__ == "__main__": edge_profiles_view.view_equatorial_plane_and_diverter_density(ax4, time_slice, logscale=args.logscale) - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas.fig.subplots_adjust(top=0.93, bottom=0.067, left=0.026, right=0.953, hspace=0.287, wspace=0.12) - canvas.fig.suptitle(get_title(args, "Edge Profiles", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) if args.save: diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index f93810d6..0ce393fc 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -266,13 +266,13 @@ if __name__ == "__main__": if args.overlay: create_overlays(show_legend=not args.plots) + title = get_database_path(args, time_value=time_value) if args.provenance: - database_label = get_database_path(args, time_value=time_value) if md_overlay_state["provenance_lines"]: - database_label += "\n" + "\n".join(md_overlay_state["provenance_lines"]) + title += "\n" + "\n".join(md_overlay_state["provenance_lines"]) if database_text: - database_label += f"\n{database_text}" - canvas.set_sup_title(database_label, fontsize=8, y=0.985) + title += f"\n{database_text}" + canvas.set_sup_title(title, fontsize=8, y=0.985) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) if args.save: diff --git a/idstools/scripts/bin/plothcd b/idstools/scripts/bin/plothcd index 3dd6fd51..23f6a753 100644 --- a/idstools/scripts/bin/plothcd +++ b/idstools/scripts/bin/plothcd @@ -16,7 +16,6 @@ from idstools.database import DBMaster from idstools.utils.clihelper import ( get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -67,7 +66,6 @@ def _show_waves_plots(connargs, args, hold=False, dd_update=False, rc=""): canvas = PlotCanvas(rows, 2) canvas.update_style(rc) # canvas.setStyle(style="retro") - canvas.set_sup_title(f"HCD Waves Plot {connargs.uri} Time : {time_value:.3f}") ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0) ax2 = canvas.add_axes(title="", xlabel="", row=0, col=1) @@ -96,8 +94,6 @@ def _show_waves_plots(connargs, args, hold=False, dd_update=False, rc=""): else: ax4.get_legend().remove() - canvas.set_text(text=f"{get_database_path(connargs, time_value=time_value)}") - canvas.fig.subplots_adjust( top=0.92, bottom=0.122, @@ -107,7 +103,7 @@ def _show_waves_plots(connargs, args, hold=False, dd_update=False, rc=""): wspace=0.13, ) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(connargs, "HCD Waves Plot", time_value)) + canvas.set_sup_title(get_database_path(connargs, time_value=time_value)) if args["save"]: fname = get_file_name(connargs, "hcd_waves_plot", time_value) canvas.save(fname) @@ -168,7 +164,6 @@ def _show_distribution_plots(connargs, args, hold=False, dd_update=False, rc="") canvas = PlotCanvas(3, 2) canvas.update_style(rc) # canvas.setStyle(style="retro") - canvas.set_sup_title(f"HCD Distributions Plot {connargs.uri} Time : {time_value:.3f}") if ntime == 1: logger.info("Only one time slice --> Power and CD waveforms not displayed") ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0) @@ -184,8 +179,6 @@ def _show_distribution_plots(connargs, args, hold=False, dd_update=False, rc="") distributions_view.plot_nbi_fus_power_and_cd_waveforms(ax4, time_slice) distributions_view.plot_cd_waveform(ax5, time_slice) - canvas.set_text(text=f"{get_database_path(connargs, time_value=time_value)}") - canvas.fig.subplots_adjust( top=0.92, bottom=0.122, @@ -195,7 +188,7 @@ def _show_distribution_plots(connargs, args, hold=False, dd_update=False, rc="") wspace=0.328, ) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(connargs, "HCD Distributions Plot", time_value)) + canvas.set_sup_title(get_database_path(connargs, time_value=time_value)) if args["save"]: fname = get_file_name(connargs, os.path.basename(__file__) + "_Distributions_profile_time", time_value) canvas.save(fname) diff --git a/idstools/scripts/bin/plothcddistributions b/idstools/scripts/bin/plothcddistributions index 1c41665b..672d31a4 100644 --- a/idstools/scripts/bin/plothcddistributions +++ b/idstools/scripts/bin/plothcddistributions @@ -16,7 +16,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -67,8 +66,6 @@ def show_plots(args): distributions_view.plot_nbi_fus_power_and_cd_waveforms(ax4, time_slice) distributions_view.plot_cd_waveform(ax5, time_slice) - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas.fig.subplots_adjust( top=0.92, bottom=0.122, @@ -78,7 +75,7 @@ def show_plots(args): wspace=0.328, ) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(args, "Distributions profile", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) if args.save: fname = get_file_name(args, os.path.basename(__file__) + "_Distributions_profile", time_value) diff --git a/idstools/scripts/bin/plothcdwaves b/idstools/scripts/bin/plothcdwaves index f9c28758..cc9a5f15 100644 --- a/idstools/scripts/bin/plothcdwaves +++ b/idstools/scripts/bin/plothcdwaves @@ -17,7 +17,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -96,8 +95,6 @@ def show_plots(args): else: ax4.get_legend().remove() - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas.fig.subplots_adjust( top=0.92, bottom=0.122, @@ -107,7 +104,7 @@ def show_plots(args): wspace=0.13, ) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(args, "HCD Waves Plot", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) if args.save: fname = get_file_name(args, os.path.basename(__file__) + "_heating_profiles_time", time_value) diff --git a/idstools/scripts/bin/plotkineticprofiles b/idstools/scripts/bin/plotkineticprofiles index dd3d556c..d089d927 100644 --- a/idstools/scripts/bin/plotkineticprofiles +++ b/idstools/scripts/bin/plotkineticprofiles @@ -11,7 +11,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -80,12 +79,10 @@ if __name__ == "__main__": kp_view.view_density_profiles(ax7, logscale=args.logscale) # Density profiles kp_view.view_vphi_profile(ax8, logscale=args.logscale) # Vtol profiles - canvas.set_text(text=f"{get_database_path(args, time_value=kp_view.k_profiles.time_value_core_profiles)}") - canvas.fig.subplots_adjust(top=0.928, bottom=0.11, left=0.033, right=0.91, hspace=0.435, wspace=0.518) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(args, "Plasma kinetic profiles ", kp_view.k_profiles.time_value_core_profiles)) + canvas.set_sup_title(get_database_path(args, time_value=kp_view.k_profiles.time_value_core_profiles)) if args.save: fname = get_file_name( diff --git a/idstools/scripts/bin/plotmachinedescription b/idstools/scripts/bin/plotmachinedescription index 992c9254..dab5f62e 100644 --- a/idstools/scripts/bin/plotmachinedescription +++ b/idstools/scripts/bin/plotmachinedescription @@ -74,7 +74,7 @@ if __name__ == "__main__": suptitle = "Machine Description" if md_provenance: suptitle += "\n" + md_provenance - mdcanvas.fig.suptitle(suptitle, fontsize=8) + mdcanvas.set_sup_title(suptitle, fontsize=8) if args.save: fname = os.path.basename(__file__) + "_machine_description.png" if args.directory: diff --git a/idstools/scripts/bin/plotneutron b/idstools/scripts/bin/plotneutron index f9f40ce5..467b6edf 100644 --- a/idstools/scripts/bin/plotneutron +++ b/idstools/scripts/bin/plotneutron @@ -19,7 +19,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -125,11 +124,9 @@ if __name__ == "__main__": distribution_sources_view.view_neutrons(ax, time_slice) distribution_sources_view.view_time(ax, time_value) - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas.fig.subplots_adjust(top=0.916, bottom=0.09, left=0.044, right=0.953, hspace=0.287, wspace=0.2) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(args, "Neutrons profiles", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) if args.save: fname = get_file_name(args, os.path.basename(__file__) + "_Neutrons", time_value) diff --git a/idstools/scripts/bin/plotpressure b/idstools/scripts/bin/plotpressure index c263cda2..b1b97736 100644 --- a/idstools/scripts/bin/plotpressure +++ b/idstools/scripts/bin/plotpressure @@ -19,7 +19,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -69,11 +68,9 @@ if __name__ == "__main__": time_array = ids_core_profiles.time time_slice, time_value = get_nearest_time(time_array, args.time) - title = "Profiles displayed for t = " + "%.1f" % time_value + " s" canvas = PlotCanvas(3, 1) canvas.update_style(args.rc) - canvas.fig.suptitle(title) ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0, colspan=1) ax2 = canvas.add_axes(title="", xlabel="", row=1, col=0, colspan=1) @@ -85,11 +82,9 @@ if __name__ == "__main__": coreprofiles_view.plot_ion_pressure_properties(ax2, time_slice) coreprofiles_view.plot_electron_pressure_properties(ax3, time_slice) - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas.fig.subplots_adjust(top=0.916, bottom=0.09, left=0.044, right=0.953, hspace=0.287, wspace=0.2) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(args, "Pressure", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) if args.save: fname = get_file_name(args, os.path.basename(__file__) + "_Pressure", time_value) diff --git a/idstools/scripts/bin/plotrotation b/idstools/scripts/bin/plotrotation index 2dc5a291..50a50ada 100644 --- a/idstools/scripts/bin/plotrotation +++ b/idstools/scripts/bin/plotrotation @@ -18,7 +18,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -87,11 +86,9 @@ if __name__ == "__main__": ax1.sharex(ax3) ax2.sharex(ax4) - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas.fig.subplots_adjust(top=0.916, bottom=0.09, left=0.044, right=0.953, hspace=0.174, wspace=0.117) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - canvas.fig.suptitle(get_title(args, "Kinetic profiles", time_value)) + canvas.set_sup_title(get_database_path(args, time_value=time_value)) if args.save: fname = get_file_name(args, os.path.basename(__file__) + "_Kinetic_profiles", time_value) diff --git a/idstools/scripts/bin/plotscenario b/idstools/scripts/bin/plotscenario index ffc2e978..8ac729b4 100644 --- a/idstools/scripts/bin/plotscenario +++ b/idstools/scripts/bin/plotscenario @@ -18,7 +18,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idshelper import get_available_ids_and_occurrences @@ -205,11 +204,10 @@ if __name__ == "__main__": plotequilibrium = EquilibriumView(ids_equilibrium) plotequilibrium.plotequilibrium(ax5, time_slice) - title = get_title(args, "Scenario") if not args.no_profiles: - title = get_title(args, "Scenario", time_value) + title = get_database_path(args, time_value=time_value) else: - title = get_title(args, "Scenario") + title = get_database_path(args) if args.info: title += ( f"\nprovider={ids_summary.ids_properties.provider}, " @@ -218,10 +216,6 @@ if __name__ == "__main__": f"access_layer={ids_summary.ids_properties.version_put.access_layer}" ) - if not args.no_profiles: - canvas.set_text(text=f"{get_database_path(args, time_value=time_value)}") - else: - canvas.set_text(text=f"{get_database_path(args)}") canvas.set_sup_title(title) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) canvas.fig.subplots_adjust(top=0.914, bottom=0.099, left=0.042, right=0.9, hspace=0.113, wspace=0.43) diff --git a/idstools/scripts/bin/plotspectrometry b/idstools/scripts/bin/plotspectrometry index 384e783d..620c62df 100644 --- a/idstools/scripts/bin/plotspectrometry +++ b/idstools/scripts/bin/plotspectrometry @@ -19,7 +19,6 @@ from idstools.utils.clihelper import ( dbentry_parser, get_database_path, get_file_name, - get_title, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -97,8 +96,7 @@ if __name__ == "__main__": ax.get_legend().remove() canvas_radiance.fig.subplots_adjust(top=0.88, bottom=0.11, left=0.065, right=0.893, hspace=0.497, wspace=0.243) - canvas_radiance.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas_radiance.fig.suptitle(get_title(args, "Spectrum (Radiance) from spectrometer_visible", time_value)) + canvas_radiance.set_sup_title(get_database_path(args, time_value=time_value)) canvas_radiance.get_current_fig_manager().set_window_title(os.path.basename(__file__) + "-radiance") if args.save: @@ -124,8 +122,7 @@ if __name__ == "__main__": if column_counter != 0: ax.get_legend().remove() - canvas_intensity.set_text(text=f"{get_database_path(args, time_value=time_value)}") - canvas_intensity.fig.suptitle(get_title(args, "Spectrum (Intensity) from spectrometer_visible", time_value)) + canvas_intensity.set_sup_title(get_database_path(args, time_value=time_value)) canvas_intensity.fig.subplots_adjust(top=0.88, bottom=0.113, left=0.033, right=0.891, hspace=0.497, wspace=0.18) canvas_intensity.get_current_fig_manager().set_window_title(os.path.basename(__file__) + "-intensity") diff --git a/idstools/view/domain/ecstray.py b/idstools/view/domain/ecstray.py index 66bfacec..91255980 100644 --- a/idstools/view/domain/ecstray.py +++ b/idstools/view/domain/ecstray.py @@ -84,25 +84,32 @@ def plot_resonance_layer(self, ax, coherent_wave_index, time_slice, init=1, verb else: ax.set_data(res_layer[i_harm]["r"], res_layer[i_harm]["z"]) - def plot_poloidal_view(self, ax, coherent_wave_index, time_slice): + def plot_poloidal_view(self, ax, coherent_wave_index, time_slice, equilibrium_time_slice=None): n_harm = [1, 2, 3, 4] - - resonance_data = self.ecstray_object.get_resonance_layer(coherent_wave_index, time_slice, n_harm=n_harm) + if equilibrium_time_slice is None: + equilibrium_time_slice = time_slice + + resonance_data = self.ecstray_object.get_resonance_layer( + coherent_wave_index, + time_slice, + n_harm=n_harm, + equilibrium_time_slice=equilibrium_time_slice, + ) profile2d_index = resonance_data["profile2d_index"] resonance_layer = resonance_data["resonance_layer"] - grid_data = self.equilibrium_compute.get2d_cartesian_grid(time_slice, profile2d_index) + grid_data = self.equilibrium_compute.get2d_cartesian_grid(equilibrium_time_slice, profile2d_index) r2d = grid_data["r2d"] z2d = grid_data["z2d"] psi2d = grid_data["psi2d"] - rho2d = self.equilibrium_compute.get_rho2d(time_slice, profile2d_index) + rho2d = self.equilibrium_compute.get_rho2d(equilibrium_time_slice, profile2d_index) # Poloidal view plot - contour_lines = ax.contour(r2d, z2d, psi2d, 50, cmap="summer") + contour_lines = ax.contour(r2d, z2d, psi2d.T, 50, cmap="summer") cbar_psi = plt.colorbar(contour_lines, ax=ax, orientation="horizontal", pad=0.08, fraction=0.03) cbar_psi.set_label(r"$\psi$ [Wb]") if rho2d is not None and len(rho2d) > 0: - contour_lines_rho = ax.contour(r2d, z2d, rho2d, 50, cmap="YlOrBr") + contour_lines_rho = ax.contour(r2d, z2d, rho2d.T, 50, cmap="YlOrBr") cbar_rho = plt.colorbar(contour_lines_rho, ax=ax, orientation="horizontal", pad=0.08, fraction=0.03) cbar_rho.set_label(r"$\rho$ [Wb]") # ax_polview.set_xlim(r2d.min(),r2d.max()) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 236996bc..7150c924 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -463,7 +463,7 @@ def plot_poloidal_equilibrium(self, ax, time_slice: int): z2d = data["z2d"] # rho2d = data["rho2d"] psi2d = data["psi2d"] - cntr = ax.contour(r2d, z2d, psi2d, 50, cmap="summer") + cntr = ax.contour(r2d, z2d, psi2d.T, 50, cmap="summer") cbar = plt.colorbar(cntr, ax=ax, pad=0.08, fraction=0.03) cbar.set_label(r"$\psi$ [Wb]") # if len(rho2d)>0: From 25d7befd17d798580f896817da460166923a28b9 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 15 Jun 2026 22:10:58 +0200 Subject: [PATCH 30/35] removed provenance flag and always showed title --- idstools/scripts/bin/plotequilibrium | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 0ce393fc..d3b2381a 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -79,11 +79,7 @@ if __name__ == "__main__": "testpulse.nc" """, ) - parser.add_argument( - "--provenance", - help="Show equilibrium and machine-description URIs on the figure", - action="store_true", - ) + parser.add_argument( "--debug", help="Show diagnostic logging", @@ -267,11 +263,10 @@ if __name__ == "__main__": create_overlays(show_legend=not args.plots) title = get_database_path(args, time_value=time_value) - if args.provenance: - if md_overlay_state["provenance_lines"]: - title += "\n" + "\n".join(md_overlay_state["provenance_lines"]) - if database_text: - title += f"\n{database_text}" + if md_overlay_state["provenance_lines"]: + title += "\n" + "\n".join(md_overlay_state["provenance_lines"]) + if database_text: + title += f"\n{database_text}" canvas.set_sup_title(title, fontsize=8, y=0.985) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) From a06d803b8ef209091463ddc9413de5b6cdecbac3 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 19 Jun 2026 15:12:19 +0200 Subject: [PATCH 31/35] added --no-provenance --- docs/source/plotequilibrium.rst | 5 +++-- idstools/scripts/bin/plotequilibrium | 17 +++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/source/plotequilibrium.rst b/docs/source/plotequilibrium.rst index a5f3325b..e0df3b6f 100644 --- a/docs/source/plotequilibrium.rst +++ b/docs/source/plotequilibrium.rst @@ -22,7 +22,8 @@ shows pf coils position and toroidal flux. $ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3" --rho -md pf_active wall --plots $ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3" --rho -md "imas:mdsplus?user=public;pulse=111001;run=103;database=ITER_MD;version=3#pf_active" "imas:mdsplus?user=public;pulse=116000;run=4;database=ITER_MD;version=3#wall" $ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134173;run=2326;database=TEST;version=3" --rho --md "imas:mdsplus?user=public;pulse=111001;run=103;database=ITER_MD;version=3#pf_active" "imas:hdf5?user=public;pulse=116000;run=4;database=ITER_MD;version=3#wall" - + $ plotequilibrium --uri "imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/100507/5" --md "imas:hdf5?path=/work/imas/shared/imasdb/ITER_MD/3/116000/5#wall" -p --no-provenance + .. image:: _static/images/plotequilibrium.png :alt: image not found :align: center @@ -33,4 +34,4 @@ shows pf coils position and toroidal flux. .. image:: _static/images/plotequilibrium3.png :alt: image not found - :align: center \ No newline at end of file + :align: center diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index d3b2381a..ff971efb 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -79,6 +79,11 @@ if __name__ == "__main__": "testpulse.nc" """, ) + parser.add_argument( + "--no-provenance", + help="Hide URI provenance information from the plot title", + action="store_true", + ) parser.add_argument( "--debug", @@ -131,7 +136,6 @@ if __name__ == "__main__": time_slice, time_value = get_nearest_time(ids_obj_equilibrium.time, args.time) view_object = EquilibriumView(ids_obj_equilibrium) - database_text = "" if args.plots: compute_obj = EquilibriumCompute(ids_obj_equilibrium) profiles_1d_quantities = compute_obj.get_profiles_1d_quantities(time_slice, ["pressure", "q", "beta_pol"]) @@ -262,11 +266,12 @@ if __name__ == "__main__": if args.overlay: create_overlays(show_legend=not args.plots) - title = get_database_path(args, time_value=time_value) - if md_overlay_state["provenance_lines"]: - title += "\n" + "\n".join(md_overlay_state["provenance_lines"]) - if database_text: - title += f"\n{database_text}" + title_lines = [] + if not args.no_provenance: + title_lines.append(get_database_path(args, time_value=time_value)) + if md_overlay_state["provenance_lines"]: + title_lines.extend(md_overlay_state["provenance_lines"]) + title = "\n".join(title_lines) canvas.set_sup_title(title, fontsize=8, y=0.985) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) From 301de7f08021680a004a72854eaa4502117c47aa Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 19 Jun 2026 15:45:39 +0200 Subject: [PATCH 32/35] fixed units, -p to --profiles,remove y axis labels --- docs/source/plotequilibrium.rst | 4 ++-- idstools/compute/equilibrium.py | 2 +- idstools/scripts/bin/plotequilibrium | 21 ++++++++++----------- idstools/view/equilibrium.py | 14 ++++++-------- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/docs/source/plotequilibrium.rst b/docs/source/plotequilibrium.rst index e0df3b6f..4f195190 100644 --- a/docs/source/plotequilibrium.rst +++ b/docs/source/plotequilibrium.rst @@ -19,10 +19,10 @@ shows pf coils position and toroidal flux. .. code-block:: bash - $ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3" --rho -md pf_active wall --plots + $ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3" --rho -md pf_active wall --profiles $ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3" --rho -md "imas:mdsplus?user=public;pulse=111001;run=103;database=ITER_MD;version=3#pf_active" "imas:mdsplus?user=public;pulse=116000;run=4;database=ITER_MD;version=3#wall" $ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134173;run=2326;database=TEST;version=3" --rho --md "imas:mdsplus?user=public;pulse=111001;run=103;database=ITER_MD;version=3#pf_active" "imas:hdf5?user=public;pulse=116000;run=4;database=ITER_MD;version=3#wall" - $ plotequilibrium --uri "imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/100507/5" --md "imas:hdf5?path=/work/imas/shared/imasdb/ITER_MD/3/116000/5#wall" -p --no-provenance + $ plotequilibrium --uri "imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/100507/5" --md "imas:hdf5?path=/work/imas/shared/imasdb/ITER_MD/3/116000/5#wall" --profiles --no-provenance .. image:: _static/images/plotequilibrium.png :alt: image not found diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index 46610bbd..84305d69 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -1428,7 +1428,7 @@ def get_global_quantities(self, time_slice=None, attributes=None): node = eval(f"self.ids.time_slice[{ti}].global_quantities.{attribute}") if info_flag: quantities[attribute]["unit"] = node.metadata.units - quantities[attribute]["coordinate_unit"] = "t" + quantities[attribute]["coordinate_unit"] = self.ids.time.metadata.units or "s" quantities[attribute]["name"] = node.metadata.name quantities[attribute]["coordinate_name"] = "time" diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index ff971efb..89cc3f76 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# python scripts/plotequilibrium -p 134174 -r 117 +# python scripts/plotequilibrium --uri --profiles # -md "imas:mdsplus?user=public;shot=116000;run=2;database=ITER_MD;version=3#wall" # "imas:mdsplus?user=public;shot=111001;run=102;database=ITER_MD;version=3#pf_active" @@ -59,9 +59,8 @@ if __name__ == "__main__": default=True, ) parser.add_argument( - "-p", - "--plots", - help="Plots available quantities along with equilibrium", + "--profiles", + help="Plot available 1D profiles and time traces alongside the equilibrium", action="store_true", ) parser.add_argument( @@ -136,7 +135,7 @@ if __name__ == "__main__": time_slice, time_value = get_nearest_time(ids_obj_equilibrium.time, args.time) view_object = EquilibriumView(ids_obj_equilibrium) - if args.plots: + if args.profiles: compute_obj = EquilibriumCompute(ids_obj_equilibrium) profiles_1d_quantities = compute_obj.get_profiles_1d_quantities(time_slice, ["pressure", "q", "beta_pol"]) p1dcounter = sum(1 for value in profiles_1d_quantities.values() if value.has_value) @@ -236,16 +235,16 @@ if __name__ == "__main__": cbar_rho.ax.set_title(r"$\rho$", fontsize=7, pad=4) cbar_rho.ax.tick_params(labelsize=7) - if args.plots: + if args.profiles: view_object.plot_profiles_1d_quantities(axes_list1, time_slice) view_object.plot_global_quantities(axes_list2, time_value) - if args.plots: + if args.profiles: canvas.fig.set_size_inches(10 + col_size * 1.6, 8) - canvas.fig.subplots_adjust(top=0.933, bottom=0.100, left=0.048, right=0.988, hspace=0.221, wspace=0.20) + canvas.fig.subplots_adjust(top=0.933, bottom=0.100, left=0.05, right=0.955, hspace=0.221, wspace=0.20) else: canvas.fig.set_size_inches(14, 8) - canvas.fig.subplots_adjust(top=0.933, bottom=0.100, left=0.048, right=0.988, hspace=0.221, wspace=0.25) + canvas.fig.subplots_adjust(top=0.933, bottom=0.100, left=0.05, right=0.955, hspace=0.221, wspace=0.25) def create_overlays(show_legend=True): plot_md_overlay() @@ -256,7 +255,7 @@ if __name__ == "__main__": plot_current_centre=True, plot_boundary_data=True, plot_rho=False, - plot_annotations=not args.plots, + plot_annotations=not args.profiles, plot_psi=False, ) legend = ax1.get_legend() @@ -264,7 +263,7 @@ if __name__ == "__main__": legend.set_visible(show_legend) if args.overlay: - create_overlays(show_legend=not args.plots) + create_overlays(show_legend=not args.profiles) title_lines = [] if not args.no_provenance: diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 7150c924..9449db98 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -556,13 +556,12 @@ def plot_profiles_1d_quantities(self, axes_list, time_slice, attributes=None): coordinate_normalized = (psi - psi_first) / (psi_last - psi_first) axes_list[counter].plot( - coordinate_normalized, copied_field, label=f"{field.metadata.name} ({field.metadata.units})" + coordinate_normalized, copied_field, label=f"{field.metadata.name} [{field.metadata.units}]" ) if coordinate.metadata.name == "psi": - axes_list[counter].set_xlabel(f"{coordinate.metadata.name} (normalized)") + axes_list[counter].set_xlabel(f"{coordinate.metadata.name}_norm [1]") else: - axes_list[counter].set_xlabel(f"{coordinate.metadata.name} ({coordinate.metadata.units})") - axes_list[counter].set_ylabel(name) + axes_list[counter].set_xlabel(f"{coordinate.metadata.name} [{coordinate.metadata.units}]") axes_list[counter].legend(loc="upper right") counter = counter + 1 @@ -576,11 +575,10 @@ def plot_global_quantities(self, axes_list, time_slice, attributes=None): field["node"][field["node"] == imas.ids_defs.EMPTY_FLOAT] = np.nan if field["has_value"]: if len(field["node"]) < 5: - axes_list[counter].scatter(field["coordinate"], field["node"], label=f"{name} ({field['unit']})") + axes_list[counter].scatter(field["coordinate"], field["node"], label=f"{name} [{field['unit']}]") else: - axes_list[counter].plot(field["coordinate"], field["node"], label=f"{name} ({field['unit']})") - axes_list[counter].set_xlabel(f"{field['coordinate_name']} ({field['coordinate_unit']})") - axes_list[counter].set_ylabel(name) + axes_list[counter].plot(field["coordinate"], field["node"], label=f"{name} [{field['unit']}]") + axes_list[counter].set_xlabel(f"{field['coordinate_name']} [{field['coordinate_unit']}]") self.view_time_line(axes_list[counter], time_slice) axes_list[counter].legend(loc="upper right") counter = counter + 1 From 7745398d1c8c19a7079ef89fdabcb9831bd1ab60 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 19 Jun 2026 16:08:11 +0200 Subject: [PATCH 33/35] plot machine description with psi when using --no-overlay -md --- idstools/scripts/bin/plotequilibrium | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 89cc3f76..175ca78f 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -53,10 +53,9 @@ if __name__ == "__main__": ) parser.add_argument( "--no-overlay", - dest="overlay", + dest="no_overlay", help="Hide equilibrium overlays", - action="store_false", - default=True, + action="store_true", ) parser.add_argument( "--profiles", @@ -207,9 +206,6 @@ if __name__ == "__main__": md_overlay_state["created"] = True return True - if args.save: - plot_md_overlay() - c_psi, c_rho = view_object.view_magnetic_poloidal_flux( ax1, time_slice, @@ -235,6 +231,8 @@ if __name__ == "__main__": cbar_rho.ax.set_title(r"$\rho$", fontsize=7, pad=4) cbar_rho.ax.tick_params(labelsize=7) + plot_md_overlay() + if args.profiles: view_object.plot_profiles_1d_quantities(axes_list1, time_slice) view_object.plot_global_quantities(axes_list2, time_value) @@ -262,7 +260,11 @@ if __name__ == "__main__": if legend is not None: legend.set_visible(show_legend) - if args.overlay: + if args.no_overlay: + legend = ax1.get_legend() + if legend is not None: + legend.set_visible(not args.profiles) + else: create_overlays(show_legend=not args.profiles) title_lines = [] From aaec5d80e2e36b2eaaa5b9e63e4944495358c635 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 19 Jun 2026 17:13:49 +0200 Subject: [PATCH 34/35] fix multiple URIs to single one if IDSes are coming from same data entry for machine description --- idstools/scripts/bin/plotequilibrium | 10 ++++--- idstools/scripts/bin/plotmachinedescription | 6 ++--- idstools/view/domain/mdplot.py | 30 ++++++++++----------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 175ca78f..39007f67 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -267,12 +267,14 @@ if __name__ == "__main__": else: create_overlays(show_legend=not args.profiles) - title_lines = [] + provenance_parts = [] if not args.no_provenance: - title_lines.append(get_database_path(args, time_value=time_value)) + provenance_parts.append(get_database_path(args).strip()) if md_overlay_state["provenance_lines"]: - title_lines.extend(md_overlay_state["provenance_lines"]) - title = "\n".join(title_lines) + provenance_parts.extend(md_overlay_state["provenance_lines"]) + title = " | ".join(provenance_parts) + if title and time_value is not None: + title += f"\n#time:{time_value:.3f}" canvas.set_sup_title(title, fontsize=8, y=0.985) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) diff --git a/idstools/scripts/bin/plotmachinedescription b/idstools/scripts/bin/plotmachinedescription index dab5f62e..cab0441e 100644 --- a/idstools/scripts/bin/plotmachinedescription +++ b/idstools/scripts/bin/plotmachinedescription @@ -69,12 +69,10 @@ if __name__ == "__main__": mdcanvas.update_style(args.rc) ax = mdcanvas.add_axes(title="", xlabel="R (m)", ylabel="Z (m)", row=0, col=0) md_provenance = plot_machine_description(ax, ids_data) + ax.set_title("") mdcanvas.fig.subplots_adjust(top=0.916, bottom=0.09, left=0.044, right=0.953, hspace=0.287, wspace=0.2) mdcanvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - suptitle = "Machine Description" - if md_provenance: - suptitle += "\n" + md_provenance - mdcanvas.set_sup_title(suptitle, fontsize=8) + mdcanvas.set_sup_title(md_provenance, fontsize=8) if args.save: fname = os.path.basename(__file__) + "_machine_description.png" if args.directory: diff --git a/idstools/view/domain/mdplot.py b/idstools/view/domain/mdplot.py index a46642ab..7e6a338a 100644 --- a/idstools/view/domain/mdplot.py +++ b/idstools/view/domain/mdplot.py @@ -46,14 +46,16 @@ def plot_machine_description(ax, ids_data, main_uri=None): from the provenance text (to avoid duplication when MD and main data share a URI). """ - database_path = "" + provenance_groups = {} - def _provenance_line(ids_name_label, connection_args): - """Return a provenance line for this IDS, or empty string if URI matches main_uri.""" + def _add_provenance(ids_name_label, connection_args): + """Group subsystem labels that originate from the same data-entry URI.""" uri = get_database_path(connection_args).strip() - if main_uri is not None and uri.strip() == main_uri.strip(): - return "" - return f"{ids_name_label} = {uri}\n" + if main_uri is not None and uri == main_uri.strip(): + return + labels = provenance_groups.setdefault(uri, []) + if ids_name_label not in labels: + labels.append(ids_name_label) mdlegends = [] mdlabels = [] @@ -80,7 +82,7 @@ def _provenance_line(ids_name_label, connection_args): if _legend: mdlegends.append(_legend) mdlabels.append(f"pf_active:{idsocc}/coil[{select}]") - database_path += _provenance_line("pf_active", ids_data_and_config["connectionArgs"]) + _add_provenance("pf_active", ids_data_and_config["connectionArgs"]) elif ids_name == "tf": select2 = ":" if len(matches) == 2: @@ -91,7 +93,7 @@ def _provenance_line(ids_name_label, connection_args): if _legend: mdlegends.append(_legend) mdlabels.append(f"tf:{idsocc}/coil[{select}]/conductor[{select2}]") - database_path += _provenance_line("tf", ids_data_and_config["connectionArgs"]) + _add_provenance("tf", ids_data_and_config["connectionArgs"]) elif ids_name == "pf_passive": pfpassiveview = PFPassiveView(ids_data_and_config["idsData"]) if "loop" in idsfield or idsfield == "": @@ -100,7 +102,7 @@ def _provenance_line(ids_name_label, connection_args): mdlegends.append(_legend) mdlabels.append(f"pf_passive:{idsocc}/loop[{select}]") - database_path += _provenance_line("pf_passive", ids_data_and_config["connectionArgs"]) + _add_provenance("pf_passive", ids_data_and_config["connectionArgs"]) elif ids_name == "wall": wallview = WallView(ids_data_and_config["idsData"]) select2 = ":" @@ -110,7 +112,7 @@ def _provenance_line(ids_name_label, connection_args): wallview.view_wall_vessel(ax, select_description2d=select, select_unit=select2) if "limiter" in idsfield or idsfield == "": wallview.view_wall_limiter(ax, select_description2d=select, select_unit=select2) - database_path += _provenance_line("wall", ids_data_and_config["connectionArgs"]) + _add_provenance("wall", ids_data_and_config["connectionArgs"]) elif ids_name == "magnetics": magnetics_view = MagneticsView(ids_data_and_config["idsData"]) if "b_field_phi_probe" in idsfield or idsfield == "": @@ -138,11 +140,9 @@ def _provenance_line(ids_name_label, connection_args): if _legend: mdlegends.append(_legend) mdlabels.append(f"magnetics:{idsocc}/shunt[{select}]") - database_path += _provenance_line("magnetics", ids_data_and_config["connectionArgs"]) + _add_provenance("magnetics", ids_data_and_config["connectionArgs"]) else: - database_path += _provenance_line(ids_name, ids_data_and_config["connectionArgs"]).replace( - "\n", " No visualization yet\n", 1 - ) + _add_provenance(f"{ids_name} (No visualization yet)", ids_data_and_config["connectionArgs"]) logger.info(f"Visualization is not implemented yet for machine description {ids_name}") handles, labels = ax.get_legend_handles_labels() @@ -161,4 +161,4 @@ def _provenance_line(ids_name_label, connection_args): # ax.callbacks.connect("ylim_changed", update_labels) ax.plot() - return database_path.strip() + return " | ".join(f"{', '.join(labels)} = {uri}" for uri, labels in provenance_groups.items()) From 25954044b7cd8609f3ccf99a3b86dff724c30891 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 22 Jun 2026 09:40:08 +0200 Subject: [PATCH 35/35] show boundary/outline if present --- idstools/scripts/bin/plotequilibrium | 1 + idstools/view/equilibrium.py | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 39007f67..b19895c3 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -252,6 +252,7 @@ if __name__ == "__main__": plot_magnetic_axis=True, plot_current_centre=True, plot_boundary_data=True, + plot_boundary_outline=True, plot_rho=False, plot_annotations=not args.profiles, plot_psi=False, diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 9449db98..95bc72f2 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -47,6 +47,7 @@ def view_magnetic_poloidal_flux( plot_rho: bool = False, plot_annotations: bool = True, plot_psi: bool = True, + plot_boundary_outline: bool = False, ): """ This function plots the magnetic poloidal flux contours on a 2D Cartesian grid. @@ -183,12 +184,37 @@ def view_magnetic_poloidal_flux( ) overlay_entries.append((proxy_cc, [marker])) + if plot_boundary_outline: + bd = self.compute_obj.get_boundary_data(time_slice) + if bd["bnd_r"] is not None and bd["bnd_z"] is not None: + (boundary_line,) = ax.plot( + bd["bnd_r"], + bd["bnd_z"], + color="royalblue", + linewidth=2.0, + linestyle="-", + zorder=5, + ) + proxy_boundary = ProxyLine( + [0], [0], color="royalblue", linewidth=2.0, linestyle="-", label="boundary/outline" + ) + overlay_entries.append((proxy_boundary, [boundary_line])) + if plot_boundary_data: bd = self.compute_obj.get_boundary_data(time_slice) + separatrix_is_boundary = ( + bd["bnd_r"] is not None + and bd["bnd_z"] is not None + and bd["sep_r"] is not None + and bd["sep_z"] is not None + and np.array_equal(np.asarray(bd["sep_r"]), np.asarray(bd["bnd_r"])) + and np.array_equal(np.asarray(bd["sep_z"]), np.asarray(bd["bnd_z"])) + ) + # boundary_separatrix outline - if bd["sep_r"] is not None and bd["sep_z"] is not None: + if bd["sep_r"] is not None and bd["sep_z"] is not None and not separatrix_is_boundary: (sep_line,) = ax.plot( bd["sep_r"], bd["sep_z"],