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/plotequilibrium.rst b/docs/source/plotequilibrium.rst index a5f3325b..4f195190 100644 --- a/docs/source/plotequilibrium.rst +++ b/docs/source/plotequilibrium.rst @@ -19,10 +19,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 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" --profiles --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/compute/equilibrium.py b/idstools/compute/equilibrium.py index f1903828..84305d69 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") @@ -285,6 +287,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 +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/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..b19895c3 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 @@ -23,7 +22,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 @@ -54,9 +52,14 @@ if __name__ == "__main__": 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 +78,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 +113,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 +134,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 +143,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 +151,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 +170,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 +199,85 @@ 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.rho, + 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 = 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: - 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 = 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) + + 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..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/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..95bc72f2 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, # ) + + # rho overlay 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" + cartestion_grid["r2d"], cartestion_grid["z2d"], rho2d.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="Geom 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. @@ -162,7 +489,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: @@ -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