From 54f15ae78623226f0c1ddf5e87ecde16752f1e12 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Tue, 17 Mar 2026 09:29:46 +0100 Subject: [PATCH 01/56] np.Nan->np.nan --- idstools/compute/summary.py | 2 +- idstools/domain/ecstray.py | 4 +-- idstools/domain/kineticprofiles.py | 54 +++++++++++++++--------------- idstools/utils/idshelper.py | 2 +- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/idstools/compute/summary.py b/idstools/compute/summary.py index 0847002b..f45277c4 100644 --- a/idstools/compute/summary.py +++ b/idstools/compute/summary.py @@ -90,7 +90,7 @@ def get_summary( # The `energy_mhd` variable in the `get_summary` method is used to retrieve the "energy_mhd" # waveform values from the `global_quantities` of the summary ids object. If the length of the # retrieved values is less than 1, it logs a critical error message indicating that the data - # could not be read and assigns an array of NaN values with the same length as the time vector + # could not be read and assigns an array of nan values with the same length as the time vector # to the `energy_mhd` variable. This ensures that the `energy_mhd` waveform data is handled # properly even in the case of missing or unreadable data. energy_mhd = self.ids.global_quantities.energy_mhd.value diff --git a/idstools/domain/ecstray.py b/idstools/domain/ecstray.py index 4824f9e4..0dec54fd 100644 --- a/idstools/domain/ecstray.py +++ b/idstools/domain/ecstray.py @@ -177,8 +177,8 @@ def get_cutoff_layer(self, coherent_wave_index, time_slice): ) + constants.e * b_total[ir, iz] / (2 * constants.m_e) except Exception as e: # Not defined outside LCFS logger.debug(f"{e}") - ne2d_eq[ir, iz] = -1 # np.NaN - omega_r[ir, iz] = -1 # np.NaN + ne2d_eq[ir, iz] = -1 # np.nan + omega_r[ir, iz] = -1 # np.nan # Find (R,Z) where omega_R = omega_EC (within the tolerance omega_err) [nr, nz] = np.shape(omega_r) diff --git a/idstools/domain/kineticprofiles.py b/idstools/domain/kineticprofiles.py index 80081501..6d39b90a 100644 --- a/idstools/domain/kineticprofiles.py +++ b/idstools/domain/kineticprofiles.py @@ -787,7 +787,7 @@ def get_zeff_profile(self): if len(_zeff) != self.nrho: logger.warning("core_profiles.profiles_1d[:].zeff could not be read.") logger.warning(f"Size mismatch: rho_tor_norm = {self.nrho}, " f"zeff = {len(_zeff)}") - _zeff = np.asarray([np.NaN] * self.nrho) + _zeff = np.asarray([np.nan] * self.nrho) for i in range(self.nrho): zeff[i] = _zeff[i] if self.is_edge_profiles_present: @@ -795,14 +795,14 @@ def get_zeff_profile(self): _zeff = self.edge_profiles.profiles_1d[self.time_index_edge_profiles].zeff.value if len(_zeff) < 1: logger.warning("edge_profiles.profiles_1d[:].zeff could not be read.") - _zeff = np.asarray([np.NaN] * self.erho) + _zeff = np.asarray([np.nan] * self.erho) for i in range(self.erho): zeff[self.nrho + i] = _zeff[i] else: _gset = self.edge_profiles.ggd[self.time_index_edge_profiles].zeff[self.gset].values if len(_gset) < 1: logger.warning("edge_profiles.ggd[:].zeff could not be read.") - _gset = np.asarray([np.NaN] * self.erho) + _gset = np.asarray([np.nan] * self.erho) for i in range(self.erho): zeff[self.nrho + i] = _gset[i] return zeff @@ -814,7 +814,7 @@ def getne_profile(self): Returns: The function `getne_profile` returns a list of electron density values. The electron density values are collected from different sources based on the conditions specified in the function. - The returned list contains electron density values for both core and edge profiles, with NaN + The returned list contains electron density values for both core and edge profiles, with nan values filled in case of missing data. """ electron_density = [0] * (self.nrho + self.erho) @@ -823,7 +823,7 @@ def getne_profile(self): if len(_density) != self.nrho: logger.warning("core_profiles.profiles_1d[:].electrons.density could not be read.") logger.warning(f"Size mismatch: rho_tor_norm = {self.nrho}, electrons.density =" f"{len(_density)}") - _density = np.asarray([np.NaN] * self.nrho) + _density = np.asarray([np.nan] * self.nrho) for i in range(self.nrho): electron_density[i] = _density[i] if self.is_edge_profiles_present: @@ -831,14 +831,14 @@ def getne_profile(self): _density = self.core_profiles.profiles_1d[self.time_index_edge_profiles].electrons.density.value if len(_density) < 1: logger.warning("edge_profiles.profiles_1d[:].electrons.density could not be read.") - _density = np.asarray([np.NaN] * self.erho) + _density = np.asarray([np.nan] * self.erho) for i in range(self.erho): electron_density[self.nrho + i] = _density[i] else: _gset = self.edge_profiles.ggd[self.time_index_edge_profiles].electrons.density[self.gset].values if len() < 1: logger.warning("edge_profiles.ggd[:].electrons.density could not be read.") - _gset = np.asarray([np.NaN] * self.erho) + _gset = np.asarray([np.nan] * self.erho) for i in range(self.erho): electron_density[self.nrho + i] = _gset[i] return electron_density @@ -862,7 +862,7 @@ def gette_profile(self): logger.warning( f"Size mismatch: rho_tor_norm = {self.nrho}, electrons.temperature = " f"{len(_temperature)}" ) - _temperature = np.asarray([np.NaN] * self.nrho) + _temperature = np.asarray([np.nan] * self.nrho) for i in range(self.nrho): electron_temperature[i] = _temperature[i] * 1.0e-3 if self.is_edge_profiles_present: @@ -870,14 +870,14 @@ def gette_profile(self): _temperature = self.edge_profiles.profiles_1d[self.time_index_edge_profiles].electrons.temperature if len(_temperature) < 1: logger.warning("edge_profiles.profiles_1d[:].electrons.temperature could not be read.") - _temperature = np.asarray([np.NaN] * self.erho) + _temperature = np.asarray([np.nan] * self.erho) for i in range(self.erho): electron_temperature[self.nrho + i] = _temperature[i] * 1.0e-3 else: _gset = self.edge_profiles.ggd[self.time_index_edge_profiles].electrons.temperature[self.gset].values if len() < 1: logger.warning("edge_profiles.ggd[:].electrons.temperature could not be read.") - _gset = np.asarray([np.NaN] * self.erho) + _gset = np.asarray([np.nan] * self.erho) for i in range(self.erho): electron_temperature[self.nrho + i] = _gset[i] * 1.0e-3 return electron_temperature @@ -897,7 +897,7 @@ def getti_flag(self): if len(t_i_average) != self.nrho: logger.warning("core_profiles.profiles_1d[:].t_i_average could not be read.") logger.warning(f"Size mismatch: rho_tor_norm = {self.nrho}, t_i_average = " f"{len(t_i_average)}") - t_i_average = np.asarray([np.NaN] * self.nrho) + t_i_average = np.asarray([np.nan] * self.nrho) else: ti_flag = 1 ti_e_flag = 0 @@ -907,14 +907,14 @@ def getti_flag(self): t_i_average = self.edge_profiles.profiles_1d[self.time_index_edge_profiles].t_i_average.value if len() < 1: logger.warning("edge_profiles.profiles_1d[:].t_i_average could not be read.") - t_i_average = np.asarray([np.NaN] * self.erho) + t_i_average = np.asarray([np.nan] * self.erho) else: ti_e_flag = 1 else: t_i_average = self.edge_profiles.ggd[self.time_index_edge_profiles].t_i_average[self.gset].values if len() < 1: logger.warning("edge_profiles.ggd[:].t_i_average could not be read.") - t_i_average = np.asarray([np.NaN] * self.erho) + t_i_average = np.asarray([np.nan] * self.erho) else: ti_e_flag = 1 @@ -927,7 +927,7 @@ def getti_flag(self): logger.warning( f"Size mismatch: rho_tor_norm = {self.nrho}, ion[{ispecies}].temperature = " f"{len(temp)}" ) - temp = np.asarray([np.NaN] * self.nrho) + temp = np.asarray([np.nan] * self.nrho) else: ti_flag = 2 if self.is_edge_profiles_present and ti_e_flag == 0: @@ -944,7 +944,7 @@ def getti_flag(self): logger.warning( f"edge_profiles.profiles_1d[:].ion[{jspecies}].temperature could not be read." ) - temperature = np.asarray([np.NaN] * self.erho) + temperature = np.asarray([np.nan] * self.erho) else: ti_e_flag = 2 else: @@ -954,7 +954,7 @@ def getti_flag(self): if len(temperature) < 1: if ti_e_flag != 1: logger.warning("edge_profiles.ggd[:].ion[:].temperature could not be read.") - temperature[self.gset].values = np.asarray([np.NaN] * self.erho) + temperature[self.gset].values = np.asarray([np.nan] * self.erho) else: ti_e_flag = 2 @@ -1032,7 +1032,7 @@ def get_ion_density(self): logger.warning( f"Size mismatch: rho_tor_norm = {self.nrho}, ion[{ispecies}].density = " f"{len(density)}" ) - density = np.asarray([np.NaN] * self.nrho) + density = np.asarray([np.nan] * self.nrho) for i in range(self.nrho): ion_density[ispecies][i] = density[i] if self.is_edge_profiles_present: @@ -1055,7 +1055,7 @@ def get_ion_density(self): logger.warning( f"edge_profiles.profiles_1d[:].ion[{jspecies}].density could not be read." ) - _density = np.asarray([np.NaN] * self.erho) + _density = np.asarray([np.nan] * self.erho) for i in range(self.erho): ion_density[ispecies][self.nrho + i] = _density[i] else: @@ -1091,7 +1091,7 @@ def get_ion_density(self): ) if len() < 1: logger.warning(f"edge_profiles.ggd[:].ion[{jspecies}.density could not be read.") - _density = np.asarray([np.NaN] * self.erho) + _density = np.asarray([np.nan] * self.erho) for i in range(self.erho): ion_density[ispecies][self.nrho + i] = _density[i] else: @@ -1140,7 +1140,7 @@ def get_v_phi_profile(self): f"Size mismatch: rho_tor_norm = {self.nrho}, ion[{ispecies}].velocity.toroidal = " f"{len(vphioid)}" ) - vphioid = np.asarray([np.NaN] * self.nrho) + vphioid = np.asarray([np.nan] * self.nrho) else: vphi_flag = 1 for i in range(self.nrho): @@ -1152,7 +1152,7 @@ def get_v_phi_profile(self): logger.warning( f"Size mismatch: rho_tor_norm = {self.nrho}, ion[{ispecies}].velocity_tor = " f"{len(vphi)}" ) - vphi = np.asarray([np.NaN] * self.nrho) + vphi = np.asarray([np.nan] * self.nrho) else: if vphi_flag == 0: vphi_flag = 2 @@ -1346,7 +1346,7 @@ def get_vpol_profile(self): f"Size mismatch: rho_tor_norm = {self.nrho}, ion[{ispecies}].velocity.poloidal =" f"{len(vpoloidal)}" ) - vpoloidal = np.asarray([np.NaN] * self.nrho) + vpoloidal = np.asarray([np.nan] * self.nrho) else: vpol_flag = 1 for i in range(self.nrho): @@ -1358,7 +1358,7 @@ def get_vpol_profile(self): logger.warning( f"Size mismatch: rho_tor_norm = {self.nrho}, ion[{ispecies}].velocity_pol = " f"{len(vpol)}" ) - vpol = np.asarray([np.NaN] * self.nrho) + vpol = np.asarray([np.nan] * self.nrho) else: if vpol_flag == 0: vpol_flag = 2 @@ -1920,7 +1920,7 @@ def get_waveform(self): # ) except Exception as e: logger.debug(f"{e}") - waveform["ti"]["central"] = [np.NaN] * len(self.time_array_core_profiles) + waveform["ti"]["central"] = [np.nan] * len(self.time_array_core_profiles) electrons_density = np.array([]) for i, _ in enumerate(self.time_array_core_profiles): @@ -2021,13 +2021,13 @@ def get_waveform(self): except Exception as e: logger.debug(f"{e}") waveform["n_species"][self.species[ispecies]]["density"]["central"] = [ - np.NaN + np.nan ] * self.common_time_length waveform["n_species"][self.species[ispecies]]["vpol"]["central"] = [ - np.NaN + np.nan ] * self.common_time_length waveform["n_species"][self.species[ispecies]]["vphi"]["central"] = [ - np.NaN + np.nan ] * self.common_time_length for itime in range(self.common_time_length): diff --git a/idstools/utils/idshelper.py b/idstools/utils/idshelper.py index aed61e8a..c4b0c6e7 100644 --- a/idstools/utils/idshelper.py +++ b/idstools/utils/idshelper.py @@ -439,7 +439,7 @@ def get_available_ids_and_times(db_entry_object) -> list: if homogeneous_time == imas.ids_defs.IDS_TIME_MODE_UNKNOWN: time_array = [] if homogeneous_time == imas.ids_defs.IDS_TIME_MODE_HETEROGENEOUS: - time_array = [np.NaN] + time_array = [np.nan] if homogeneous_time == imas.ids_defs.IDS_TIME_MODE_HOMOGENEOUS: if getattr(ids_object, "time", None): time_array = ids_object.time.value From cb47065527fa4248b5eeb4b5b131ae598b56b196 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 26 Mar 2026 17:27:13 +0100 Subject: [PATCH 02/56] 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 837a4df79cbd537f5949a484aea0ad1d3e3fcf4d Mon Sep 17 00:00:00 2001 From: Prasad Date: Thu, 26 Mar 2026 17:36:51 +0100 Subject: [PATCH 03/56] Merge pull request #18 from prasad-sawantdesai/fix/plotequicomp_issues_with_mastu_data Improve failsafe for plotequicomp when data is missing --- idstools/compute/equilibrium.py | 9 ++++++--- idstools/view/equilibrium.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index d103121b..5f7d440e 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -1155,6 +1155,9 @@ def get_equilibria(self, selection=None): name = self.ids.code.name if homogeneous_time == 1: time = self.ids.time + else: + # Extract time from time_slice array + time = np.array([ts.time for ts in self.ids.time_slice]) if len(self.ids.time_slice) > 0 else np.array([]) nt = time.size data = {} @@ -1378,10 +1381,10 @@ def get_equilibria(self, selection=None): z = time_slice.profiles_2d[0].grid.dim2 # Initialize boundary arrays - each time slice can have different size - if need_boundaries and n3 > 0: - rb = [] if "rb" in selection else None - zb = [] if "zb" in selection else None + rb = [] if "rb" in selection else None + zb = [] if "zb" in selection else None + if need_boundaries: for i, time_slice in enumerate(self.ids.time_slice): if time_slice.boundary.outline.r.size > 0 and rb is not None: rb.append(time_slice.boundary.outline.r) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 2416967e..e3027274 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -424,7 +424,9 @@ def view_profile_plot(self, ax, time_index1, equilibrium2_ids=None, time_index2= logger.warning(f"Equilibrium1 {name}: No valid r or jtor data available") # Set ylim with check for identical values to avoid matplotlib warning - if abs(y_max - y_min) < 1e-10: + if not np.isfinite(y_min) or not np.isfinite(y_max): + y_min, y_max = -1.0, 1.0 + elif abs(y_max - y_min) < 1e-10: # If y_min and y_max are essentially equal, create a small range around the value if abs(y_min) < 1e-10: # If both are near zero, use a default range @@ -619,14 +621,16 @@ def view_current_plot(self, ax, time_index1, equilibrium2_ids=None): line31.set_xdata(time) line31.set_ydata(ip / 1e6) xlims = np.array([min(time), max(time)]) - ylims = np.array([min(ip), max(ip)]) / 1e6 + ip_valid = ip[np.isfinite(ip)] + ylims = np.array([ip_valid.min(), ip_valid.max()]) / 1e6 if ip_valid.size > 0 else np.array([-1.0, 1.0]) if data2 is not None: line32.set_xdata(timeE) line32.set_ydata(ipE / 1e6) + ipE_valid = ipE[np.isfinite(ipE)] xlims[0] = np.minimum(xlims[0], np.min(timeE)) - ylims[0] = np.minimum(ylims[0], np.min(ipE / 1e6)) + ylims[0] = np.minimum(ylims[0], np.min(ipE_valid / 1e6)) if ipE_valid.size > 0 else ylims[0] xlims[1] = np.maximum(xlims[1], np.max(timeE)) - ylims[1] = np.maximum(ylims[1], np.max(ipE / 1e6)) + ylims[1] = np.maximum(ylims[1], np.max(ipE_valid / 1e6)) if ipE_valid.size > 0 else ylims[1] dy = ylims[1] - ylims[0] if dy > 0: ylims[0] = ylims[0] - 0.01 * dy From 7bc37fa6323abf0864fe668be257e644f6e14ba6 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 27 Mar 2026 12:35:24 +0100 Subject: [PATCH 04/56] 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 05/56] 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 06/56] 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 07/56] 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 08/56] 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 09/56] 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 10/56] 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 11/56] 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 12/56] 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 13/56] 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 14/56] 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 15/56] 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 16/56] 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 17/56] 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 d3e566c5e992431269be34ddbe1bb51c62298c1b Mon Sep 17 00:00:00 2001 From: Prasad Date: Wed, 1 Apr 2026 14:07:01 +0200 Subject: [PATCH 18/56] fix: correct psi 2D grid orientation for rectangular equilibrium grids (#17) --- idstools/compute/equilibrium.py | 62 ++++++++++++--------------------- idstools/view/equilibrium.py | 10 +++++- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index 5f7d440e..f1903828 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -41,73 +41,57 @@ def get2d_cartesian_grid(self, time_slice: int, profiles2d_index: int = 0) -> Un This function returns a dictionary containing 2D Cartesian grid coordinates and psi values from an equilibrium IDS object. - Args: time_slice (int): The time slice index of the equilibrium data to be used for generating the - 2D Cartesian grid. Defaults to 0 - profiles2d_index (int): `profiles2d_index` is an integer parameter that represents the index of the - ``profile_2d`` to be used in the calculation. It is used to access the specific 2D profile from the - list of profiles in the `time_slice` object. Defaults to 0 + 2D Cartesian grid. Defaults to 0 + profiles2d_index (int): An integer parameter that represents the index of the + ``profiles_2d`` to be used in the calculation. It is used to access the specific 2D profile from the + list of profiles in the `time_slice` object. Defaults to 0 Returns: A dictionary containing the 2D Cartesian grid coordinates (r2d and z2d) and the corresponding psi - values (psi2d). + values (psi2d), or None if the data is unavailable or invalid. Example: .. code-block:: python - import imas - connection = imas.DBEntry("imas:mdsplus?user=public;pulse=134173;run=106;database=ITER;version=3","r") - idsObj = connection.get('equilibrium') - computeObj = EquilibriumCompute(idsObj) - result = computeObj.get2d_cartesian_grid(time_slice=0) + import imas + connection = imas.DBEntry("imas:mdsplus?user=public;pulse=134173;run=106;database=ITER;version=3","r") + idsObj = connection.get('equilibrium') + computeObj = EquilibriumCompute(idsObj) + result = computeObj.get2d_cartesian_grid(time_slice=0) - {'psi2d': array([[]]), - 'r2d': array([[]]), - 'z2d': array([[]])} + {'r2d': array([...]), 'z2d': array([...]), 'psi2d': array([...])} """ - profiles2d = None + profiles2d = r1d = z1d = None try: - profiles2d = self.ids.time_slice[time_slice].profiles_2d[ - profiles2d_index - ] # using https://docs.python.org/2/glossary.html#term-eafp style + profiles2d = self.ids.time_slice[time_slice].profiles_2d[profiles2d_index] except IndexError: logger.error(f"equilibrium.time_slice[{time_slice}].profiles_2d[{profiles2d_index}] is not available") return None profiles2d = self.ids.time_slice[time_slice].profiles_2d[profiles2d_index] - r2d = profiles2d.r - z2d = profiles2d.z - psi2d = profiles2d.psi - if profiles2d.grid_type.index == 1 and np.size(r2d) == 0: - logger.warning( - f"profiles_2d[{profiles2d_index}].r is not available and grid type is 1.. Calculating from grid" + if profiles2d.grid_type.index == 1 and profiles2d.grid.dim1 is not None and profiles2d.grid.dim2 is not None: + logger.info( + f"Using equilibrium.time_slice[{time_slice}]" + f".profiles_2d[{profiles2d_index}].grid.dim1/dim2 for the 2D grid" ) r1d = profiles2d.grid.dim1 z1d = profiles2d.grid.dim2 - nr = len(r1d) - nz = len(z1d) - r2d = np.empty(shape=(nr, nz)) - z2d = np.empty(shape=(nr, nz)) - for iz in range(nz): - r2d[:, iz] = r1d - for ir in range(nr): - z2d[ir, :] = z1d + else: + logger.error("Only rectangular cylindrical grid (grid_type=1) is supported for now") + return None + + psi2d = profiles2d.psi if np.all(psi2d == 0.0): logger.error( "All values of psi2d are 0. No contour levels were found within the data range, Can not plot contour" ) return None - if np.size(r2d) != np.size(z2d) or np.size(r2d) != np.size(psi2d): - logger.error( - f"r, z and psi have not the same dimension in \ - equilibrium.time_slice[{time_slice}].profiles_2d[{profiles2d_index}]" - ) - return None - return {"r2d": r2d, "z2d": z2d, "psi2d": psi2d} + return {"r2d": r1d, "z2d": z1d, "psi2d": psi2d} def get_rho2d(self, time_slice: int, profiles2d_index: int = 0) -> Union[np.ndarray, None]: """ diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index e3027274..c059b441 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -84,8 +84,16 @@ def view_magnetic_poloidal_flux( if cartestion_grid is not None: levels = 50 + # As per IMAS data dictionary psi is stored as [R, Z] with shape (N_R, N_Z). + # Check this reference : + # https://imas-data-dictionary.readthedocs.io/en/latest/generated/identifier/poloidal_plane_coordinates_identifier.html + + # For matplotlib contour as per the documentation: + # https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.contour.html + # matplotlib.contour(r, z, Zdata) expects Zdata as [rows=z, cols=r], so the shape must be (N_Z, N_R). + # Therefore we transpose psi before plotting. contour_lines_psi = ax.contour( - cartestion_grid["r2d"], cartestion_grid["z2d"], cartestion_grid["psi2d"], levels, cmap="summer" + cartestion_grid["r2d"], cartestion_grid["z2d"], cartestion_grid["psi2d"].T, levels, cmap="summer" ) # ax.clabel( # contour_lines_psi, From 61e1fb9c30b12534bd662a8cb260416b010f2648 Mon Sep 17 00:00:00 2001 From: Olivier Hoenen Date: Tue, 2 Jun 2026 16:17:46 +0200 Subject: [PATCH 19/56] add CODEOWNERS --- .github/CODEOWNERS | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..4a9b590c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# CODEOWNERS: Controls automatic assignment of reviewers for pull requests. +# ----------------------------------------------------------------------------- +# When a pull request modifies files matching a pattern, the listed code owners will be requested as reviewers. +# Each line is a file pattern followed by one or more code owners (users or teams) who must review changes to that file. +# Do not edit unless specifically mandated to do so. + +# Global/fallback and technical modifications. +* @SimonPinches @olivhoenen @prasad-sawantdesai From 204c94203c853ba7d11bf3c24f4164cef2ea6edb Mon Sep 17 00:00:00 2001 From: Prasad Date: Tue, 2 Jun 2026 16:48:30 +0200 Subject: [PATCH 20/56] fix ci plan on bamboo (#25) --- ci-sdcc/st00-header.sh | 2 +- ci-sdcc/utils.sh | 2 +- idstools/scripts/__init__.py | 6 ++++++ idstools/scripts/bin/dbperf | 31 ++++++++++++++++++++++++++++--- idstools/scripts/bin/idsresample | 2 +- idstools/view/equilibrium.py | 2 +- pyproject.toml | 1 + 7 files changed, 39 insertions(+), 7 deletions(-) diff --git a/ci-sdcc/st00-header.sh b/ci-sdcc/st00-header.sh index 5fa6ce35..72e2e8db 100644 --- a/ci-sdcc/st00-header.sh +++ b/ci-sdcc/st00-header.sh @@ -13,7 +13,7 @@ shopt -s expand_aliases #print hostname hostname -f -IMAS_EXISTS=$(module -r -t list 2>&1 | grep -E "IMAS-AL-Core|IMAS-Core" | head -n 1) +IMAS_EXISTS=$(module -r -t list 2>&1 | grep -E "IMAS-Core" | head -n 1) if [ -n "$IMAS_EXISTS" ]; then echo "> Found already loaded IMAS Module : $IMAS_EXISTS" ACCESS_LAYER_VERSION=$(echo "$AL_VERSION" | cut -d '.' -f 1) diff --git a/ci-sdcc/utils.sh b/ci-sdcc/utils.sh index 18708f25..101272f1 100644 --- a/ci-sdcc/utils.sh +++ b/ci-sdcc/utils.sh @@ -19,7 +19,7 @@ getIMASCoreModuleName() { DD_VERSION="$3" fi #Semantic versioning - IMASVERSIONSLIST=$(module -t avail IMAS-AL-Core/ 2>&1 | grep -E "^IMAS-AL-Core/$ACCESS_LAYER_VERSION\.[0-9]+\.[0-9]+-$TOOLCHAIN_VERSION") + IMASVERSIONSLIST=$(module -t avail IMAS-Core/ 2>&1 | grep -E "^IMAS-Core/$ACCESS_LAYER_VERSION\.[0-9]+\.[0-9]+-$TOOLCHAIN_VERSION") if [[ $TOOLCHAIN_VERSION == *"intel"* ]]; then CORE_MODULE_VERSION=$(echo "$IMASVERSIONSLIST" | grep "intel" | sort -rV | head -n 1) diff --git a/idstools/scripts/__init__.py b/idstools/scripts/__init__.py index a742386d..55d1dfd1 100644 --- a/idstools/scripts/__init__.py +++ b/idstools/scripts/__init__.py @@ -89,6 +89,11 @@ def plotcoresources(): _load_script_main("plotcoresources")() +def plotcoretransport(): + """Plot core transport data.""" + _load_script_main("plotcoretransport")() + + def plotkineticprofiles(): """Plot kinetic profiles.""" _load_script_main("plotkineticprofiles")() @@ -284,6 +289,7 @@ def md_summary(): "plotrotation", "plotequicomp", "plotcoresources", + "plotcoretransport", "plotkineticprofiles", "plothcdwaves", "plotecray", diff --git a/idstools/scripts/bin/dbperf b/idstools/scripts/bin/dbperf index 32849cb7..da5d8265 100644 --- a/idstools/scripts/bin/dbperf +++ b/idstools/scripts/bin/dbperf @@ -195,13 +195,39 @@ if __name__ == "__main__": pulse = entry[0] run = entry[1] + _dummy_hdfde = imas.DBEntry( + imas.ids_defs.HDF5_BACKEND, args.database, pulse, run, args.user, data_version=args.version + ) + _dummy_hdfde.open() + availables_ids = get_available_ids_and_occurrences(_dummy_hdfde) + for idsname, occurrence in availables_ids: + try: + _dummy = _dummy_hdfde.get(idsname, lazy=True, autoconvert=False) + src_dd_version = _dummy.ids_properties.version_put.data_dictionary.value + break + except Exception as e: + pass + _dummy_hdfde.close() + mdsde = imas.DBEntry( - imas.ids_defs.MDSPLUS_BACKEND, args.database, pulse, run, args.user, data_version=args.version + imas.ids_defs.MDSPLUS_BACKEND, + args.database, + pulse, + run, + args.user, + data_version=args.version, + dd_version=src_dd_version, ) mdsde.open() hdfde = imas.DBEntry( - imas.ids_defs.HDF5_BACKEND, args.database, pulse, run, args.user, data_version=args.version + imas.ids_defs.HDF5_BACKEND, + args.database, + pulse, + run, + args.user, + data_version=args.version, + dd_version=src_dd_version, ) hdfde.open() @@ -253,7 +279,6 @@ if __name__ == "__main__": mdsde.close() hdfde.close() - if args.verbose: print("MDSPLUS database closed") print("HDF5 database closed") diff --git a/idstools/scripts/bin/idsresample b/idstools/scripts/bin/idsresample index d0c4be01..31de01f0 100644 --- a/idstools/scripts/bin/idsresample +++ b/idstools/scripts/bin/idsresample @@ -117,7 +117,7 @@ if __name__ == "__main__": except Exception as e: logger.error(f"Exception occurred detailed description : {e}") - tmp = imas.DBEntry("imas:memory?path=tmp/1/1", "w") + tmp = imas.DBEntry("imas:memory?path=tmp/1/1", "w", dd_version=src_dd_version) if tmp is None: print("Error creating temporary data-entry in memory!", file=sys.stderr) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index c059b441..dadb96d9 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -215,7 +215,7 @@ def plotequilibrium(self, ax, time_slice): ax.xaxis.tick_top() ax.xaxis.set_label_position("top") - contour_lines = ax.contour(r2d, z2d, psi2d, levels=50, cmap="summer") # ,label=r'$\Psi_{pol}$') + contour_lines = ax.contour(r2d, z2d, psi2d.T, levels=50, cmap="summer") # ,label=r'$\Psi_{pol}$') cbar = plt.colorbar(contour_lines, ax=ax, pad=0.08, fraction=0.03) cbar.set_label(r"$\psi$ [Wb]") ax.set_xlim(r2d.min(), r2d.max()) diff --git a/pyproject.toml b/pyproject.toml index 611d08cc..b987bed6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ plotpressure = "idstools.scripts:plotpressure" plotrotation = "idstools.scripts:plotrotation" plotequicomp = "idstools.scripts:plotequicomp" plotcoresources = "idstools.scripts:plotcoresources" +plotcoretransport = "idstools.scripts:plotcoretransport" plotkineticprofiles = "idstools.scripts:plotkineticprofiles" plothcdwaves = "idstools.scripts:plothcdwaves" plotecray = "idstools.scripts:plotecray" From 1090085a9288b68b950207d887a73a78b59993cc Mon Sep 17 00:00:00 2001 From: Louwrensth Date: Tue, 2 Jun 2026 08:49:07 -0600 Subject: [PATCH 21/56] ci: add dependabot.yml to update github-actions (#24) --- .github/dependabot.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..001b22a5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + # Enable version updates for Github Actions + - package-ecosystem: "github-actions" + # Look for `/.github/workflows` and `/action.yml` or `.yaml` + directory: "/" + # Check for updates once a week + schedule: + interval: "weekly" + # Group actions version bumps into a single PR + groups: + actions-deps: + patterns: + - "*" + From 37663b751ef58639397c2c135bb9fa388d82f81c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:50:09 +0000 Subject: [PATCH 22/56] Bump the actions-deps group with 4 updates Bumps the actions-deps group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/checkout` from 4 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) Updates `actions/setup-python` from 4 to 6 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v6) Updates `actions/upload-artifact` from 4 to 7 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) Updates `actions/download-artifact` from 4 to 8 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v8) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] --- .github/workflows/linting.yml | 4 ++-- .github/workflows/publish.yml | 10 +++++----- .github/workflows/security.yml | 4 ++-- .github/workflows/test_with_pytest.yml | 12 ++++++------ .github/workflows/verify_with_sphinx.yml | 6 +++--- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 392b9602..bc968975 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -15,10 +15,10 @@ jobs: steps: - name: Checkout IDStools sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: # until saxonche is available in 3.13 # https://saxonica.plan.io/issues/6561 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ac978740..619bb8d5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,11 +10,11 @@ jobs: name: Build distribution runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: # until saxonche is available in 3.13 # https://saxonica.plan.io/issues/6561 @@ -25,7 +25,7 @@ jobs: - name: Build a binary wheel and a source tarball run: python3 -m build . - name: Store the distribution packages - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python-package-distributions path: dist/ @@ -43,7 +43,7 @@ jobs: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-package-distributions path: dist/ @@ -63,7 +63,7 @@ jobs: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-package-distributions path: dist/ diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 63f082c4..0e66533e 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.11' cache: 'pip' diff --git a/.github/workflows/test_with_pytest.yml b/.github/workflows/test_with_pytest.yml index ad96ceeb..211cbbcd 100644 --- a/.github/workflows/test_with_pytest.yml +++ b/.github/workflows/test_with_pytest.yml @@ -17,11 +17,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Display Python version @@ -40,7 +40,7 @@ jobs: TEST_PROFILE: ci - name: Upload coverage report ${{ matrix.python-version }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverage-report-${{ matrix.python-version }} path: htmlcov @@ -48,7 +48,7 @@ jobs: - name: Upload coverage XML ${{ matrix.python-version }} if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverage-xml-${{ matrix.python-version }} path: coverage.xml @@ -56,7 +56,7 @@ jobs: - name: Upload test report ${{ matrix.python-version }} if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-report-${{ matrix.python-version }} path: junit.xml @@ -64,7 +64,7 @@ jobs: - name: Upload test output logs ${{ matrix.python-version }} if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-output-${{ matrix.python-version }} path: test_output.log diff --git a/.github/workflows/verify_with_sphinx.yml b/.github/workflows/verify_with_sphinx.yml index dc89a842..4cbf054e 100644 --- a/.github/workflows/verify_with_sphinx.yml +++ b/.github/workflows/verify_with_sphinx.yml @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout IDStools sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: # until saxonche is available in 3.13 # https://saxonica.plan.io/issues/6561 @@ -50,7 +50,7 @@ jobs: - name: Upload Sphinx documentation as artifact if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sphinx-documentation path: docs/build/html/ From b189deda407b6a8966abe704660a918e5dadf1bd Mon Sep 17 00:00:00 2001 From: Prasad Date: Thu, 4 Jun 2026 09:03:54 +0200 Subject: [PATCH 23/56] use polycollection from matplotlib and use cells instead of nodes (#22) * use polycollection from matplotlib and use cells instead of nodes * Use Outer midplane, Outer divertor target, Inner divertor target values and plot the same --- idstools/compute/edge_profiles.py | 236 ++++++++++++++++++++++++++++++ idstools/view/edge_profiles.py | 119 +++++++++------ 2 files changed, 311 insertions(+), 44 deletions(-) diff --git a/idstools/compute/edge_profiles.py b/idstools/compute/edge_profiles.py index 715c3677..a82d82a5 100644 --- a/idstools/compute/edge_profiles.py +++ b/idstools/compute/edge_profiles.py @@ -952,6 +952,242 @@ def get_neutral_density(self, time_slice, x, y): n_neutral_edge = interpolate.griddata((r_edge, z_edge), temp, (x, y)) return n_neutral_edge + def get_cell_polygons_and_values(self, time_slice, values): + """ + Build a list of polygon vertex arrays and corresponding per-cell scalar values for + direct rendering with ``matplotlib.collections.PolyCollection``. + + Args: + time_slice: time index + values: 1-D array of scalar values, either per-cell or per-node. + + Returns: + tuple: ``(polygons, cell_values)`` where *polygons* is a list of ``(n_corners, 2)`` + float arrays (R, Z columns) and *cell_values* is a 1-D float array of the + same length. Returns ``(None, None)`` when the cells grid subset is unavailable. + """ + r_nodes, z_nodes = self.get_rz(time_slice) + values = np.asarray(values, dtype=float) + + cells_subset = None + for grid_subset in self.ids.grid_ggd[time_slice].grid_subset: + if grid_subset.identifier.name.lower() == "cells": + cells_subset = grid_subset + break + + if cells_subset is None or len(cells_subset.element) == 0: + logger.warning("edge_profiles: cells grid subset not found, cannot build PolyCollection") + return None, None + + n_cells = len(cells_subset.element) + per_cell_mode = len(values) == n_cells # True → direct 1:1, False → average from nodes + + polygons = [] + cell_values = [] + n_nodes = len(r_nodes) + i = 0 + for element in cells_subset.element: + for obj in element.object: + space_index = obj.space - 1 + dimension_index = obj.dimension - 1 + object_index = obj.index - 1 + nodes = ( + self.ids.grid_ggd[time_slice] + .space[space_index] + .objects_per_dimension[dimension_index] + .object[object_index] + .nodes + ) + nodes = np.asarray(nodes, dtype=int) - 1 # convert to 0-based + if np.any(nodes >= n_nodes) or np.any(nodes < 0): + logger.warning("edge_profiles: cell node index out of range, skipping cell") + i += 1 + continue + verts = np.column_stack((r_nodes[nodes], z_nodes[nodes])) + polygons.append(verts) + if per_cell_mode: + cell_values.append(float(values[i])) + else: + cell_values.append(float(np.mean(values[nodes]))) + i += 1 + + return polygons, np.array(cell_values, dtype=float) + + def get_electron_density_on_nodes(self, time_slice): + """ + Return raw electron density values at mesh nodes (no rectangular resampling). + + Args: + time_slice: time index + + Returns: + numpy array of node-centred electron density values, or ``None`` if unavailable. + """ + for electrons_density in self.ids.ggd[time_slice].electrons.density: + if electrons_density.grid_subset_index == 1: # nodes + return np.asarray(electrons_density.values) + logger.warning("edge_profiles: electron density values not found for nodes grid_subset") + return None + + def get_electron_density_on_cells(self, time_slice): + """ + Return electron density values. + + cell-centred values (``grid_subset_index == 5``, one value per cell. Falls + back to node values (``grid_subset_index == 1``) which will be averaged per cell further + + Args: + time_slice: time index + + Returns: + numpy array of density values, or ``None`` if unavailable. + """ + for ed in self.ids.ggd[time_slice].electrons.density: + if ed.grid_subset_index == 5: # cell-centred + return np.asarray(ed.values) + return self.get_electron_density_on_nodes(time_slice) + + def get_ion_density_on_nodes(self, time_slice): + """ + Return raw ion density values at mesh nodes (no rectangular resampling). + + Args: + time_slice: time index + + Returns: + numpy array of node-centred ion density values, or ``None`` if unavailable. + """ + for ion_density in self.ids.ggd[time_slice].ion[0].density: + if ion_density.grid_subset_index == 1: # nodes + return np.asarray(ion_density.values) + logger.warning("edge_profiles: ion density values not found for nodes grid_subset") + return None + + def get_ion_density_on_cells(self, time_slice): + """ + Return ion density values. + cell-centred values (``grid_subset_index == 5``), falls back to node values. + """ + for id_ in self.ids.ggd[time_slice].ion[0].density: + if id_.grid_subset_index == 5: + return np.asarray(id_.values) + return self.get_ion_density_on_nodes(time_slice) + + def get_neutral_density_on_nodes(self, time_slice): + """ + Return raw neutral density values at mesh nodes (no rectangular resampling). + + Args: + time_slice: time index + + Returns: + numpy array of node-centred neutral density values, or ``None`` if unavailable. + """ + for neutral_density in self.ids.ggd[time_slice].neutral[0].density: + if neutral_density.grid_subset_index == 1: # nodes + return np.asarray(neutral_density.values) + logger.warning("edge_profiles: neutral density values not found for nodes grid_subset") + return None + + def get_neutral_density_on_cells(self, time_slice): + """ + Return neutral density values. + cell-centred values (``grid_subset_index == 5``), falls back to node values. + """ + for nd in self.ids.ggd[time_slice].neutral[0].density: + if nd.grid_subset_index == 5: + return np.asarray(nd.values) + return self.get_neutral_density_on_nodes(time_slice) + + def get_rz_and_ne_on_subset(self, time_slice, subset_identifier_index): + """ + Return R, Z coordinates and electron density for every element in a named GGD grid subset. + + For 0-D elements (nodes) the node geometry is used directly. For higher-dimension + elements (edges, faces, cells) the centroid of the element's nodes is used. + + Args: + time_slice: time index + subset_identifier_index: GGD ``identifier.index`` of the target subset, + e.g. 11 = outer midplane, 13 = inner divertor target, 14 = outer divertor target. + + Returns: + ``(r, z, ne)`` numpy float arrays in grid-subset element order, + or ``(None, None, None)`` when the subset or its density data is unavailable. + """ + subset_array_index = None + for iset, gs in enumerate(self.ids.grid_ggd[time_slice].grid_subset): + if gs.identifier.index == subset_identifier_index: + subset_array_index = iset + break + + if subset_array_index is None: + logger.warning(f"edge_profiles: grid subset with identifier.index={subset_identifier_index} not found") + return None, None, None + + grid_subset = self.ids.grid_ggd[time_slice].grid_subset[subset_array_index] + if len(grid_subset.element) == 0: + logger.warning(f"edge_profiles: grid subset {subset_identifier_index} has no elements") + return None, None, None + + # grid_subset_index in ggd arrays stores identifier.index (semantic index), not array position + ne_entry = None + ne_all_nodes = None + for dens in self.ids.ggd[time_slice].electrons.density: + if dens.grid_subset_index == subset_identifier_index: + ne_entry = dens + break + if dens.grid_subset_index == 1: # nodes — keep as fallback + ne_all_nodes = np.asarray(dens.values, dtype=float) + + spaces = self.ids.grid_ggd[time_slice].space + r_list, z_list, ne_list = [], [], [] + + for element in grid_subset.element: + for obj in element.object: + space_idx = obj.space - 1 # space + dim_idx = obj.dimension - 1 # 0=nodes, 1=edges, 2=faces, 3=cells + obj_idx = obj.index - 1 # convert to 0-based + space = spaces[space_idx] + n_nodes = len(space.objects_per_dimension[0].object) + ggd_obj = space.objects_per_dimension[dim_idx].object[obj_idx] + if dim_idx == 0: + g = ggd_obj.geometry + r_list.append(float(g[0])) + z_list.append(float(g[1])) + if ne_entry is None and ne_all_nodes is not None: + ne_list.append(float(ne_all_nodes[obj_idx]) if obj_idx < n_nodes else np.nan) + else: + nodes = np.asarray(ggd_obj.nodes, dtype=int) - 1 # 0-based node indices + valid = nodes[(nodes >= 0) & (nodes < n_nodes)] + geom = [space.objects_per_dimension[0].object[n].geometry for n in valid] + r_list.append(float(np.mean([g[0] for g in geom]))) + z_list.append(float(np.mean([g[1] for g in geom]))) + if ne_entry is None and ne_all_nodes is not None: + valid_ne = valid[valid < len(ne_all_nodes)] + ne_list.append(float(np.mean(ne_all_nodes[valid_ne])) if len(valid_ne) else np.nan) + + r = np.array(r_list, dtype=float) + z = np.array(z_list, dtype=float) + + if ne_entry is not None: + ne_values = np.asarray(ne_entry.values, dtype=float) + if len(ne_values) != len(r): + logger.warning( + f"edge_profiles: per-subset density count {len(ne_values)} != element count {len(r)} " + f"for subset {subset_identifier_index}, falling back to node interpolation" + ) + if len(ne_list) == len(r): + return r, z, np.array(ne_list, dtype=float) + return None, None, None + return r, z, ne_values + + if len(ne_list) == len(r): + return r, z, np.array(ne_list, dtype=float) + + logger.warning(f"edge_profiles: electron density not found for grid subset {subset_identifier_index}") + return None, None, None + def get_outer_midplane_array_index(self, time_slice): """ This function `get_outer_midplane_array_index` searches for a specific grid subset with an diff --git a/idstools/view/edge_profiles.py b/idstools/view/edge_profiles.py index 225e9fe0..13e12b4a 100644 --- a/idstools/view/edge_profiles.py +++ b/idstools/view/edge_profiles.py @@ -1,6 +1,7 @@ import logging import numpy as np +from matplotlib.collections import PolyCollection from idstools.compute.edge_profiles import EdgeProfilesCompute @@ -109,8 +110,8 @@ def _print_specis_concentration(self, composition_data): def view_electrons_density(self, ax, time_slice, show_separatrix=False): """ - The function `view_electrons_density` plots the electron density on a rectangular grid and adds a - separatrix line. + The function `view_electrons_density` plots the electron density on the original GGD mesh + using tripcolor (Gouraud shading) and adds a separatrix line. Args: ax: The parameter "ax" is an instance of the matplotlib Axes class. It represents the axes on @@ -119,25 +120,31 @@ def view_electrons_density(self, ax, time_slice, show_separatrix=False): plotted. It is an optional parameter with a default value of 0. Defaults to 0 Returns: - the pcolormesh object 'c'. + the tripcolor object 'c'. """ - x, y = self.edge_profiles_compute.get_rectangular_grid(500) + ne_values = self.edge_profiles_compute.get_electron_density_on_cells(time_slice) - ne_edge = self.edge_profiles_compute.get_electron_density(time_slice, x, y) - if ne_edge is not None: + if ne_values is not None: + polygons, cell_values = self.edge_profiles_compute.get_cell_polygons_and_values(time_slice, ne_values) + all_r = np.concatenate([v[:, 0] for v in polygons]) + all_z = np.concatenate([v[:, 1] for v in polygons]) ax.grid(False) - c = ax.pcolormesh(x, y, ne_edge, vmin=0, vmax=5e19, shading="auto") + pc = PolyCollection(polygons, array=cell_values, cmap="coolwarm", linewidths=0) + pc.set_clim(float(cell_values.min()), float(cell_values.max())) + ax.add_collection(pc) + ax.set_xlim(all_r.min(), all_r.max()) + ax.set_ylim(all_z.min(), all_z.max()) core_boundry = self.edge_profiles_compute.get_core_boundry(time_slice) - ax.fill(core_boundry[:, 0], core_boundry[:, 1], facecolor="w", edgecolor="r", linewidth=0) + ax.fill(core_boundry[:, 0], core_boundry[:, 1], facecolor="w", edgecolor="none") if show_separatrix: separatrix = self.edge_profiles_compute.get_separatrix(time_slice) if separatrix is not None: - ax.scatter(separatrix[:, 0], separatrix[:, 1], color="#FF1493", marker="x") + ax.scatter(separatrix[:, 0], separatrix[:, 1], color="#FF1493", marker="x", s=10) ax.set_aspect("equal", adjustable="box") ax.set_xlabel("R,m") ax.set_ylabel("Z,m") ax.set_title("Electron density") - return c + return pc else: xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() @@ -152,7 +159,8 @@ def view_electrons_density(self, ax, time_slice, show_separatrix=False): def view_ion_density(self, ax, time_slice, show_separatrix=False): """ - The function `view_ion_density` plots the ion density on a rectangular grid and adds a separatrix line. + The function `view_ion_density` plots the ion density on the original GGD mesh + using tripcolor (Gouraud shading) and adds a separatrix line. Args: ax: The parameter "ax" is an instance of the matplotlib Axes class. It represents the axes on @@ -161,26 +169,31 @@ def view_ion_density(self, ax, time_slice, show_separatrix=False): be plotted. It is an optional parameter with a default value of 0. Defaults to 0 Returns: - the pcolormesh object 'c'. + the tripcolor object 'c'. """ - x, y = self.edge_profiles_compute.get_rectangular_grid(500) + ni_values = self.edge_profiles_compute.get_ion_density_on_cells(time_slice) - ni_edge = self.edge_profiles_compute.get_ion_density(time_slice, x, y) - if ni_edge is not None: + if ni_values is not None: + polygons, cell_values = self.edge_profiles_compute.get_cell_polygons_and_values(time_slice, ni_values) + all_r = np.concatenate([v[:, 0] for v in polygons]) + all_z = np.concatenate([v[:, 1] for v in polygons]) ax.grid(False) - c = ax.pcolormesh(x, y, ni_edge, vmin=0, vmax=5e19, shading="auto") + pc = PolyCollection(polygons, array=cell_values, cmap="coolwarm", linewidths=0) + pc.set_clim(float(cell_values.min()), float(cell_values.max())) + ax.add_collection(pc) + ax.set_xlim(all_r.min(), all_r.max()) + ax.set_ylim(all_z.min(), all_z.max()) core_boundry = self.edge_profiles_compute.get_core_boundry(time_slice) - ax.fill(core_boundry[:, 0], core_boundry[:, 1], facecolor="w", edgecolor="r", linewidth=0) + ax.fill(core_boundry[:, 0], core_boundry[:, 1], facecolor="w", edgecolor="none") if show_separatrix: separatrix = self.edge_profiles_compute.get_separatrix(time_slice) if separatrix is not None: - ax.scatter(separatrix[:, 0], separatrix[:, 1], color="#FF1493", marker="x") - + ax.scatter(separatrix[:, 0], separatrix[:, 1], color="#FF1493", marker="x", s=10) ax.set_aspect("equal", adjustable="box") ax.set_xlabel("R,m") ax.set_ylabel("Z,m") ax.set_title("Ion density") - return c + return pc else: xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() @@ -195,8 +208,8 @@ def view_ion_density(self, ax, time_slice, show_separatrix=False): def view_neutral_density(self, ax, time_slice, show_separatrix=False): """ - The function `view_neutral_density` plots the neutral density on a rectangular grid and adds a - separatrix line. + The function `view_neutral_density` plots the neutral density on the original GGD mesh + using tripcolor (Gouraud shading) and adds a separatrix line. Args: ax: The parameter "ax" is an instance of the matplotlib Axes class. It represents the axes @@ -205,26 +218,33 @@ def view_neutral_density(self, ax, time_slice, show_separatrix=False): will be plotted. It is an optional parameter with a default value of 0. Defaults to 0 Returns: - the pcolormesh object 'c'. + the tripcolor object 'c'. """ - x, y = self.edge_profiles_compute.get_rectangular_grid(500) - - n_neutral_edge = self.edge_profiles_compute.get_neutral_density(time_slice, x, y) + n_neutral_values = self.edge_profiles_compute.get_neutral_density_on_cells(time_slice) - if n_neutral_edge is not None: + if n_neutral_values is not None: + polygons, cell_values = self.edge_profiles_compute.get_cell_polygons_and_values( + time_slice, n_neutral_values + ) + all_r = np.concatenate([v[:, 0] for v in polygons]) + all_z = np.concatenate([v[:, 1] for v in polygons]) ax.grid(False) - c = ax.pcolormesh(x, y, n_neutral_edge, vmin=0, vmax=5e19, shading="auto") + pc = PolyCollection(polygons, array=cell_values, cmap="coolwarm", linewidths=0) + pc.set_clim(float(cell_values.min()), float(cell_values.max())) + ax.add_collection(pc) + ax.set_xlim(all_r.min(), all_r.max()) + ax.set_ylim(all_z.min(), all_z.max()) core_boundry = self.edge_profiles_compute.get_core_boundry(time_slice) - ax.fill(core_boundry[:, 0], core_boundry[:, 1], facecolor="w", edgecolor="r", linewidth=0) + ax.fill(core_boundry[:, 0], core_boundry[:, 1], facecolor="w", edgecolor="none") if show_separatrix: separatrix = self.edge_profiles_compute.get_separatrix(time_slice) if separatrix is not None: - ax.scatter(separatrix[:, 0], separatrix[:, 1], color="#FF1493", marker="x") + ax.scatter(separatrix[:, 0], separatrix[:, 1], color="#FF1493", marker="x", s=10) ax.set_aspect("equal", adjustable="box") ax.set_xlabel("R,m") ax.set_ylabel("Z,m") ax.set_title("Neutral density") - return c + return pc else: xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() @@ -237,23 +257,34 @@ def view_neutral_density(self, ax, time_slice, show_separatrix=False): ) return None - def view_equatorial_plane_and_diverter_density(self, ax, time_slice, logscale=False): - x, y = self.edge_profiles_compute.get_rectangular_grid(500) - ne_edge = self.edge_profiles_compute.get_electron_density(time_slice, x, y) - if ne_edge is not None: - # choose Z position for a radial profile: - z0 = 0.0 - ind = np.argmin(abs(y[:, 0] - z0)) - ax.plot(x[ind, :], ne_edge[ind, :], label="Equatorial plane") + def view_equatorial_plane_and_diverter_density(self, ax, time_slice, logscale=True): + OUTER_MIDPLANE_INDEX = 11 + OUTER_TARGET_INDEX = 13 + INNER_TARGET_INDEX = 14 - z0 = -4.0 - ind = np.argmin(abs(y[:, 0] - z0)) - ax.plot(x[ind, :], ne_edge[ind, :], label="Divertor") + any_data = False + + for subset_idx, label in ( + (OUTER_MIDPLANE_INDEX, "Outer midplane"), + (OUTER_TARGET_INDEX, "Outer divertor target"), + (INNER_TARGET_INDEX, "Inner divertor target"), + ): + r_t, z_t, ne_t = self.edge_profiles_compute.get_rz_and_ne_on_subset(time_slice, subset_idx) + if r_t is None or len(r_t) == 0: + continue + # Arc-length as x-axis + coords = np.column_stack([r_t, z_t]) + seg_len = np.sqrt(np.sum(np.diff(coords, axis=0) ** 2, axis=1)) + arc = np.concatenate([[0.0], np.cumsum(seg_len)]) + ax.plot(arc, ne_t, label=label) + any_data = True + + if any_data: if logscale: ax.set_yscale("log") ax.set_title("Electron density") - ax.set_xlabel("R,m") - # ax.set_ylim([0, 1.5e21]) + ax.set_xlabel("Distance along profile (m)") + ax.set_ylabel("n$_e$ (m$^{-3}$)") ax.legend() else: xmin, xmax = ax.get_xlim() From ef439cf3bf477d2b3d1fc3f667d24b1b882b6eee Mon Sep 17 00:00:00 2001 From: ZijianBian Date: Thu, 4 Jun 2026 09:42:15 +0200 Subject: [PATCH 24/56] fix: correct electron power calculation and improve plot visibility (#21) --- idstools/compute/core_sources.py | 14 ++++++------ idstools/view/core_sources.py | 37 ++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/idstools/compute/core_sources.py b/idstools/compute/core_sources.py index 534900c6..6dc3c9b5 100644 --- a/idstools/compute/core_sources.py +++ b/idstools/compute/core_sources.py @@ -365,8 +365,8 @@ def get_single_and_total_electrons_ions_waveforms(self, time_slice): # single_power_waveform = dict() # waveform # single_particles_waveform = dict() # waveform time_length = len(self.ids.time) - total_power_waveform = np.zeros(time_length) - total_particles_waveform = np.zeros(time_length) + total_ion_power_waveform = np.zeros(time_length) + total_ion_particles_waveform = np.zeros(time_length) single_power_waveform = {} single_particles_waveform = {} dict_single_and_total_electrons_waveforms = self.get_single_and_total_electrons_waveforms(time_slice) @@ -398,16 +398,14 @@ def get_single_and_total_electrons_ions_waveforms(self, time_slice): if total_ion_power < 0: total_ion_power = 0.0 - total_power_waveform[time_index] = ( - total_electron_power_waveform[time_index] + electrons_power + total_ion_power - ) - total_particles_waveform[time_index] = ( - total_electron_particles_waveform[time_index] + electrons_particles - ) + total_ion_particles + total_ion_power_waveform[time_index] += total_ion_power + total_ion_particles_waveform[time_index] += total_ion_particles single_power_waveform[source_index].append(electrons_power + total_ion_power) single_particles_waveform[source_index].append(electrons_particles + total_ion_particles) single_power_waveform[source_index] = np.array(single_power_waveform[source_index]) single_particles_waveform[source_index] = np.array(single_particles_waveform[source_index]) + total_power_waveform = total_electron_power_waveform + total_ion_power_waveform + total_particles_waveform = total_electron_particles_waveform + total_ion_particles_waveform return { "total_power_waveform": total_power_waveform, "total_particles_waveform": total_particles_waveform, diff --git a/idstools/view/core_sources.py b/idstools/view/core_sources.py index 247c5878..67ea777e 100644 --- a/idstools/view/core_sources.py +++ b/idstools/view/core_sources.py @@ -209,7 +209,6 @@ def view_power_and_particle_waveforms(self, ax, time_slice, *args, **kwargs): """ The function `view_power_and_particle_waveforms` plots power waveforms for different sources and particles over time. - Args: ax: The parameter `ax` is an instance of the `Axes` class from the `matplotlib.pyplot` module. It represents the axes on which the waveforms will be plotted. @@ -231,37 +230,71 @@ def view_power_and_particle_waveforms(self, ax, time_slice, *args, **kwargs): ) source_names = self.core_sources_compute.get_source_names(time_slice) ax.set_title("POWER AND PARTICLE WAVEFORMS") + + # --- Marker spacing: ~8 markers per curve --- + markevery = max(1, ntime // 8) + + # --- Per-source markers cycle --- + markers = ["o", "s", "D", "^", "v", "P", "X", "*"] + ax.plot( time_array, single_and_total_electrons_ions_waveforms["total_power_waveform"] * 1.0e-6, label=r"Total electrons+ions", + linestyle="-", + linewidth=3.5, + alpha=0.5, + color="black", ) ax.plot( time_array, single_and_total_electrons_waveforms["total_electron_power_waveform"] * 1.0e-6, label=r"Total electrons", + linestyle="--", + linewidth=3.0, + alpha=0.5, + color="tab:blue", ) ax.plot( time_array, single_and_total_ions_waveforms["total_ion_power_waveform"] * 1.0e-6, label=r"Total ions", + linestyle=":", + linewidth=3.0, + alpha=0.5, + color="tab:red", ) - for isource, name in source_names.items(): + marker = markers[isource % len(markers)] ax.plot( time_array, single_and_total_electrons_ions_waveforms["single_power_waveform"][isource] * 1.0e-6, label=name + " [" + str(isource) + "]" + " electrons+ions", + linestyle="-", + linewidth=1.8, + marker=marker, + markersize=5, + markevery=markevery, ) ax.plot( time_array, single_and_total_electrons_waveforms["single_electron_power_waveform"][isource] * 1.0e-6, label=name + " [" + str(isource) + "]" + " electrons", + linestyle="--", + linewidth=1.5, + marker=marker, + markersize=4, + markevery=markevery, ) ax.plot( time_array, single_and_total_ions_waveforms["single_ion_power_waveform"][isource] * 1.0e-6, label=name + " [" + str(isource) + "]" + " ions", + linestyle=":", + linewidth=1.5, + marker=marker, + markersize=4, + markevery=markevery, ) ax.set_ylabel("Power waveforms $\\mathrm{[MW]}$") ax.set_xlabel("Time (s)") From 58b63529b95481aa150137f549cd0d4cc0c53df2 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 11 Jun 2026 12:51:42 +0200 Subject: [PATCH 25/56] 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 26/56] 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 27/56] 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 28/56] 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 29/56] 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 30/56] 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 31/56] 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 32/56] 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 33/56] 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 890fa34d0cbe90fc7ac5bc2d5b1defc874e48111 Mon Sep 17 00:00:00 2001 From: Prasad Date: Fri, 12 Jun 2026 17:57:52 +0200 Subject: [PATCH 34/56] Feature/show shape and terminal plot in idsprint (#28) --- docs/source/idsprint.rst | 22 ++-- idstools/scripts/bin/idsprint | 186 ++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 3 files changed, 191 insertions(+), 18 deletions(-) diff --git a/docs/source/idsprint.rst b/docs/source/idsprint.rst index a8c44544..d80a27a8 100644 --- a/docs/source/idsprint.rst +++ b/docs/source/idsprint.rst @@ -183,16 +183,18 @@ It can also save generated figures to a specified directory or the default locat $ idsprint --uri "imas:mdsplus?user=public;pulse=122481;run=1;database=ITER;version=3#edge_profiles/ggd[:]/electrons/density[1].values" -p - $ idsprint --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3#core_profiles/profiles_1d(:)/electrons/temperature" -p + $ idsprint --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3#core_profiles/profiles_1d(:)/electrons/temperature" -p - .. image:: _static/images/idsprint_1.png - :alt: image not found - :align: center + $ idsprint --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3#summary/time" - .. image:: _static/images/idsprint_2.png - :alt: image not found - :align: center +.. image:: _static/images/idsprint_1.png + :alt: image not found + :align: center - .. image:: _static/images/idsprint_3.png - :alt: image not found - :align: center \ No newline at end of file +.. image:: _static/images/idsprint_2.png + :alt: image not found + :align: center + +.. image:: _static/images/idsprint_3.png + :alt: image not found + :align: center diff --git a/idstools/scripts/bin/idsprint b/idstools/scripts/bin/idsprint index f3a05934..669b8560 100644 --- a/idstools/scripts/bin/idsprint +++ b/idstools/scripts/bin/idsprint @@ -3,16 +3,17 @@ import argparse import logging import os +import shutil import sys try: import imaspy as imas except ImportError: import imas -from itertools import tee import numpy as np import rich +from rich.panel import Panel from rich.columns import Columns from rich.console import Console from rich.pretty import Pretty @@ -34,6 +35,135 @@ from idstools.view.common import PlotCanvas logger = setup_logger("module", stdout_level=logging.INFO) +TERMINAL_PLOT_WIDTH = 120 +TERMINAL_PLOT_HEIGHT = 30 +TERMINAL_PLOT_TICKS = 5 +TERMINAL_PLOT_SCIENTIFIC_THRESHOLD = 1e4 + + +def _node_value(node): + if isinstance(node, imas.ids_primitive.IDSPrimitive): + if not node.has_value: + return None + return node.value + return node + + +def _coordinate_1d_array(coordinate, size): + coordinate = _node_value(coordinate) + if coordinate is None or isinstance(coordinate, int): + return np.arange(size, dtype=float) + try: + coordinate = np.asarray(coordinate) + except Exception: + return np.arange(size, dtype=float) + if coordinate.ndim != 1 or coordinate.size != size or not np.issubdtype(coordinate.dtype, np.number): + return np.arange(size, dtype=float) + return coordinate.astype(float, copy=False) + + +def _scientific_ticks(values, count=TERMINAL_PLOT_TICKS): + values = np.asarray(values, dtype=float) + values = values[np.isfinite(values)] + if values.size == 0: + return [], [] + lower = float(values.min()) + upper = float(values.max()) + if lower == upper: + ticks = [lower] + else: + ticks = np.linspace(lower, upper, min(count, values.size)).tolist() + use_scientific = np.max(np.abs(values)) >= TERMINAL_PLOT_SCIENTIFIC_THRESHOLD + labels = [f"{tick:.2e}" if use_scientific else f"{tick:.4g}" for tick in ticks] + return ticks, labels + + +def print_terminal_plot( + field, + coordinate, + field_name="", + coordinate_name="Index", + field_unit="", + coordinate_unit="", +): + raw = _node_value(field) + try: + data = np.asarray(raw).astype(float) + except (ValueError, TypeError): + data = None + + if data is None or data.size == 0 or data.ndim != 1: + logger.warning("Terminal plot supports only 1D numeric arrays") + return False + + # Extract units and coordinate from IDS metadata if not provided + if isinstance(field, imas.ids_primitive.IDSPrimitive): + if not field_unit: + field_unit = str(field.metadata.units or "") + if coordinate is None and isinstance(field, imas.ids_primitive.IDSNumericArray): + for candidate in field.coordinates: + if isinstance(candidate, imas.ids_primitive.IDSPrimitive) and candidate.has_value: + coordinate = candidate + coordinate_name = candidate.metadata.name or candidate.metadata.path or coordinate_name + if not coordinate_unit: + coordinate_unit = str(candidate.metadata.units or "") + break + if isinstance(coordinate, imas.ids_primitive.IDSPrimitive) and not coordinate_unit: + coordinate_unit = str(coordinate.metadata.units or "") + + import plotext as ptx + from rich.align import Align + from rich.text import Text + + console = Console() + + y_label = f"{field_name} [{field_unit}]" if field_unit else field_name + x_label = f"{coordinate_name} [{coordinate_unit}]" if coordinate_unit else (coordinate_name or "Index") + + xdata = _coordinate_1d_array(coordinate, data.size) + finite = np.isfinite(data) & np.isfinite(xdata) + if not np.any(finite): + logger.warning("Terminal plot supports only arrays with finite numeric values") + return False + data, xdata = data[finite], xdata[finite] + + ptx.clear_figure() + ptx.canvas_color("default") + ptx.axes_color("default") + ptx.ticks_color("default") + terminal_width, terminal_height = ptx.terminal_size() + plot_width = min(TERMINAL_PLOT_WIDTH, max(40, terminal_width - 4)) + plot_height = min(TERMINAL_PLOT_HEIGHT, max(12, terminal_height - 8)) + ptx.plot_size(plot_width, plot_height) + xticks, xlabels = _scientific_ticks(xdata) + yticks, ylabels = _scientific_ticks(data) + ptx.xticks(xticks, xlabels) + ptx.yticks(yticks, ylabels) + ptx.plot(xdata.tolist(), data.tolist(), color="cyan", marker="braille") + + stats = Text(justify="center") + stats.append("min: ", style="bright_black") + stats.append(f"{data.min():.2e}", style="bold cyan") + stats.append(" max: ", style="bright_black") + stats.append(f"{data.max():.2e}", style="bold cyan") + stats.append(" mean: ", style="bright_black") + stats.append(f"{data.mean():.2e}", style="bold cyan") + console.print( + Align.center(Panel(Align.center(stats), border_style="bright_blue", padding=(0, 1), width=plot_width)) + ) + + plot = Text.from_ansi(ptx.build()) + console.print(Align.center(plot)) + + axis_labels = Table.grid(padding=(0, 1)) + axis_labels.add_column(justify="right", style="bright_black", no_wrap=True) + axis_labels.add_column(style="cyan") + axis_labels.add_row("x:", x_label) + if y_label: + axis_labels.add_row("y:", y_label) + console.print(Align.center(Panel(axis_labels, border_style="bright_black", padding=(0, 1), width=plot_width))) + return True + def view_plot( ax, @@ -117,12 +247,25 @@ def view_plot( def print_tree(structure, hide_empty_nodes, compact, full_array, depth=None): + console = Console() if full_array: with np.printoptions(threshold=sys.maxsize, linewidth=1024, precision=4): - rich.print(_make_tree(structure, hide_empty_nodes, compact, depth=depth)) + console.print( + Panel( + _make_tree(structure, hide_empty_nodes, compact, depth=depth), + border_style="bright_blue", + padding=(0, 1), + ) + ) else: with np.printoptions(threshold=5, linewidth=1024, precision=4): - rich.print(_make_tree(structure, hide_empty_nodes, compact, depth=depth)) + console.print( + Panel( + _make_tree(structure, hide_empty_nodes, compact, depth=depth), + border_style="bright_blue", + padding=(0, 1), + ) + ) def _make_tree(structure, hide_empty_nodes, compact, *, tree=None, depth=None, current_depth=0): @@ -173,7 +316,19 @@ def _make_tree(structure, hide_empty_nodes, compact, *, tree=None, depth=None, c group = Columns([txt]) tree.add(group) return tree - elif isinstance(structure, np.float64) or isinstance(structure, imas.ids_primitive.IDSPrimitive): + elif isinstance(structure, imas.ids_primitive.IDSPrimitive): + tree = Tree(f"[magenta]{type(structure)}") + if not structure.has_value: + tree.add(f"[grey62]{structure.metadata.name}[/]") + else: + value = Pretty(structure.value) + txt = f"[bright_yellow]{structure.metadata.name}[/]:" + group = Columns([txt, value]) + if compact: + group = Columns([f"[bright_yellow]{structure.metadata.name}[/]"]) + tree.add(group) + return tree + elif isinstance(structure, np.float64): return structure else: tree = Tree(f"[magenta]{structure.metadata.name}") @@ -212,9 +367,9 @@ def _make_tree(structure, hide_empty_nodes, compact, *, tree=None, depth=None, c if isinstance(child, imas.ids_structure.IDSStructure): txt = f"[magenta]{child._path}[/]" # check if structure is not empty - iterator, iterator_copy = tee(child.iter_nonempty_(accept_lazy=True)) + _iter = child.iter_nonempty_(accept_lazy=True) try: - next(iterator_copy) + next(_iter) except StopIteration: pass else: @@ -269,7 +424,7 @@ def _make_dict_tree(structure, hide_empty_nodes, compact, *, tree=None): for counter, ids_structure in enumerate(child): ntree = {} nlist.append(ntree) - nlist.append(_make_dict_tree(ids_structure, hide_empty_nodes, compact, tree=ntree)) + _make_dict_tree(ids_structure, hide_empty_nodes, compact, tree=ntree) return tree @@ -332,7 +487,7 @@ if __name__ == "__main__": "-p", "--plot", action="store_true", - help="plot 1d arrays from leaf nodes", + help="Plot arrays from leaf nodes with Matplotlib", ) parser.add_argument( "--coordinate", @@ -597,4 +752,19 @@ if __name__ == "__main__": print_tree( node, hide_empty_nodes=not args.show_empty, compact=args.compact, full_array=args.full, depth=args.depth ) + arr = _node_value(node) + try: + arr = np.asarray(arr) + except Exception: + arr = None + if arr is not None and arr.ndim == 1 and arr.size > 0 and np.issubdtype(arr.dtype, np.number): + print_terminal_plot( + node, + coordinate, + field_name=f"{ids_name}/{original_ids_path}", + coordinate_name=coordinate_name or "Index", + field_unit=node_unit, + coordinate_unit=coordinate_unit, + ) + connection.close() diff --git a/pyproject.toml b/pyproject.toml index b987bed6..1f3a64f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ dependencies = [ "numpy", "packaging", "pandas", + "plotext>=5.0", "pyparsing", "python-dateutil", "pyyaml", From 692ead85528584591f79ff90e43cf03de7101c53 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 12 Jun 2026 18:09:17 +0200 Subject: [PATCH 35/56] 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 36/56] 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 37/56] 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 38/56] 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 39/56] 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 40/56] 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 41/56] 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 42/56] 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 43/56] 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 44/56] 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 45/56] 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"], From 497a33cc285e4d8cdde1a24a2491108fd1e12aba Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 22 Jun 2026 13:42:13 +0200 Subject: [PATCH 46/56] generalized provenance info in plotting scripts --- idstools/scripts/bin/plotequicomp | 47 ++++++++++++--------- idstools/scripts/bin/plotequilibrium | 6 --- idstools/scripts/bin/plothcd | 2 + idstools/scripts/bin/plotmachinedescription | 2 +- idstools/utils/clihelper.py | 15 ++++--- idstools/view/common.py | 5 +++ 6 files changed, 45 insertions(+), 32 deletions(-) diff --git a/idstools/scripts/bin/plotequicomp b/idstools/scripts/bin/plotequicomp index f6716148..b59d7619 100644 --- a/idstools/scripts/bin/plotequicomp +++ b/idstools/scripts/bin/plotequicomp @@ -19,7 +19,7 @@ from idstools.utils.clihelper import ( rcparam_parser, ) from idstools.utils.idslogger import setup_logger -from idstools.view.common import PlotCanvas +from idstools.view.common import PROVENANCE_TITLE_STYLE, PlotCanvas from idstools.view.equilibrium import EquilibriumView from idstools.view.wall import WallView @@ -67,6 +67,11 @@ if __name__ == "__main__": ) args = parser.parse_args() + def entry_title(uri, time_value): + if args.no_provenance: + return "" + return f"{uri}\n#time:{time_value:.2f}" + log_level = getattr(logging, args.log_level) logger = setup_logger("module", stdout_level=log_level) equilibrium1 = None @@ -126,7 +131,7 @@ if __name__ == "__main__": canvas = PlotCanvas(1, 4) canvas.update_style(args.rc) - canvas.fig.subplots_adjust(top=0.90, bottom=0.12, left=0.044, right=0.946, hspace=0.216, wspace=0.240) + canvas.fig.subplots_adjust(top=0.90, bottom=0.16, left=0.044, right=0.946, hspace=0.216, wspace=0.240) ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0) ax2 = canvas.add_axes(title="", xlabel="", row=0, col=1) @@ -137,7 +142,7 @@ if __name__ == "__main__": current_time_slice1 = [time_slice1] - ax_slider = canvas.fig.add_axes([0.15, 0.02, 0.65, 0.03]) + ax_slider = canvas.fig.add_axes((0.15, 0.055, 0.65, 0.025)) time_slider = Slider( ax_slider, @@ -153,7 +158,7 @@ if __name__ == "__main__": time_text = canvas.fig.text( 0.82, - 0.02, + 0.055, f"Index: {time_slice1} | Time: {time_array1[time_slice1]:.2f} s", ha="left", va="bottom", @@ -212,34 +217,35 @@ if __name__ == "__main__": text_artists["title"] = None if len(args.uri) == 2: - y = 0.99 text_artists["text1"] = canvas.fig.text( 0.5, - y, - args.uri[0] + f"(time {time_val1:.2f})", + 0.985, + entry_title(args.uri[0], time_val1), ha="center", va="top", color="tab:green", transform=canvas.fig.transFigure, + **PROVENANCE_TITLE_STYLE, ) text_artists["text2"] = canvas.fig.text( 0.5, - y - 0.03, - args.uri[1] + f"(time {time_val2:.2f})", + 0.94, + entry_title(args.uri[1], time_val2), ha="center", va="top", color="tab:blue", transform=canvas.fig.transFigure, + **PROVENANCE_TITLE_STYLE, ) else: text_artists["title"] = canvas.fig.text( 0.5, - 0.99, - f"{args.uri[0]} time {time_val1:.2f}", + 0.985, + entry_title(args.uri[0], time_val1), ha="center", va="top", - fontsize=12, transform=canvas.fig.transFigure, + **PROVENANCE_TITLE_STYLE, ) canvas.fig.canvas.draw() @@ -362,34 +368,35 @@ if __name__ == "__main__": # Set initial title using the text_artists dictionary to avoid overdrawing if len(args.uri) == 2: - y = 0.99 text_artists["text1"] = canvas.fig.text( 0.5, - y, - args.uri[0] + f"(time {time_value1:.2f})", + 0.985, + entry_title(args.uri[0], time_value1), ha="center", va="top", color="tab:green", transform=canvas.fig.transFigure, + **PROVENANCE_TITLE_STYLE, ) text_artists["text2"] = canvas.fig.text( 0.5, - y - 0.03, - args.uri[1] + f"(time {time_value2:.2f})", + 0.94, + entry_title(args.uri[1], time_value2), ha="center", va="top", color="tab:blue", transform=canvas.fig.transFigure, + **PROVENANCE_TITLE_STYLE, ) else: text_artists["title"] = canvas.fig.text( 0.5, - 0.99, - f"{args.uri[0]} time {time_value1:.2f}", + 0.985, + entry_title(args.uri[0], time_value1), ha="center", va="top", - fontsize=12, transform=canvas.fig.transFigure, + **PROVENANCE_TITLE_STYLE, ) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index b19895c3..b30a5e44 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -77,12 +77,6 @@ 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", help="Show diagnostic logging", diff --git a/idstools/scripts/bin/plothcd b/idstools/scripts/bin/plothcd index 23f6a753..d87007ff 100644 --- a/idstools/scripts/bin/plothcd +++ b/idstools/scripts/bin/plothcd @@ -27,6 +27,7 @@ logger = setup_logger("module", stdout_level=logging.INFO) def _show_waves_plots(connargs, args, hold=False, dd_update=False, rc=""): + connargs.no_provenance = args.get("no_provenance", False) conn = DBMaster.get_connection(connargs) if conn is None: logger.critical(f"data entry not found : {connargs}") @@ -141,6 +142,7 @@ def show_waves_plots(args, hold=False, dd_update=False, rc=""): def _show_distribution_plots(connargs, args, hold=False, dd_update=False, rc=""): + connargs.no_provenance = args.get("no_provenance", False) conn = DBMaster.get_connection(connargs) if conn is None: logger.critical(f"data entry not found : {connargs}") diff --git a/idstools/scripts/bin/plotmachinedescription b/idstools/scripts/bin/plotmachinedescription index cab0441e..5c8ee293 100644 --- a/idstools/scripts/bin/plotmachinedescription +++ b/idstools/scripts/bin/plotmachinedescription @@ -72,7 +72,7 @@ if __name__ == "__main__": 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__)) - mdcanvas.set_sup_title(md_provenance, fontsize=8) + mdcanvas.set_sup_title("" if args.no_provenance else md_provenance, 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 3151f239..d6d4ceaf 100644 --- a/idstools/utils/clihelper.py +++ b/idstools/utils/clihelper.py @@ -67,6 +67,11 @@ default="", help="Semicolon-separated rcParams string (e.g., 'lines.linewidth=2;axes.titlesize=14')", ) +rcparam_parser.add_argument( + "--no-provenance", + action="store_true", + help="Hide URI provenance information from plot titles", +) def get_backend_id(name): @@ -218,6 +223,9 @@ def get_database_path(imasargs, time_value=None) -> str: Returns: the absolute path of the database. """ + if getattr(imasargs, "no_provenance", False): + return "" + pulse_info = "" database_absolute_path = "" if "uri" in imasargs.__dict__ and imasargs.uri: @@ -238,12 +246,9 @@ def get_database_path(imasargs, time_value=None) -> str: ) pulse_info = f"pulse {imasargs.pulse},{imasargs.run}" database_absolute_path = database_absolute_path[:-2] - time_string = "" - if time_value: - 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}" + if time_value is not None: + hostdir = hostdir.rstrip() + f"\n#time:{time_value:.3f}" return hostdir diff --git a/idstools/view/common.py b/idstools/view/common.py index e8ba5c3c..e2772951 100644 --- a/idstools/view/common.py +++ b/idstools/view/common.py @@ -45,6 +45,8 @@ def _is_jupyter() -> bool: logger = logging.getLogger("module") +PROVENANCE_TITLE_STYLE = {"fontsize": 8, "fontweight": "normal"} + current_directory = os.path.abspath(os.path.dirname(__file__)) # reach to `share` directory (sys.prefix won't work if using --prefix option) share_directory = os.path.abspath(os.path.join(current_directory, "../../../../../")) @@ -217,6 +219,9 @@ def set_sup_title(self, text="", *args, **kwargs): >>> canvas = PlotCanvas(nrows=2, ncols=2) >>> canvas.set_sup_title("Main Figure Title", fontsize=16, fontweight='bold') """ + for key, value in PROVENANCE_TITLE_STYLE.items(): + kwargs.setdefault(key, value) + kwargs.setdefault("y", 0.985) plt.suptitle(text, *args, **kwargs) def show(self, *args, **kwargs): From 7ff222ff21b20a104751a683e1fd0597a7a446c0 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 22 Jun 2026 15:36:38 +0200 Subject: [PATCH 47/56] fixed yaml file to latest version and autoconvert+false --- idstools/database.py | 4 ++-- resources/input/ec_waveforms.yaml | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/idstools/database.py b/idstools/database.py index bb96ae12..3fbf6296 100644 --- a/idstools/database.py +++ b/idstools/database.py @@ -954,14 +954,14 @@ def read_scenario_with_args( if test_mode: ids = connection.get_slice(ids_name, test_args_list) else: - ids = connection.get(ids_name) + ids = connection.get(ids_name, autoconvert=False) in_ids_dict[ids_name] = ids for ids_name in out_ids_list: if test_mode: ids = connection.get_slice(ids_name, test_args_list) else: - ids = connection.get(ids_name) + ids = connection.get(ids_name, autoconvert=False) out_ids_dict[ids_name] = ids connection.close() diff --git a/resources/input/ec_waveforms.yaml b/resources/input/ec_waveforms.yaml index b1292dd3..e8a45c3c 100644 --- a/resources/input/ec_waveforms.yaml +++ b/resources/input/ec_waveforms.yaml @@ -68,17 +68,17 @@ dynamic_variables: beam[9].frequency.data: [1.70000e+11] beam[10].frequency.data: [1.70000e+11] - beam[0].mode: -1 # (1) O-mode, (-1) X-mode - beam[1].mode: -1 - beam[2].mode: -1 - beam[3].mode: -1 - beam[4].mode: -1 - beam[5].mode: -1 - beam[6].mode: -1 - beam[7].mode: -1 - beam[8].mode: -1 - beam[9].mode: -1 - beam[10].mode: -1 + beam[0].polarization.o_mode_fraction: [0.0] # 1.0 = O-mode, 0.0 = X-mode + beam[1].polarization.o_mode_fraction: [0.0] + beam[2].polarization.o_mode_fraction: [0.0] + beam[3].polarization.o_mode_fraction: [0.0] + beam[4].polarization.o_mode_fraction: [0.0] + beam[5].polarization.o_mode_fraction: [0.0] + beam[6].polarization.o_mode_fraction: [0.0] + beam[7].polarization.o_mode_fraction: [0.0] + beam[8].polarization.o_mode_fraction: [0.0] + beam[9].polarization.o_mode_fraction: [0.0] + beam[10].polarization.o_mode_fraction: [0.0] beam[0].steering_angle_pol: [0.00178] beam[1].steering_angle_pol: [0.30776] From 5cca20dbf1958cd915f05a6bfc24aaf4a2ba042d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:24:05 +0000 Subject: [PATCH 48/56] Bump actions/checkout from 6 to 7 in the actions-deps group Bumps the actions-deps group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] --- .github/workflows/linting.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/security.yml | 2 +- .github/workflows/test_with_pytest.yml | 2 +- .github/workflows/verify_with_sphinx.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index bc968975..c3401293 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout IDStools sources - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 619bb8d5..60d3fe83 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,7 +10,7 @@ jobs: name: Build distribution runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Python diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 0e66533e..25338750 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/test_with_pytest.yml b/.github/workflows/test_with_pytest.yml index 211cbbcd..ec11f697 100644 --- a/.github/workflows/test_with_pytest.yml +++ b/.github/workflows/test_with_pytest.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/verify_with_sphinx.yml b/.github/workflows/verify_with_sphinx.yml index 4cbf054e..63c91756 100644 --- a/.github/workflows/verify_with_sphinx.yml +++ b/.github/workflows/verify_with_sphinx.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout IDStools sources - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 From a812a52ed3d8ddf20dc96748391eaee065d735c3 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Mon, 22 Jun 2026 14:20:24 +0200 Subject: [PATCH 49/56] show labels with indices, if more than 20 labels then show first and last 5 labels --- idstools/scripts/bin/idsprint | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/idstools/scripts/bin/idsprint b/idstools/scripts/bin/idsprint index 669b8560..857a0168 100644 --- a/idstools/scripts/bin/idsprint +++ b/idstools/scripts/bin/idsprint @@ -3,6 +3,7 @@ import argparse import logging import os +import re import shutil import sys @@ -78,6 +79,29 @@ def _scientific_ticks(values, count=TERMINAL_PLOT_TICKS): return ticks, labels +def _series_labels(field_name, count): + """Expand the first path slice into one label per plotted series.""" + match = re.search(r"[\[(]([^\])]*:[^\])]*?)[\])]", field_name) + if match: + parts = match.group(1).split(":") + if len(parts) <= 3: + try: + start = int(parts[0]) if parts[0] else None + step = int(parts[2]) if len(parts) == 3 and parts[2] else 1 + except ValueError: + start = None + step = None + if step: + if start is None: + start = count - 1 if step < 0 else 0 + labels = [] + for series_index in range(count): + selected_index = start + series_index * step + labels.append(f"{field_name[: match.start()]}[{selected_index}]{field_name[match.end() :]}") + return labels + return [f"{field_name} [{series_index}]" for series_index in range(count)] + + def print_terminal_plot( field, coordinate, @@ -228,11 +252,13 @@ def view_plot( ax.plot(coordinate, data, label=field_name, marker="o", color="red") elif len(data.shape) == 2: + series_labels = _series_labels(field_name, data.shape[1]) + legend_indices = set(range(data.shape[1])) + if data.shape[1] > 20: + legend_indices = set(range(5)) | set(range(data.shape[1] - 5, data.shape[1])) for j in range(data.shape[1]): - if j == 0: - ax.plot(coordinate, data[:, j], label=f"{field_name}") - else: - ax.plot(coordinate, data[:, j]) + label = series_labels[j] if j in legend_indices else "_nolegend_" + ax.plot(coordinate, data[:, j], label=label) else: ax.plot(coordinate, data, label=field_name) From c2a84979ede760436eacfa73764386766942d38c Mon Sep 17 00:00:00 2001 From: Prasad Date: Thu, 2 Jul 2026 11:22:48 +0200 Subject: [PATCH 50/56] Feature/add separatrix and magnetic axes in plotequilibrium (#20) --- README.md | 13 +- docs/source/cheatsheet.rst | 2 +- docs/source/plotequilibrium.rst | 21 +- idstools/compute/equilibrium.py | 442 ++++++++++++++++++- 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 | 158 +++++-- idstools/scripts/bin/plothcd | 11 +- idstools/scripts/bin/plothcddistributions | 5 +- idstools/scripts/bin/plothcdwaves | 5 +- idstools/scripts/bin/plotkineticprofiles | 5 +- idstools/scripts/bin/plotmachinedescription | 5 +- 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/utils/clihelper.py | 6 +- idstools/view/domain/ecstray.py | 25 +- idstools/view/domain/mdplot.py | 42 +- idstools/view/equilibrium.py | 361 ++++++++++++++- tests/st03_test_analysis_scripts_with_uri.sh | 4 +- 27 files changed, 1025 insertions(+), 219 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: diff --git a/docs/source/cheatsheet.rst b/docs/source/cheatsheet.rst index c4557ff5..d3bf0f87 100644 --- a/docs/source/cheatsheet.rst +++ b/docs/source/cheatsheet.rst @@ -73,7 +73,7 @@ This cheat sheet provides quick reference of commonly used commands in IDStools. | | | | | $ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134173;run=2326;database=TEST;version=3" | | | -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" --rho | +| | "imas:hdf5?user=public;pulse=116000;run=4;database=ITER_MD;version=3#wall" --phi | | | | +----------------------------+------------------------------------------------------------------------------------------------------------------------------+ | *plotequicomp* | Shows plasma equilibrium and quantities related with it | diff --git a/docs/source/plotequilibrium.rst b/docs/source/plotequilibrium.rst index a5f3325b..e2bf5851 100644 --- a/docs/source/plotequilibrium.rst +++ b/docs/source/plotequilibrium.rst @@ -3,9 +3,17 @@ ################# *plotequilibrium* script shows plasma equilibrium. Optionally it also -shows pf coils position and toroidal flux. +shows PF coil positions and a toroidal-flux contour overlay. `refer data dictionary `_. +``--phi`` overlays contours of the toroidal magnetic flux Φ(R,Z) [Wb] read directly from +``equilibrium/time_slice/profiles_2d/phi``: + +.. math:: + + \Phi(R,Z) \quad [\mathrm{Wb}] + + ************************ Syntax plotequilibrium ************************ @@ -19,10 +27,11 @@ 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 "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:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3" --phi -md pf_active wall --profiles + $ plotequilibrium --uri "imas:mdsplus?user=public;pulse=134174;run=117;database=ITER;version=3" --phi -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" --phi --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" --profiles --no-provenance + .. image:: _static/images/plotequilibrium.png :alt: image not found :align: center @@ -33,4 +42,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/compute/equilibrium.py b/idstools/compute/equilibrium.py index f1903828..941a25c8 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") @@ -93,22 +95,20 @@ def get2d_cartesian_grid(self, time_slice: int, profiles2d_index: int = 0) -> Un return {"r2d": r1d, "z2d": z1d, "psi2d": psi2d} - def get_rho2d(self, time_slice: int, profiles2d_index: int = 0) -> Union[np.ndarray, None]: + def get_phi2d(self, time_slice: int, profiles2d_index: int = 0) -> Union[np.ndarray, None]: """ - This function calculates rho(R,Z) using toroidal flux and returns a dictionary containing the result. + Returns the toroidal magnetic flux Φ(R,Z) on the 2D grid. + + Reads ``equilibrium.time_slice[i].profiles_2d[j].phi`` directly from the IDS. Args: - time_slice (int): The time slice is an integer value that represents the index of the time slice in - the equilibrium ids. It is used to select a specific time slice for the calculation of rho(R,Z). - Defaults to 0 - profiles2d_index (int): `profiles2d_index` is an integer parameter that represents the index of the - ``profiles_2d`` to be used for the calculation of rho(R,Z). It is used to access the `profiles_2d` - list in the `time_slice` object. Defaults to 0 + time_slice (int): Index of the time slice in the equilibrium IDS. Defaults to 0. + profiles2d_index (int): Index into ``profiles_2d`` from which + ``phi`` (toroidal flux, Wb) is read. Defaults to 0. Returns: - a value containing the square root of the toroidal flux values divided by the maximum toroidal - flux value, if the length of toroidal flux is greater than 0. If the length of toroidal flux is - less than 1, it returns None. + np.ndarray or None: 2-D array of toroidal flux Φ [Wb] with the same shape as + the ``profiles_2d`` grid, or None if ``phi`` is unavailable or all-NaN. Examples: .. code-block:: python @@ -117,7 +117,7 @@ def get_rho2d(self, time_slice: int, profiles2d_index: int = 0) -> Union[np.ndar connection = imas.DBEntry("imas:mdsplus?user=public;pulse=134173;run=106;database=ITER;version=3", "r") idsObj = connection.get('equilibrium') computeObj = EquilibriumCompute(idsObj) - result = computeObj.get_rho2d(time_slice=0) + result = computeObj.get_phi2d(time_slice=0) """ phi = None @@ -134,7 +134,7 @@ def get_rho2d(self, time_slice: int, profiles2d_index: int = 0) -> Union[np.ndar f"all values are nan for equilibrium.time_slice[{time_slice}].profiles_2d[{profiles2d_index}].phi " ) return None - return np.sqrt(phi / np.amax(phi)) + return phi def get_b_total(self, time_slice: int) -> tuple: """ @@ -243,8 +243,8 @@ def get_flux_surfaces(self, time_slice: int) -> dict: Returns: a dictionary containing information about flux surfaces at a specific time slice. The dictionary includes - a 2D Cartesian grid, a 2D profile index, and a 2D array of rho values. If no profiles are found, - the function returns None. + a 2D Cartesian grid, a 2D profile index, and a 2D array of rho_tor_norm [-] values (dimensionless, + range [0, 1]). If no profiles are found, the function returns None. """ GRID_TYPE_RECTANGULAR = 1 list_of_profiles = self.get2d_profiles_indices(time_slice, GRID_TYPE_RECTANGULAR) @@ -255,10 +255,10 @@ def get_flux_surfaces(self, time_slice: int) -> dict: profile2d_index = list_of_profiles[0] result_dict = self.get2d_cartesian_grid(time_slice, profile2d_index) - rho2d = self.get_rho2d(time_slice, profile2d_index) - if rho2d is None: - rho2d = [] - result_dict["rho2d"] = rho2d + phi2d = self.get_phi2d(time_slice, profile2d_index) + if phi2d is None: + phi2d = [] + result_dict["phi2d"] = phi2d return result_dict def get_ip(self) -> list: @@ -285,6 +285,408 @@ def get_ip(self) -> list: for time_index in range(len(self.ids.time_slice)) ] + def get_boundary_data(self, time_slice: int) -> dict: + """Return boundary data for a given time slice. + + 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. + + 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): + a = np.asarray(arr, dtype=float) + 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) < _IDS_VALID_THRESHOLD + except Exception as exc: + logger.debug(f"get_boundary_data: invalid scalar {val!r} ({exc})") + return False + + def _clean(arr): + a = np.array(arr, dtype=float, copy=True) + a[(~np.isfinite(a)) | (np.abs(a) >= _IDS_VALID_THRESHOLD)] = 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 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 + 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, ids_path): + pts = [] + try: + arr = getattr(node, attr) + except AttributeError: + 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 {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 {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 {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: {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): + """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 as exc: + logger.debug(f"get_boundary_data: could not access contour_tree.node: {exc}") + return sep_r, sep_z, xpoints + + 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 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 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 + + try: + r = np.asarray(node.levelset.r, dtype=float) + z = np.asarray(node.levelset.z, dtype=float) + 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 = { + "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: + ts = self.ids.time_slice[time_slice] + except Exception as exc: + logger.debug(f"get_boundary_data: could not access time_slice[{time_slice}]: {exc}") + return result + + # boundary + try: + bnd = ts.boundary + result["bnd_r"], result["bnd_z"] = _read_outline(bnd) + 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( + 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'])})" + ) + + bnd_type = int(bnd.type) + if _valid_scalar(bnd_type): + result["bnd_type"] = bnd_type + except Exception as 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 time_slice[{time_slice}]/boundary/psi_norm: {exc}") + + 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 as 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", 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( + 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 time_slice[{time_slice}]/boundary_separatrix: {exc}") + + # 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 + + # 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}, " + 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]: + """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) < _IDS_VALID_THRESHOLD + + 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: + 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)): + 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} + + 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 below the IDS fill + value threshold), 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) < _IDS_VALID_THRESHOLD + 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. @@ -1024,7 +1426,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/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 f4a3f03e..13c46eb8 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -1,11 +1,10 @@ #!/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" import argparse -import copy import logging import os @@ -13,6 +12,7 @@ try: import imaspy as imas except ImportError: import imas +from mpl_toolkits.axes_grid1.inset_locator import inset_axes from rich_argparse import RichHelpFormatter from idstools.compute.common import get_nearest_time @@ -23,7 +23,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 @@ -49,14 +48,19 @@ if __name__ == "__main__": ) 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", + "--phi", + help=("Overlay contours of toroidal magnetic flux Φ(R,Z) [Wb] " "from equilibrium profiles_2d/phi"), action="store_true", ) parser.add_argument( - "-p", - "--plots", - help="Plots available quantities along with equilibrium", + "--no-overlay", + dest="no_overlay", + help="Hide equilibrium overlays", + action="store_true", + ) + parser.add_argument( + "--profiles", + help="Plot available 1D profiles and time traces alongside the equilibrium", action="store_true", ) parser.add_argument( @@ -75,8 +79,14 @@ if __name__ == "__main__": """, ) parser.add_argument( - "--show-labels", - help="Show labels", + "--no-provenance", + help="Hide URI provenance information from the plot title", + action="store_true", + ) + + parser.add_argument( + "--debug", + help="Show diagnostic logging", action="store_true", ) parser.add_argument( @@ -104,7 +114,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: @@ -125,9 +135,7 @@ 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: + 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) @@ -136,7 +144,6 @@ if __name__ == "__main__": 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: @@ -145,6 +152,7 @@ if __name__ == "__main__": col_size = int(total_plots / 2) col_size = col_size + 1 + canvas = PlotCanvas(2, col_size) ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0, rowspan=2) axes_list1 = [] @@ -163,7 +171,11 @@ if __name__ == "__main__": ax1 = canvas.add_axes(title="", xlabel="", row=0, col=0) canvas.update_style(args.rc) - if args.md: + md_overlay_state = {"created": False, "provenance_lines": []} + + def plot_md_overlay(): + if not args.md or md_overlay_state["created"]: + return md_overlay_state["provenance_lines"] idses = "" mduris = [] for mduri in args.md: @@ -188,35 +200,103 @@ 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) + 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 + + 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.phi, + plot_annotations=False, + ) + legend = ax1.get_legend() + if legend is not None: + legend.set_visible(False) - c_psi, c_rho = view_object.view_magnetic_poloidal_flux(ax1, time_slice, plot_rho=args.rho) 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 = inset_axes( + ax1, + width=0.24, + height="88%", + loc="lower left", + bbox_to_anchor=(-0.24, 0.05, 1, 1), + bbox_transform=ax1.transAxes, + borderpad=0, + ) + cbar_psi = canvas.fig.colorbar(c_psi, cax=cax_psi) + cbar_psi.set_label(r"$\psi$ [Wb]", fontsize=7, rotation=90, labelpad=4) + cbar_psi.ax.yaxis.set_label_position("left") + cbar_psi.ax.tick_params(labelsize=7) + 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() - 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, - ) - if args.plots: + cax_rho = inset_axes( + ax1, + width=0.24, + height="88%", + loc="lower left", + bbox_to_anchor=(-0.58, 0.05, 1, 1), + bbox_transform=ax1.transAxes, + borderpad=0, + ) + cbar_rho = canvas.fig.colorbar(c_rho, cax=cax_rho) + cbar_rho.set_label(r"$\Phi$ [Wb]", fontsize=7, rotation=90, labelpad=4) + cbar_rho.ax.yaxis.set_label_position("left") + 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) - canvas.fig.suptitle(get_title(args, "Equilibrium", time_value)) - 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) + 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.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.05, right=0.955, 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_boundary_outline=True, + plot_rho=False, + plot_annotations=not args.profiles, + plot_psi=False, + ) + legend = ax1.get_legend() + if legend is not None: + legend.set_visible(show_legend) + + 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) + + provenance_parts = [] + if not args.no_provenance: + provenance_parts.append(get_database_path(args).strip()) + if md_overlay_state["provenance_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__)) if args.save: fname = get_file_name(args, f"{os.path.basename(__file__)}_Equilibrium", time_value) 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 cc5c3d2b..cab0441e 100644 --- a/idstools/scripts/bin/plotmachinedescription +++ b/idstools/scripts/bin/plotmachinedescription @@ -68,10 +68,11 @@ 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) + 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__)) - mdcanvas.fig.suptitle("Machine Description") + 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/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/utils/clihelper.py b/idstools/utils/clihelper.py index c7b4cc19..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 @@ -241,11 +240,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/ecstray.py b/idstools/view/domain/ecstray.py index 66bfacec..591951b3 100644 --- a/idstools/view/domain/ecstray.py +++ b/idstools/view/domain/ecstray.py @@ -84,27 +84,34 @@ 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) + phi2d = self.equilibrium_compute.get_phi2d(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") + if phi2d is not None and len(phi2d) > 0: + contour_lines_rho = ax.contour(r2d, z2d, phi2d.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]") + cbar_rho.set_label(r"$\Phi$ [Wb]") # ax_polview.set_xlim(r2d.min(),r2d.max()) ax.set_title("Poloidal view (R,Z)") ax.set_xlabel("R [m]", labelpad=lpad) diff --git a/idstools/view/domain/mdplot.py b/idstools/view/domain/mdplot.py index e93fc1ed..7e6a338a 100644 --- a/idstools/view/domain/mdplot.py +++ b/idstools/view/domain/mdplot.py @@ -36,14 +36,26 @@ def update_labels(ax): ax.figure.canvas.draw_idle() -def plot_machine_description(ax, ids_data): +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 = "" + provenance_groups = {} + + 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 == main_uri.strip(): + return + labels = provenance_groups.setdefault(uri, []) + if ids_name_label not in labels: + labels.append(ids_name_label) mdlegends = [] mdlabels = [] @@ -70,7 +82,7 @@ def plot_machine_description(ax, ids_data): 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" + _add_provenance("pf_active", ids_data_and_config["connectionArgs"]) elif ids_name == "tf": select2 = ":" if len(matches) == 2: @@ -81,7 +93,7 @@ def plot_machine_description(ax, ids_data): 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" + _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 == "": @@ -90,7 +102,7 @@ def plot_machine_description(ax, ids_data): mdlegends.append(_legend) mdlabels.append(f"pf_passive:{idsocc}/loop[{select}]") - database_path += "pf_passive = " + get_database_path(ids_data_and_config["connectionArgs"]) + "\n" + _add_provenance("pf_passive", ids_data_and_config["connectionArgs"]) elif ids_name == "wall": wallview = WallView(ids_data_and_config["idsData"]) select2 = ":" @@ -100,7 +112,7 @@ def plot_machine_description(ax, ids_data): 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" + _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 == "": @@ -128,11 +140,9 @@ def plot_machine_description(ax, ids_data): if _legend: mdlegends.append(_legend) mdlabels.append(f"magnetics:{idsocc}/shunt[{select}]") - database_path += "magnetics = " + get_database_path(ids_data_and_config["connectionArgs"]) + "\n" + _add_provenance("magnetics", ids_data_and_config["connectionArgs"]) else: - database_path += ( - f"{ids_name} = " + get_database_path(ids_data_and_config["connectionArgs"]) + "No visualization yet\n" - ) + _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() @@ -151,14 +161,4 @@ 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, - ) + return " | ".join(f"{', '.join(labels)} = {uri}" for uri, labels in provenance_groups.items()) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index dadb96d9..03c68e88 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,13 @@ def view_magnetic_poloidal_flux( ax: plt.axes, time_slice: int, profiles2d_index: int = 0, + plot_magnetic_axis: bool = True, + plot_current_centre: bool = True, + plot_boundary_data: bool = True, 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. @@ -80,9 +87,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 : @@ -103,23 +112,341 @@ def view_magnetic_poloidal_flux( # # fmt="%.2e", # inline_spacing=1, # ) + + # phi (toroidal flux) overlay if plot_rho: - rho2d = self.compute_obj.get_rho2d(time_slice) - if rho2d is not None: + phi2d = self.compute_obj.get_phi2d(time_slice) + if phi2d 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"], phi2d.T, levels=levels, cmap="YlOrBr" ) 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) + + # 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 = [] + + 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="saddlebrown", + markersize=6, + markeredgewidth=1.4, + linestyle="None", + zorder=6, + ) + proxy_mag = ProxyLine( + [0], + [0], + color="saddlebrown", + marker="+", + markersize=6, + markeredgewidth=1.4, + linestyle="None", + label="magnetic axis", + ) + overlay_entries.append((proxy_mag, [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="deeppink", + markersize=6, + markeredgewidth=1.4, + linestyle="None", + zorder=6, + ) + proxy_cc = ProxyLine( + [0], + [0], + color="deeppink", + marker="+", + markersize=6, + markeredgewidth=1.4, + linestyle="None", + label="current centre", + ) + 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 and not separatrix_is_boundary: + (sep_line,) = ax.plot( + bd["sep_r"], + bd["sep_z"], + color="firebrick", + linewidth=2.0, + linestyle="--", + zorder=4, + ) + proxy_sep_bnd = ProxyLine( + [0], [0], color="firebrick", linewidth=2.0, linestyle="--", label="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="x", + color="darkcyan", + markersize=6, + markeredgewidth=1.4, + linestyle="None", + zorder=6, + ) + proxy_gax = ProxyLine( + [0], + [0], + color="darkcyan", + marker="x", + markersize=6, + markeredgewidth=1.4, + linestyle="None", + label="Geometric axis", + ) + 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"), + ] + 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=point_marker_size, + markeredgewidth=point_marker_edgewidth, + linestyle="None", + zorder=7, + ) + ann = ax.annotate( + f"X{xp_idx}", + xy=(xr, xz), + xytext=(-6, 6), + textcoords="offset points", + fontsize=point_label_fontsize, + 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=point_marker_size, + markeredgewidth=point_marker_edgewidth, + linestyle="None", + label=xp_label, + ) + overlay_entries.append((proxy_xp, _xp_artists)) + + # strike-points (boundary_separatrix) + _sp_groups = [ + (bd["sep_strikepoints"], "red", "strike_point"), + ] + 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=point_marker_size, + markeredgewidth=point_marker_edgewidth, + linestyle="None", + zorder=7, + ) + ann = ax.annotate( + f"S{sp_idx}", + xy=(sr, sz), + xytext=(-6, 6), + textcoords="offset points", + fontsize=point_label_fontsize, + 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=point_marker_size, + markeredgewidth=point_marker_edgewidth, + linestyle="None", + label=sp_label, + ) + overlay_entries.append((proxy_sp, _sp_artists)) + + if plot_annotations: + self.view_global_quantities_annotation(ax, time_slice) + + # --- clickable legend + 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=all_handles, + labels=all_labels, + loc="upper left", + bbox_to_anchor=(1.15, 1), + fancybox=True, + frameon=False, + framealpha=1.0, + facecolor="white", + fontsize=10, + labelspacing=1.2, + ) + legend.set_zorder(1000) + for text in legend.get_texts(): + text.set_ha("left") + + 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 + 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) + 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) + 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) + 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.15, + 0.0, + textstr, + transform=ax.transAxes, + fontsize=9, + horizontalalignment="left", + verticalalignment="bottom", + clip_on=False, + bbox=dict(boxstyle="round,pad=0.5", facecolor="white", alpha=1.0, edgecolor="none"), + ) + return txt + def plot_ip(self, ax): """ This function plots the plasma current over time on a given axis. @@ -160,9 +487,9 @@ def plot_poloidal_equilibrium(self, ax, time_slice: int): data = self.compute_obj.get_flux_surfaces(time_slice) r2d = data["r2d"] z2d = data["z2d"] - # rho2d = data["rho2d"] + # phi2d = data["phi2d"] 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: @@ -255,13 +582,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 @@ -275,11 +601,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 diff --git a/tests/st03_test_analysis_scripts_with_uri.sh b/tests/st03_test_analysis_scripts_with_uri.sh index f2b7234a..d4fd805e 100755 --- a/tests/st03_test_analysis_scripts_with_uri.sh +++ b/tests/st03_test_analysis_scripts_with_uri.sh @@ -33,7 +33,7 @@ SCRIPTS=( "plotecray --uri \"imas:mdsplus?user=public;pulse=134173;run=2326;database=TEST;version=3\" --save --directory $LOG_DIR" "plotecray --uri \"imas:mdsplus?user=public;pulse=134173;run=2326;database=TEST;version=3\" -md wall --save --directory $LOG_DIR" "plotedgeprofiles --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" --wall --time 60 --save --directory $LOG_DIR" - "plotequilibrium --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" --rho -md \"imas:hdf5?user=public;pulse=111001;run=103;database=ITER_MD;version=3#pf_active\" \"imas:hdf5?user=public;pulse=116000;run=5;database=ITER_MD;version=3#wall\" --save --directory $LOG_DIR" + "plotequilibrium --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" --phi -md \"imas:hdf5?user=public;pulse=111001;run=103;database=ITER_MD;version=3#pf_active\" \"imas:hdf5?user=public;pulse=116000;run=5;database=ITER_MD;version=3#wall\" --save --directory $LOG_DIR" "plotequilibrium --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" -md pf_active wall --save --directory $LOG_DIR" "printfluxes --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" -m CLOSEST" "plothcd -ech 134173/101/public/MDSPLUS/TEST/3 -nbi 130012/115/public/MDSPLUS/TEST/3 -fus 130012/115/public/MDSPLUS/TEST/3 -icrh 130012/115/public/MDSPLUS/TEST/3 --save --directory $LOG_DIR" @@ -64,7 +64,7 @@ return $? # "plotecray --uri \"imas:hdf5?user=public;pulse=134173;run=2326;database=TEST;version=3\" --dd-update --save --directory $LOG_DIR" # "plotecray --uri \"imas:hdf5?user=public;pulse=134173;run=2326;database=TEST;version=3\" -md wall --dd-update --save --directory $LOG_DIR" # "plotedgeprofiles --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" --wall --time 60 --dd-update --save --directory $LOG_DIR" -# "plotequilibrium --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" --rho -md \"imas:hdf5?user=public;pulse=111001;run=103;database=ITER_MD;version=3#pf_active\" \"imas:hdf5?user=public;pulse=116000;run=5;database=ITER_MD;version=3#wall\" --dd-update --save --directory $LOG_DIR" +# "plotequilibrium --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" --phi -md \"imas:hdf5?user=public;pulse=111001;run=103;database=ITER_MD;version=3#pf_active\" \"imas:hdf5?user=public;pulse=116000;run=5;database=ITER_MD;version=3#wall\" --dd-update --save --directory $LOG_DIR" # "plotequilibrium --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" -md pf_active wall --dd-update --dd-update --save --directory $LOG_DIR" # "printfluxes --uri \"imas:hdf5?user=public;pulse=134174;run=117;database=ITER;version=3\" -m CLOSEST --dd-update" # "plothcd -ech 134173/101/public/MDSPLUS/TEST/3 -nbi 130012/115/public/MDSPLUS/TEST/3 -fus 130012/115/public/MDSPLUS/TEST/3 -icrh 130012/115/public/MDSPLUS/TEST/3 --dd-update --save --directory $LOG_DIR" From 9ebd166622d70407217749b8b4a149475fb7b9b3 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 2 Jul 2026 15:05:30 +0200 Subject: [PATCH 51/56] removed duplicate --no-provenance arguement --- idstools/scripts/bin/plotequilibrium | 6 ------ 1 file changed, 6 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 13c46eb8..4ce02cb0 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -78,12 +78,6 @@ 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", help="Show diagnostic logging", From a2c31c8402208f12b5be9f350fc472b924fb015d Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 2 Jul 2026 15:09:34 +0200 Subject: [PATCH 52/56] plot_rho -> plot_phi internal variable --- idstools/scripts/bin/plotequilibrium | 4 ++-- idstools/view/equilibrium.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 4ce02cb0..677a46ab 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -207,7 +207,7 @@ if __name__ == "__main__": plot_magnetic_axis=False, plot_current_centre=False, plot_boundary_data=False, - plot_rho=args.phi, + plot_phi=args.phi, plot_annotations=False, ) legend = ax1.get_legend() @@ -266,7 +266,7 @@ if __name__ == "__main__": plot_current_centre=True, plot_boundary_data=True, plot_boundary_outline=True, - plot_rho=False, + plot_phi=False, plot_annotations=not args.profiles, plot_psi=False, ) diff --git a/idstools/view/equilibrium.py b/idstools/view/equilibrium.py index 03c68e88..590c31bd 100644 --- a/idstools/view/equilibrium.py +++ b/idstools/view/equilibrium.py @@ -44,7 +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, + plot_phi: bool = False, plot_annotations: bool = True, plot_psi: bool = True, plot_boundary_outline: bool = False, @@ -89,7 +89,7 @@ def view_magnetic_poloidal_flux( contour_lines_psi = contour_lines_rho = None levels = 50 cartestion_grid = None - if plot_psi or plot_rho: + if plot_psi or plot_phi: cartestion_grid = self.compute_obj.get2d_cartesian_grid(time_slice, profiles2d_index) if cartestion_grid is not None and plot_psi: @@ -114,7 +114,7 @@ def view_magnetic_poloidal_flux( # ) # phi (toroidal flux) overlay - if plot_rho: + if plot_phi: phi2d = self.compute_obj.get_phi2d(time_slice) if phi2d is not None: contour_lines_rho = ax.contour( From 5a6ecaf2460332f6fabbee07eaf64b53de636103 Mon Sep 17 00:00:00 2001 From: Prasad Date: Fri, 3 Jul 2026 13:58:22 +0200 Subject: [PATCH 53/56] Feature/allow custom matplotlib backend (#33) --- idstools/scripts/bin/idsprint | 15 ++---- idstools/scripts/bin/plotcoresources | 11 +--- idstools/scripts/bin/plotcoretransport | 11 +--- idstools/scripts/bin/ploteccomposition | 11 +--- idstools/scripts/bin/plotecray | 11 +--- idstools/scripts/bin/plotecstrayradiation | 13 ++--- idstools/scripts/bin/plotedgeprofiles | 11 +--- idstools/scripts/bin/plotequicomp | 11 +--- idstools/scripts/bin/plotequilibrium | 11 +--- idstools/scripts/bin/plothcd | 21 ++++---- idstools/scripts/bin/plothcddistributions | 11 +--- idstools/scripts/bin/plothcdwaves | 11 +--- idstools/scripts/bin/plotkineticprofiles | 13 +---- idstools/scripts/bin/plotmachinedescription | 12 +---- idstools/scripts/bin/plotneutron | 11 +--- idstools/scripts/bin/plotpressure | 11 +--- idstools/scripts/bin/plotrotation | 11 +--- idstools/scripts/bin/plotscenario | 15 ++---- idstools/scripts/bin/plotspectrometry | 23 ++------ idstools/utils/clihelper.py | 18 +++++++ idstools/view/common.py | 59 +++++++++++++++------ 21 files changed, 117 insertions(+), 204 deletions(-) diff --git a/idstools/scripts/bin/idsprint b/idstools/scripts/bin/idsprint index 857a0168..a0f65aef 100644 --- a/idstools/scripts/bin/idsprint +++ b/idstools/scripts/bin/idsprint @@ -25,6 +25,7 @@ from rich_argparse import RichHelpFormatter from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, get_title, @@ -32,7 +33,6 @@ from idstools.utils.clihelper import ( ) from idstools.utils.idshelper import get_available_ids_and_times, parse_uri, partial_get from idstools.utils.idslogger import setup_logger -from idstools.view.common import PlotCanvas logger = setup_logger("module", stdout_level=logging.INFO) @@ -531,6 +531,9 @@ if __name__ == "__main__": default=None, ) args = parser.parse_args() + if args.plot: + from idstools.view.common import PlotCanvas + time_slice = 0.0 if args.time: time_slice = float(args.time) @@ -764,15 +767,7 @@ if __name__ == "__main__": canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) canvas.fig.suptitle(get_title(args, f"{ids_name}/{original_ids_path}", None)) - if args.save: - fname = get_file_name(args, os.path.basename(__file__), None) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__), None) else: print_tree( diff --git a/idstools/scripts/bin/plotcoresources b/idstools/scripts/bin/plotcoresources index 9c0e2499..6e7f268c 100644 --- a/idstools/scripts/bin/plotcoresources +++ b/idstools/scripts/bin/plotcoresources @@ -15,6 +15,7 @@ from idstools.compute.common import get_nearest_time from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -100,12 +101,4 @@ if __name__ == "__main__": 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.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: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_core_sources", time_value) diff --git a/idstools/scripts/bin/plotcoretransport b/idstools/scripts/bin/plotcoretransport index 2a692b31..7eb2c9da 100644 --- a/idstools/scripts/bin/plotcoretransport +++ b/idstools/scripts/bin/plotcoretransport @@ -22,6 +22,7 @@ from rich_argparse import RichHelpFormatter from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -178,12 +179,4 @@ if __name__ == "__main__": 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() - if args.save: - fname = get_file_name(args, os.path.basename(__file__) + "_core_transport", time_value) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_core_transport", time_value) diff --git a/idstools/scripts/bin/ploteccomposition b/idstools/scripts/bin/ploteccomposition index 9f243e5d..8b94d9f8 100644 --- a/idstools/scripts/bin/ploteccomposition +++ b/idstools/scripts/bin/ploteccomposition @@ -14,6 +14,7 @@ from idstools.compute.common import get_nearest_time from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -116,15 +117,7 @@ if __name__ == "__main__": 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__)) - if args.save: - fname = get_file_name(args, os.path.basename(__file__) + "_EC_Composition", time_value) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_EC_Composition", time_value) conn.close() # def StrToList(string): # if string[0] == "[": diff --git a/idstools/scripts/bin/plotecray b/idstools/scripts/bin/plotecray index eeae1085..b9968b75 100644 --- a/idstools/scripts/bin/plotecray +++ b/idstools/scripts/bin/plotecray @@ -19,6 +19,7 @@ from idstools.compute.waves import WavesCompute from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, get_title, @@ -261,15 +262,7 @@ if __name__ == "__main__": canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) 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) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_EC_rays", time_value) # def StrToList(string): # if string[0] == "[": diff --git a/idstools/scripts/bin/plotecstrayradiation b/idstools/scripts/bin/plotecstrayradiation index 6b7f8841..8d734f26 100644 --- a/idstools/scripts/bin/plotecstrayradiation +++ b/idstools/scripts/bin/plotecstrayradiation @@ -18,10 +18,11 @@ from idstools.input_processing import ( read_wall, ) from idstools.utils.clihelper import ( + dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, - dbentry_parser, ) from idstools.utils.idslogger import setup_logger from idstools.view.common import PlotCanvas @@ -193,12 +194,4 @@ if __name__ == "__main__": ) 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) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, f"{os.path.basename(__file__)}_Equilibrium", time_value_equilibrium) diff --git a/idstools/scripts/bin/plotedgeprofiles b/idstools/scripts/bin/plotedgeprofiles index aceeb5ce..76656958 100644 --- a/idstools/scripts/bin/plotedgeprofiles +++ b/idstools/scripts/bin/plotedgeprofiles @@ -18,6 +18,7 @@ from idstools.compute.wall import WallCompute from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -190,12 +191,4 @@ if __name__ == "__main__": 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: - fname = get_file_name(args, os.path.basename(__file__) + "_edge_profiles", time_value) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_edge_profiles", time_value) diff --git a/idstools/scripts/bin/plotequicomp b/idstools/scripts/bin/plotequicomp index b59d7619..19f5a53d 100644 --- a/idstools/scripts/bin/plotequicomp +++ b/idstools/scripts/bin/plotequicomp @@ -16,6 +16,7 @@ from rich_argparse import RichHelpFormatter from idstools.compute.common import get_nearest_time from idstools.utils.clihelper import ( + show_plot, rcparam_parser, ) from idstools.utils.idslogger import setup_logger @@ -401,12 +402,4 @@ if __name__ == "__main__": canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) - if args.save: - fname = f"plotequicomp_time_{time_value1}.png" - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, fname=f"plotequicomp_time_{time_value1}.png") diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index 677a46ab..d6053d4f 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -21,6 +21,7 @@ from idstools.database import DBMaster from idstools.machinedescription import get_md_data from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -292,14 +293,6 @@ if __name__ == "__main__": 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: - fname = get_file_name(args, f"{os.path.basename(__file__)}_Equilibrium", time_value) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, f"{os.path.basename(__file__)}_Equilibrium", time_value) else: logger.warning("Can not produce plot, equilibrium/time is None") diff --git a/idstools/scripts/bin/plothcd b/idstools/scripts/bin/plothcd index d87007ff..8b90ec57 100644 --- a/idstools/scripts/bin/plothcd +++ b/idstools/scripts/bin/plothcd @@ -14,6 +14,7 @@ from idstools.compute.common import get_nearest_time from idstools.compute.waves import WavesCompute from idstools.database import DBMaster from idstools.utils.clihelper import ( + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -28,6 +29,8 @@ logger = setup_logger("module", stdout_level=logging.INFO) def _show_waves_plots(connargs, args, hold=False, dd_update=False, rc=""): connargs.no_provenance = args.get("no_provenance", False) + connargs.save = args["save"] + connargs.directory = args["directory"] conn = DBMaster.get_connection(connargs) if conn is None: logger.critical(f"data entry not found : {connargs}") @@ -105,11 +108,7 @@ def _show_waves_plots(connargs, args, hold=False, dd_update=False, rc=""): ) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) 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) - else: - canvas.show() + show_plot(canvas, connargs, "hcd_waves_plot", time_value) conn.close() @@ -143,6 +142,8 @@ def show_waves_plots(args, hold=False, dd_update=False, rc=""): def _show_distribution_plots(connargs, args, hold=False, dd_update=False, rc=""): connargs.no_provenance = args.get("no_provenance", False) + connargs.save = args["save"] + connargs.directory = args["directory"] conn = DBMaster.get_connection(connargs) if conn is None: logger.critical(f"data entry not found : {connargs}") @@ -191,11 +192,7 @@ def _show_distribution_plots(connargs, args, hold=False, dd_update=False, rc="") ) canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) 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) - else: - canvas.show() + show_plot(canvas, connargs, os.path.basename(__file__) + "_Distributions_profile_time", time_value) conn.close() @@ -446,22 +443,26 @@ if __name__ == "__main__": ec_args["force_psi"] = None ec_args["hide_legend"] = False ec_args["save"] = args["save"] + ec_args["directory"] = args["directory"] show_waves_plots(ec_args, hold=holder["ech_hold"], dd_update=args["dd_update"], rc=args["rc"]) # NUCLEAR REACTIONS if hcd_sources["fus"]["run"]: fus_args = hcd_sources["fus"] fus_args["save"] = args["save"] + fus_args["directory"] = args["directory"] show_distribution_plots(fus_args, hold=holder["icrh_hold"], dd_update=args["dd_update"], rc=args["rc"]) # NBI if hcd_sources["nbi"]["run"]: nbi_args = hcd_sources["nbi"] nbi_args["save"] = args["save"] + nbi_args["directory"] = args["directory"] show_distribution_plots(nbi_args, hold=holder["nbi_hold"], dd_update=args["dd_update"], rc=args["rc"]) # ICRH if hcd_sources["icrh"]["run"]: icrh_args = hcd_sources["icrh"] icrh_args["save"] = args["save"] + icrh_args["directory"] = args["directory"] show_distribution_plots(icrh_args, hold=holder["fus_hold"], dd_update=args["dd_update"], rc=args["rc"]) diff --git a/idstools/scripts/bin/plothcddistributions b/idstools/scripts/bin/plothcddistributions index 672d31a4..5ceeeaa6 100644 --- a/idstools/scripts/bin/plothcddistributions +++ b/idstools/scripts/bin/plothcddistributions @@ -14,6 +14,7 @@ from idstools.compute.common import get_nearest_time from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -77,15 +78,7 @@ def show_plots(args): canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) 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) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_Distributions_profile", time_value) conn.close() diff --git a/idstools/scripts/bin/plothcdwaves b/idstools/scripts/bin/plothcdwaves index cc9a5f15..5eddd9e3 100644 --- a/idstools/scripts/bin/plothcdwaves +++ b/idstools/scripts/bin/plothcdwaves @@ -15,6 +15,7 @@ from idstools.compute.waves import WavesCompute from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -106,15 +107,7 @@ def show_plots(args): canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) 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) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_heating_profiles_time", time_value) conn.close() diff --git a/idstools/scripts/bin/plotkineticprofiles b/idstools/scripts/bin/plotkineticprofiles index d089d927..1016cf72 100644 --- a/idstools/scripts/bin/plotkineticprofiles +++ b/idstools/scripts/bin/plotkineticprofiles @@ -9,6 +9,7 @@ from rich_argparse import RichHelpFormatter from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -84,14 +85,4 @@ if __name__ == "__main__": 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( - args, os.path.basename(__file__) + "_Kinetic", kp_view.k_profiles.time_value_core_profiles - ) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_Kinetic", kp_view.k_profiles.time_value_core_profiles) diff --git a/idstools/scripts/bin/plotmachinedescription b/idstools/scripts/bin/plotmachinedescription index 5c8ee293..bf661565 100644 --- a/idstools/scripts/bin/plotmachinedescription +++ b/idstools/scripts/bin/plotmachinedescription @@ -9,7 +9,7 @@ import os from rich_argparse import RichHelpFormatter from idstools.machinedescription import get_md_data -from idstools.utils.clihelper import rcparam_parser +from idstools.utils.clihelper import show_plot, rcparam_parser from idstools.utils.idslogger import setup_logger from idstools.view.common import PlotCanvas from idstools.view.domain.mdplot import plot_machine_description @@ -73,12 +73,4 @@ if __name__ == "__main__": 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.set_sup_title("" if args.no_provenance else md_provenance, fontsize=8) - if args.save: - fname = os.path.basename(__file__) + "_machine_description.png" - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - mdcanvas.save(fname) - else: - mdcanvas.show() + show_plot(mdcanvas, args, fname=os.path.basename(__file__) + "_machine_description.png") diff --git a/idstools/scripts/bin/plotneutron b/idstools/scripts/bin/plotneutron index 467b6edf..ec26631f 100644 --- a/idstools/scripts/bin/plotneutron +++ b/idstools/scripts/bin/plotneutron @@ -17,6 +17,7 @@ from idstools.compute.common import get_nearest_time from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -128,12 +129,4 @@ if __name__ == "__main__": canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) 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) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_Neutrons", time_value) diff --git a/idstools/scripts/bin/plotpressure b/idstools/scripts/bin/plotpressure index b1b97736..2ab2f476 100644 --- a/idstools/scripts/bin/plotpressure +++ b/idstools/scripts/bin/plotpressure @@ -17,6 +17,7 @@ from idstools.compute.common import get_nearest_time from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -86,12 +87,4 @@ if __name__ == "__main__": canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) 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) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_Pressure", time_value) diff --git a/idstools/scripts/bin/plotrotation b/idstools/scripts/bin/plotrotation index 50a50ada..819d83ff 100644 --- a/idstools/scripts/bin/plotrotation +++ b/idstools/scripts/bin/plotrotation @@ -16,6 +16,7 @@ from idstools.compute.common import get_nearest_time from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -90,12 +91,4 @@ if __name__ == "__main__": canvas.get_current_fig_manager().set_window_title(os.path.basename(__file__)) 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) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) - else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_Kinetic_profiles", time_value) diff --git a/idstools/scripts/bin/plotscenario b/idstools/scripts/bin/plotscenario index 8ac729b4..87ce34a3 100644 --- a/idstools/scripts/bin/plotscenario +++ b/idstools/scripts/bin/plotscenario @@ -16,6 +16,7 @@ from idstools.compute.common import get_nearest_time from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -220,15 +221,7 @@ if __name__ == "__main__": 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) - if args.save: - if not args.no_profiles: - fname = get_file_name(args, os.path.basename(__file__) + "_summary", time_value) - else: - fname = get_file_name(args, os.path.basename(__file__) + "_Scenario_shot") - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas.save(fname) + if not args.no_profiles: + show_plot(canvas, args, os.path.basename(__file__) + "_summary", time_value) else: - canvas.show() + show_plot(canvas, args, os.path.basename(__file__) + "_Scenario_shot") diff --git a/idstools/scripts/bin/plotspectrometry b/idstools/scripts/bin/plotspectrometry index 620c62df..d5d86d4b 100644 --- a/idstools/scripts/bin/plotspectrometry +++ b/idstools/scripts/bin/plotspectrometry @@ -17,6 +17,7 @@ from idstools.compute.spectrometer_visible import SpectrometerVisibleCompute from idstools.database import DBMaster from idstools.utils.clihelper import ( dbentry_parser, + show_plot, get_database_path, get_file_name, rcparam_parser, @@ -99,15 +100,7 @@ if __name__ == "__main__": 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: - fname = get_file_name(args, os.path.basename(__file__) + "-radiance", time_value) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas_radiance.save(fname) - else: - canvas_radiance.show(block=False) + show_plot(canvas_radiance, args, os.path.basename(__file__) + "-radiance", time_value, show_kwargs={"block": False}) canvas_intensity = PlotCanvas(rows, columns) canvas_intensity.update_style(args.rc) row_counter = 0 @@ -126,13 +119,7 @@ if __name__ == "__main__": 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") - if args.save: - fname = get_file_name(args, os.path.basename(__file__) + "_intensity", time_value) - if args.directory: - if not os.path.exists(args.directory): - os.makedirs(args.directory) - fname = os.path.join(args.directory, fname) - canvas_intensity.save(fname) - else: - canvas_intensity.show(block=True) + show_plot( + canvas_intensity, args, os.path.basename(__file__) + "_intensity", time_value, show_kwargs={"block": True} + ) connection.close() diff --git a/idstools/utils/clihelper.py b/idstools/utils/clihelper.py index b138fe4d..d5c9a438 100644 --- a/idstools/utils/clihelper.py +++ b/idstools/utils/clihelper.py @@ -213,6 +213,24 @@ def get_file_name(imasargs, title="", time_value=None): return _file_name +def show_plot(canvas, imasargs, title="", time_value=None, fname=None, show_kwargs=None): + """Save non-interactive canvases automatically, otherwise show the plot.""" + noninteractive = getattr(canvas.fig.canvas, "required_interactive_framework", None) is None + if not imasargs.save and not noninteractive: + canvas.show(**(show_kwargs or {})) + return + + if fname is None: + fname = get_file_name(imasargs, title, time_value) + if noninteractive and not imasargs.save: + extension = canvas.fig.canvas.get_default_filetype() + fname = f"{os.path.splitext(fname)[0]}.{extension}" + if imasargs.directory: + os.makedirs(imasargs.directory, exist_ok=True) + fname = os.path.join(imasargs.directory, fname) + canvas.save(fname) + + def get_database_path(imasargs, time_value=None) -> str: """ The function `get_database_path` returns the absolute path of a database based on the provided arguments. diff --git a/idstools/view/common.py b/idstools/view/common.py index e2772951..d38e79ac 100644 --- a/idstools/view/common.py +++ b/idstools/view/common.py @@ -1,8 +1,35 @@ +import ast import logging import os import sys -import matplotlib + +def _backend_from_cli_rc(argv): + """Return a backend requested by a --rc backend=... command-line option.""" + for index, argument in enumerate(argv): + if argument == "--rc" and index + 1 < len(argv): + rc_string = argv[index + 1] + elif argument.startswith("--rc="): + rc_string = argument.split("=", 1)[1] + else: + continue + for item in rc_string.split(";"): + key, separator, value = item.partition("=") + if separator and key.strip() == "backend": + value = value.strip() + try: + value = ast.literal_eval(value) + except (SyntaxError, ValueError): + pass + return str(value) + return None + + +_requested_backend = _backend_from_cli_rc(sys.argv) +if _requested_backend: + os.environ["MPLBACKEND"] = _requested_backend + +import matplotlib # noqa: E402 - backend env must be set before importing matplotlib def _is_jupyter() -> bool: @@ -21,25 +48,25 @@ def _is_jupyter() -> bool: return False -# Select the appropriate matplotlib backend -if _is_jupyter(): - if "matplotlib.pyplot" not in sys.modules: - try: - import ipympl # noqa: F401 - imported to check availability +if not os.environ.get("MPLBACKEND"): + if _is_jupyter(): + if "matplotlib.pyplot" not in sys.modules: + try: + import ipympl # noqa: F401 - imported to check availability - matplotlib.use("widget") - except ImportError: - matplotlib.use("agg") -elif sys.platform.startswith("win") or "DISPLAY" in os.environ: + matplotlib.use("widget") + except ImportError: + matplotlib.use("agg") + elif sys.platform.startswith("win") or "DISPLAY" in os.environ: - try: - import tkinter # noqa: F401 - imported to check availability + try: + import tkinter # noqa: F401 - imported to check availability - matplotlib.use("TkAgg") - except (ImportError, ModuleNotFoundError): + matplotlib.use("TkAgg") + except (ImportError, ModuleNotFoundError): + matplotlib.use("agg") + else: matplotlib.use("agg") -else: - matplotlib.use("agg") import matplotlib.pyplot as plt # noqa: E402 From 4f98020c28974a006661faadd73baee70afde13f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:24:17 +0000 Subject: [PATCH 54/56] Bump actions/setup-python from 6 to 7 in the actions-deps group Bumps the actions-deps group with 1 update: [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/setup-python` from 6 to 7 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-deps ... Signed-off-by: dependabot[bot] --- .github/workflows/linting.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/security.yml | 2 +- .github/workflows/test_with_pytest.yml | 2 +- .github/workflows/verify_with_sphinx.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index c3401293..beb1fa74 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: # until saxonche is available in 3.13 # https://saxonica.plan.io/issues/6561 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 60d3fe83..4198a096 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: # until saxonche is available in 3.13 # https://saxonica.plan.io/issues/6561 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 25338750..ddc45e04 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.11' cache: 'pip' diff --git a/.github/workflows/test_with_pytest.yml b/.github/workflows/test_with_pytest.yml index ec11f697..048cded3 100644 --- a/.github/workflows/test_with_pytest.yml +++ b/.github/workflows/test_with_pytest.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Display Python version diff --git a/.github/workflows/verify_with_sphinx.yml b/.github/workflows/verify_with_sphinx.yml index 63c91756..f5a79d4c 100644 --- a/.github/workflows/verify_with_sphinx.yml +++ b/.github/workflows/verify_with_sphinx.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: # until saxonche is available in 3.13 # https://saxonica.plan.io/issues/6561 From 4f03438e2a7b83443678c963f2702580df67ccfd Mon Sep 17 00:00:00 2001 From: Prasad Date: Fri, 24 Jul 2026 11:09:21 +0200 Subject: [PATCH 55/56] Let matplotlib choose its default backend (#35) * Let matplotlib chooses its default backend * Update idstools/view/common.py Co-authored-by: Simon Pinches * added documentation and fix interactive logic * do not save if interactive backend is available * moved backend setup and Jupyter detection into shared module and delayed Matplotlib imports so --rc backend=... is applied before Matplotlib loads. * fix(matplotlib): add backend config helper --------- Co-authored-by: Simon Pinches --- docs/source/jupyter.rst | 51 ++++++++++++++ docs/source/tools.rst | 1 + idstools/compute/equilibrium.py | 2 +- idstools/scripts/bin/idsdiff | 10 +-- idstools/scripts/bin/idsprint | 5 +- idstools/scripts/bin/plotcoresources | 5 +- idstools/scripts/bin/plotcoretransport | 5 +- idstools/scripts/bin/ploteccomposition | 5 +- idstools/scripts/bin/plotecray | 5 +- idstools/scripts/bin/plotecstrayradiation | 5 +- idstools/scripts/bin/plotedgeprofiles | 5 +- idstools/scripts/bin/plotequicomp | 9 ++- idstools/scripts/bin/plotequilibrium | 8 ++- idstools/scripts/bin/plothcd | 5 +- idstools/scripts/bin/plothcddistributions | 5 +- idstools/scripts/bin/plothcdwaves | 5 +- idstools/scripts/bin/plotkineticprofiles | 5 +- idstools/scripts/bin/plotmachinedescription | 5 +- idstools/scripts/bin/plotneutron | 5 +- idstools/scripts/bin/plotpressure | 5 +- idstools/scripts/bin/plotrotation | 5 +- idstools/scripts/bin/plotscenario | 5 +- idstools/scripts/bin/plotspectrometry | 5 +- idstools/utils/clihelper.py | 56 ++++++++++----- idstools/utils/matplotlib_backend.py | 47 +++++++++++++ idstools/view/common.py | 77 +++------------------ 26 files changed, 233 insertions(+), 113 deletions(-) create mode 100644 docs/source/jupyter.rst create mode 100644 idstools/utils/matplotlib_backend.py diff --git a/docs/source/jupyter.rst b/docs/source/jupyter.rst new file mode 100644 index 00000000..9c48301e --- /dev/null +++ b/docs/source/jupyter.rst @@ -0,0 +1,51 @@ +####################### + Jupyter notebook usage +####################### + +IDStools command-line tools can also be used directly from Jupyter notebooks. +Import ``idstools`` once in the notebook kernel to register the IDStools +commands as IPython line magics: + +.. code-block:: python + + import idstools + +After that, commands such as ``idsprint`` and ``plotequilibrium`` can be +called with ``%``: + +.. code-block:: python + + %idsprint -u "imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/134174/117#core_profiles/profiles_1d[0]/electrons/temperature" -p + +.. code-block:: python + + %plotequilibrium -u "imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/100507/5" + + +****************************** + Interactive Matplotlib plots +****************************** + +For interactive Matplotlib figures in Jupyter, use the ``ipympl`` backend. If +``ipympl`` is installed in the same Python environment as IDStools, the backend +can be selected with Matplotlib's notebook magic before plotting: + +.. code-block:: python + + %matplotlib widget + import idstools + + %idsprint -u "imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/134174/117#core_profiles/profiles_1d[0]/electrons/temperature" -p + +Alternatively, the backend can be selected through the IDStools ``--rc`` +option before Matplotlib has been imported in the current kernel: + +.. code-block:: python + + import idstools + + %idsprint -u "imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/134174/117#core_profiles/profiles_1d[0]/electrons/temperature" -p --rc "backend='module://ipympl.backend_nbagg'" + +.. code-block:: python + + %plotequilibrium -u "imas:hdf5?path=/work/imas/shared/imasdb/ITER/3/100507/5" --rc "backend='module://ipympl.backend_nbagg'" diff --git a/docs/source/tools.rst b/docs/source/tools.rst index a90cc39d..1b5a1685 100644 --- a/docs/source/tools.rst +++ b/docs/source/tools.rst @@ -12,3 +12,4 @@ Following are the different command line tools available in the ids_manipulation_tools database_tools scenariodb_tools + jupyter diff --git a/idstools/compute/equilibrium.py b/idstools/compute/equilibrium.py index 941a25c8..5f84d4cb 100644 --- a/idstools/compute/equilibrium.py +++ b/idstools/compute/equilibrium.py @@ -1384,7 +1384,7 @@ def get_profiles_1d_quantities(self, time_slice, attributes=None): if ids_field.has_value: quantities[attribute] = eval(f"self.ids.time_slice[{time_slice}].profiles_1d.{attribute}") else: - logger.error(f"self.ids.time_slice[{time_slice}].profiles_1d.{attribute} not found") + logger.warning(f"self.ids.time_slice[{time_slice}].profiles_1d.{attribute} not found") return quantities def get_global_quantities(self, time_slice=None, attributes=None): diff --git a/idstools/scripts/bin/idsdiff b/idstools/scripts/bin/idsdiff index a7cb192b..6cec2dcb 100644 --- a/idstools/scripts/bin/idsdiff +++ b/idstools/scripts/bin/idsdiff @@ -10,6 +10,7 @@ import sys import time from io import BytesIO +from idstools.view.common import PlotCanvas import matplotlib.pyplot as plt import numpy as np import rich @@ -43,8 +44,6 @@ slicing_methods = { def view_plot(ax, field, coordinate, field_name="", coordinate_name="", field_unit="", coordinate_unit="", **kwargs): - from idstools.view.common import PlotCanvas - if not isinstance(field, (imas.ids_primitive.IDSNumericArray, np.ndarray)): print("Not a numeric array, Please select ids path") return @@ -506,7 +505,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( @@ -702,8 +704,6 @@ if __name__ == "__main__": ] = compare_data if args.plot and not args.html: - from idstools.view.common import PlotCanvas - canvas = PlotCanvas(1, 1) canvas.update_style(args.rc) ax = canvas.add_axes(title="", xlabel="", row=0, col=0) diff --git a/idstools/scripts/bin/idsprint b/idstools/scripts/bin/idsprint index a0f65aef..2c5e381c 100644 --- a/idstools/scripts/bin/idsprint +++ b/idstools/scripts/bin/idsprint @@ -522,7 +522,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotcoresources b/idstools/scripts/bin/plotcoresources index 6e7f268c..6388fc40 100644 --- a/idstools/scripts/bin/plotcoresources +++ b/idstools/scripts/bin/plotcoresources @@ -35,7 +35,10 @@ if __name__ == "__main__": parser.add_argument("-t", "--time", help="time", required=False, type=float, default=-99.0) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotcoretransport b/idstools/scripts/bin/plotcoretransport index 7eb2c9da..da0a4c96 100644 --- a/idstools/scripts/bin/plotcoretransport +++ b/idstools/scripts/bin/plotcoretransport @@ -64,7 +64,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/ploteccomposition b/idstools/scripts/bin/ploteccomposition index 8b94d9f8..adf983df 100644 --- a/idstools/scripts/bin/ploteccomposition +++ b/idstools/scripts/bin/ploteccomposition @@ -52,7 +52,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotecray b/idstools/scripts/bin/plotecray index b9968b75..c858fa90 100644 --- a/idstools/scripts/bin/plotecray +++ b/idstools/scripts/bin/plotecray @@ -68,7 +68,10 @@ if __name__ == "__main__": parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotecstrayradiation b/idstools/scripts/bin/plotecstrayradiation index 8d734f26..7604bfd3 100644 --- a/idstools/scripts/bin/plotecstrayradiation +++ b/idstools/scripts/bin/plotecstrayradiation @@ -56,7 +56,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotedgeprofiles b/idstools/scripts/bin/plotedgeprofiles index 76656958..f554f9de 100644 --- a/idstools/scripts/bin/plotedgeprofiles +++ b/idstools/scripts/bin/plotedgeprofiles @@ -59,7 +59,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotequicomp b/idstools/scripts/bin/plotequicomp index 19f5a53d..cea4782d 100644 --- a/idstools/scripts/bin/plotequicomp +++ b/idstools/scripts/bin/plotequicomp @@ -10,6 +10,9 @@ try: except ImportError: import imas import numpy as np + +# PlotCanvas configures a --rc backend before Matplotlib is imported. +from idstools.view.common import PROVENANCE_TITLE_STYLE, PlotCanvas from matplotlib.animation import FuncAnimation from matplotlib.widgets import Slider from rich_argparse import RichHelpFormatter @@ -20,7 +23,6 @@ from idstools.utils.clihelper import ( rcparam_parser, ) from idstools.utils.idslogger import setup_logger -from idstools.view.common import PROVENANCE_TITLE_STYLE, PlotCanvas from idstools.view.equilibrium import EquilibriumView from idstools.view.wall import WallView @@ -58,7 +60,10 @@ if __name__ == "__main__": parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotequilibrium b/idstools/scripts/bin/plotequilibrium index d6053d4f..1da27c78 100644 --- a/idstools/scripts/bin/plotequilibrium +++ b/idstools/scripts/bin/plotequilibrium @@ -12,6 +12,8 @@ try: import imaspy as imas except ImportError: import imas +# PlotCanvas configures a --rc backend before mpl_toolkits imports Matplotlib. +from idstools.view.common import PlotCanvas from mpl_toolkits.axes_grid1.inset_locator import inset_axes from rich_argparse import RichHelpFormatter @@ -27,7 +29,6 @@ from idstools.utils.clihelper import ( 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 @@ -86,7 +87,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plothcd b/idstools/scripts/bin/plothcd index 8b90ec57..d9001d3f 100644 --- a/idstools/scripts/bin/plothcd +++ b/idstools/scripts/bin/plothcd @@ -272,7 +272,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plothcddistributions b/idstools/scripts/bin/plothcddistributions index 5ceeeaa6..a03d0c2e 100644 --- a/idstools/scripts/bin/plothcddistributions +++ b/idstools/scripts/bin/plothcddistributions @@ -92,7 +92,10 @@ if __name__ == "__main__": parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plothcdwaves b/idstools/scripts/bin/plothcdwaves index 5eddd9e3..462123e4 100644 --- a/idstools/scripts/bin/plothcdwaves +++ b/idstools/scripts/bin/plothcdwaves @@ -127,7 +127,10 @@ if __name__ == "__main__": parser.add_argument("-l", "--hide_legend", help="remove the legend from graphs", action="store_true") parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotkineticprofiles b/idstools/scripts/bin/plotkineticprofiles index 1016cf72..078cae1b 100644 --- a/idstools/scripts/bin/plotkineticprofiles +++ b/idstools/scripts/bin/plotkineticprofiles @@ -36,7 +36,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotmachinedescription b/idstools/scripts/bin/plotmachinedescription index bf661565..cad8e9eb 100644 --- a/idstools/scripts/bin/plotmachinedescription +++ b/idstools/scripts/bin/plotmachinedescription @@ -52,7 +52,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotneutron b/idstools/scripts/bin/plotneutron index ec26631f..efc29de4 100644 --- a/idstools/scripts/bin/plotneutron +++ b/idstools/scripts/bin/plotneutron @@ -38,7 +38,10 @@ if __name__ == "__main__": parser.add_argument("-t", "--time", help="Time", required=False, type=float, default=-99.0) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotpressure b/idstools/scripts/bin/plotpressure index 2ab2f476..6751a4ef 100644 --- a/idstools/scripts/bin/plotpressure +++ b/idstools/scripts/bin/plotpressure @@ -40,7 +40,10 @@ if __name__ == "__main__": parser.add_argument("-t", "--time", type=float, help="Time", default=-99) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotrotation b/idstools/scripts/bin/plotrotation index 819d83ff..43fcbd46 100644 --- a/idstools/scripts/bin/plotrotation +++ b/idstools/scripts/bin/plotrotation @@ -36,7 +36,10 @@ if __name__ == "__main__": parser.add_argument("-t", "--time", help="Time", required=False, type=float, default=99.0) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotscenario b/idstools/scripts/bin/plotscenario index 87ce34a3..1b2a2ad2 100644 --- a/idstools/scripts/bin/plotscenario +++ b/idstools/scripts/bin/plotscenario @@ -54,7 +54,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/scripts/bin/plotspectrometry b/idstools/scripts/bin/plotspectrometry index d5d86d4b..45ff434e 100644 --- a/idstools/scripts/bin/plotspectrometry +++ b/idstools/scripts/bin/plotspectrometry @@ -42,7 +42,10 @@ if __name__ == "__main__": ) parser.add_argument( "--save", - help="Save figure at default location", + help=( + "Save the plot to a file instead of opening a window. " + "If no plot window is available, it is saved automatically." + ), action="store_true", ) parser.add_argument( diff --git a/idstools/utils/clihelper.py b/idstools/utils/clihelper.py index d5c9a438..9df05215 100644 --- a/idstools/utils/clihelper.py +++ b/idstools/utils/clihelper.py @@ -1,6 +1,17 @@ import argparse +import logging import os import re +from datetime import datetime + +from idstools.utils.matplotlib_backend import ( + _configure_backend_from_cli_rc, + _is_jupyter, +) + +_configure_backend_from_cli_rc() + +logger = logging.getLogger("module") try: import imaspy as imas @@ -196,38 +207,45 @@ def get_title(imasargs, title="", time_value=None): def get_file_name(imasargs, title="", time_value=None): - _file_name = "" - if title: - _file_name += f"{title}_" - if "uri" in imasargs.__dict__ and imasargs.uri: - param = get_details_from_uri(imasargs.uri) - if param["pathPresent"]: - _file_name += f"PATH_{param['path'].replace('/', '_')}_" - else: - _file_name += f"PULSE_{param['pulse']}_RUN_{param['run']}_" - else: - _file_name += f"PULSE_{imasargs.pulse}_RUN_{imasargs.run}_" - if time_value: - _file_name += f"TIME_{time_value:.3f}" - _file_name += ".png" - return _file_name + tool_name = title or "plot" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + return f"{tool_name}_{timestamp}.png" + + +def _is_interactive_backend(canvas): + """Return True for GUI and notebook widget backends.""" + import matplotlib + + backend = matplotlib.get_backend().lower() + if backend in ("widget", "ipympl", "module://ipympl.backend_nbagg"): + return True + return getattr(canvas.fig.canvas, "required_interactive_framework", None) is not None def show_plot(canvas, imasargs, title="", time_value=None, fname=None, show_kwargs=None): - """Save non-interactive canvases automatically, otherwise show the plot.""" - noninteractive = getattr(canvas.fig.canvas, "required_interactive_framework", None) is None - if not imasargs.save and not noninteractive: + """Display interactive plots and save non-interactive plots automatically.""" + noninteractive = not _is_interactive_backend(canvas) + if not imasargs.save and (_is_jupyter() or not noninteractive): canvas.show(**(show_kwargs or {})) return + automatic_save = noninteractive and not imasargs.save if fname is None: fname = get_file_name(imasargs, title, time_value) - if noninteractive and not imasargs.save: + if automatic_save: extension = canvas.fig.canvas.get_default_filetype() fname = f"{os.path.splitext(fname)[0]}.{extension}" if imasargs.directory: os.makedirs(imasargs.directory, exist_ok=True) fname = os.path.join(imasargs.directory, fname) + if automatic_save: + import matplotlib + + logger.info( + "Non-interactive Matplotlib backend '%s' detected; saving figure to %s", + matplotlib.get_backend(), + fname, + ) canvas.save(fname) diff --git a/idstools/utils/matplotlib_backend.py b/idstools/utils/matplotlib_backend.py new file mode 100644 index 00000000..0b726b1a --- /dev/null +++ b/idstools/utils/matplotlib_backend.py @@ -0,0 +1,47 @@ +"""Configure Matplotlib before importing Matplotlib itself.""" + +import ast +import os +import sys + + +def _backend_from_cli_rc(argv): + """Return a backend requested by a ``--rc backend=...`` option.""" + for index, argument in enumerate(argv): + if argument == "--rc" and index + 1 < len(argv): + rc_string = argv[index + 1] + elif argument.startswith("--rc="): + rc_string = argument.split("=", 1)[1] + else: + continue + for item in rc_string.split(";"): + key, separator, value = item.partition("=") + if separator and key.strip() == "backend": + value = value.strip() + try: + value = ast.literal_eval(value) + except (SyntaxError, ValueError): + pass + return str(value) + return None + + +def _configure_backend_from_cli_rc(argv=None): + """Set ``MPLBACKEND`` from command-line rcParams before Matplotlib loads.""" + requested_backend = _backend_from_cli_rc(sys.argv if argv is None else argv) + if requested_backend: + os.environ["MPLBACKEND"] = requested_backend + return requested_backend + + +def _is_jupyter(): + """Return True if running inside a Jupyter notebook/lab/Colab kernel.""" + try: + from IPython import get_ipython + + shell = get_ipython() + if shell is None: + return False + return shell.__class__.__name__ in ("ZMQInteractiveShell", "Shell") + except ImportError: + return False diff --git a/idstools/view/common.py b/idstools/view/common.py index d38e79ac..67ad60f9 100644 --- a/idstools/view/common.py +++ b/idstools/view/common.py @@ -1,73 +1,16 @@ -import ast import logging import os import sys +from idstools.utils.matplotlib_backend import ( + _configure_backend_from_cli_rc, + _is_jupyter, +) -def _backend_from_cli_rc(argv): - """Return a backend requested by a --rc backend=... command-line option.""" - for index, argument in enumerate(argv): - if argument == "--rc" and index + 1 < len(argv): - rc_string = argv[index + 1] - elif argument.startswith("--rc="): - rc_string = argument.split("=", 1)[1] - else: - continue - for item in rc_string.split(";"): - key, separator, value = item.partition("=") - if separator and key.strip() == "backend": - value = value.strip() - try: - value = ast.literal_eval(value) - except (SyntaxError, ValueError): - pass - return str(value) - return None - - -_requested_backend = _backend_from_cli_rc(sys.argv) -if _requested_backend: - os.environ["MPLBACKEND"] = _requested_backend +_configure_backend_from_cli_rc() import matplotlib # noqa: E402 - backend env must be set before importing matplotlib - -def _is_jupyter() -> bool: - """Return True if running inside a Jupyter notebook/lab/Colab kernel.""" - try: - from IPython import get_ipython - - shell = get_ipython() - if shell is None: - return False - shell_class = shell.__class__.__name__ - # ZMQInteractiveShell: Jupyter Notebook/Lab - # Shell: Google Colab - return shell_class in ("ZMQInteractiveShell", "Shell") - except ImportError: - return False - - -if not os.environ.get("MPLBACKEND"): - if _is_jupyter(): - if "matplotlib.pyplot" not in sys.modules: - try: - import ipympl # noqa: F401 - imported to check availability - - matplotlib.use("widget") - except ImportError: - matplotlib.use("agg") - elif sys.platform.startswith("win") or "DISPLAY" in os.environ: - - try: - import tkinter # noqa: F401 - imported to check availability - - matplotlib.use("TkAgg") - except (ImportError, ModuleNotFoundError): - matplotlib.use("agg") - else: - matplotlib.use("agg") - import matplotlib.pyplot as plt # noqa: E402 logger = logging.getLogger("module") @@ -266,8 +209,8 @@ def show(self, *args, **kwargs): None Notes: - Uses the TkAgg backend for window resizing when available. - Other backends (agg, Qt) may not support window maximization. + Window maximization depends on the backend selected by Matplotlib. + Some backends may not support it. Examples: >>> canvas = PlotCanvas() @@ -281,14 +224,14 @@ def show(self, *args, **kwargs): from IPython.display import display display(self.fig) - if backend != "module://matplotlib_ipympl.backend_nbagg": - plt.close("all") + if backend not in ("widget", "ipympl", "module://ipympl.backend_nbagg"): + plt.close(self.fig) except ImportError: pass return wm = self.get_current_fig_manager() try: - # Try to maximize the window (only works with TkAgg backend) + # Try to maximize the window when the active backend exposes one. window = wm.window screen_y = window.winfo_screenheight() screen_x = window.winfo_screenwidth() From 17074e1a4378a84f2f3e92e54ca6e666357e03c9 Mon Sep 17 00:00:00 2001 From: Prasad Date: Wed, 29 Jul 2026 17:38:57 +0200 Subject: [PATCH 56/56] Fix/incorrect handling of error/warning when idslist is applied to a uda uri (#38) * show warning/exception from IMAS-Python when time is not readble when using UDA uri * add cache_mode=none when not used in UDA URI * Moved URI handling into utils, simplified it using urllib.parse, added a reusable backend-specific query helper, automatically appended cache_mode=none to UDA URIs * removed add_default_uda_cache_mode function, simplified _replace and updated documentation --- idstools/database.py | 11 +++++++++-- idstools/scripts/bin/dblist | 20 ++++++++++++++++++-- idstools/scripts/bin/idslist | 4 ++-- idstools/scripts/bin/idsprint | 5 ++++- idstools/scripts/bin/idsquery | 8 +++++++- idstools/scripts/bin/plotequicomp | 12 ++++++++++-- idstools/utils/idshelper.py | 8 ++++++-- idstools/utils/utility_functions.py | 25 +++++++++++++++++++++++++ 8 files changed, 81 insertions(+), 12 deletions(-) diff --git a/idstools/database.py b/idstools/database.py index 3fbf6296..a882f05f 100644 --- a/idstools/database.py +++ b/idstools/database.py @@ -10,6 +10,8 @@ import imas import yaml +from idstools.utils.utility_functions import add_query_to_uri + logger = logging.getLogger(f"module.{__name__}") @@ -701,11 +703,16 @@ def create_connection(cls, imasargs, target_dd_version=None): def get_connection(cls, imasargs): connection = None if imasargs.uri != "" and imasargs.uri is not None: + uri = add_query_to_uri( + imasargs.uri, + backend="uda", + query="cache_mode=none", + ) if "mode" in imasargs.__dict__: - connection = imas.DBEntry(imasargs.uri, imasargs.mode) + connection = imas.DBEntry(uri, imasargs.mode) else: try: - connection = imas.DBEntry(imasargs.uri, "r") + connection = imas.DBEntry(uri, "r") except Exception as e: print(e) return connection diff --git a/idstools/scripts/bin/dblist b/idstools/scripts/bin/dblist index 57d9e72a..d30a9e75 100644 --- a/idstools/scripts/bin/dblist +++ b/idstools/scripts/bin/dblist @@ -234,7 +234,15 @@ def print_times(dbs, args, print_times=False, pulse_number=None, run_number=None print(TAB * 4 + " Run: " + extended(str(run), RUN_STR_LEN)) available_ids_and_times = get_available_ids_and_times(connection) for idsname, times in available_ids_and_times: - if len(times) == 1 and np.isnan(times[0]): + if times is None: + print( + TAB * 5 + + extended(idsname, IDSNAME_STR_LEN) + + ": " + + extended("N/A", SLICENUM_STR_LEN) + + " slices ( time unavailable )" + ) + elif len(times) == 1 and np.isnan(times[0]): print( TAB * 5 + extended(idsname, IDSNAME_STR_LEN) @@ -311,7 +319,15 @@ def print_times_with_folder(dbs, print_times=False, pulse_number=None, run_numbe continue available_ids_and_times = get_available_ids_and_times(connection) for idsname, times in available_ids_and_times: - if times is not None: + if times is None: + print( + TAB * 15 + + extended(idsname, IDSNAME_STR_LEN) + + ": " + + extended("N/A", SLICENUM_STR_LEN) + + " slices ( time unavailable )" + ) + else: if len(times) == 1 and np.isnan(times[0]): print( TAB * 15 diff --git a/idstools/scripts/bin/idslist b/idstools/scripts/bin/idslist index 63578322..c5cf7456 100644 --- a/idstools/scripts/bin/idslist +++ b/idstools/scripts/bin/idslist @@ -184,8 +184,8 @@ if __name__ == "__main__": type = time_array elif time_array is None: value = f"{not_applicable}" - type = "unknown" - table.add_row(ids_name, value, Pretty(type)) + type = not_applicable + table.add_row(ids_name, value, type if isinstance(type, str) else Pretty(type)) if args.fullarray is True: with np.printoptions(threshold=sys.maxsize, linewidth=1024, precision=4): console.print(table) diff --git a/idstools/scripts/bin/idsprint b/idstools/scripts/bin/idsprint index 2c5e381c..e85df44e 100644 --- a/idstools/scripts/bin/idsprint +++ b/idstools/scripts/bin/idsprint @@ -562,7 +562,10 @@ if __name__ == "__main__": table.add_column("SLICES", style="green") table.add_column("TIME", style="green") for ids_name, time_array in available_ids_and_times: - if len(time_array) == 1 and np.isnan(time_array[0]): + if time_array is None: + value = "N/A" + type = "unknown" + elif len(time_array) == 1 and np.isnan(time_array[0]): value = f"{question_string}" type = "heterogeneous IDS" elif len(time_array) == 1 and time_array[0] == -np.inf: diff --git a/idstools/scripts/bin/idsquery b/idstools/scripts/bin/idsquery index e23047b9..9d346101 100644 --- a/idstools/scripts/bin/idsquery +++ b/idstools/scripts/bin/idsquery @@ -41,6 +41,7 @@ from idstools.utils.idshelper import ( get_ids_values, ) from idstools.utils.idslogger import setup_logger +from idstools.utils.utility_functions import add_query_to_uri logger = setup_logger("module", stdout_level=logging.INFO) @@ -108,7 +109,12 @@ Example: valpath = path[1 + len(idsname) :] paths_info.append((path, idsname, valpath.replace("(", "[").replace(")", "]").replace("/", "."))) - ids_values = get_ids_values(args.uri, paths_info, dd_update=args.dd_update, verbose=args.verbose) + ids_values = get_ids_values( + add_query_to_uri(args.uri, backend="uda", query="cache_mode=none"), + paths_info, + dd_update=args.dd_update, + verbose=args.verbose, + ) if ids_values: for _path, _value in ids_values.items(): if _value is None: diff --git a/idstools/scripts/bin/plotequicomp b/idstools/scripts/bin/plotequicomp index cea4782d..0f114532 100644 --- a/idstools/scripts/bin/plotequicomp +++ b/idstools/scripts/bin/plotequicomp @@ -23,6 +23,8 @@ from idstools.utils.clihelper import ( rcparam_parser, ) from idstools.utils.idslogger import setup_logger +from idstools.utils.utility_functions import add_query_to_uri +from idstools.view.common import PROVENANCE_TITLE_STYLE, PlotCanvas from idstools.view.equilibrium import EquilibriumView from idstools.view.wall import WallView @@ -86,7 +88,10 @@ if __name__ == "__main__": wall2 = None if len(args.uri) >= 1: - connection1 = imas.DBEntry(args.uri[0], "r") + connection1 = imas.DBEntry( + add_query_to_uri(args.uri[0], backend="uda", query="cache_mode=none"), + "r", + ) if args.dd_update: equilibrium1 = connection1.get("equilibrium", autoconvert=False) equilibrium1 = imas.convert_ids(equilibrium1, connection1.factory.version) @@ -110,7 +115,10 @@ if __name__ == "__main__": logger.critical("----> Could not open first data entry. Aborted.") exit(1) if len(args.uri) == 2: - connection2 = imas.DBEntry(args.uri[1], "r") + connection2 = imas.DBEntry( + add_query_to_uri(args.uri[1], backend="uda", query="cache_mode=none"), + "r", + ) if args.dd_update: equilibrium2 = connection2.get("equilibrium", autoconvert=False) equilibrium2 = imas.convert_ids(equilibrium2, connection2.factory.version) diff --git a/idstools/utils/idshelper.py b/idstools/utils/idshelper.py index c4b0c6e7..e2c41956 100644 --- a/idstools/utils/idshelper.py +++ b/idstools/utils/idshelper.py @@ -447,8 +447,12 @@ def get_available_ids_and_times(db_entry_object) -> list: time_array = [-np.inf] except Exception as e: logger.debug(f"{e}") - time_array = [] - logger.info(f"ERROR! IDS {_ids_name} : Reading time array fails due to following problem : {e}") + time_array = None + logger.warning( + "Unable to read the time array for IDS %s: %s", + _ids_name, + e, + ) if occurrence != 0: result.append((f"{_ids_name}/{occurrence}", time_array)) else: diff --git a/idstools/utils/utility_functions.py b/idstools/utils/utility_functions.py index d65458b6..d8544b2f 100644 --- a/idstools/utils/utility_functions.py +++ b/idstools/utils/utility_functions.py @@ -1,8 +1,33 @@ import logging +from urllib.parse import urlsplit, urlunsplit logger = logging.getLogger(f"module.{__name__}") +def add_query_to_uri(uri: str, *, query: str, backend=None): + """Add a query to the URI. + + If a backend is given, add the query only when it matches the URI's + backend. Otherwise, return the original URI unchanged. When backend is + None, add the query to any URI. + """ + uri_parts = urlsplit(uri) + uri_backend = uri_parts.path.rsplit("/", 1)[-1] + if backend is not None and uri_backend != backend: + return uri + + updated_query = f"{uri_parts.query};{query}" + return urlunsplit( + ( + uri_parts.scheme, + uri_parts.netloc, + uri_parts.path, + updated_query, + uri_parts.fragment, + ) + ) + + def get_slice_from_array(arr, slice_str): if ":" not in slice_str: index = int(slice_str)