From 59f1b223d26b70195afeb4e8901d18b96f2ea752 Mon Sep 17 00:00:00 2001 From: Jordi Ferrer Orri <9082687+jordiferrero@users.noreply.github.com> Date: Sun, 29 Jan 2023 13:14:23 +0000 Subject: [PATCH 1/5] add changes --- lumispy/__init__.py | 1 + lumispy/signals/common_luminescence.py | 38 +----- lumispy/utils/__init__.py | 5 + lumispy/utils/signals.py | 180 +++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 35 deletions(-) diff --git a/lumispy/__init__.py b/lumispy/__init__.py index ad5ce7999..dc0538c6a 100644 --- a/lumispy/__init__.py +++ b/lumispy/__init__.py @@ -23,6 +23,7 @@ from lumispy.utils.axes import nm2eV, eV2nm, nm2invcm, invcm2nm, join_spectra from lumispy.utils.io import to_array, savetxt +from lumispy.utils import crop_edges from lumispy import signals from lumispy import components diff --git a/lumispy/signals/common_luminescence.py b/lumispy/signals/common_luminescence.py index ca6331cca..4a5fde525 100644 --- a/lumispy/signals/common_luminescence.py +++ b/lumispy/signals/common_luminescence.py @@ -23,47 +23,15 @@ from numpy import isnan from warnings import warn +from lumispy.utils.signals import crop_edges class CommonLumi: """**General luminescence signal class (dimensionless)**""" def crop_edges(self, crop_px): - """Crop the amount of pixels from the four edges of the scanning - region, from out the edges inwards. - - Parameters - ---------- - crop_px : int - Amount of pixels to be cropped on each side individually. - - Returns - ------- - signal_cropped : CommonLuminescence - A smaller cropped CL signal object. If inplace is True, the original - object is modified and no LumiSpectrum is returned. - """ - - width = self.axes_manager.shape[0] - height = self.axes_manager.shape[1] - - if crop_px * 2 > width or crop_px * 2 > height: - raise ValueError( - "The pixels to be cropped cannot be larger than half the width or the length!" - ) - else: - signal_cropped = self.inav[ - crop_px + 1 : width - crop_px + 1, crop_px + 1 : height - crop_px + 1 - ] - - # Store transformation in metadata (or update the value if already previously transformed) - - try: - signal_cropped.metadata.Signal.cropped_edges += crop_px - except AttributeError: - signal_cropped.metadata.set_item("Signal.cropped_edges", crop_px) - - return signal_cropped + warn("This function is deprecated and will be deleted in v1.0. Please use ``sc = lum.utils.crop_edges(s)`` instead with the ``crop_range`` parameter instead.", DeprecationWarning) + return crop_edges(self, crop_px=crop_px) def remove_negative(self, basevalue=1, inplace=False): """Sets all negative values to 'basevalue', e.g. for logarithmic scale diff --git a/lumispy/utils/__init__.py b/lumispy/utils/__init__.py index 45b5b60eb..a74d66f1f 100644 --- a/lumispy/utils/__init__.py +++ b/lumispy/utils/__init__.py @@ -30,3 +30,8 @@ join_spectra, solve_grating_equation, ) + +from .signals import ( + com, + crop_edges, +) \ No newline at end of file diff --git a/lumispy/utils/signals.py b/lumispy/utils/signals.py index 8467b3eac..d1c153e95 100644 --- a/lumispy/utils/signals.py +++ b/lumispy/utils/signals.py @@ -1,7 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2019-2023 The LumiSpy developers +# +# This file is part of LumiSpy. +# +# LumiSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the license, or +# (at your option) any later version. +# +# LumiSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LumiSpy. If not, see . + import numpy as np from hyperspy.axes import FunctionalDataAxis from scipy.ndimage import center_of_mass from scipy.interpolate import interp1d +from warnings import warn def com(spectrum_intensities, signal_axis, **kwargs): @@ -79,3 +98,164 @@ def _interpolate_signal(axis_array, index, **kwargs): raise ValueError("The parmeter `signal_axis` must be a HyperSpy Axis object.") return com_val + +# +# navigation axis manipulation +# + +def crop_edges( + S, + crop_range=None, + rebin_nav=False, + **kwargs, +): + """ + Cropping along the navigation axes of a list of signal objects. + Crop the amount of pixels from the four edges of the scanning + region, from the edges inwards. Cropping can happen uniformly on all + sides or by specifying the cropping range for each axis or each side. If the navigation axes shape is different, all signals can be rebinned to match the shape of the first signal in the list. + Parameters + ---------- + S : list of HyperSpy Signal objects or a single HyperSpy Signal object. + crop_range : {int | float | str} or tuple of {ints | floats | strs} + If int the values are taken as indices. If float the values are converted to indices. If str HyperSpy fancy indexing is used (e.g. ``rel0.1`` will crop 10% on each side, or ``100 nm`` will crop 100 nm on each side). + If a number or a tuple of size 1 is passed, all sides are cropped by the same amount. If a tuple of size 2 is passed (``crop_x``, ``crop_y``), a different + amount is cropped from the x and y directions, respectively. If a tuple of size 4 is passed (``crop_left``, ``crop_bottom``, ``crop_right``, ``crop_top``), a different amount is cropped from each edge individually. + rebin_nav : bool + If the navigation axes shape is different between signals in the list S, all signals will be rebinned to match the shape of the first signal in the list. Note this does not take into account the calibration values of the navigation axes. + kwrgs + To account for the deprecated ``crop_px`` parameter. + Returns + ------- + S_cropped : Signal or list of Signals + A list of smaller, cropped Signal objects or a cropped single Signal if only one signal object is passed as input. + """ + def str_formatting(str_list): + if len(str_list) == 2: + px = S[0].inav[:str_list[0]].axes_manager.navigation_shape[0] + px_list = [px, -px] + elif len(str_list) == 4: + # Pairwaise formatting with [top, left, right, bottom] + px_x = S[0].inav[:str_list[0],:].axes_manager.navigation_shape[0] + px_y = S[0].inav[:,:str_list[0]].axes_manager.navigation_shape[1] + px_list = [px_x,-px_y,-px_x,px_y] + return np.array(px_list, dtype=int) + + # Deprecation warning (for compatibility with ``crop_px``) + if 'crop_px' in kwargs and crop_range is not None: + warn( + "Both ``crop_range`` and the deprecated ``crop_px`` were passed. Only ``crop_range`` is being used.", + DeprecationWarning, + 2, + ) + elif 'crop_px' in kwargs: + warn( + "``crop_px`` is deprecated; use ``crop_range`` instead.", + DeprecationWarning, + 2, + ) + crop_range = int(kwargs['crop_px']) + elif crop_range is None: + crop_range = 0 + + # Check that S is a list + no_list = False + if type(S) is not list: + no_list = True + S = [S] + + # Check all signals in list are compatible (same range) and rebin + nav_shape = S[0].axes_manager.navigation_shape + for i, s in enumerate(S): + if i == 0: + continue + if len(nav_shape) != len(s.axes_manager.navigation_shape): + raise ValueError( + "The signal list contains a mix of navigation axes dimensions which cannot be broadcasted." + ) + if nav_shape != s.axes_manager.navigation_shape: + if not rebin_nav: + warn( + f"The navivigation axes of the first signal in index = 0 and in index = {i} have different shapes of {nav_shape} and {s.axes_manager.navigation_shape} respectively. This may cause errors during cropping. You can turn `rebin_nav` to True to rebin navigation axes.", + UserWarning, + ) + if rebin_nav: + scale = np.array(s.axes_manager.navigation_shape) / np.array(nav_shape) + signal_dim = len(s.axes_manager.signal_shape) + scale = np.append(scale, [1] * signal_dim) + S[i] = s.rebin(scale=scale) + + # Check for the size of the navigation axis + line_scan = False + nav_shape = s.axes_manager.navigation_shape + if len(nav_shape) == 1: + line_scan = True + elif len(nav_shape) > 2: + raise NotImplementedError( + "`crop_edges` is not supported for navigation axes with more than 2 dimensions." + ) + + crop_range_type = type(crop_range) + # Create a list of [top, left, right, bottom] or for line_scan only [left, right] + n = 2 if line_scan else 4 + if crop_range_type in (int, float, str): + crop_vals = [crop_range] * n + elif crop_range_type is tuple: + if len(crop_range) == 2: + crop_vals = (list(crop_range) * (n // 2),) + crop_vals = crop_vals[0] + elif len(crop_range) == 4 and not line_scan: + crop_vals = list(crop_range) + else: + raise ValueError( + f"The ``crop_range`` tuple must be either a 2-tuple (x,y) or a 4-tuple (left, bottom, right, top). You provided a {len(crop_range)}-tuple. For line scans, the tuple must be a 2-tuple (left, right)." + ) + else: + raise ValueError( + f"The crop_range value must be a number, a string, or a tuple, not a {crop_range_type}" + ) + + # Negative means reverse indexing + if type(crop_vals[0]) is not str: + if line_scan: + crop_vals = np.array(crop_vals) * [1, -1] + else: + crop_vals = np.array(crop_vals) * [1, -1, -1, 1] + else: + # Check if input was already fine or if str need to be reformatted + if (len(crop_range) == 4) or (len(crop_range) == 2 and line_scan): + pass + else: + crop_vals = str_formatting(crop_vals) + + S_cropped = [] + for s in S: + + # Remove 0 for None + crop_ids = [x if x != 0 else None for x in crop_vals] + + # Crop accordingly + if line_scan: + signal_cropped = s.inav[crop_ids[0] : crop_ids[1]] + else: + signal_cropped = s.inav[ + crop_ids[0] : crop_ids[2], crop_ids[3] : crop_ids[1] + ] + + # Check if cropping went too far + if 0 in signal_cropped.axes_manager.navigation_shape: + raise IndexError( + "The pixels to be cropped surpassed the width/height of the signal navigation axes." + ) + + # Store transformation in metadata (or update the value if already previously transformed) + if signal_cropped.metadata.has_item("Signal.cropped_edges"): + signal_cropped.metadata.Signal.cropped_edges = np.vstack((signal_cropped.metadata.Signal.cropped_edges, crop_vals)) + else: + signal_cropped.metadata.set_item("Signal.cropped_edges", crop_vals) + S_cropped.append(signal_cropped) + + if no_list: + return S_cropped[0] + else: + return S_cropped \ No newline at end of file From 53cb9e9470f6842a5bd4a10bf565e1501b1e3ed1 Mon Sep 17 00:00:00 2001 From: Jordi Ferrer Orri <9082687+jordiferrero@users.noreply.github.com> Date: Sun, 29 Jan 2023 13:14:33 +0000 Subject: [PATCH 2/5] add tests --- .../tests/signals/test_common_luminescence.py | 16 +- lumispy/tests/utils/test_signals.py | 237 ++++++++++++++++-- 2 files changed, 228 insertions(+), 25 deletions(-) diff --git a/lumispy/tests/signals/test_common_luminescence.py b/lumispy/tests/signals/test_common_luminescence.py index 1fdcea849..e51616693 100644 --- a/lumispy/tests/signals/test_common_luminescence.py +++ b/lumispy/tests/signals/test_common_luminescence.py @@ -23,18 +23,12 @@ class TestCommonLumi: - def test_crop_edges(self): + def test_crop_edges_deprecated(self): s1 = LumiSpectrum(np.ones((10, 10, 10))) - s2 = LumiTransientSpectrum(np.ones((10, 10, 10, 10))) - s3 = LumiSpectrum(np.ones((3, 3, 10))) - s1 = s1.crop_edges(crop_px=2) - s2 = s2.crop_edges(crop_px=2) - assert s1.axes_manager.navigation_shape[0] == 6 - assert s1.axes_manager.navigation_shape[1] == 6 - assert s2.axes_manager.navigation_shape[0] == 6 - assert s2.axes_manager.navigation_shape[1] == 6 - with pytest.raises(ValueError): - s3.crop_edges(crop_px=2) + with pytest.warns(DeprecationWarning, match="This function is deprecated"): + s2 = s1.crop_edges(crop_px=1) + assert s2.axes_manager.navigation_shape[0] == 8 + assert s2.axes_manager.navigation_shape[1] == 8 def test_remove_negative(self): s1 = LumiSpectrum(np.random.random((10, 10, 10))) - 0.3 diff --git a/lumispy/tests/utils/test_signals.py b/lumispy/tests/utils/test_signals.py index 53f021bae..6cade32c2 100644 --- a/lumispy/tests/utils/test_signals.py +++ b/lumispy/tests/utils/test_signals.py @@ -1,11 +1,32 @@ -import numpy as np -import pytest -from numpy.testing import assert_allclose -from lumispy.utils.signals import com +# -*- coding: utf-8 -*- +# Copyright 2019-2023 The LumiSpy developers +# +# This file is part of LumiSpy. +# +# LumiSpy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the license, or +# (at your option) any later version. +# +# LumiSpy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LumiSpy. If not, see . + +from lumispy.utils.signals import com, crop_edges from hyperspy.axes import FunctionalDataAxis, DataAxis, UniformDataAxis +from numpy import ones, array +from numpy.testing import assert_allclose +from pytest import raises, mark, warns +from hyperspy.signals import Signal1D, Signal2D +from lumispy.signals import LumiSpectrum -@pytest.mark.parametrize( + +@mark.parametrize( "axis, output", [ ( @@ -24,7 +45,7 @@ ], ) def test_com_axes(axis, output): - intensities = np.array([1, 1]) + intensities = array([1, 1]) centroid = com(intensities, axis) assert_allclose(centroid, output, atol=0.1) @@ -33,7 +54,7 @@ def test_com_axes(axis, output): def test_com_list(): # Float without decimals as index for centroid wavelengths = [200, 300, 400, 500, 600, 700] - intensities = np.array([1, 2, 3, 2, 1, 0]) + intensities = array([1, 2, 3, 2, 1, 0]) centroid = com(intensities, wavelengths) assert_allclose(centroid, 400.0, atol=0.1) @@ -42,7 +63,7 @@ def test_com_list(): 200, 300, ] - intensities = np.array( + intensities = array( [ 1, 1, @@ -53,11 +74,199 @@ def test_com_list(): def test_com_inputs(): - with pytest.raises(ValueError, match="The length of the spectrum array"): - com(np.ones(2), np.ones(3)) - with pytest.raises(ValueError): - com(np.ones(3), np.ones(2)) - with pytest.raises( + with raises(ValueError, match="The length of the spectrum array"): + com(ones(2), ones(3)) + with raises(ValueError): + com(ones(3), ones(2)) + with raises( ValueError, match="The parmeter `signal_axis` must be a HyperSpy Axis object." ): - com(np.ones(3), "string") + com(ones(3), "string") + +# +# Test navigation axis utils +# + +@mark.parametrize( + "range, output", + [ + (2, (6, 6)), + (2., (6, 6)), + ((2, 4), (6, 2)), + ((2., 4.), (6, 2)), + ((1, 2, 3, 4), (6, 4)), + ((1., 2., 3., 4.), (6, 4)), + ((1, 2, 3), ()), + ((1, 2, 3, 4, 5), ()), + (True, ()), + ((1, 0, 0, 3), (9, 7)), + ((None), (10, 10)), + ], +) +def test_crop_edges_s(range, output): + s1 = [LumiSpectrum(ones((10, 10, 10)))] + + # Check for bad input range + if type(range) not in (int, float, tuple, str, type(None)): + with raises(ValueError, match="value must be a number,"): + crop_edges(s1, range) + + elif type(range) == tuple and len(range) not in (1, 2, 4): + with raises(ValueError, match="tuple must be either a"): + crop_edges(s1, range) + + else: + s1 = crop_edges(s1, range) + assert s1[0].axes_manager.navigation_shape[0] == output[0] + assert s1[0].axes_manager.navigation_shape[1] == output[1] + +@mark.parametrize( + "range, output", + [ + ("1nm", (8, 8)), + ("rel0.1", (8, 8)), + (("rel0.2", "rel0.4"), (6, 2)), + (("2nm", "4nm"), (6, 2)), + (("rel0.1", "rel0.8", "rel0.3", "rel0.2"), (2, 5)), + (("1nm", "2nm", "3nm", "4nm"), (6, 4)), + ("a", ()), + ("11", ()), + ], +) +def test_crop_edges_fancy_str(range, output): + s1 = [LumiSpectrum(ones((10, 10, 10)))] + s1[0].axes_manager.navigation_axes[0].units = 'nm' + s1[0].axes_manager.navigation_axes[1].units = 'nm' + + # Check for bad input range + if ("rel" not in range) or ("nm" not in range): + with raises(ValueError, match="not a suitable string for slicing,"): + crop_edges(s1, range) + + else: + s1 = crop_edges(s1, range) + assert s1[0].axes_manager.navigation_shape[0] == output[0] + assert s1[0].axes_manager.navigation_shape[1] == output[1] + +def test_crop_single_spectrum(): + s1 = LumiSpectrum(ones((10, 10, 10))) + s2 = crop_edges(s1, crop_range=1.,) + assert s2.axes_manager.navigation_shape[0] == 8 + assert s2.axes_manager.navigation_shape[1] == 8 + s2 = crop_edges(s1, crop_range=1,) + assert s2.axes_manager.navigation_shape[0] == 8 + assert s2.axes_manager.navigation_shape[1] == 8 + + +def test_crop_edges_metadata(): + s1 = LumiSpectrum(ones((10, 10, 10))) + s1 = crop_edges(s1, crop_range=2) + assert s1.metadata.Signal.cropped_edges == array([2, 2, 2, 2]) + s1 = crop_edges(s1, crop_range="rel0.1") + assert s1.metadata.Signal.cropped_edges == array(["rel0.1","rel0.9","rel0.9","rel0.1"]) + +def test_crop_edges_too_far(): + s1 = LumiSpectrum(ones((10, 10, 10))) + with raises(IndexError, match="The pixels to be cropped"): + crop_edges(s1, crop_range=6) + + +@mark.parametrize( + "range, output", + [ + (2, (6)), + ((2, 4), (4)), + ((1, 2, 3, 4), ()), + ((1, 2, 3), ()), + ], +) +def test_crop_edges_linescan(range, output): + s1 = [LumiSpectrum(ones((10, 10)))] + + if type(range) == tuple and len(range) not in (1, 2): + with raises(ValueError, match="tuple must be either a"): + crop_edges(s1, range) + + else: + s1 = crop_edges(s1, range) + assert s1[0].axes_manager.navigation_shape[0] == output + + +@mark.parametrize("data", [((10,) * 4), ((10,) * 5)]) +def test_crop_edges_multidim(data): + s1 = [LumiSpectrum(ones((data)))] + with raises(NotImplementedError, match="navigation axes with more than 2"): + crop_edges(s1, 2) + + +def test_crop_edges_deprecated(): + s1 = LumiSpectrum(ones((10, 10, 10))) + with warns(DeprecationWarning, match="is deprecated"): + s2 = crop_edges(s1, crop_px=1) + assert s2.axes_manager.navigation_shape[0] == 8 + assert s2.axes_manager.navigation_shape[1] == 8 + with warns(DeprecationWarning, match="Both"): + s2 = crop_edges(s1, crop_range=1, crop_px=2) + assert s2.axes_manager.navigation_shape[0] == 8 + assert s2.axes_manager.navigation_shape[1] == 8 + + +def test_crop_edges_multiple_no_rebin(): + s1 = [LumiSpectrum(ones((10, 10, 10)))] * 2 + s2 = crop_edges(s1, crop_range=1, rebin_nav=False) + for s in s2: + assert s.axes_manager.navigation_shape[0] == 8 + assert s.axes_manager.navigation_shape[1] == 8 + s1 = [ + LumiSpectrum(ones((10, 10, 10))), + ] * 3 + s2 = crop_edges(s1, crop_range=1, rebin_nav=False) + for s in s2: + assert s.axes_manager.navigation_shape[0] == 8 + assert s.axes_manager.navigation_shape[1] == 8 + + s1 = [ + LumiSpectrum(ones((10, 10, 10))), + Signal1D(ones((10, 10, 10))), + Signal2D(ones((10, 10, 10, 10))), + ] + s2 = crop_edges(s1, crop_range=1, rebin_nav=False) + for s in s2: + assert s.axes_manager.navigation_shape[0] == 8 + assert s.axes_manager.navigation_shape[1] == 8 + + s1 = [ + LumiSpectrum(ones((10, 10, 10))), + LumiSpectrum(ones((20, 5, 10))), + ] + s2 = crop_edges(s1, crop_range=1, rebin_nav=False) + assert s2[0].axes_manager.navigation_shape[0] == 8 + assert s2[0].axes_manager.navigation_shape[1] == 8 + assert s2[1].axes_manager.navigation_shape[0] == 3 + assert s2[1].axes_manager.navigation_shape[1] == 18 + + +def test_crop_edges_multiple_error(): + s1 = [ + LumiSpectrum(ones((10, 10, 10))), + LumiSpectrum(ones((10, 10))), + ] + with raises(ValueError, match="mix of navigation axes"): + crop_edges(s1, 1) + + +def test_crop_edges_multiple_rebin(): + s1 = [ + LumiSpectrum(ones((10, 10, 10))), + LumiSpectrum(ones((20, 20, 10))), + LumiSpectrum(ones((5, 5, 10))), + LumiSpectrum(ones((13, 7, 10))), + LumiSpectrum(ones((5, 20, 5))), + ] + s2 = crop_edges(s1, crop_range=1, rebin_nav=True) + for s in s2: + assert s.axes_manager.navigation_shape[0] == 8 + assert s.axes_manager.navigation_shape[1] == 8 + # Check signal axis has not been changed + assert s2[0].axes_manager.signal_shape[0] == 10 + assert s2[-1].axes_manager.signal_shape[0] == 5 \ No newline at end of file From 06e88abf4d9759beed1dc4702e9e2a71afee4e63 Mon Sep 17 00:00:00 2001 From: Jordi Ferrer Orri <9082687+jordiferrero@users.noreply.github.com> Date: Sun, 29 Jan 2023 13:14:42 +0000 Subject: [PATCH 3/5] add documentation --- CHANGELOG.rst | 1 + doc/source/user_guide/utilities.rst | 33 +++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6a0913979..53155b579 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,7 @@ Changed for integrity check as `it replaces LGTM `_ - Added a centroid/center of mass functionality to analyse peak position of a spectrum (both in `utils`` and in `LumiSpectrum``) +- `s.crop_edges` moved to utils. It also now can take 1,2 or 4 values as input (can crop each axis independently) and also accepts percentages. The use of it under the `CommonLumiSpectrum` class is deprecated. Maintenance ----------- diff --git a/doc/source/user_guide/utilities.rst b/doc/source/user_guide/utilities.rst index 5f78e6fe7..a80013bd5 100644 --- a/doc/source/user_guide/utilities.rst +++ b/doc/source/user_guide/utilities.rst @@ -29,19 +29,34 @@ signals in the range of +/- 50 pixels around the centre of the overlapping regio .. _spectral_map_utils: -Utilities for spectral maps -=========================== - -The function :py:meth:`~.signals.common_luminescence.CommonLumi.crop_edges` -removes the specified number of pixels from all four edges of a spectral map. -It is a convenience wrapper for the ``inav`` :external+hyperspy:ref:`method in -HyperSpy `. +Cropping multiple signals in the navigation axis +================================================ + +The function :py:meth:`~.utils.axes.crop_edges` +removes the specified number of pixels or % from the four edges of a spectral map, +from the edges inwards. It takes a list of `Signals` and cropping can happen +uniformly on all sides or by specifying the cropping range for each axis or each +side. If the navigation axes shape across the list of signals is different, all +signals can be rebinned to match the shape of the first signal in the list. +It is a convenience wrapper for the ``inav`` `method in HyperSpy +`_. .. code-block:: python + >>> signals = [cl_map, sem_image] + >>> signals + [CLSpectrum <256,256|1024>, Signal2D <128,128|1>] + >>> signals_cropped = lum.utils.crop_edges(signals, crop_range=5, crop_units="%", rebin_nav=True) + >>> signals_cropped + [CLSpectrum <243,243|1024>, Signal2D <243,243|1>] +.. Note:: + + Many scanning luminescence techniques result in edge defects at the edges of the scanned region. + This function enables the same cropping of the navigation axis for a list of signals in the same + region to correct for such defect. - >>> s.crop_edges(crop_px=2) +.. Note:: -*[TODO: add possibility to crop different amounts of pixels on different sides]* + Before version `0.2.2` this function belonged to the class `CommonLumi` as :py:meth:`~.signals.common_luminescence.CommonLumi.crop_edges`. This use is now deprecated. .. _unit_conversion: From 2571e45f7b6faae362ecf4d4abc9da8e21fbed34 Mon Sep 17 00:00:00 2001 From: Jordi Ferrer Orri <9082687+jordiferrero@users.noreply.github.com> Date: Sun, 29 Jan 2023 13:15:16 +0000 Subject: [PATCH 4/5] lint correction --- lumispy/signals/common_luminescence.py | 5 +++- lumispy/tests/utils/test_signals.py | 32 ++++++++++++++++++-------- lumispy/utils/__init__.py | 2 +- lumispy/utils/signals.py | 25 ++++++++++++-------- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/lumispy/signals/common_luminescence.py b/lumispy/signals/common_luminescence.py index 4a5fde525..fd38890f7 100644 --- a/lumispy/signals/common_luminescence.py +++ b/lumispy/signals/common_luminescence.py @@ -30,7 +30,10 @@ class CommonLumi: """**General luminescence signal class (dimensionless)**""" def crop_edges(self, crop_px): - warn("This function is deprecated and will be deleted in v1.0. Please use ``sc = lum.utils.crop_edges(s)`` instead with the ``crop_range`` parameter instead.", DeprecationWarning) + warn( + "This function is deprecated and will be deleted in v1.0. Please use ``sc = lum.utils.crop_edges(s)`` instead with the ``crop_range`` parameter instead.", + DeprecationWarning, + ) return crop_edges(self, crop_px=crop_px) def remove_negative(self, basevalue=1, inplace=False): diff --git a/lumispy/tests/utils/test_signals.py b/lumispy/tests/utils/test_signals.py index 6cade32c2..f66cbd040 100644 --- a/lumispy/tests/utils/test_signals.py +++ b/lumispy/tests/utils/test_signals.py @@ -25,7 +25,6 @@ from lumispy.signals import LumiSpectrum - @mark.parametrize( "axis, output", [ @@ -83,19 +82,21 @@ def test_com_inputs(): ): com(ones(3), "string") + # # Test navigation axis utils # + @mark.parametrize( "range, output", [ (2, (6, 6)), - (2., (6, 6)), + (2.0, (6, 6)), ((2, 4), (6, 2)), - ((2., 4.), (6, 2)), + ((2.0, 4.0), (6, 2)), ((1, 2, 3, 4), (6, 4)), - ((1., 2., 3., 4.), (6, 4)), + ((1.0, 2.0, 3.0, 4.0), (6, 4)), ((1, 2, 3), ()), ((1, 2, 3, 4, 5), ()), (True, ()), @@ -120,6 +121,7 @@ def test_crop_edges_s(range, output): assert s1[0].axes_manager.navigation_shape[0] == output[0] assert s1[0].axes_manager.navigation_shape[1] == output[1] + @mark.parametrize( "range, output", [ @@ -135,8 +137,8 @@ def test_crop_edges_s(range, output): ) def test_crop_edges_fancy_str(range, output): s1 = [LumiSpectrum(ones((10, 10, 10)))] - s1[0].axes_manager.navigation_axes[0].units = 'nm' - s1[0].axes_manager.navigation_axes[1].units = 'nm' + s1[0].axes_manager.navigation_axes[0].units = "nm" + s1[0].axes_manager.navigation_axes[1].units = "nm" # Check for bad input range if ("rel" not in range) or ("nm" not in range): @@ -148,12 +150,19 @@ def test_crop_edges_fancy_str(range, output): assert s1[0].axes_manager.navigation_shape[0] == output[0] assert s1[0].axes_manager.navigation_shape[1] == output[1] + def test_crop_single_spectrum(): s1 = LumiSpectrum(ones((10, 10, 10))) - s2 = crop_edges(s1, crop_range=1.,) + s2 = crop_edges( + s1, + crop_range=1.0, + ) assert s2.axes_manager.navigation_shape[0] == 8 assert s2.axes_manager.navigation_shape[1] == 8 - s2 = crop_edges(s1, crop_range=1,) + s2 = crop_edges( + s1, + crop_range=1, + ) assert s2.axes_manager.navigation_shape[0] == 8 assert s2.axes_manager.navigation_shape[1] == 8 @@ -163,7 +172,10 @@ def test_crop_edges_metadata(): s1 = crop_edges(s1, crop_range=2) assert s1.metadata.Signal.cropped_edges == array([2, 2, 2, 2]) s1 = crop_edges(s1, crop_range="rel0.1") - assert s1.metadata.Signal.cropped_edges == array(["rel0.1","rel0.9","rel0.9","rel0.1"]) + assert s1.metadata.Signal.cropped_edges == array( + ["rel0.1", "rel0.9", "rel0.9", "rel0.1"] + ) + def test_crop_edges_too_far(): s1 = LumiSpectrum(ones((10, 10, 10))) @@ -269,4 +281,4 @@ def test_crop_edges_multiple_rebin(): assert s.axes_manager.navigation_shape[1] == 8 # Check signal axis has not been changed assert s2[0].axes_manager.signal_shape[0] == 10 - assert s2[-1].axes_manager.signal_shape[0] == 5 \ No newline at end of file + assert s2[-1].axes_manager.signal_shape[0] == 5 diff --git a/lumispy/utils/__init__.py b/lumispy/utils/__init__.py index a74d66f1f..7fd34827e 100644 --- a/lumispy/utils/__init__.py +++ b/lumispy/utils/__init__.py @@ -34,4 +34,4 @@ from .signals import ( com, crop_edges, -) \ No newline at end of file +) diff --git a/lumispy/utils/signals.py b/lumispy/utils/signals.py index d1c153e95..6d65a68e0 100644 --- a/lumispy/utils/signals.py +++ b/lumispy/utils/signals.py @@ -99,10 +99,12 @@ def _interpolate_signal(axis_array, index, **kwargs): return com_val + # # navigation axis manipulation # + def crop_edges( S, crop_range=None, @@ -130,31 +132,32 @@ def crop_edges( S_cropped : Signal or list of Signals A list of smaller, cropped Signal objects or a cropped single Signal if only one signal object is passed as input. """ + def str_formatting(str_list): if len(str_list) == 2: - px = S[0].inav[:str_list[0]].axes_manager.navigation_shape[0] + px = S[0].inav[: str_list[0]].axes_manager.navigation_shape[0] px_list = [px, -px] elif len(str_list) == 4: # Pairwaise formatting with [top, left, right, bottom] - px_x = S[0].inav[:str_list[0],:].axes_manager.navigation_shape[0] - px_y = S[0].inav[:,:str_list[0]].axes_manager.navigation_shape[1] - px_list = [px_x,-px_y,-px_x,px_y] + px_x = S[0].inav[: str_list[0], :].axes_manager.navigation_shape[0] + px_y = S[0].inav[:, : str_list[0]].axes_manager.navigation_shape[1] + px_list = [px_x, -px_y, -px_x, px_y] return np.array(px_list, dtype=int) # Deprecation warning (for compatibility with ``crop_px``) - if 'crop_px' in kwargs and crop_range is not None: + if "crop_px" in kwargs and crop_range is not None: warn( "Both ``crop_range`` and the deprecated ``crop_px`` were passed. Only ``crop_range`` is being used.", DeprecationWarning, 2, ) - elif 'crop_px' in kwargs: + elif "crop_px" in kwargs: warn( "``crop_px`` is deprecated; use ``crop_range`` instead.", DeprecationWarning, 2, ) - crop_range = int(kwargs['crop_px']) + crop_range = int(kwargs["crop_px"]) elif crop_range is None: crop_range = 0 @@ -224,7 +227,7 @@ def str_formatting(str_list): else: # Check if input was already fine or if str need to be reformatted if (len(crop_range) == 4) or (len(crop_range) == 2 and line_scan): - pass + pass else: crop_vals = str_formatting(crop_vals) @@ -250,7 +253,9 @@ def str_formatting(str_list): # Store transformation in metadata (or update the value if already previously transformed) if signal_cropped.metadata.has_item("Signal.cropped_edges"): - signal_cropped.metadata.Signal.cropped_edges = np.vstack((signal_cropped.metadata.Signal.cropped_edges, crop_vals)) + signal_cropped.metadata.Signal.cropped_edges = np.vstack( + (signal_cropped.metadata.Signal.cropped_edges, crop_vals) + ) else: signal_cropped.metadata.set_item("Signal.cropped_edges", crop_vals) S_cropped.append(signal_cropped) @@ -258,4 +263,4 @@ def str_formatting(str_list): if no_list: return S_cropped[0] else: - return S_cropped \ No newline at end of file + return S_cropped From 8d66281210b0492f59bb2ae2dc6b76621226037b Mon Sep 17 00:00:00 2001 From: Jordi Ferrer Orri <9082687+jordiferrero@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:06:52 +0000 Subject: [PATCH 5/5] fix coverage --- lumispy/tests/utils/test_signals.py | 43 ++++++++++++++++------------- lumispy/utils/signals.py | 17 +++++++----- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/lumispy/tests/utils/test_signals.py b/lumispy/tests/utils/test_signals.py index f66cbd040..d52dae043 100644 --- a/lumispy/tests/utils/test_signals.py +++ b/lumispy/tests/utils/test_signals.py @@ -96,7 +96,7 @@ def test_com_inputs(): ((2, 4), (6, 2)), ((2.0, 4.0), (6, 2)), ((1, 2, 3, 4), (6, 4)), - ((1.0, 2.0, 3.0, 4.0), (6, 4)), + ((1.0, 8.0, 7.0, 4.0), (6, 4)), ((1, 2, 3), ()), ((1, 2, 3, 4, 5), ()), (True, ()), @@ -123,26 +123,26 @@ def test_crop_edges_s(range, output): @mark.parametrize( - "range, output", + "error, range, output", [ - ("1nm", (8, 8)), - ("rel0.1", (8, 8)), - (("rel0.2", "rel0.4"), (6, 2)), - (("2nm", "4nm"), (6, 2)), - (("rel0.1", "rel0.8", "rel0.3", "rel0.2"), (2, 5)), - (("1nm", "2nm", "3nm", "4nm"), (6, 4)), - ("a", ()), - ("11", ()), + (False, "1nm", (8, 8)), + (False, "rel0.1", (8, 8)), + (False, ("rel0.2", "rel0.4"), (6, 2)), + (False, ("2nm", "4nm"), (6, 2)), + (False, ("rel0.1", "rel0.8", "rel0.3", "rel0.2"), (2, 5)), + (False, ("1nm", "8nm", "7nm", "4nm"), (6, 4)), + (True, "a", ()), + (True, "11", ()), ], ) -def test_crop_edges_fancy_str(range, output): +def test_crop_edges_fancy_str(error, range, output): s1 = [LumiSpectrum(ones((10, 10, 10)))] s1[0].axes_manager.navigation_axes[0].units = "nm" s1[0].axes_manager.navigation_axes[1].units = "nm" # Check for bad input range - if ("rel" not in range) or ("nm" not in range): - with raises(ValueError, match="not a suitable string for slicing,"): + if error: + with raises(ValueError): crop_edges(s1, range) else: @@ -169,12 +169,14 @@ def test_crop_single_spectrum(): def test_crop_edges_metadata(): s1 = LumiSpectrum(ones((10, 10, 10))) - s1 = crop_edges(s1, crop_range=2) - assert s1.metadata.Signal.cropped_edges == array([2, 2, 2, 2]) - s1 = crop_edges(s1, crop_range="rel0.1") - assert s1.metadata.Signal.cropped_edges == array( - ["rel0.1", "rel0.9", "rel0.9", "rel0.1"] - ) + s2 = crop_edges(s1, crop_range=2) + assert (s2.metadata.Signal.cropped_edges == array([2, -2, -2, 2])).all() + s2 = crop_edges(s1, crop_range="rel0.1") + assert (s2.metadata.Signal.cropped_edges == array([1, -1, -1, 1])).all() + s3 = crop_edges(s2, crop_range=1) + assert ( + s3.metadata.Signal.cropped_edges == array([[1, -1, -1, 1], [1, -1, -1, 1]]) + ).all() def test_crop_edges_too_far(): @@ -190,10 +192,13 @@ def test_crop_edges_too_far(): ((2, 4), (4)), ((1, 2, 3, 4), ()), ((1, 2, 3), ()), + ("2nm", (6)), + (("2nm", "4nm"), (2)), ], ) def test_crop_edges_linescan(range, output): s1 = [LumiSpectrum(ones((10, 10)))] + s1[0].axes_manager.navigation_axes[0].units = "nm" if type(range) == tuple and len(range) not in (1, 2): with raises(ValueError, match="tuple must be either a"): diff --git a/lumispy/utils/signals.py b/lumispy/utils/signals.py index 6d65a68e0..c83e31dde 100644 --- a/lumispy/utils/signals.py +++ b/lumispy/utils/signals.py @@ -133,14 +133,14 @@ def crop_edges( A list of smaller, cropped Signal objects or a cropped single Signal if only one signal object is passed as input. """ - def str_formatting(str_list): + def range_formatting(str_list): if len(str_list) == 2: px = S[0].inav[: str_list[0]].axes_manager.navigation_shape[0] px_list = [px, -px] - elif len(str_list) == 4: - # Pairwaise formatting with [top, left, right, bottom] + else: + # Pairwaise formatting with [top, left, right, bottom] when len(str_list) == 4 px_x = S[0].inav[: str_list[0], :].axes_manager.navigation_shape[0] - px_y = S[0].inav[:, : str_list[0]].axes_manager.navigation_shape[1] + px_y = S[0].inav[:, : str_list[1]].axes_manager.navigation_shape[1] px_list = [px_x, -px_y, -px_x, px_y] return np.array(px_list, dtype=int) @@ -203,6 +203,7 @@ def str_formatting(str_list): n = 2 if line_scan else 4 if crop_range_type in (int, float, str): crop_vals = [crop_range] * n + crop_range = [crop_range] elif crop_range_type is tuple: if len(crop_range) == 2: crop_vals = (list(crop_range) * (n // 2),) @@ -219,17 +220,19 @@ def str_formatting(str_list): ) # Negative means reverse indexing - if type(crop_vals[0]) is not str: + if type(crop_vals[0]) is int: if line_scan: crop_vals = np.array(crop_vals) * [1, -1] else: crop_vals = np.array(crop_vals) * [1, -1, -1, 1] else: - # Check if input was already fine or if str need to be reformatted + # Check if input was already fine or if str/float needs to be reformatted if (len(crop_range) == 4) or (len(crop_range) == 2 and line_scan): + # Shuffle order from [left, bottom, right, top] to [top, left, right, bottom] + # crop_vals = [crop_vals[3], crop_vals[0], crop_vals[2], crop_vals[1]] pass else: - crop_vals = str_formatting(crop_vals) + crop_vals = range_formatting(crop_vals) S_cropped = [] for s in S: