From 43c9e5ceed0edf8e2280be7ace9aef04a905b9a6 Mon Sep 17 00:00:00 2001 From: Emir Karamehmetoglu Date: Wed, 15 Jun 2022 11:18:57 +0200 Subject: [PATCH 1/3] Allow non-IndexErrors to surface to the user Change generic exception catching to IndexError, so that things like connection error are reported correctly for diagnosing. --- svo_filters/svo.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/svo_filters/svo.py b/svo_filters/svo.py index b6293ef..23c82f0 100755 --- a/svo_filters/svo.py +++ b/svo_filters/svo.py @@ -160,19 +160,21 @@ def __init__(self, band, filter_directory=None, wave_units=q.um, flux_units=q.er # Otherwise try a Web query or throw an error else: - err = """No filters match {}\n\nFILTERS ON FILE: {}\n\nA full list of available filters from the\nSVO Filter Profile Service can be found at\nhttp: //svo2.cab.inta-csic.es/theory/fps3/\n\nTry again with the desired filter as '/.', e.g. '2MASS/2MASS.J'""".format(band, ', '.join(bands)) + err = f"No filters match {band}\n\nFILTERS ON FILE: \n\nFILTERS ON FILE: {', '.join(bands)}\n\n" + "A full list of available filters from the\nSVO Filter Profile Service can be found at\n" + "http: //svo2.cab.inta-csic.es/theory/fps3/\n\nTry again with the desired filter as " + "'/.', e.g. '2MASS/2MASS.J'" + + # Valid SVO filter names have backslash + if '/' not in band: + raise IndexError(err) # Try a web query - if '/' in band: - try: - self.load_web(band) - except: - raise IndexError(err) - - else: - - # Or throw an error + try: + self.load_web(band) + except IndexError: raise IndexError(err) + # Make sure we return e.g. Connection Error, but are not catching e.g. SysExit calls. # Set the wavelength and throughput self._wave_units = q.AA From 2b294a0695c286d87c2c7decfb41d4dc382caaab Mon Sep 17 00:00:00 2001 From: Emir Karamehmetoglu Date: Wed, 15 Jun 2022 23:50:43 +0200 Subject: [PATCH 2/3] add check for strictly monotonically increasing wavelength Which is required by common astro-python packages like specutils, etc. --- svo_filters/svo.py | 18 +++++- svo_filters/test_svo.py | 6 ++ svo_filters/utils.py | 123 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 svo_filters/utils.py diff --git a/svo_filters/svo.py b/svo_filters/svo.py index b6293ef..39d8d4b 100755 --- a/svo_filters/svo.py +++ b/svo_filters/svo.py @@ -18,6 +18,7 @@ from bokeh.plotting import figure, show import bokeh.palettes as bpal import numpy as np +from .utils import incremented_monotonic warnings.simplefilter('ignore', category=AstropyWarning) @@ -94,7 +95,8 @@ class Filter: The SVO filter ID """ - def __init__(self, band, filter_directory=None, wave_units=q.um, flux_units=q.erg/q.s/q.cm**2/q.AA, **kwargs): + def __init__(self, band, filter_directory=None, wave_units=q.um, flux_units=q.erg/q.s/q.cm**2/q.AA, + monotonic=True, **kwargs): """ Loads the bandpass data into the Filter object @@ -108,10 +110,16 @@ def __init__(self, band, filter_directory=None, wave_units=q.um, flux_units=q.er The wavelength units flux_units: str, astropy.units.core.PrefixUnit (optional) The zeropoint flux units + monotonic: bool + Default = True. Whether to add a small offset to repeated elements in wavelength array + to ensure montonically increasing wavelengths. """ if filter_directory is None: filter_directory = resource_filename('svo_filters', 'data/filters/') + # Whether to ensure wavelengths returned should be strictly monotonically increasing. + self.monotonic = monotonic + # Check if TopHat if band.lower().replace('-', '').replace(' ', '') == 'tophat': @@ -126,6 +134,7 @@ def __init__(self, band, filter_directory=None, wave_units=q.um, flux_units=q.er else: # Load the filter n_pix = kwargs.get('n_pixels', 100) + self.monotonic = False # Never ensure monotonic for tophat as that can mess up a tophat profile? self.load_TopHat(wave_min, wave_max, n_pix) else: @@ -725,6 +734,9 @@ def plot(self, details=False, fig=None, draw=True): else: return fig + + + @property def rsr(self): """A getter for the relative spectral response (rsr) curve""" @@ -755,6 +767,10 @@ def throughput(self, points): @property def wave(self): """A getter for the wavelength""" + # Check wavelength is monotonically increasing + + if self.monotonic: + self._wave = incremented_monotonic(self._wave, increment_step=1000) return self._wave @wave.setter diff --git a/svo_filters/test_svo.py b/svo_filters/test_svo.py index 36c2a72..5606a27 100644 --- a/svo_filters/test_svo.py +++ b/svo_filters/test_svo.py @@ -137,6 +137,12 @@ def test_plot(self): self.assertTrue(type(plt) == Figure) + def test_filter_monotonic(self): + """Test that non-monotonic filters are treated correctly""" + filt = svo.Filter('Palomar/ZTF.g') + self.assertFalse(np.any(np.diff(filt.wave)<=0)) + + class TestFilterList(unittest.TestCase): """Tests for filter function""" def setUp(self): diff --git a/svo_filters/utils.py b/svo_filters/utils.py new file mode 100644 index 0000000..6aa1ea0 --- /dev/null +++ b/svo_filters/utils.py @@ -0,0 +1,123 @@ +import astropy.units as q +import numpy as np + + +def _padded_differences(arr, pad_val=1e20): + """ + + Parameters + ---------- + arr: array-like + pad_val: astropy.units.quantity, float, int + value for padding the first element of the difference array by when calculating + monotonically increasing (or not) differences. + """ + if isinstance(arr, q.Quantity): + # Quantity units cannot be padded by non-quantity unless using this wrapper. + diff = arr.ediff1d(to_begin=pad_val) + else: + diff = np.ediff1d(arr, to_begin=pad_val) + return diff + +def incremented_monotonic(arr, increment=None, increment_step=1000): + """Returns input if monotonically increasing. Otherwise + increment repeated elements by `increment`, which will be set to 1/`increment_step` + of the smallest difference in array if `None`, the default. + If not monotonically increasing (ignoring repeated elements), raises `ValueError`. + + Parameters + ---------- + arr: array-like + array to check for increment. Also handles astropy.units.quantity.Quantity arrays. + increment: astropy.units.quantity, float, int (optional) + value to increment repeated elements by. Set to 1/`increment_step` of the smallest difference + in array if `None`, the default. Unit conversion will be attempted if array is an instance + of astropy.units.quantity.Quantity. + increment_step: float, int + The relative size difference between repeated elements if automatically determining. Only + used if `increment = None`. + + Returns + ------- + array-like + Input array if monotonically increasing else input array where repeat values + have been incremented by `increment`. + """ + diff = _padded_differences(arr) + + if np.any(diff<0): + raise ValueError("Input array must be monotonically increasing except for repeated values.") + non_monotonic_mask = diff<=0 + + # Exit early if monotonic + if np.all(~non_monotonic_mask): + return arr + + if increment is None: + # a thousanth of the minimimum non-zero increment. + increment = np.nanmin(diff[~non_monotonic_mask])/increment_step + # Try to help user with unit conversion, will fail if unconvertable. + elif isinstance(arr, q.Quanity): + increment = increment << arr.unit + + #non_monotonic_mask = non_monotonic_mask.reshape(arr.shape) + repeats, multiples = get_multipliers(non_monotonic_mask) + if isinstance(increment, q.Quantity): + multiples = multiples << q.dimensionless_unscaled + multiples *= increment + + flatarr = arr.flatten() + flatarr[repeats] += multiples + return flatarr.reshape(arr.shape) + + +def breadth_first(repeats, state, row): + """somewhat convoluted 1D breadth first search + for getting multiples of repeated elements. + """ + queue = [repeats[row]] + index = 1 + additions = [index] + state.append(row) + while len(queue) > 0: + idx = queue.pop(0) + + #print(idx, idx+1, repeats) + if idx+1 in repeats: + neighbor = idx+1 + state.append(row+index) + index += 1 + queue.append(neighbor) + additions.append(index) + + return additions, state + + +def get_multipliers(mask): + """Get all repeats and their multiples using breadth first search + + Parameters + ---------- + mask: array_like + array of booleans representing repeated elements. True for repeated. + + Returns + ------- + tuple + tuple of array_like of repeated indexes and their multiples for use in + shifting multiple repeated indexes. + """ + repeats = np.nonzero(mask.flatten())[0] + if len(repeats)==0: + raise ValueError("No repeats found. Input mask all False") + groups = [] + state = [] + for j,val in enumerate(repeats): + if j in state: + continue + additions, state = breadth_first(repeats, state, j) + groups.append(additions) + groups = np.array([element for sublist in groups for element in sublist]) + + return repeats, groups + \ No newline at end of file From 65e7e9be315f20666e207bbf1a0911257b983554 Mon Sep 17 00:00:00 2001 From: Joe Filippazzo Date: Wed, 22 Jun 2022 09:20:49 -0400 Subject: [PATCH 3/3] Update setup.py Bump to v0.4.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b7229ae..13da3f2 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup( name='svo_filters', - version='0.4.2', + version='0.4.3', description='A Python wrapper for the SVO Filter Profile Service', packages=find_packages( ".",