From 48c4129a223e568a2944f95e4625c31a2ba4f24f Mon Sep 17 00:00:00 2001 From: Arora0 Date: Mon, 15 Jan 2024 17:20:55 +0100 Subject: [PATCH 01/20] adds a class for interactive draggable lines --- specsanalyzer/img_tools.py | 81 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/specsanalyzer/img_tools.py b/specsanalyzer/img_tools.py index f86494b..ca7cbaf 100755 --- a/specsanalyzer/img_tools.py +++ b/specsanalyzer/img_tools.py @@ -4,10 +4,91 @@ from typing import Sequence from typing import Union +from matplotlib import lines import numpy as np import xarray as xr +class DraggableLines: + """ + Class to run an interactive tool to drag lines over an image + an store the last positions. Used by the cropping tool in + specsscan.load_scan() method. + """ + def __init__(self, ax, fig, kind, range_dict, xarray): + self.ax = ax + self.c = ax.get_figure().canvas + self.o = kind + self.xory = range_dict[kind]['val'] + self.xarray = xarray + self.follower = None + self.releaser = None + dims = xarray.dims + props = {"boxstyle": 'round', "facecolor": 'white', "alpha": 0.5} + self.text = ax.text( + range_dict[kind]['x'], + range_dict[kind]['y'], + f"{self.o} " + f"{self.xory:.2f}", + transform=fig.transFigure, + bbox=props + ) + + if kind in ("Ang1", "Ang2"): + self.x = xarray[f"{dims[1]}"].data + self.y = [range_dict[kind]['val']] * len(self.x) + + elif kind in ("Ek1", "Ek2"): + self.y = xarray[f"{dims[0]}"].data + self.x = [range_dict[kind]['val']] * len(self.y) + + self.line = lines.Line2D(self.x, self.y, picker=5) + self.ax.add_line(self.line) + self.c.draw_idle() + + self.sid = self.c.mpl_connect('button_press_event', self.clickonline) + + def clickonline(self, event): + """ + Checks if the line clicked belongs to this instance. + """ + if event.inaxes != self.line.axes: + return + contains = self.line.contains(event) + if not contains[0]: + return + + self.follower = self.c.mpl_connect("motion_notify_event", self.followmouse) + self.releaser = self.c.mpl_connect("button_release_event", self.releaseonclick) + + def followmouse(self, event): + """ + Sets the selected line position to the mouse position, + while updating the text box in real time. + """ + if self.o in ("Ang1", "Ang2"): + if event.ydata: + self.line.set_ydata([event.ydata] * len(self.x)) + else: + self.line.set_ydata([event.ydata] * len(self.x)) + self.xory = self.line.get_ydata()[0] + self.text.set_text(f"{self.o} " + f"{self.xory:.2f}") + + elif self.o in ("Ek1", "Ek2"): + self.line.set_xdata([event.xdata] * len(self.y)) + self.xory = self.line.get_xdata()[0] + self.text.set_text(f"{self.o} " + f"{self.xory:.2f}") + + self.c.draw_idle() + + def releaseonclick(self, event): # pylint: disable=unused-argument + """ + Disconnects the interaction on mouse release. + """ + self.c.draw_idle() + self.c.mpl_disconnect(self.releaser) + self.c.mpl_disconnect(self.follower) + + def gauss2d( # pylint: disable=invalid-name, too-many-arguments x: Union[float, np.ndarray], From 09de27bd91622291e85645852cbfa25f09ea5cd0 Mon Sep 17 00:00:00 2001 From: Arora0 Date: Mon, 15 Jan 2024 17:23:02 +0100 Subject: [PATCH 02/20] adds crop_tool method for cropping functionality --- specsscan/core.py | 121 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/specsscan/core.py b/specsscan/core.py index 44239cf..a5c58e9 100755 --- a/specsscan/core.py +++ b/specsscan/core.py @@ -10,6 +10,11 @@ from typing import Dict from typing import Sequence from typing import Union +import matplotlib +import matplotlib.pyplot as plt +import ipywidgets as ipw +from matplotlib.widgets import Button +from IPython.display import display import numpy as np import xarray as xr @@ -18,6 +23,8 @@ from specsanalyzer.io import to_h5 from specsanalyzer.io import to_nexus from specsanalyzer.io import to_tiff +from specsanalyzer.img_tools import DraggableLines +from specsanalyzer.img_tools import crop_xarray from specsscan.helpers import find_scan from specsscan.helpers import get_coords @@ -93,6 +100,11 @@ def __repr__(self): def config(self): """Get config""" return self._config + + @property + def result(self): + """Get result xarray""" + return self._result @config.setter def config(self, config: Union[dict, str]): @@ -117,6 +129,7 @@ def load_scan( Sequence[slice], ] = None, metadata: dict = None, + **kwds, ) -> xr.DataArray: """Load scan with given scan number. When iterations is given, average is performed over the iterations over @@ -141,6 +154,7 @@ def load_scan( and optionally a third scanned axis (for ex., delay, temperature) as coordinates. """ + cid_refs = None if path: path = Path(path).joinpath(str(scan).zfill(4)) if not path.is_dir(): @@ -223,7 +237,6 @@ def load_scan( for name in res_xarray.dims: res_xarray[name].attrs["unit"] = default_units[name] - self.metadata.update( **handle_meta( df_lut, @@ -237,10 +250,110 @@ def load_scan( self.metadata.update(**metadata) res_xarray.attrs["metadata"] = self.metadata - self._result = res_xarray + try: + range_dict = self.spa.correction_matrix_dict[lens_mode][kin_energy][pass_energy][ + work_function + ]["crop_params"] + except KeyError: + old_matrix_check = False + range_dict = { + "Ek1":{"x":0.15, "y":0.9, "val":20.2}, + "Ek2":{"x":0.30, "y":0.9, "val":20.8}, + "Ang1":{"x":0.45, "y":0.9, "val":-3.0}, + "Ang2":{"x":0.60, "y":0.9, "val":3.0} + } + else: + old_matrix_check = True + + crop = kwds.pop("crop", self._config.get("crop", False)) + if crop: + cid_refs = self.crop_tool( + res_xarray, + range_dict, + old_matrix_check, + ) + return cid_refs + + def crop_tool( + self, + res_xarray: xr.DataArray, + range_dict: dict, + old_matrix_check: bool, + ): + """Crop tool for the loader + Args: + res_xarray: xarray obtained from the converted raw data + range_dict: Dictionary containing box positions and the + text value + old_matrix_check: True if crop params exist alreadty, + false otherwise. + returns: + cid_refs: cid references to the callbacks for interaction + """ + + matplotlib.use("module://ipympl.backend_nbagg") + fig = plt.figure() + ax = fig.add_subplot(111) + plt.subplots_adjust(top=0.75) + + if len(self._result.dims) == 3: + self._result[:,:,0].plot(ax = ax) + else: # dim == 2 + self._result.plot(ax = ax) + + Vline = DraggableLines(ax, fig, "Ek1", range_dict, self._result) + Vline2 = DraggableLines(ax, fig, "Ek2", range_dict, self._result) + Tline = DraggableLines(ax, fig, "Ang1", range_dict, self._result) + Tline2 = DraggableLines(ax, fig, "Ang2", range_dict, self._result) + + def cropit(val): + ang_min = min([Tline.xory, Tline2.xory]) + ang_max = max([Tline.xory, Tline2.xory]) + ek_min = min([Vline.xory, Vline2.xory]) + ek_max = max([Vline.xory, Vline2.xory]) + self._result = crop_xarray( + res_xarray, + ang_min, + ang_max, + ek_min, + ek_max + ) + + self.spa.correction_matrix_dict[self._scan_info['LensMode']][ + self._scan_info["KineticEnergy"]][self._scan_info["PassEnergy"]][ + self._scan_info["WorkFunction"] + ] = {"crop_params":{ + "Ek1":{"x":0.15, "y":0.9, "val":ek_min}, + "Ek2":{"x":0.30, "y":0.9, "val":ek_max}, + "Ang1":{"x":0.45, "y":0.9, "val":ang_min}, + "Ang2":{"x":0.60, "y":0.9, "val":ang_max} + } + + } + + Vline.c.mpl_disconnect(Vline.sid) + Vline2.c.mpl_disconnect(Vline2.sid) + Tline.c.mpl_disconnect(Tline.sid) + Tline2.c.mpl_disconnect(Tline2.sid) + + # axes = plt.axes([0.81, 0.000001, 0.1, 0.075]) + # bnext = Button(axes, 'Crop') + # bnext.on_clicked(cropit) + apply_button = ipw.Button(description="Crop") + display(apply_button) + apply_button.on_click(cropit) + plt.show() + + if old_matrix_check: + print("Using existing crop parameters") + cropit(True) + apply_button.close() + # plt.close(fig) + + cid_refs = [Vline, Vline2, Tline, Tline2] + return cid_refs - return res_xarray def check_scan( self, @@ -251,6 +364,7 @@ def check_scan( ], path: Union[str, Path] = "", metadata: dict = None, + **kwds, ) -> xr.DataArray: """Function to explore a given 3-D scan as a function of iterations for a given range of delays @@ -327,6 +441,7 @@ def check_scan( kin_energy, pass_energy, work_function, + **kwds, ), ) From 6c10abe6d41351b0b4bea7ce7dea5578ca4a2a44 Mon Sep 17 00:00:00 2001 From: Arora0 Date: Mon, 15 Jan 2024 17:23:35 +0100 Subject: [PATCH 03/20] minor fixes --- specsanalyzer/core.py | 19 ------------------- specsscan/helpers.py | 13 +++++++------ 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index 3018519..6cfdfa3 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -15,7 +15,6 @@ from specsanalyzer.config import parse_config from specsanalyzer.convert import calculate_matrix_correction from specsanalyzer.convert import physical_unit_data -from specsanalyzer.img_tools import crop_xarray from specsanalyzer.img_tools import fourier_filter_2d from specsanalyzer.metadata import MetaHandler @@ -33,7 +32,6 @@ def __init__( ): self._config = parse_config(config, **kwds,) - self._attributes = MetaHandler(meta=metadata) try: @@ -221,23 +219,6 @@ def convert_image( # parameters in the config, or should we store one set per pass energy/ # lens mode/ kinetic energy in the dict? - crop = kwds.pop("crop", self._config.get("crop", False)) - if crop: - try: - ek_min = kwds.pop("ek_min", self._config["ek_min"]) - ek_max = kwds.pop("ek_max", self._config["ek_max"]) - ang_min = kwds.pop("ang_min", self._config["ang_min"]) - ang_max = kwds.pop("ang_max", self._config["ang_max"]) - data_array = crop_xarray( - data_array, - ang_min, - ang_max, - ek_min, - ek_max, - ) - except KeyError: - pass - return data_array diff --git a/specsscan/helpers.py b/specsscan/helpers.py index 9d41636..50ce72c 100644 --- a/specsscan/helpers.py +++ b/specsscan/helpers.py @@ -363,6 +363,7 @@ def handle_meta( # pylint:disable=too-many-branches # get metadata from LUT dataframe lut_meta = {} + energy_scan_mode = "fixed" if df_lut is not None: for col in df_lut.columns: col_array = df_lut[f"{col}"].to_numpy() @@ -371,6 +372,10 @@ def handle_meta( # pylint:disable=too-many-branches else: lut_meta[col] = col_array + kinetic_energy = df_lut["KineticEnergy"].to_numpy() + if len(set(kinetic_energy)) > 1 and scan_info["ScanType"] == "voltage": + energy_scan_mode = "sweep" + scan_meta = complete_dictionary(lut_meta, scan_info) # merging two dictionaries # Get metadata from Epics archive, if not present already @@ -380,6 +385,8 @@ def handle_meta( # pylint:disable=too-many-branches config, ) + metadata_dict["scan_info"]["energy_scan_mode"] = energy_scan_mode + lens_modes_all = { "real": config["spa_params"]["calib2d_dict"]["supported_space_modes"], "reciprocal": config["spa_params"]["calib2d_dict"]["supported_angle_modes"], @@ -396,12 +403,6 @@ def handle_meta( # pylint:disable=too-many-branches fast, ] - kinetic_energy = df_lut["KineticEnergy"].to_numpy() - if len(set(kinetic_energy)) > 1 and scan_meta["ScanType"] == "voltage": - metadata_dict["scan_info"]["energy_scan_mode"] = "sweep" - else: - metadata_dict["scan_info"]["energy_scan_mode"] = "fixed" - print("Done!") return metadata_dict From 284af5f5f45a0d0ed2fa3201783715bf9132b2c9 Mon Sep 17 00:00:00 2001 From: Arora0 Date: Mon, 15 Jan 2024 17:35:43 +0100 Subject: [PATCH 04/20] update example notebook --- tutorial/example.ipynb | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/tutorial/example.ipynb b/tutorial/example.ipynb index 3a7860a..1801fc8 100644 --- a/tutorial/example.ipynb +++ b/tutorial/example.ipynb @@ -24,7 +24,7 @@ "outputs": [], "source": [ "from specsscan import SpecsScan\n", - "from pathlib import Path" + "import matplotlib.pyplot as plt" ] }, { @@ -58,7 +58,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The load_scan method loads the scan as an xarray along with the metadata needed for nexus conversion. The progress bars can be activated by changing the config parameter, enable_nested_progress_bar, to true in config.yaml " + "The load_scan method loads the scan as an xarray along with the metadata needed for nexus conversion. The progress bars can be activated by changing the config parameter, enable_nested_progress_bar, to true in config.yaml. Additionally, a cropping tool can be activated by passing a boolean \"crop\" to the loader. " ] }, { @@ -67,21 +67,13 @@ "metadata": {}, "outputs": [], "source": [ - "res_xarray = sps.load_scan(\n", + "cid_refs = sps.load_scan(\n", " scan=4450, # Scan number for an example mirror scan\n", - " path = path\n", + " path = path,\n", + " crop=True\n", ")" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "res_xarray" - ] - }, { "attachments": {}, "cell_type": "markdown", @@ -96,7 +88,7 @@ "metadata": {}, "outputs": [], "source": [ - "res_xarray.dims" + "sps.result.dims" ] }, { @@ -105,7 +97,9 @@ "metadata": {}, "outputs": [], "source": [ - "res_xarray[:,:,0].plot()" + "plt.figure()\n", + "sps.result[:,:,0].plot()\n", + "plt.show()" ] }, { @@ -122,7 +116,7 @@ "metadata": {}, "outputs": [], "source": [ - "res_xarray.attrs[\"metadata\"].keys()" + "sps.result.attrs[\"metadata\"].keys()" ] }, { @@ -139,7 +133,7 @@ "metadata": {}, "outputs": [], "source": [ - "res_xarray_iter = sps.load_scan(\n", + "sps.load_scan(\n", " scan=4450,\n", " path=path,\n", " iterations=[0]\n", @@ -152,7 +146,9 @@ "metadata": {}, "outputs": [], "source": [ - "res_xarray_iter[:,:,0].plot()" + "plt.figure()\n", + "sps.result[:,:,0].plot()\n", + "plt.show()" ] }, { @@ -200,7 +196,7 @@ "metadata": {}, "outputs": [], "source": [ - "from nexusutils.dataconverter.convert import convert" + "from pynxtools.dataconverter.convert import convert" ] }, { @@ -212,7 +208,7 @@ "convert(input_file=[\"../tests/data/phoibos_config.json\", # config file for translating local metadata paths to NeXus paths\n", " \"../tests/data/phoibos_eln_data.yaml\" # ELN file that adds/overwrites additional metadata \n", " ],\n", - " objects=res_xarray, # xarray object obtained from the specsscan loader\n", + " objects=sps.result, # xarray object obtained from the specsscan loader\n", " reader='mpes',\n", " nxdl='NXmpes',\n", " output='spectest.mpes.nxs')" From 2296a252240dbcf3c3f38f55731e71567431b191 Mon Sep 17 00:00:00 2001 From: Arora0 Date: Mon, 15 Jan 2024 17:51:21 +0100 Subject: [PATCH 05/20] update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index ef6b646..6d5ec17 100755 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ pyyaml>=6.0 xarray>=0.20.2 tifffile>=2022.5.4 scipy>=1.8.0,<1.9.0 +ipywidgets>=8.1.1 From 360c0c29a2e5ec2723c29c74cb83f4d35f2cb81f Mon Sep 17 00:00:00 2001 From: Arora0 Date: Fri, 2 Feb 2024 20:36:44 +0100 Subject: [PATCH 06/20] move cropping to specsanalyzer --- specsanalyzer/core.py | 122 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 119 insertions(+), 3 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index 6cfdfa3..c71771f 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -6,11 +6,16 @@ from typing import Dict from typing import Generator from typing import Tuple -from typing import Union +from typing import Union, Any import numpy as np import xarray as xr - +import ipywidgets as ipw +from IPython.display import display +import matplotlib +import matplotlib.pyplot as plt +from specsanalyzer.img_tools import DraggableLines +from specsanalyzer.img_tools import crop_xarray from specsanalyzer import io from specsanalyzer.config import parse_config from specsanalyzer.convert import calculate_matrix_correction @@ -33,7 +38,12 @@ def __init__( self._config = parse_config(config, **kwds,) self._attributes = MetaHandler(meta=metadata) - + self._data_array = None + self.vline = Any + self.tline = Any + self.vline2 = Any + self.tline2 = Any + self.print_msg = True try: self._config["calib2d_dict"] = io.parse_calib2d_to_dict( self._config["calib2d_file"], @@ -218,9 +228,115 @@ def convert_image( # TODO discuss how to handle cropping. Can he store one set of cropping # parameters in the config, or should we store one set per pass energy/ # lens mode/ kinetic energy in the dict? + saved_rangedict = False + crop = kwds.pop("crop", self._config.get("crop", False)) + if crop: + try: + range_dict: dict = self._correction_matrix_dict[lens_mode][ + kinetic_energy][pass_energy][ + work_function + ]["crop_params"] + except KeyError: + if self.print_msg: + print( + "Warning: Cropping parameters not found, " + "use method crop_tool() after loading." + ) + self.print_msg = False + else: + saved_rangedict = True + + if saved_rangedict: + if self.print_msg: + print("Using existing crop parameters...") + self.print_msg = False + ang_min = min(range_dict['Ang1']['val'], range_dict['Ang2']['val']) + ang_max = max(range_dict['Ang1']['val'], range_dict['Ang2']['val']) + ek_min = min(range_dict['Ek1']['val'], range_dict['Ek2']['val']) + ek_max = max(range_dict['Ek1']['val'], range_dict['Ek2']['val']) + data_array = crop_xarray( + data_array, + ang_min, + ang_max, + ek_min, + ek_max + ) return data_array + def crop_tool( + self, + res_xarray: xr.DataArray, + scan_info_dict: dict, + ): + """Crop tool + Args: + res_xarray: xarray obtained from the converted raw data + scan_info_dict: dict containing the contents of info.txt file + """ + range_dict = { + "Ek1": {"x": 0.15, "y": 0.9, "val": 20.2}, + "Ek2": {"x": 0.30, "y": 0.9, "val": 20.8}, + "Ang1": {"x": 0.45, "y": 0.9, "val": -3.0}, + "Ang2": {"x": 0.60, "y": 0.9, "val": 3.0} + } + + matplotlib.use("module://ipympl.backend_nbagg") + fig = plt.figure() + ax = fig.add_subplot(111) + plt.subplots_adjust(top=0.75) + try: + if len(res_xarray.dims) == 3: + res_xarray[:, :, 0].plot(ax=ax) + else: # dim == 2 + res_xarray.plot(ax=ax) + except AttributeError: + print("Load the scan first!") + raise + + self.vline = DraggableLines(ax, fig, "Ek1", range_dict, res_xarray) + self.vline2 = DraggableLines(ax, fig, "Ek2", range_dict, res_xarray) + self.tline = DraggableLines(ax, fig, "Ang1", range_dict, res_xarray) + self.tline2 = DraggableLines(ax, fig, "Ang2", range_dict, res_xarray) + + def cropit(val): # pylint: disable=unused-argument + ang_min = min([self.tline.xory, self.tline2.xory]) + ang_max = max([self.tline.xory, self.tline2.xory]) + ek_min = min([self.vline.xory, self.vline2.xory]) + ek_max = max([self.vline.xory, self.vline2.xory]) + self._data_array = crop_xarray( + res_xarray, + ang_min, + ang_max, + ek_min, + ek_max + ) + self._correction_matrix_dict[scan_info_dict['LensMode']][ + scan_info_dict["KineticEnergy"] + ][ + scan_info_dict["PassEnergy"] + ][ + scan_info_dict["WorkFunction"] + ] = { + "crop_params": { + "Ek1": {"x": 0.15, "y": 0.9, "val": ek_min}, + "Ek2": {"x": 0.30, "y": 0.9, "val": ek_max}, + "Ang1": {"x": 0.45, "y": 0.9, "val": ang_min}, + "Ang2": {"x": 0.60, "y": 0.9, "val": ang_max} + } + } + + self.vline.c.mpl_disconnect(self.vline.sid) + self.vline2.c.mpl_disconnect(self.vline2.sid) + self.tline.c.mpl_disconnect(self.tline.sid) + self.tline2.c.mpl_disconnect(self.tline2.sid) + apply_button.close() + + apply_button = ipw.Button(description="Crop") + display(apply_button) + apply_button.on_click(cropit) + plt.show() + def mergedicts( dict1: dict, From 5157e3ebf0f169026139465e205780c027c6d935 Mon Sep 17 00:00:00 2001 From: Arora0 Date: Fri, 2 Feb 2024 20:39:29 +0100 Subject: [PATCH 07/20] interface crop_tool in specsscan as a function --- specsscan/core.py | 121 ++++++++-------------------------------------- 1 file changed, 19 insertions(+), 102 deletions(-) diff --git a/specsscan/core.py b/specsscan/core.py index a5c58e9..5d93c00 100755 --- a/specsscan/core.py +++ b/specsscan/core.py @@ -154,7 +154,7 @@ def load_scan( and optionally a third scanned axis (for ex., delay, temperature) as coordinates. """ - cid_refs = None + if path: path = Path(path).joinpath(str(scan).zfill(4)) if not path.is_dir(): @@ -209,9 +209,10 @@ def load_scan( kin_energy, pass_energy, work_function, - ), + **kwds, + ) ) - + self.spa.print_msg = True coords, dim = get_coords( scan_path=path, scan_type=scan_type, @@ -251,108 +252,24 @@ def load_scan( res_xarray.attrs["metadata"] = self.metadata self._result = res_xarray - try: - range_dict = self.spa.correction_matrix_dict[lens_mode][kin_energy][pass_energy][ - work_function - ]["crop_params"] - except KeyError: - old_matrix_check = False - range_dict = { - "Ek1":{"x":0.15, "y":0.9, "val":20.2}, - "Ek2":{"x":0.30, "y":0.9, "val":20.8}, - "Ang1":{"x":0.45, "y":0.9, "val":-3.0}, - "Ang2":{"x":0.60, "y":0.9, "val":3.0} - } - else: - old_matrix_check = True - - crop = kwds.pop("crop", self._config.get("crop", False)) - if crop: - cid_refs = self.crop_tool( - res_xarray, - range_dict, - old_matrix_check, - ) - return cid_refs - def crop_tool( - self, - res_xarray: xr.DataArray, - range_dict: dict, - old_matrix_check: bool, - ): - """Crop tool for the loader - Args: - res_xarray: xarray obtained from the converted raw data - range_dict: Dictionary containing box positions and the - text value - old_matrix_check: True if crop params exist alreadty, - false otherwise. - returns: - cid_refs: cid references to the callbacks for interaction - """ + return res_xarray - matplotlib.use("module://ipympl.backend_nbagg") - fig = plt.figure() - ax = fig.add_subplot(111) - plt.subplots_adjust(top=0.75) - - if len(self._result.dims) == 3: - self._result[:,:,0].plot(ax = ax) - else: # dim == 2 - self._result.plot(ax = ax) - - Vline = DraggableLines(ax, fig, "Ek1", range_dict, self._result) - Vline2 = DraggableLines(ax, fig, "Ek2", range_dict, self._result) - Tline = DraggableLines(ax, fig, "Ang1", range_dict, self._result) - Tline2 = DraggableLines(ax, fig, "Ang2", range_dict, self._result) - - def cropit(val): - ang_min = min([Tline.xory, Tline2.xory]) - ang_max = max([Tline.xory, Tline2.xory]) - ek_min = min([Vline.xory, Vline2.xory]) - ek_max = max([Vline.xory, Vline2.xory]) - self._result = crop_xarray( - res_xarray, - ang_min, - ang_max, - ek_min, - ek_max - ) - self.spa.correction_matrix_dict[self._scan_info['LensMode']][ - self._scan_info["KineticEnergy"]][self._scan_info["PassEnergy"]][ - self._scan_info["WorkFunction"] - ] = {"crop_params":{ - "Ek1":{"x":0.15, "y":0.9, "val":ek_min}, - "Ek2":{"x":0.30, "y":0.9, "val":ek_max}, - "Ang1":{"x":0.45, "y":0.9, "val":ang_min}, - "Ang2":{"x":0.60, "y":0.9, "val":ang_max} - } - - } - - Vline.c.mpl_disconnect(Vline.sid) - Vline2.c.mpl_disconnect(Vline2.sid) - Tline.c.mpl_disconnect(Tline.sid) - Tline2.c.mpl_disconnect(Tline2.sid) - - # axes = plt.axes([0.81, 0.000001, 0.1, 0.075]) - # bnext = Button(axes, 'Crop') - # bnext.on_clicked(cropit) - apply_button = ipw.Button(description="Crop") - display(apply_button) - apply_button.on_click(cropit) - plt.show() - - if old_matrix_check: - print("Using existing crop parameters") - cropit(True) - apply_button.close() - # plt.close(fig) - - cid_refs = [Vline, Vline2, Tline, Tline2] - return cid_refs + def crop_tool(self): + """ + Croping tool interface to crop_tool method + of the SpecsAnalyzer class. + """ + self.spa.crop_tool(self._result, self._scan_info) + def to_specsscan(val): + if self.spa._data_array is not None: + self._result = self.spa._data_array + load_button.close() + + load_button = ipw.Button(description="Load") + display(load_button) + load_button.on_click(to_specsscan) def check_scan( From 324a05e25a0c836007d350fdbcf8911ec1f13008 Mon Sep 17 00:00:00 2001 From: Arora0 Date: Fri, 2 Feb 2024 20:39:53 +0100 Subject: [PATCH 08/20] update example notebook --- tutorial/example.ipynb | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tutorial/example.ipynb b/tutorial/example.ipynb index 1801fc8..6e1ca1b 100644 --- a/tutorial/example.ipynb +++ b/tutorial/example.ipynb @@ -58,7 +58,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The load_scan method loads the scan as an xarray along with the metadata needed for nexus conversion. The progress bars can be activated by changing the config parameter, enable_nested_progress_bar, to true in config.yaml. Additionally, a cropping tool can be activated by passing a boolean \"crop\" to the loader. " + "The load_scan method loads the scan as an xarray along with the metadata needed for nexus conversion. The progress bars can be activated by changing the config parameter, enable_nested_progress_bar, to true in config.yaml. Additionally, the data can be cropped by passing a boolean \"crop\" to the loader, provided the crop parameters already exist in the given instance. " ] }, { @@ -67,13 +67,29 @@ "metadata": {}, "outputs": [], "source": [ - "cid_refs = sps.load_scan(\n", + "res_xarray = sps.load_scan(\n", " scan=4450, # Scan number for an example mirror scan\n", " path = path,\n", " crop=True\n", ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The loader has given a warning saying that the cropping parameters do not exist yet. Therefore, a cropping tool can be used to crop the data while also saving the crop ranges into a class attribute for later scans." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sps.crop_tool() # Press crop followed by load which adds the cropped xarray to the attribute \"result\"" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -88,7 +104,7 @@ "metadata": {}, "outputs": [], "source": [ - "sps.result.dims" + "sps.result.coords" ] }, { From 90523363dd24000fcc32ea1eec20d4375e6a9539 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 2 Feb 2024 23:40:37 +0100 Subject: [PATCH 09/20] cropping interface with RangeSliders --- specsanalyzer/core.py | 115 +++++++++++++++++++---------------------- specsscan/core.py | 31 ++++++----- tutorial/example.ipynb | 21 ++++++-- 3 files changed, 89 insertions(+), 78 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index c71771f..c0afe60 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -6,20 +6,20 @@ from typing import Dict from typing import Generator from typing import Tuple -from typing import Union, Any +from typing import Union -import numpy as np -import xarray as xr import ipywidgets as ipw -from IPython.display import display import matplotlib import matplotlib.pyplot as plt -from specsanalyzer.img_tools import DraggableLines -from specsanalyzer.img_tools import crop_xarray +import numpy as np +import xarray as xr +from IPython.display import display + from specsanalyzer import io from specsanalyzer.config import parse_config from specsanalyzer.convert import calculate_matrix_correction from specsanalyzer.convert import physical_unit_data +from specsanalyzer.img_tools import crop_xarray from specsanalyzer.img_tools import fourier_filter_2d from specsanalyzer.metadata import MetaHandler @@ -36,13 +36,12 @@ def __init__( **kwds, ): - self._config = parse_config(config, **kwds,) + self._config = parse_config( + config, + **kwds, + ) self._attributes = MetaHandler(meta=metadata) self._data_array = None - self.vline = Any - self.tline = Any - self.vline2 = Any - self.tline2 = Any self.print_msg = True try: self._config["calib2d_dict"] = io.parse_calib2d_to_dict( @@ -232,15 +231,14 @@ def convert_image( crop = kwds.pop("crop", self._config.get("crop", False)) if crop: try: - range_dict: dict = self._correction_matrix_dict[lens_mode][ - kinetic_energy][pass_energy][ - work_function - ]["crop_params"] + range_dict: dict = self._correction_matrix_dict[lens_mode][kinetic_energy][ + pass_energy + ][work_function]["crop_params"] except KeyError: if self.print_msg: print( "Warning: Cropping parameters not found, " - "use method crop_tool() after loading." + "use method crop_tool() after loading.", ) self.print_msg = False else: @@ -250,17 +248,11 @@ def convert_image( if self.print_msg: print("Using existing crop parameters...") self.print_msg = False - ang_min = min(range_dict['Ang1']['val'], range_dict['Ang2']['val']) - ang_max = max(range_dict['Ang1']['val'], range_dict['Ang2']['val']) - ek_min = min(range_dict['Ek1']['val'], range_dict['Ek2']['val']) - ek_max = max(range_dict['Ek1']['val'], range_dict['Ek2']['val']) - data_array = crop_xarray( - data_array, - ang_min, - ang_max, - ek_min, - ek_max - ) + ang_min = min(range_dict["Ang1"]["val"], range_dict["Ang2"]["val"]) + ang_max = max(range_dict["Ang1"]["val"], range_dict["Ang2"]["val"]) + ek_min = min(range_dict["Ek1"]["val"], range_dict["Ek2"]["val"]) + ek_max = max(range_dict["Ek1"]["val"], range_dict["Ek2"]["val"]) + data_array = crop_xarray(data_array, ang_min, ang_max, ek_min, ek_max) return data_array @@ -274,17 +266,10 @@ def crop_tool( res_xarray: xarray obtained from the converted raw data scan_info_dict: dict containing the contents of info.txt file """ - range_dict = { - "Ek1": {"x": 0.15, "y": 0.9, "val": 20.2}, - "Ek2": {"x": 0.30, "y": 0.9, "val": 20.8}, - "Ang1": {"x": 0.45, "y": 0.9, "val": -3.0}, - "Ang2": {"x": 0.60, "y": 0.9, "val": 3.0} - } matplotlib.use("module://ipympl.backend_nbagg") fig = plt.figure() ax = fig.add_subplot(111) - plt.subplots_adjust(top=0.75) try: if len(res_xarray.dims) == 3: res_xarray[:, :, 0].plot(ax=ax) @@ -294,42 +279,50 @@ def crop_tool( print("Load the scan first!") raise - self.vline = DraggableLines(ax, fig, "Ek1", range_dict, res_xarray) - self.vline2 = DraggableLines(ax, fig, "Ek2", range_dict, res_xarray) - self.tline = DraggableLines(ax, fig, "Ang1", range_dict, res_xarray) - self.tline2 = DraggableLines(ax, fig, "Ang2", range_dict, res_xarray) + vline = ipw.FloatRangeSlider( + value=[res_xarray.Ekin[0], res_xarray.Ekin[-1]], + min=res_xarray.Ekin[0], + max=res_xarray.Ekin[-1], + step=0.01, + ) + tline = ipw.FloatRangeSlider( + value=[res_xarray.Angle[0], res_xarray.Angle[-1]], + min=res_xarray.Angle[0], + max=res_xarray.Angle[-1], + step=0.01, + ) + + def update(tline, vline): + return + + ipw.interact( + update, + tline=tline, + vline=vline, + ) def cropit(val): # pylint: disable=unused-argument - ang_min = min([self.tline.xory, self.tline2.xory]) - ang_max = max([self.tline.xory, self.tline2.xory]) - ek_min = min([self.vline.xory, self.vline2.xory]) - ek_max = max([self.vline.xory, self.vline2.xory]) - self._data_array = crop_xarray( - res_xarray, - ang_min, - ang_max, - ek_min, - ek_max - ) - self._correction_matrix_dict[scan_info_dict['LensMode']][ + ang_min = min(tline.value) + ang_max = max(tline.value) + ek_min = min(vline.value) + ek_max = max(vline.value) + self._data_array = crop_xarray(res_xarray, ang_min, ang_max, ek_min, ek_max) + self._correction_matrix_dict[scan_info_dict["LensMode"]][ scan_info_dict["KineticEnergy"] - ][ - scan_info_dict["PassEnergy"] - ][ - scan_info_dict["WorkFunction"] - ] = { + ][scan_info_dict["PassEnergy"]][scan_info_dict["WorkFunction"]] = { "crop_params": { "Ek1": {"x": 0.15, "y": 0.9, "val": ek_min}, "Ek2": {"x": 0.30, "y": 0.9, "val": ek_max}, "Ang1": {"x": 0.45, "y": 0.9, "val": ang_min}, - "Ang2": {"x": 0.60, "y": 0.9, "val": ang_max} - } + "Ang2": {"x": 0.60, "y": 0.9, "val": ang_max}, + }, } + ax.cla() + self._data_array.plot(ax=ax, add_colorbar=False) + fig.canvas.draw_idle() - self.vline.c.mpl_disconnect(self.vline.sid) - self.vline2.c.mpl_disconnect(self.vline2.sid) - self.tline.c.mpl_disconnect(self.tline.sid) - self.tline2.c.mpl_disconnect(self.tline2.sid) + vline.close() + tline.close() apply_button.close() apply_button = ipw.Button(description="Crop") diff --git a/specsscan/core.py b/specsscan/core.py index 5d93c00..09c2e92 100755 --- a/specsscan/core.py +++ b/specsscan/core.py @@ -10,22 +10,18 @@ from typing import Dict from typing import Sequence from typing import Union -import matplotlib -import matplotlib.pyplot as plt -import ipywidgets as ipw -from matplotlib.widgets import Button -from IPython.display import display +import ipywidgets as ipw +import matplotlib import numpy as np import xarray as xr +from IPython.display import display + from specsanalyzer import SpecsAnalyzer from specsanalyzer.config import parse_config from specsanalyzer.io import to_h5 from specsanalyzer.io import to_nexus from specsanalyzer.io import to_tiff -from specsanalyzer.img_tools import DraggableLines -from specsanalyzer.img_tools import crop_xarray - from specsscan.helpers import find_scan from specsscan.helpers import get_coords from specsscan.helpers import handle_meta @@ -100,7 +96,7 @@ def __repr__(self): def config(self): """Get config""" return self._config - + @property def result(self): """Get result xarray""" @@ -210,7 +206,7 @@ def load_scan( pass_energy, work_function, **kwds, - ) + ), ) self.spa.print_msg = True coords, dim = get_coords( @@ -255,13 +251,23 @@ def load_scan( return res_xarray - def crop_tool(self): """ Croping tool interface to crop_tool method of the SpecsAnalyzer class. """ - self.spa.crop_tool(self._result, self._scan_info) + matplotlib.use("module://ipympl.backend_nbagg") + image = self.metadata["loader"]["raw_data"][0] + converted = self.spa.convert_image( + raw_img=image, + lens_mode=self._scan_info["LensMode"], + kin_energy=self._scan_info["KineticEnergy"], + pass_energy=self._scan_info["PassEnergy"], + work_function=self._scan_info["WorkFunction"], + crop=False, + ) + self.spa.crop_tool(converted, self._scan_info) + def to_specsscan(val): if self.spa._data_array is not None: self._result = self.spa._data_array @@ -271,7 +277,6 @@ def to_specsscan(val): display(load_button) load_button.on_click(to_specsscan) - def check_scan( self, scan: int, diff --git a/tutorial/example.ipynb b/tutorial/example.ipynb index 6e1ca1b..57ef970 100644 --- a/tutorial/example.ipynb +++ b/tutorial/example.ipynb @@ -23,8 +23,12 @@ "metadata": {}, "outputs": [], "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", "from specsscan import SpecsScan\n", - "import matplotlib.pyplot as plt" + "import matplotlib.pyplot as plt\n", + "\n", + "%matplotlib widget" ] }, { @@ -74,6 +78,16 @@ ")" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "res_xarray[:,:,0].plot()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -222,7 +236,7 @@ "outputs": [], "source": [ "convert(input_file=[\"../tests/data/phoibos_config.json\", # config file for translating local metadata paths to NeXus paths\n", - " \"../tests/data/phoibos_eln_data.yaml\" # ELN file that adds/overwrites additional metadata \n", + " \"../tests/data/phoibos_eln_data.yaml\" # ELN file that adds/overwrites additional metadata\n", " ],\n", " objects=sps.result, # xarray object obtained from the specsscan loader\n", " reader='mpes',\n", @@ -285,9 +299,8 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.8.12" }, - "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "a164666994e9db75450cd7016dd7e51d42ea6e7c1e5e8017af1f8068ca906367" From 6d64e00564534e091cffed5bad6012ff438a862b Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 2 Feb 2024 23:46:43 +0100 Subject: [PATCH 10/20] fix bug and remove button --- specsanalyzer/core.py | 2 +- specsscan/core.py | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index c0afe60..8364e73 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -292,7 +292,7 @@ def crop_tool( step=0.01, ) - def update(tline, vline): + def update(tline, vline): # pylint: disable=unused-argument return ipw.interact( diff --git a/specsscan/core.py b/specsscan/core.py index 09c2e92..b18452c 100755 --- a/specsscan/core.py +++ b/specsscan/core.py @@ -11,11 +11,9 @@ from typing import Sequence from typing import Union -import ipywidgets as ipw import matplotlib import numpy as np import xarray as xr -from IPython.display import display from specsanalyzer import SpecsAnalyzer from specsanalyzer.config import parse_config @@ -261,22 +259,13 @@ def crop_tool(self): converted = self.spa.convert_image( raw_img=image, lens_mode=self._scan_info["LensMode"], - kin_energy=self._scan_info["KineticEnergy"], + kinetic_energy=self._scan_info["KineticEnergy"], pass_energy=self._scan_info["PassEnergy"], work_function=self._scan_info["WorkFunction"], crop=False, ) self.spa.crop_tool(converted, self._scan_info) - def to_specsscan(val): - if self.spa._data_array is not None: - self._result = self.spa._data_array - load_button.close() - - load_button = ipw.Button(description="Load") - display(load_button) - load_button.on_click(to_specsscan) - def check_scan( self, scan: int, From df1afcf63c62a97efed00be04ecf11386bb5d736 Mon Sep 17 00:00:00 2001 From: Arora0 Date: Tue, 6 Feb 2024 15:11:04 +0100 Subject: [PATCH 11/20] add moving lines and slider for colorbar --- specsanalyzer/core.py | 93 ++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 33 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index 8364e73..375c75e 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -227,13 +227,21 @@ def convert_image( # TODO discuss how to handle cropping. Can he store one set of cropping # parameters in the config, or should we store one set per pass energy/ # lens mode/ kinetic energy in the dict? - saved_rangedict = False crop = kwds.pop("crop", self._config.get("crop", False)) if crop: try: range_dict: dict = self._correction_matrix_dict[lens_mode][kinetic_energy][ pass_energy ][work_function]["crop_params"] + if self.print_msg: + print("Using saved crop parameters...") + self.print_msg = False + + ang_min = range_dict["Ang1"] + ang_max = range_dict["Ang2"] + ek_min = range_dict["Ek1"] + ek_max = range_dict["Ek2"] + data_array = crop_xarray(data_array, ang_min, ang_max, ek_min, ek_max) except KeyError: if self.print_msg: print( @@ -241,18 +249,6 @@ def convert_image( "use method crop_tool() after loading.", ) self.print_msg = False - else: - saved_rangedict = True - - if saved_rangedict: - if self.print_msg: - print("Using existing crop parameters...") - self.print_msg = False - ang_min = min(range_dict["Ang1"]["val"], range_dict["Ang2"]["val"]) - ang_max = max(range_dict["Ang1"]["val"], range_dict["Ang2"]["val"]) - ek_min = min(range_dict["Ek1"]["val"], range_dict["Ek2"]["val"]) - ek_max = max(range_dict["Ek1"]["val"], range_dict["Ek2"]["val"]) - data_array = crop_xarray(data_array, ang_min, ang_max, ek_min, ek_max) return data_array @@ -272,57 +268,88 @@ def crop_tool( ax = fig.add_subplot(111) try: if len(res_xarray.dims) == 3: - res_xarray[:, :, 0].plot(ax=ax) + mesh_obj = res_xarray[:, :, 0].plot(ax=ax) else: # dim == 2 - res_xarray.plot(ax=ax) + mesh_obj = res_xarray.plot(ax=ax) except AttributeError: print("Load the scan first!") raise - vline = ipw.FloatRangeSlider( - value=[res_xarray.Ekin[0], res_xarray.Ekin[-1]], + lineh1 = ax.axhline(y=res_xarray.Angle[0]) + lineh2 = ax.axhline(y=res_xarray.Angle[-1]) + linev1 = ax.axvline(x=res_xarray.Ekin[0]) + linev2 = ax.axvline(x=res_xarray.Ekin[-1]) + + try: + range_dict = self._correction_matrix_dict[scan_info_dict["LensMode"]][ + scan_info_dict["KineticEnergy"] + ][scan_info_dict["PassEnergy"]][scan_info_dict["WorkFunction"]]["crop_params"] + + vline_range = [range_dict["Ek1"], range_dict["Ek2"]] + hline_range = [range_dict["Ang1"], range_dict["Ang2"]] + except KeyError: + vline_range = [res_xarray.Ekin[0], res_xarray.Ekin[-1]] + hline_range = [res_xarray.Angle[0], res_xarray.Angle[-1]] + + vline_slider = ipw.FloatRangeSlider( + description="Ekin", + value=vline_range, min=res_xarray.Ekin[0], max=res_xarray.Ekin[-1], step=0.01, ) - tline = ipw.FloatRangeSlider( - value=[res_xarray.Angle[0], res_xarray.Angle[-1]], + hline_slider = ipw.FloatRangeSlider( + description="Angle", + value=hline_range, min=res_xarray.Angle[0], max=res_xarray.Angle[-1], step=0.01, ) + clim_slider = ipw.FloatRangeSlider( + description="colorbar limits", + value=[res_xarray.data.min(), res_xarray.data.max()], + min=res_xarray.data.min(), + max=res_xarray.data.max(), + ) - def update(tline, vline): # pylint: disable=unused-argument - return + def update(hline, vline, v_vals): + lineh1.set_ydata(hline[0]) + lineh2.set_ydata(hline[1]) + linev1.set_xdata(vline[0]) + linev2.set_xdata(vline[1]) + mesh_obj.set_clim(vmin=v_vals[0], vmax=v_vals[1]) + fig.canvas.draw_idle() ipw.interact( update, - tline=tline, - vline=vline, + hline=hline_slider, + vline=vline_slider, + v_vals=clim_slider, ) def cropit(val): # pylint: disable=unused-argument - ang_min = min(tline.value) - ang_max = max(tline.value) - ek_min = min(vline.value) - ek_max = max(vline.value) + ang_min = min(hline_slider.value) + ang_max = max(hline_slider.value) + ek_min = min(vline_slider.value) + ek_max = max(vline_slider.value) self._data_array = crop_xarray(res_xarray, ang_min, ang_max, ek_min, ek_max) self._correction_matrix_dict[scan_info_dict["LensMode"]][ scan_info_dict["KineticEnergy"] ][scan_info_dict["PassEnergy"]][scan_info_dict["WorkFunction"]] = { "crop_params": { - "Ek1": {"x": 0.15, "y": 0.9, "val": ek_min}, - "Ek2": {"x": 0.30, "y": 0.9, "val": ek_max}, - "Ang1": {"x": 0.45, "y": 0.9, "val": ang_min}, - "Ang2": {"x": 0.60, "y": 0.9, "val": ang_max}, + "Ek1": ek_min, + "Ek2": ek_max, + "Ang1": ang_min, + "Ang2": ang_max, }, } ax.cla() self._data_array.plot(ax=ax, add_colorbar=False) fig.canvas.draw_idle() - vline.close() - tline.close() + vline_slider.close() + hline_slider.close() + clim_slider.close() apply_button.close() apply_button = ipw.Button(description="Crop") From ddb2d22838ea921f17441b83df942571ebdde7ed Mon Sep 17 00:00:00 2001 From: Arora0 Date: Tue, 27 Feb 2024 14:06:20 +0100 Subject: [PATCH 12/20] remove draggable lines class --- specsanalyzer/img_tools.py | 81 -------------------------------------- 1 file changed, 81 deletions(-) diff --git a/specsanalyzer/img_tools.py b/specsanalyzer/img_tools.py index ca7cbaf..f86494b 100755 --- a/specsanalyzer/img_tools.py +++ b/specsanalyzer/img_tools.py @@ -4,91 +4,10 @@ from typing import Sequence from typing import Union -from matplotlib import lines import numpy as np import xarray as xr -class DraggableLines: - """ - Class to run an interactive tool to drag lines over an image - an store the last positions. Used by the cropping tool in - specsscan.load_scan() method. - """ - def __init__(self, ax, fig, kind, range_dict, xarray): - self.ax = ax - self.c = ax.get_figure().canvas - self.o = kind - self.xory = range_dict[kind]['val'] - self.xarray = xarray - self.follower = None - self.releaser = None - dims = xarray.dims - props = {"boxstyle": 'round', "facecolor": 'white', "alpha": 0.5} - self.text = ax.text( - range_dict[kind]['x'], - range_dict[kind]['y'], - f"{self.o} " + f"{self.xory:.2f}", - transform=fig.transFigure, - bbox=props - ) - - if kind in ("Ang1", "Ang2"): - self.x = xarray[f"{dims[1]}"].data - self.y = [range_dict[kind]['val']] * len(self.x) - - elif kind in ("Ek1", "Ek2"): - self.y = xarray[f"{dims[0]}"].data - self.x = [range_dict[kind]['val']] * len(self.y) - - self.line = lines.Line2D(self.x, self.y, picker=5) - self.ax.add_line(self.line) - self.c.draw_idle() - - self.sid = self.c.mpl_connect('button_press_event', self.clickonline) - - def clickonline(self, event): - """ - Checks if the line clicked belongs to this instance. - """ - if event.inaxes != self.line.axes: - return - contains = self.line.contains(event) - if not contains[0]: - return - - self.follower = self.c.mpl_connect("motion_notify_event", self.followmouse) - self.releaser = self.c.mpl_connect("button_release_event", self.releaseonclick) - - def followmouse(self, event): - """ - Sets the selected line position to the mouse position, - while updating the text box in real time. - """ - if self.o in ("Ang1", "Ang2"): - if event.ydata: - self.line.set_ydata([event.ydata] * len(self.x)) - else: - self.line.set_ydata([event.ydata] * len(self.x)) - self.xory = self.line.get_ydata()[0] - self.text.set_text(f"{self.o} " + f"{self.xory:.2f}") - - elif self.o in ("Ek1", "Ek2"): - self.line.set_xdata([event.xdata] * len(self.y)) - self.xory = self.line.get_xdata()[0] - self.text.set_text(f"{self.o} " + f"{self.xory:.2f}") - - self.c.draw_idle() - - def releaseonclick(self, event): # pylint: disable=unused-argument - """ - Disconnects the interaction on mouse release. - """ - self.c.draw_idle() - self.c.mpl_disconnect(self.releaser) - self.c.mpl_disconnect(self.follower) - - def gauss2d( # pylint: disable=invalid-name, too-many-arguments x: Union[float, np.ndarray], From 19d6fd462084a80a8c5cbf0a50f3729dfdacac8c Mon Sep 17 00:00:00 2001 From: Arora0 Date: Tue, 27 Feb 2024 14:08:09 +0100 Subject: [PATCH 13/20] move raw image conversion to spa crop tool --- specsanalyzer/core.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index 375c75e..723a9fd 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -19,8 +19,8 @@ from specsanalyzer.config import parse_config from specsanalyzer.convert import calculate_matrix_correction from specsanalyzer.convert import physical_unit_data -from specsanalyzer.img_tools import crop_xarray from specsanalyzer.img_tools import fourier_filter_2d +from specsanalyzer.img_tools import crop_xarray from specsanalyzer.metadata import MetaHandler package_dir = os.path.dirname(__file__) @@ -235,7 +235,6 @@ def convert_image( ][work_function]["crop_params"] if self.print_msg: print("Using saved crop parameters...") - self.print_msg = False ang_min = range_dict["Ang1"] ang_max = range_dict["Ang2"] @@ -248,14 +247,16 @@ def convert_image( "Warning: Cropping parameters not found, " "use method crop_tool() after loading.", ) - self.print_msg = False return data_array def crop_tool( self, - res_xarray: xr.DataArray, - scan_info_dict: dict, + raw_image: np.ndarray, + lens_mode: str, + kinetic_energy: float, + pass_energy: float, + work_function: float, ): """Crop tool Args: @@ -263,14 +264,20 @@ def crop_tool( scan_info_dict: dict containing the contents of info.txt file """ + res_xarray = self.convert_image( + raw_img=raw_image, + lens_mode=lens_mode, + kinetic_energy=kinetic_energy, + pass_energy=pass_energy, + work_function=work_function, + crop=False, + ) + matplotlib.use("module://ipympl.backend_nbagg") fig = plt.figure() ax = fig.add_subplot(111) try: - if len(res_xarray.dims) == 3: - mesh_obj = res_xarray[:, :, 0].plot(ax=ax) - else: # dim == 2 - mesh_obj = res_xarray.plot(ax=ax) + mesh_obj = res_xarray.plot(ax=ax) except AttributeError: print("Load the scan first!") raise @@ -281,9 +288,9 @@ def crop_tool( linev2 = ax.axvline(x=res_xarray.Ekin[-1]) try: - range_dict = self._correction_matrix_dict[scan_info_dict["LensMode"]][ - scan_info_dict["KineticEnergy"] - ][scan_info_dict["PassEnergy"]][scan_info_dict["WorkFunction"]]["crop_params"] + range_dict = self._correction_matrix_dict[lens_mode][kinetic_energy][ + pass_energy + ][work_function]["crop_params"] vline_range = [range_dict["Ek1"], range_dict["Ek2"]] hline_range = [range_dict["Ang1"], range_dict["Ang2"]] @@ -333,9 +340,9 @@ def cropit(val): # pylint: disable=unused-argument ek_min = min(vline_slider.value) ek_max = max(vline_slider.value) self._data_array = crop_xarray(res_xarray, ang_min, ang_max, ek_min, ek_max) - self._correction_matrix_dict[scan_info_dict["LensMode"]][ - scan_info_dict["KineticEnergy"] - ][scan_info_dict["PassEnergy"]][scan_info_dict["WorkFunction"]] = { + self._correction_matrix_dict[lens_mode][kinetic_energy][ + pass_energy + ][work_function] = { "crop_params": { "Ek1": ek_min, "Ek2": ek_max, From f2f9d26309629296e6de1bda3f483dac0901bc90 Mon Sep 17 00:00:00 2001 From: Arora0 Date: Tue, 27 Feb 2024 14:09:14 +0100 Subject: [PATCH 14/20] set print msg to false for 1st image and update docs --- specsscan/core.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/specsscan/core.py b/specsscan/core.py index b18452c..5307d50 100755 --- a/specsscan/core.py +++ b/specsscan/core.py @@ -10,6 +10,11 @@ from typing import Dict from typing import Sequence from typing import Union +import matplotlib +import matplotlib.pyplot as plt +import ipywidgets as ipw +from matplotlib.widgets import Button +from IPython.display import display import matplotlib import numpy as np @@ -140,6 +145,8 @@ def load_scan( np.s_[1:10, 15, -1] would be a valid input for iterations. metadata (dict, optional): Metadata dictionary with additional metadata for the scan + kwds: Additional arguments for the SpecsAnalyzer converter. For ex., passing + crop=True crops the data if cropping data is already present in the given instance. Raises: FileNotFoundError, IndexError @@ -195,7 +202,8 @@ def load_scan( ) xr_list = [] - for image in data: + for i in range(len(data)): + image = data[i] xr_list.append( self.spa.convert_image( image, @@ -206,7 +214,10 @@ def load_scan( **kwds, ), ) + if i == 0: + self.spa.print_msg = False self.spa.print_msg = True + coords, dim = get_coords( scan_path=path, scan_type=scan_type, @@ -256,15 +267,13 @@ def crop_tool(self): """ matplotlib.use("module://ipympl.backend_nbagg") image = self.metadata["loader"]["raw_data"][0] - converted = self.spa.convert_image( - raw_img=image, - lens_mode=self._scan_info["LensMode"], - kinetic_energy=self._scan_info["KineticEnergy"], - pass_energy=self._scan_info["PassEnergy"], - work_function=self._scan_info["WorkFunction"], - crop=False, + self.spa.crop_tool( + image, + self._scan_info["LensMode"], + self._scan_info["KineticEnergy"], + self._scan_info["PassEnergy"], + self._scan_info["WorkFunction"], ) - self.spa.crop_tool(converted, self._scan_info) def check_scan( self, @@ -286,6 +295,8 @@ def check_scan( path: Either a string of the path to the folder containing the scan or a Path object metadata (dict, optional): Metadata dictionary with additional metadata for the scan + kwds: Additional arguments for the SpecsAnalyzer converter. For ex., passing + crop=True crops the data if cropping data is already present in the given instance. Raises: FileNotFoundError Returns: @@ -344,7 +355,8 @@ def check_scan( "Invalid input. A 3-D scan is expected, a 2-D single scan was provided instead.", ) xr_list = [] - for image in data: + for i in range(len(data)): + image = data[i] xr_list.append( self.spa.convert_image( image, @@ -355,6 +367,9 @@ def check_scan( **kwds, ), ) + if i == 0: + self.spa.print_msg = False + self.spa.print_msg = True dims = get_coords( scan_path=path, From 9c8395ff835c0ec48294d29aaa7a41be4c105b3f Mon Sep 17 00:00:00 2001 From: Arora0 Date: Tue, 27 Feb 2024 17:31:00 +0100 Subject: [PATCH 15/20] add units for temperature scan --- specsscan/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/specsscan/core.py b/specsscan/core.py index 5307d50..0d0e416 100755 --- a/specsscan/core.py +++ b/specsscan/core.py @@ -49,6 +49,7 @@ "tilt": "degrees", "azimuth": "degrees", "voltage": "V", + "temperature": "K", } From 1b85193519f653bb6f33667112de055b18ff1566 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 27 Feb 2024 21:31:25 +0100 Subject: [PATCH 16/20] several small fixes and updates --- specsanalyzer/core.py | 33 ++++++++++++------------- specsscan/core.py | 44 ++++++++++++--------------------- specsscan/helpers.py | 55 +++++++++++++++++++++++------------------- tutorial/example.ipynb | 23 +++++++++++++++++- 4 files changed, 82 insertions(+), 73 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index 723a9fd..aba5d58 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -19,8 +19,8 @@ from specsanalyzer.config import parse_config from specsanalyzer.convert import calculate_matrix_correction from specsanalyzer.convert import physical_unit_data -from specsanalyzer.img_tools import fourier_filter_2d from specsanalyzer.img_tools import crop_xarray +from specsanalyzer.img_tools import fourier_filter_2d from specsanalyzer.metadata import MetaHandler package_dir = os.path.dirname(__file__) @@ -91,13 +91,10 @@ def convert_image( ) -> xr.DataArray: """Converts an imagin in physical unit data, angle vs energy - Args: raw_img (np.ndarray): Raw image data, numpy 2d matrix - lens_mode (str): - analzser lens mode, check calib2d for a list - of modes Camelback naming convention e.g. "WideAngleMode" - + lens_mode (str): analzser lens mode, check calib2d for a list + of modes Camelcase naming convention e.g. "WideAngleMode" kinetic_energy (float): set analyser kinetic energy pass_energy (float): set analyser pass energy work_function (float): set analyser work function @@ -224,9 +221,7 @@ def convert_image( dims=["Position", "Ekin"], ) - # TODO discuss how to handle cropping. Can he store one set of cropping - # parameters in the config, or should we store one set per pass energy/ - # lens mode/ kinetic energy in the dict? + # Handle cropping based on parameters stored in correction dictionary crop = kwds.pop("crop", self._config.get("crop", False)) if crop: try: @@ -258,10 +253,14 @@ def crop_tool( pass_energy: float, work_function: float, ): - """Crop tool + """Crop tool for selecting cropping parameters Args: - res_xarray: xarray obtained from the converted raw data - scan_info_dict: dict containing the contents of info.txt file + raw_img (np.ndarray): Raw image data, numpy 2d matrix + lens_mode (str): analzser lens mode, check calib2d for a list + of modes Camelcase naming convention e.g. "WideAngleMode" + kinetic_energy (float): set analyser kinetic energy + pass_energy (float): set analyser pass energy + work_function (float): set analyser work function """ res_xarray = self.convert_image( @@ -288,9 +287,9 @@ def crop_tool( linev2 = ax.axvline(x=res_xarray.Ekin[-1]) try: - range_dict = self._correction_matrix_dict[lens_mode][kinetic_energy][ - pass_energy - ][work_function]["crop_params"] + range_dict = self._correction_matrix_dict[lens_mode][kinetic_energy][pass_energy][ + work_function + ]["crop_params"] vline_range = [range_dict["Ek1"], range_dict["Ek2"]] hline_range = [range_dict["Ang1"], range_dict["Ang2"]] @@ -340,9 +339,7 @@ def cropit(val): # pylint: disable=unused-argument ek_min = min(vline_slider.value) ek_max = max(vline_slider.value) self._data_array = crop_xarray(res_xarray, ang_min, ang_max, ek_min, ek_max) - self._correction_matrix_dict[lens_mode][kinetic_energy][ - pass_energy - ][work_function] = { + self._correction_matrix_dict[lens_mode][kinetic_energy][pass_energy][work_function] = { "crop_params": { "Ek1": ek_min, "Ek2": ek_max, diff --git a/specsscan/core.py b/specsscan/core.py index 0d0e416..eb6b838 100755 --- a/specsscan/core.py +++ b/specsscan/core.py @@ -10,11 +10,6 @@ from typing import Dict from typing import Sequence from typing import Union -import matplotlib -import matplotlib.pyplot as plt -import ipywidgets as ipw -from matplotlib.widgets import Button -from IPython.display import display import matplotlib import numpy as np @@ -32,11 +27,10 @@ from specsscan.helpers import parse_info_to_dict from specsscan.helpers import parse_lut_to_df -# from specsanalyzer.io import to_h5, load_h5, to_tiff, load_tiff package_dir = os.path.dirname(find_spec("specsscan").origin) -default_units = { +default_units: Dict[Any, Any] = { "Angle": "degrees", "Ekin": "eV", "delay": "fs", @@ -84,7 +78,7 @@ def __init__( # pylint: disable=dangerous-default-value except KeyError: self.spa = SpecsAnalyzer() - self._result = None + self._result: xr.DataArray = None def __repr__(self): if self._config is None: @@ -101,11 +95,6 @@ def config(self): """Get config""" return self._config - @property - def result(self): - """Get result xarray""" - return self._result - @config.setter def config(self, config: Union[dict, str]): """Set config""" @@ -118,6 +107,11 @@ def config(self, config: Union[dict, str]): except KeyError: self.spa = SpecsAnalyzer() + @property + def result(self): + """Get result xarray""" + return self._result + def load_scan( self, scan: int, @@ -146,8 +140,8 @@ def load_scan( np.s_[1:10, 15, -1] would be a valid input for iterations. metadata (dict, optional): Metadata dictionary with additional metadata for the scan - kwds: Additional arguments for the SpecsAnalyzer converter. For ex., passing - crop=True crops the data if cropping data is already present in the given instance. + **kwds: Additional arguments for the SpecsAnalyzer converter. For ex., passing + crop=True crops the data if cropping data is already present in the given instance. Raises: FileNotFoundError, IndexError @@ -203,8 +197,7 @@ def load_scan( ) xr_list = [] - for i in range(len(data)): - image = data[i] + for image in data: xr_list.append( self.spa.convert_image( image, @@ -215,8 +208,7 @@ def load_scan( **kwds, ), ) - if i == 0: - self.spa.print_msg = False + self.spa.print_msg = False self.spa.print_msg = True coords, dim = get_coords( @@ -279,10 +271,7 @@ def crop_tool(self): def check_scan( self, scan: int, - delays: Union[ - Sequence[int], - int, - ], + delays: Union[Sequence[int], int], path: Union[str, Path] = "", metadata: dict = None, **kwds, @@ -296,7 +285,7 @@ def check_scan( path: Either a string of the path to the folder containing the scan or a Path object metadata (dict, optional): Metadata dictionary with additional metadata for the scan - kwds: Additional arguments for the SpecsAnalyzer converter. For ex., passing + **kwds: Additional arguments for the SpecsAnalyzer converter. For ex., passing crop=True crops the data if cropping data is already present in the given instance. Raises: FileNotFoundError @@ -313,7 +302,6 @@ def check_scan( else: # search for the given scan using the default path path = Path(self._config["data_path"]) - # path_scan = sorted(path.glob(f"20[1,2][9,0-9]/*/*/Raw Data/{scan}")) path_scan_list = find_scan(path, scan) if not path_scan_list: raise FileNotFoundError( @@ -356,8 +344,7 @@ def check_scan( "Invalid input. A 3-D scan is expected, a 2-D single scan was provided instead.", ) xr_list = [] - for i in range(len(data)): - image = data[i] + for image in data: xr_list.append( self.spa.convert_image( image, @@ -368,8 +355,7 @@ def check_scan( **kwds, ), ) - if i == 0: - self.spa.print_msg = False + self.spa.print_msg = False self.spa.print_msg = True dims = get_coords( diff --git a/specsscan/helpers.py b/specsscan/helpers.py index 50ce72c..b74cf28 100644 --- a/specsscan/helpers.py +++ b/specsscan/helpers.py @@ -14,9 +14,10 @@ import numpy as np import pandas as pd -from specsanalyzer.config import complete_dictionary # name can be generalized from tqdm.auto import tqdm +from specsanalyzer.config import complete_dictionary # name can be generalized + def load_images( scan_path: Path, @@ -35,32 +36,36 @@ def load_images( Sequence[slice], ] = None, tqdm_enable_nested: bool = False, -) -> np.ndarray: +) -> List[np.ndarray]: """Loads a 2D/3D numpy array of images for the given - scan path with an optional averaging - over the given iterations/delays. The function provides - functionality to both load_scan and check_scan methods of - the SpecsScan class. When iterations/delays is provided, - average is performed over the iterations/delays for all - delays/iterations. + scan path with an optional averaging + over the given iterations/delays. The function provides + functionality to both load_scan and check_scan methods of + the SpecsScan class. When iterations/delays is provided, + average is performed over the iterations/delays for all + delays/iterations. + Args: - scan_path: object of class Path pointing - to the scan folder - df_lut: Pandas dataframe containing the contents of LUT.txt - as obtained from parse_lut_to_df() - iterations: A 1-D array of the indices of iterations over - which the images are to be averaged. The array - can be a list, numpy array or a Tuple consisting of - slice objects and integers. For ex., - np.s_[1:10, 15, -1] would be a valid input. - delays: A 1-D array of the indices of delays over - which the images are to be averaged. The array can - be a list, numpy array or a Tuple consisting of - slice objects and integers. For ex., - np.s_[1:10, 15, -1] would be a valid input. + scan_path (Path): object of class Path pointing to the scan folder + df_lut (Union[pd.DataFrame, None], optional): Pandas dataframe containing the contents + of LUT.txt as obtained from parse_lut_to_df(). Defaults to None. + iterations (Union[ np.ndarray, slice, Sequence[int], Sequence[slice], ], optional): A 1-D + array of the indices of iterations over which the images are to be averaged. The array + can be a list, numpy array or a Tuple consisting of slice objects and integers. For + ex., np.s_[1:10, 15, -1] would be a valid input. Defaults to None. + delays (Union[ np.ndarray, slice, int, Sequence[int], Sequence[slice], ], optional): A 1-D + array of the indices of delays over which the images are to be averaged. The array can + be a list, numpy array or a Tuple consisting of slice objects and integers. For ex., + np.s_[1:10, 15, -1] would be a valid input. Defaults to None. + tqdm_enable_nested (bool, optional): Option to enable a nested progress bar. + Defaults to False. + + Raises: + ValueError: Raised if both iterations and delays is provided. + IndexError: Raised if no valid dimension for averaging is found. + Returns: - data: A 2-D or 3-D (concatenated) numpy array consisting - of raw data + List[np.ndarray]: A list of 2-D numpy arrays of raw data """ scan_list = sorted( @@ -137,7 +142,7 @@ def load_images( new_im = np.loadtxt(file, delimiter="\t") data.append(new_im) - return np.array(data) + return data def get_raw2d( diff --git a/tutorial/example.ipynb b/tutorial/example.ipynb index 57ef970..54e6f9a 100644 --- a/tutorial/example.ipynb +++ b/tutorial/example.ipynb @@ -101,7 +101,28 @@ "metadata": {}, "outputs": [], "source": [ - "sps.crop_tool() # Press crop followed by load which adds the cropped xarray to the attribute \"result\"" + "sps.crop_tool()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Press crop applies the cropping to the test image, and stores the cropping information in the class.\n", + "Load the scan again to apply it to all images:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "res_xarray = sps.load_scan(\n", + " scan=4450, # Scan number for an example mirror scan\n", + " path = path,\n", + " crop=True\n", + ")" ] }, { From 456034a1c2fe552ee9a5f130864c863cd3b04ae6 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 27 Feb 2024 22:42:11 +0100 Subject: [PATCH 17/20] add option to specify crop ranges in config --- specsanalyzer/core.py | 181 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 147 insertions(+), 34 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index aba5d58..1fdca7c 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -228,20 +228,56 @@ def convert_image( range_dict: dict = self._correction_matrix_dict[lens_mode][kinetic_energy][ pass_energy ][work_function]["crop_params"] + ang_min = range_dict["ang_min"] + ang_max = range_dict["ang_max"] + ek_min = range_dict["ek_min"] + ek_max = range_dict["ek_max"] if self.print_msg: print("Using saved crop parameters...") - - ang_min = range_dict["Ang1"] - ang_max = range_dict["Ang2"] - ek_min = range_dict["Ek1"] - ek_max = range_dict["Ek2"] data_array = crop_xarray(data_array, ang_min, ang_max, ek_min, ek_max) except KeyError: - if self.print_msg: - print( - "Warning: Cropping parameters not found, " - "use method crop_tool() after loading.", + try: + ang_min = ( + kwds.get("ang_range_min", self._config["ang_range_min"]) + * ( + data_array.coords[data_array.dims[0]][-1] + - data_array.coords[data_array.dims[0]][0] + ) + + data_array.coords[data_array.dims[0]][0] + ) + ang_max = ( + kwds.get("ang_range_max", self._config["ang_range_max"]) + * ( + data_array.coords[data_array.dims[0]][-1] + - data_array.coords[data_array.dims[0]][0] + ) + + data_array.coords[data_array.dims[0]][0] ) + ek_min = ( + kwds.get("ek_range_min", self._config["ek_range_min"]) + * ( + data_array.coords[data_array.dims[1]][-1] + - data_array.coords[data_array.dims[1]][0] + ) + + data_array.coords[data_array.dims[1]][0] + ) + ek_max = ( + kwds.get("ek_range_max", self._config["ek_range_max"]) + * ( + data_array.coords[data_array.dims[1]][-1] + - data_array.coords[data_array.dims[1]][0] + ) + + data_array.coords[data_array.dims[1]][0] + ) + if self.print_msg: + print("Cropping parameters not found, using cropping ranges from config...") + data_array = crop_xarray(data_array, ang_min, ang_max, ek_min, ek_max) + except KeyError: + if self.print_msg: + print( + "Warning: Cropping parameters not found, " + "use method crop_tool() after loading.", + ) return data_array @@ -252,6 +288,7 @@ def crop_tool( kinetic_energy: float, pass_energy: float, work_function: float, + **kwds, ): """Crop tool for selecting cropping parameters Args: @@ -261,9 +298,15 @@ def crop_tool( kinetic_energy (float): set analyser kinetic energy pass_energy (float): set analyser pass energy work_function (float): set analyser work function + **kwds: Keyword parameters for the crop tool: + + -ek_range_min + -ek_range_max + -ang_range_min + -ang_range_max """ - res_xarray = self.convert_image( + data_array = self.convert_image( raw_img=raw_image, lens_mode=lens_mode, kinetic_energy=kinetic_energy, @@ -276,46 +319,87 @@ def crop_tool( fig = plt.figure() ax = fig.add_subplot(111) try: - mesh_obj = res_xarray.plot(ax=ax) + mesh_obj = data_array.plot(ax=ax) except AttributeError: print("Load the scan first!") raise - lineh1 = ax.axhline(y=res_xarray.Angle[0]) - lineh2 = ax.axhline(y=res_xarray.Angle[-1]) - linev1 = ax.axvline(x=res_xarray.Ekin[0]) - linev2 = ax.axvline(x=res_xarray.Ekin[-1]) + lineh1 = ax.axhline(y=data_array.Angle[0]) + lineh2 = ax.axhline(y=data_array.Angle[-1]) + linev1 = ax.axvline(x=data_array.Ekin[0]) + linev2 = ax.axvline(x=data_array.Ekin[-1]) try: range_dict = self._correction_matrix_dict[lens_mode][kinetic_energy][pass_energy][ work_function ]["crop_params"] - vline_range = [range_dict["Ek1"], range_dict["Ek2"]] - hline_range = [range_dict["Ang1"], range_dict["Ang2"]] + ek_min = range_dict["ek_min"] + ek_max = range_dict["ek_max"] + ang_min = range_dict["ang_min"] + ang_max = range_dict["ang_max"] except KeyError: - vline_range = [res_xarray.Ekin[0], res_xarray.Ekin[-1]] - hline_range = [res_xarray.Angle[0], res_xarray.Angle[-1]] + try: + ang_min = ( + kwds.get("ang_range_min", self._config["ang_range_min"]) + * ( + data_array.coords[data_array.dims[0]][-1] + - data_array.coords[data_array.dims[0]][0] + ) + + data_array.coords[data_array.dims[0]][0] + ) + ang_max = ( + kwds.get("ang_range_max", self._config["ang_range_max"]) + * ( + data_array.coords[data_array.dims[0]][-1] + - data_array.coords[data_array.dims[0]][0] + ) + + data_array.coords[data_array.dims[0]][0] + ) + ek_min = ( + kwds.get("ek_range_min", self._config["ek_range_min"]) + * ( + data_array.coords[data_array.dims[1]][-1] + - data_array.coords[data_array.dims[1]][0] + ) + + data_array.coords[data_array.dims[1]][0] + ) + ek_max = ( + kwds.get("ek_range_max", self._config["ek_range_max"]) + * ( + data_array.coords[data_array.dims[1]][-1] + - data_array.coords[data_array.dims[1]][0] + ) + + data_array.coords[data_array.dims[1]][0] + ) + except KeyError: + ek_min = data_array.coords[data_array.dims[1]][0] + ek_max = data_array.coords[data_array.dims[1]][-1] + ang_min = data_array.coords[data_array.dims[0]][-1] + ang_max = data_array.coords[data_array.dims[0]][-1] + + vline_range = [ek_min, ek_max] + hline_range = [ang_min, ang_max] vline_slider = ipw.FloatRangeSlider( description="Ekin", value=vline_range, - min=res_xarray.Ekin[0], - max=res_xarray.Ekin[-1], - step=0.01, + min=data_array.Ekin[0], + max=data_array.Ekin[-1], + step=0.1, ) hline_slider = ipw.FloatRangeSlider( description="Angle", value=hline_range, - min=res_xarray.Angle[0], - max=res_xarray.Angle[-1], - step=0.01, + min=data_array.Angle[0], + max=data_array.Angle[-1], + step=0.1, ) clim_slider = ipw.FloatRangeSlider( description="colorbar limits", - value=[res_xarray.data.min(), res_xarray.data.max()], - min=res_xarray.data.min(), - max=res_xarray.data.max(), + value=[data_array.data.min(), data_array.data.max()], + min=data_array.data.min(), + max=data_array.data.max(), ) def update(hline, vline, v_vals): @@ -338,15 +422,44 @@ def cropit(val): # pylint: disable=unused-argument ang_max = max(hline_slider.value) ek_min = min(vline_slider.value) ek_max = max(vline_slider.value) - self._data_array = crop_xarray(res_xarray, ang_min, ang_max, ek_min, ek_max) + self._data_array = crop_xarray(data_array, ang_min, ang_max, ek_min, ek_max) self._correction_matrix_dict[lens_mode][kinetic_energy][pass_energy][work_function] = { "crop_params": { - "Ek1": ek_min, - "Ek2": ek_max, - "Ang1": ang_min, - "Ang2": ang_max, + "ek_min": ek_min, + "ek_max": ek_max, + "ang_min": ang_min, + "ang_max": ang_max, }, } + self._config["ek_range_min"] = ( + (ek_min - data_array.coords[data_array.dims[1]][0]) + / ( + data_array.coords[data_array.dims[1]][-1] + - data_array.coords[data_array.dims[1]][0] + ) + ).item() + self._config["ek_range_max"] = ( + (ek_max - data_array.coords[data_array.dims[1]][0]) + / ( + data_array.coords[data_array.dims[1]][-1] + - data_array.coords[data_array.dims[1]][0] + ) + ).item() + self._config["ang_range_min"] = ( + (ang_min - data_array.coords[data_array.dims[0]][0]) + / ( + data_array.coords[data_array.dims[0]][-1] + - data_array.coords[data_array.dims[0]][0] + ) + ).item() + self._config["ang_range_max"] = ( + (ang_max - data_array.coords[data_array.dims[0]][0]) + / ( + data_array.coords[data_array.dims[0]][-1] + - data_array.coords[data_array.dims[0]][0] + ) + ).item() + ax.cla() self._data_array.plot(ax=ax, add_colorbar=False) fig.canvas.draw_idle() @@ -371,7 +484,7 @@ def mergedicts( Args: dict1 (dict): dictionary 1 - dict2 (dict): dictiontary 2 + dict2 (dict): dictionary 2 Yields: dict: merged dictionary generator From 25fd21203590e381cb638c8a3cb9cf1f10025259 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 27 Feb 2024 23:36:24 +0100 Subject: [PATCH 18/20] add option to specify fixed cropping ranges fixed cropping range is stored and applied if no copping specifically defined add tests for cropping --- specsanalyzer/core.py | 39 ++++++++++++++---- specsscan/core.py | 8 +++- tests/test_convert.py | 92 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 10 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index 1fdca7c..cf55f59 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -283,11 +283,12 @@ def convert_image( def crop_tool( self, - raw_image: np.ndarray, + raw_img: np.ndarray, lens_mode: str, kinetic_energy: float, pass_energy: float, work_function: float, + apply: bool = False, **kwds, ): """Crop tool for selecting cropping parameters @@ -298,6 +299,8 @@ def crop_tool( kinetic_energy (float): set analyser kinetic energy pass_energy (float): set analyser pass energy work_function (float): set analyser work function + apply (bool, optional): Option to directly apply the pre-selected cropping parameters. + Defaults to False. **kwds: Keyword parameters for the crop tool: -ek_range_min @@ -307,7 +310,7 @@ def crop_tool( """ data_array = self.convert_image( - raw_img=raw_image, + raw_img=raw_img, lens_mode=lens_mode, kinetic_energy=kinetic_energy, pass_energy=pass_energy, @@ -340,8 +343,28 @@ def crop_tool( ang_max = range_dict["ang_max"] except KeyError: try: + ang_range_min = ( + kwds["ang_range_min"] + if "ang_range_min" in kwds.keys() + else self._config["ang_range_min"] + ) + ang_range_max = ( + kwds["ang_range_max"] + if "ang_range_max" in kwds.keys() + else self._config["ang_range_max"] + ) + ek_range_min = ( + kwds["ek_range_min"] + if "ek_range_min" in kwds.keys() + else self._config["ang_range_min"] + ) + ek_range_max = ( + kwds["ek_range_max"] + if "ek_range_max" in kwds.keys() + else self._config["ek_range_max"] + ) ang_min = ( - kwds.get("ang_range_min", self._config["ang_range_min"]) + ang_range_min * ( data_array.coords[data_array.dims[0]][-1] - data_array.coords[data_array.dims[0]][0] @@ -349,7 +372,7 @@ def crop_tool( + data_array.coords[data_array.dims[0]][0] ) ang_max = ( - kwds.get("ang_range_max", self._config["ang_range_max"]) + ang_range_max * ( data_array.coords[data_array.dims[0]][-1] - data_array.coords[data_array.dims[0]][0] @@ -357,7 +380,7 @@ def crop_tool( + data_array.coords[data_array.dims[0]][0] ) ek_min = ( - kwds.get("ek_range_min", self._config["ek_range_min"]) + ek_range_min * ( data_array.coords[data_array.dims[1]][-1] - data_array.coords[data_array.dims[1]][0] @@ -365,7 +388,7 @@ def crop_tool( + data_array.coords[data_array.dims[1]][0] ) ek_max = ( - kwds.get("ek_range_max", self._config["ek_range_max"]) + ek_range_max * ( data_array.coords[data_array.dims[1]][-1] - data_array.coords[data_array.dims[1]][0] @@ -375,7 +398,7 @@ def crop_tool( except KeyError: ek_min = data_array.coords[data_array.dims[1]][0] ek_max = data_array.coords[data_array.dims[1]][-1] - ang_min = data_array.coords[data_array.dims[0]][-1] + ang_min = data_array.coords[data_array.dims[0]][0] ang_max = data_array.coords[data_array.dims[0]][-1] vline_range = [ek_min, ek_max] @@ -473,6 +496,8 @@ def cropit(val): # pylint: disable=unused-argument display(apply_button) apply_button.on_click(cropit) plt.show() + if apply: + cropit("") def mergedicts( diff --git a/specsscan/core.py b/specsscan/core.py index eb6b838..9b95bd9 100755 --- a/specsscan/core.py +++ b/specsscan/core.py @@ -253,19 +253,23 @@ def load_scan( return res_xarray - def crop_tool(self): + def crop_tool(self, **kwds): """ Croping tool interface to crop_tool method of the SpecsAnalyzer class. """ matplotlib.use("module://ipympl.backend_nbagg") - image = self.metadata["loader"]["raw_data"][0] + try: + image = self.metadata["loader"]["raw_data"][0] + except KeyError as exc: + raise ValueError("No image loaded, load image first!") from exc self.spa.crop_tool( image, self._scan_info["LensMode"], self._scan_info["KineticEnergy"], self._scan_info["PassEnergy"], self._scan_info["WorkFunction"], + **kwds, ) def check_scan( diff --git a/tests/test_convert.py b/tests/test_convert.py index d567875..f1c86bf 100755 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -199,7 +199,7 @@ def test_conversion(): def test_recycling(): - """Test function for chceking that the class correctly re-uses the + """Test function for checking that the class correctly re-uses the precalculated parameters """ # get the raw data @@ -237,3 +237,93 @@ def test_recycling(): ) assert spa.correction_matrix_dict["old_matrix_check"] is True + + +def test_cropping(): + """Test function for checking that cropping parameters are correctly appield""" + # get the raw data + raw_image_name = os.fspath( + f"{test_dir}/data/dataEPFL/R9132/Data9132_RAWDATA.tsv", + ) + with open(raw_image_name, encoding="utf-8") as file: + tsv_data = np.loadtxt(file, delimiter="\t") + + config_path = os.fspath(f"{test_dir}/data/dataEPFL/config/config.yaml") + spa = SpecsAnalyzer(config=config_path) + lens_mode = "WideAngleMode" + kinetic_energy = 35.000000 + pass_energy = 35.000000 + work_function = 4.2 + + converted = spa.convert_image( + raw_img=tsv_data, + lens_mode=lens_mode, + kinetic_energy=kinetic_energy, + pass_energy=pass_energy, + work_function=work_function, + crop=True, + ) + assert converted.Angle[0] == -18 + assert converted.Angle[-1] == 17.859375 + + spa.crop_tool( + raw_img=tsv_data, + lens_mode=lens_mode, + kinetic_energy=kinetic_energy, + pass_energy=pass_energy, + work_function=work_function, + ek_range_min=0, + ek_range_max=1, + ang_range_min=0.1, + ang_range_max=0.9, + apply=True, + ) + + converted = spa.convert_image( + raw_img=tsv_data, + lens_mode=lens_mode, + kinetic_energy=kinetic_energy, + pass_energy=pass_energy, + work_function=work_function, + crop=True, + ) + + assert converted.Angle[0] == -14.34375 + assert converted.Angle[-1] == 14.203125 + + spa.crop_tool( + raw_img=tsv_data, + lens_mode=lens_mode, + kinetic_energy=45.0, + pass_energy=pass_energy, + work_function=work_function, + ek_range_min=0, + ek_range_max=1, + ang_range_min=0.2, + ang_range_max=0.8, + apply=True, + ) + + converted = spa.convert_image( + raw_img=tsv_data, + lens_mode=lens_mode, + kinetic_energy=50.0, + pass_energy=pass_energy, + work_function=work_function, + crop=True, + ) + + assert converted.Angle[0] == -10.828125 + assert converted.Angle[-1] == 10.6875 + + converted = spa.convert_image( + raw_img=tsv_data, + lens_mode=lens_mode, + kinetic_energy=kinetic_energy, + pass_energy=pass_energy, + work_function=work_function, + crop=True, + ) + + assert converted.Angle[0] == -14.34375 + assert converted.Angle[-1] == 14.203125 From ed153c6a7a004b0e14042acee57f0825c88114b3 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 27 Feb 2024 23:48:24 +0100 Subject: [PATCH 19/20] fix linting and requirements --- requirements.txt | 1 + specsanalyzer/core.py | 12 ++++-------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6d5ec17..ad2ae23 100755 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ pyyaml>=6.0 xarray>=0.20.2 tifffile>=2022.5.4 scipy>=1.8.0,<1.9.0 +ipympl>=0.9.1 ipywidgets>=8.1.1 diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index cf55f59..53dd200 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -345,23 +345,19 @@ def crop_tool( try: ang_range_min = ( kwds["ang_range_min"] - if "ang_range_min" in kwds.keys() + if "ang_range_min" in kwds else self._config["ang_range_min"] ) ang_range_max = ( kwds["ang_range_max"] - if "ang_range_max" in kwds.keys() + if "ang_range_max" in kwds else self._config["ang_range_max"] ) ek_range_min = ( - kwds["ek_range_min"] - if "ek_range_min" in kwds.keys() - else self._config["ang_range_min"] + kwds["ek_range_min"] if "ek_range_min" in kwds else self._config["ek_range_min"] ) ek_range_max = ( - kwds["ek_range_max"] - if "ek_range_max" in kwds.keys() - else self._config["ek_range_max"] + kwds["ek_range_max"] if "ek_range_max" in kwds else self._config["ek_range_max"] ) ang_min = ( ang_range_min From 3ee4bced5089546949429316fd0ed29b85ac4c5c Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 28 Feb 2024 00:02:46 +0100 Subject: [PATCH 20/20] add additional tests --- specsanalyzer/core.py | 28 ++++++++++++++++++++++++---- tests/test_convert.py | 33 +++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/specsanalyzer/core.py b/specsanalyzer/core.py index 53dd200..c4b0022 100755 --- a/specsanalyzer/core.py +++ b/specsanalyzer/core.py @@ -237,8 +237,28 @@ def convert_image( data_array = crop_xarray(data_array, ang_min, ang_max, ek_min, ek_max) except KeyError: try: + ang_range_min = ( + kwds["ang_range_min"] + if "ang_range_min" in kwds + else self._config["ang_range_min"] + ) + ang_range_max = ( + kwds["ang_range_max"] + if "ang_range_max" in kwds + else self._config["ang_range_max"] + ) + ek_range_min = ( + kwds["ek_range_min"] + if "ek_range_min" in kwds + else self._config["ek_range_min"] + ) + ek_range_max = ( + kwds["ek_range_max"] + if "ek_range_max" in kwds + else self._config["ek_range_max"] + ) ang_min = ( - kwds.get("ang_range_min", self._config["ang_range_min"]) + ang_range_min * ( data_array.coords[data_array.dims[0]][-1] - data_array.coords[data_array.dims[0]][0] @@ -246,7 +266,7 @@ def convert_image( + data_array.coords[data_array.dims[0]][0] ) ang_max = ( - kwds.get("ang_range_max", self._config["ang_range_max"]) + ang_range_max * ( data_array.coords[data_array.dims[0]][-1] - data_array.coords[data_array.dims[0]][0] @@ -254,7 +274,7 @@ def convert_image( + data_array.coords[data_array.dims[0]][0] ) ek_min = ( - kwds.get("ek_range_min", self._config["ek_range_min"]) + ek_range_min * ( data_array.coords[data_array.dims[1]][-1] - data_array.coords[data_array.dims[1]][0] @@ -262,7 +282,7 @@ def convert_image( + data_array.coords[data_array.dims[1]][0] ) ek_max = ( - kwds.get("ek_range_max", self._config["ek_range_max"]) + ek_range_max * ( data_array.coords[data_array.dims[1]][-1] - data_array.coords[data_array.dims[1]][0] diff --git a/tests/test_convert.py b/tests/test_convert.py index f1c86bf..bb7e61e 100755 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -265,6 +265,25 @@ def test_cropping(): ) assert converted.Angle[0] == -18 assert converted.Angle[-1] == 17.859375 + assert converted.Ekin[0] == 32.69 + assert converted.Ekin[-1] == 37.296569767441866 + + converted = spa.convert_image( + raw_img=tsv_data, + lens_mode=lens_mode, + kinetic_energy=kinetic_energy, + pass_energy=pass_energy, + work_function=work_function, + ek_range_min=0.1, + ek_range_max=0.9, + ang_range_min=0.1, + ang_range_max=0.9, + crop=True, + ) + assert converted.Angle[0] == -14.34375 + assert converted.Angle[-1] == 14.203125 + assert converted.Ekin[0] == 33.16005813953488 + assert converted.Ekin[-1] == 36.82651162790698 spa.crop_tool( raw_img=tsv_data, @@ -272,8 +291,8 @@ def test_cropping(): kinetic_energy=kinetic_energy, pass_energy=pass_energy, work_function=work_function, - ek_range_min=0, - ek_range_max=1, + ek_range_min=0.1, + ek_range_max=0.9, ang_range_min=0.1, ang_range_max=0.9, apply=True, @@ -290,6 +309,8 @@ def test_cropping(): assert converted.Angle[0] == -14.34375 assert converted.Angle[-1] == 14.203125 + assert converted.Ekin[0] == 33.16005813953488 + assert converted.Ekin[-1] == 36.82651162790698 spa.crop_tool( raw_img=tsv_data, @@ -297,8 +318,8 @@ def test_cropping(): kinetic_energy=45.0, pass_energy=pass_energy, work_function=work_function, - ek_range_min=0, - ek_range_max=1, + ek_range_min=0.2, + ek_range_max=0.8, ang_range_min=0.2, ang_range_max=0.8, apply=True, @@ -315,6 +336,8 @@ def test_cropping(): assert converted.Angle[0] == -10.828125 assert converted.Angle[-1] == 10.6875 + assert converted.Ekin[0] == 48.616686046511624 + assert converted.Ekin[-1] == 51.36988372093023 converted = spa.convert_image( raw_img=tsv_data, @@ -327,3 +350,5 @@ def test_cropping(): assert converted.Angle[0] == -14.34375 assert converted.Angle[-1] == 14.203125 + assert converted.Ekin[0] == 33.16005813953488 + assert converted.Ekin[-1] == 36.82651162790698