[Git][debian-gis-team/pyspectral][master] 5 commits: New upstream version 0.11.0+ds
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Sun Mar 6 08:46:53 GMT 2022
Antonio Valentino pushed to branch master at Debian GIS Project / pyspectral
Commits:
98af4161 by Antonio Valentino at 2022-03-06T08:26:03+00:00
New upstream version 0.11.0+ds
- - - - -
9f2a7535 by Antonio Valentino at 2022-03-06T08:26:08+00:00
Update upstream source from tag 'upstream/0.11.0+ds'
Update to upstream version '0.11.0+ds'
with Debian dir 9b32fa6ee6ae4da1464ecc0dea1cab6237f2ab08
- - - - -
0092c972 by Antonio Valentino at 2022-03-06T08:26:46+00:00
New upstream release
- - - - -
4553780f by Antonio Valentino at 2022-03-06T08:30:11+00:00
Update d/copyright
- - - - -
9694907e by Antonio Valentino at 2022-03-06T08:40:05+00:00
Set distribution to unstable
- - - - -
13 changed files:
- + AUTHORS.md
- CHANGELOG.md
- debian/changelog
- debian/copyright
- pyspectral/config.py
- pyspectral/etc/pyspectral.yaml
- pyspectral/near_infrared_reflectance.py
- pyspectral/rayleigh.py
- pyspectral/tests/data.py
- pyspectral/tests/test_rayleigh.py
- pyspectral/utils.py
- rsr_convert_scripts/README.rst
- + rsr_convert_scripts/msu_gs_reader.py
Changes:
=====================================
AUTHORS.md
=====================================
@@ -0,0 +1,19 @@
+# Project Contributors
+
+The following people have made contributions to this project:
+
+<!--- Use your GitHub account or any other personal reference URL --->
+<!--- If you wish to not use your real name, please use your github username --->
+<!--- The list should be alphabetical by last name if possible, with github usernames at the bottom --->
+<!--- See https://gist.github.com/djhoese/52220272ec73b12eb8f4a29709be110d for auto-generating parts of this list --->
+
+- [K.-Michael Aye](https://github.com/michaelaye)
+- [Adam Dybbroe (adybbroe)](https://github.com/adybbroe)
+- [David Hoese (djhoese)](https://github.com/djhoese)
+- [Christian Kliche](https://github.com/chk)
+- [Panu Lahtinen (pnuu)](https://github.com/pnuu)
+- [Simon R. Proud (simonrp84)](https://github.com/simonrp84)
+- [Martin Raspaud (mraspaud)](https://github.com/mraspaud)
+- [Ronald Scheirer](https://github.com/)
+- [Xin Zhang (zxdawn)](https://github.com/zxdawn)
+- [Rolf Helge ...](https://github.com/HelgeCPH)
\ No newline at end of file
=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,16 @@
+## Version <v0.11.0> (2022/03/01)
+
+
+### Pull Requests Merged
+
+#### Features added
+
+* [PR 144](https://github.com/pytroll/pyspectral/pull/144) - Add converter for Electro-L N2 MSU/GS spectral response functions
+* [PR 133](https://github.com/pytroll/pyspectral/pull/133) - Refactor rayleigh correction to be more dask friendly
+
+In this release 2 pull requests were closed.
+
+
## Version <v0.10.6> (2021/12/22)
### Issues Closed
=====================================
debian/changelog
=====================================
@@ -1,3 +1,10 @@
+pyspectral (0.11.0+ds-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Update d/copyright.
+
+ -- Antonio Valentino <antonio.valentino at tiscali.it> Sun, 06 Mar 2022 08:39:23 +0000
+
pyspectral (0.10.6+ds-1) unstable; urgency=medium
[ Bas Couwenberg ]
=====================================
debian/copyright
=====================================
@@ -16,14 +16,14 @@ Files-Excluded: pyspectral/data/modis
doc/_static/mersi2_rsr_band_0040_0070_missingbands.png
Files: *
-Copyright: 2013-2021 Pytroll Developers
+Copyright: 2013-2022 Pytroll Developers
Adam Dybbroe
PyTroll Community
Pytroll
License: GPL-3+
Files: debian/*
-Copyright: 2018-2021 Antonio Valentino <antonio.valentino at tiscali.it>
+Copyright: 2018-2022 Antonio Valentino <antonio.valentino at tiscali.it>
License: GPL-3+
License: GPL-3+
=====================================
pyspectral/config.py
=====================================
@@ -87,5 +87,7 @@ def get_config():
user_datadir = app_dirs.user_data_dir
config['rsr_dir'] = expanduser(config.get('rsr_dir', user_datadir))
config['rayleigh_dir'] = expanduser(config.get('rayleigh_dir', user_datadir))
+ os.makedirs(config['rsr_dir'], exist_ok=True)
+ os.makedirs(config['rayleigh_dir'], exist_ok=True)
return config
=====================================
pyspectral/etc/pyspectral.yaml
=====================================
@@ -226,6 +226,19 @@ download_from_internet: True
# ch13: FY4A_AGRI_SRF_CH13.txt
# ch14: FY4A_AGRI_SRF_CH14.txt
+#Electro-L-N2-msu-gs:
+# path: /path/to/rsrs/rtcoef_electro-l_2_msugs_srf/
+# ch1: rtcoef_electro-l_2_msugs_srf_ch01.txt
+# ch2: rtcoef_electro-l_2_msugs_srf_ch02.txt
+# ch3: rtcoef_electro-l_2_msugs_srf_ch03.txt
+# ch4: rtcoef_electro-l_2_msugs_srf_ch04.txt
+# ch5: rtcoef_electro-l_2_msugs_srf_ch05.txt
+# ch6: rtcoef_electro-l_2_msugs_srf_ch06.txt
+# ch7: rtcoef_electro-l_2_msugs_srf_ch07.txt
+# ch8: rtcoef_electro-l_2_msugs_srf_ch08.txt
+# ch9: rtcoef_electro-l_2_msugs_srf_ch09.txt
+# ch10: rtcoef_electro-l_2_msugs_srf_ch10.txt
+
#FY-3B-virr:
# path: /Users/davidh/repos/git/pyspectral/virr_srf/FY3B-VIRR
# ch1: ch1.prn
=====================================
pyspectral/near_infrared_reflectance.py
=====================================
@@ -31,6 +31,7 @@ window channel (usually around 11-12 microns).
import os
import logging
+import tempfile
import numpy as np
try:
from dask.array import where, logical_or, asanyarray, isnan
@@ -40,7 +41,6 @@ except ImportError:
from pyspectral.solar import (SolarIrradianceSpectrum,
TOTAL_IRRADIANCE_SPECTRUM_2000ASTM)
from pyspectral.utils import BANDNAMES, get_bandname_from_wavelength
-from pyspectral.utils import TB2RAD_DIR
from pyspectral.utils import WAVE_LENGTH
from pyspectral.radiance_tb_conversion import RadTbConverter
from pyspectral.config import get_config
@@ -108,6 +108,7 @@ class Calculator(RadTbConverter):
self.lutfile = None
platform_sensor = platform_name + '-' + instrument
+ tb2rad_dir = options.get('tb2rad_dir', tempfile.gettempdir())
if platform_sensor in options and 'tb2rad_lut_filename' in options[platform_sensor]:
if isinstance(options[platform_sensor]['tb2rad_lut_filename'], dict):
for item in options[platform_sensor]['tb2rad_lut_filename']:
@@ -126,14 +127,14 @@ class Calculator(RadTbConverter):
if self.lutfile and not os.path.exists(os.path.dirname(self.lutfile)):
LOG.warning(
"Directory %s does not exist! Check config file", os.path.dirname(self.lutfile))
- self.lutfile = os.path.join(TB2RAD_DIR, os.path.basename(self.lutfile))
+ self.lutfile = os.path.join(tb2rad_dir, os.path.basename(self.lutfile))
if self.lutfile is None:
LOG.info("No lut filename available in config file. "
"Will generate filename automatically")
lutname = 'tb2rad_lut_{0}_{1}_{band}'.format(
self.platform_name.lower(), self.instrument.lower(), band=self.bandname.lower())
- self.lutfile = os.path.join(TB2RAD_DIR, lutname + '.npz')
+ self.lutfile = os.path.join(tb2rad_dir, lutname + '.npz')
LOG.info("lut filename: " + str(self.lutfile))
if not os.path.exists(self.lutfile):
=====================================
pyspectral/rayleigh.py
=====================================
@@ -24,48 +24,52 @@
"""Atmospheric correction of shortwave imager bands in the wavelength range 400 to 800 nm."""
import os
-import time
import logging
+import numbers
import h5py
import numpy as np
try:
- from dask.array import (where, zeros, clip, rad2deg, deg2rad, cos, arccos,
- atleast_2d, Array, map_blocks, from_array, nan_to_num)
import dask.array as da
- HAVE_DASK = True
- # try:
- # # use serializable h5py wrapper to make sure files are closed properly
- # import h5pickle as h5py
- # except ImportError:
- # pass
except ImportError:
- from numpy import where, zeros, clip, rad2deg, deg2rad, cos, arccos, atleast_2d, nan_to_num
da = None
- map_blocks = None
- from_array = None
- Array = None
- HAVE_DASK = False
from geotiepoints.multilinear import MultilinearInterpolator
from pyspectral.rsr_reader import RelativeSpectralResponse
-from pyspectral.utils import (INSTRUMENTS, RAYLEIGH_LUT_DIRS,
+from pyspectral.utils import (INSTRUMENTS, get_rayleigh_lut_dir,
AEROSOL_TYPES, ATMOSPHERES,
BANDNAMES,
ATM_CORRECTION_LUT_VERSION,
- download_luts, get_central_wave,
- get_bandname_from_wavelength)
-
+ download_luts, get_central_wave)
from pyspectral.config import get_config
LOG = logging.getLogger(__name__)
-class BandFrequencyOutOfRange(ValueError):
- """Exception when the band frequency is out of the visible range."""
+def _map_blocks_or_direct_call(func, *args, **kwargs):
+ """Call dask's map_blocks or call func directly if dask is not available."""
+ if da is None:
+ kwargs.pop("meta", None)
+ kwargs.pop("dtype", None)
+ kwargs.pop("chunks", None)
+ return func(*args, **kwargs)
+ return da.map_blocks(func, *args, **kwargs)
+
+
+def _clip_angles_inside_coordinate_range(zenith_angle, zenith_secant_max):
+ """Clipping solar- or satellite zenith angles to be inside the allowed coordinate range.
- pass
+ The "allowed" coordinate range is 0 to the angle corresponiding to a
+ maximum secant. The maximum secant is here the maximum available in the
+ Rayleigh LUT file.
+
+ Also the nan's in the angle array are filled with zeros (0)!
+
+ """
+ clip_angle = np.nan_to_num(np.rad2deg(np.arccos(1. / zenith_secant_max)))
+ zang = np.nan_to_num(zenith_angle)
+ return np.clip(zang, 0, clip_angle)
class RayleighConfigBaseClass(object):
@@ -74,12 +78,9 @@ class RayleighConfigBaseClass(object):
def __init__(self, aerosol_type, atm_type='us-standard'):
"""Initialize class and determine LUT to use."""
options = get_config()
- self.do_download = False
+ self.do_download = 'download_from_internet' in options and options['download_from_internet']
self._lutfiles_version_uptodate = False
- if 'download_from_internet' in options and options['download_from_internet']:
- self.do_download = True
-
if atm_type not in ATMOSPHERES:
raise AttributeError('Atmosphere type not supported! ' +
'Need to be one of {}'.format(str(ATMOSPHERES)))
@@ -99,14 +100,13 @@ class RayleighConfigBaseClass(object):
self.lutfiles_version_uptodate = True
def _get_lutfiles_version(self):
- """
- Get LUT file version.
+ """Get LUT file version.
Check the version of the atm correction luts from the version file in the
specific aerosol correction directory.
"""
- basedir = RAYLEIGH_LUT_DIRS[self._aerosol_type]
+ basedir = get_rayleigh_lut_dir(self._aerosol_type)
lutfiles_version_path = os.path.join(basedir,
ATM_CORRECTION_LUT_VERSION[self._aerosol_type]['filename'])
@@ -157,8 +157,7 @@ class Rayleigh(RayleighConfigBaseClass):
self.sensor = sensor.replace('/', '')
- rayleigh_dir = RAYLEIGH_LUT_DIRS[aerosol_type]
-
+ rayleigh_dir = get_rayleigh_lut_dir(aerosol_type)
ext = atm_type.replace(' ', '_')
lutname = "rayleigh_lut_{0}.h5".format(ext)
self.reflectance_lut_filename = os.path.join(rayleigh_dir, lutname)
@@ -174,216 +173,180 @@ class Rayleigh(RayleighConfigBaseClass):
str(self.reflectance_lut_filename))
LOG.debug('LUT filename: %s', str(self.reflectance_lut_filename))
- self._rayl = None
- self._wvl_coord = None
- self._azid_coord = None
- self._satz_sec_coord = None
- self._sunz_sec_coord = None
-
- def get_effective_wavelength(self, bandname):
- """Get the effective wavelength with Rayleigh scattering in mind."""
- try:
- rsr = RelativeSpectralResponse(self.platform_name, self.sensor)
- except OSError:
- LOG.exception(
- "No spectral responses for this platform and sensor: %s %s", self.platform_name, self.sensor)
- if isinstance(bandname, (float, int)):
- LOG.warning(
- "Effective wavelength is set to the requested band wavelength = %f", bandname)
- return bandname
-
- msg = ("Can't get effective wavelength for band %s on platform %s and sensor %s" %
- (str(bandname), self.platform_name, self.sensor))
- raise KeyError(msg)
-
- if isinstance(bandname, str):
- bandname = BANDNAMES.get(self.sensor, BANDNAMES['generic']).get(bandname, bandname)
- elif isinstance(bandname, (float, int)):
- if not(0.4 < bandname < 0.8):
- raise BandFrequencyOutOfRange(
- 'Requested band frequency should be between 0.4 and 0.8 microns!')
- bandname = get_bandname_from_wavelength(self.sensor, bandname, rsr.rsr)
-
- wvl, resp = (rsr.rsr[bandname]['det-1']['wavelength'],
- rsr.rsr[bandname]['det-1']['response'])
-
- cwvl = get_central_wave(wvl, resp, weight=1. / wvl**4)
- LOG.debug("Band name: %s Effective wavelength: %f", bandname, cwvl)
-
- return cwvl
-
- def get_reflectance_lut(self):
- """Get reflectance LUT.
-
- If not already cached (read previously) read the file with Look-Up
- Tables of reflectances as a function of wavelength, satellite zenith
- secant, azimuth difference angle, and sun zenith secant.
-
- """
- if self._rayl is None:
- lut_vars = get_reflectance_lut_from_file(self.reflectance_lut_filename)
- self._rayl = lut_vars[0]
- self._wvl_coord = lut_vars[1]
- self._azid_coord = lut_vars[2]
- self._satz_sec_coord = lut_vars[3]
- self._sunz_sec_coord = lut_vars[4]
- return self._rayl, self._wvl_coord, self._azid_coord,\
- self._satz_sec_coord, self._sunz_sec_coord
-
- @staticmethod
- def _do_interp(minterp, sunzsec, azidiff, satzsec):
- interp_points2 = np.vstack((sunzsec.ravel(), 180 - azidiff.ravel(), satzsec.ravel()))
- res = minterp(interp_points2)
- return res.reshape(sunzsec.shape)
- def _clip_angles_inside_coordinate_range(self, zenith_angle, zenith_secant_max):
- """Clipping solar- or satellite zenith angles to be inside the allowed coordinate range.
+ def _get_effective_wavelength_and_band_name(self, band_name_or_wavelength):
+ """Get the effective wavelength in nanometers and name of the band/channel.
- The "allowed" coordinate range is 0 to the angle corresponiding to a
- maximum secant. The maximum secant is here the maximum available in the
- Rayleigh LUT file.
+ The provided argument can be either the name of a band/channel that
+ pyspectral knows about in its RSR files or it can be a wavelength
+ specified in micrometers. If a wavelength is provided it is converted
+ to nanometers and returned. If a band name is provided then it is used
+ to lookup the spectral response and determine an effective wavelength
+ useful for rayleigh correction.
- Also the nan's in the angle array are filled with zeros (0)!
+ Returns:
+ A two element tuple of (wavelength in nanometers, name of band). If
+ a wavelength is provided then the band name is a string
+ representation of the wavelength.
"""
- clip_angle = nan_to_num(rad2deg(arccos(1. / zenith_secant_max)))
- zang = nan_to_num(zenith_angle)
- return clip(zang, 0, clip_angle)
-
- def get_reflectance(self, sun_zenith, sat_zenith, azidiff, bandname, redband=None):
- """Get the reflectance from the three sun-sat angles."""
# Get wavelength in nm for band:
- if isinstance(bandname, float):
+ if isinstance(band_name_or_wavelength, numbers.Real):
LOG.warning('A wavelength is provided instead of band name - ' +
'disregard the relative spectral responses and assume ' +
- 'it is the effective wavelength: %f (micro meter)', bandname)
- wvl = bandname * 1000.0
+ 'it is the effective wavelength: %f (micro meter)', band_name_or_wavelength)
+ wvl = band_name_or_wavelength
+ band_name = f'{wvl:f}um'
else:
- wvl = self.get_effective_wavelength(bandname)
- wvl = wvl * 1000.0
-
- rayl, wvl_coord, azid_coord, satz_sec_coord, sunz_sec_coord = self.get_reflectance_lut()
-
- # force dask arrays
- compute = False
- if HAVE_DASK and not isinstance(sun_zenith, Array):
- compute = True
- sun_zenith = from_array(sun_zenith, chunks=sun_zenith.shape)
- sat_zenith = from_array(sat_zenith, chunks=sat_zenith.shape)
- azidiff = from_array(azidiff, chunks=azidiff.shape)
- if redband is not None:
- redband = from_array(redband, chunks=redband.shape)
-
- sun_zenith = self._clip_angles_inside_coordinate_range(sun_zenith, sunz_sec_coord.max())
- sunzsec = 1. / cos(deg2rad(sun_zenith))
+ band_name = band_name_or_wavelength
+ wvl = _get_rsr_wavelength_from_band_name(
+ self.platform_name,
+ self.sensor,
+ band_name,
+ )
+ band_name_map = BANDNAMES.get(self.sensor, BANDNAMES['generic'])
+ band_name_or_wavelength = band_name_map.get(band_name, band_name)
+ LOG.debug("Band name: %s Effective wavelength: %fum", band_name_or_wavelength, wvl)
+ return wvl * 1000.0, band_name
- sat_zenith = self._clip_angles_inside_coordinate_range(sat_zenith, satz_sec_coord.max())
- satzsec = 1. / cos(deg2rad(sat_zenith))
+ @staticmethod
+ def _interp_rayleigh_refl_by_angles(sun_zenith, sat_zenith, azidiff,
+ rayleigh_refl, reflectance_lut_filename):
+ azid_coord, satz_sec_coord, sunz_sec_coord = get_reflectance_lut_from_file(
+ reflectance_lut_filename)
- shape = sun_zenith.shape
+ sun_zenith = _clip_angles_inside_coordinate_range(sun_zenith, sunz_sec_coord.max())
+ sunzsec = 1. / np.cos(np.deg2rad(sun_zenith))
- if not wvl_coord.min() < wvl < wvl_coord.max():
- LOG.warning(
- "Effective wavelength for band %s outside 400-800 nm range!",
- str(bandname))
- LOG.info(
- "Set the rayleigh/aerosol reflectance contribution to zero!")
- if HAVE_DASK:
- chunks = sun_zenith.chunks if redband is None else redband.chunks
- res = zeros(shape, chunks=chunks)
- return res.compute() if compute else res
-
- return zeros(shape)
-
- idx = np.searchsorted(wvl_coord, wvl)
- wvl1 = wvl_coord[idx - 1]
- wvl2 = wvl_coord[idx]
-
- fac = (wvl2 - wvl) / (wvl2 - wvl1)
- raylwvl = fac * rayl[idx - 1, :, :, :] + (1 - fac) * rayl[idx, :, :, :]
- tic = time.time()
+ sat_zenith = _clip_angles_inside_coordinate_range(sat_zenith, satz_sec_coord.max())
+ satzsec = 1. / np.cos(np.deg2rad(sat_zenith))
smin = [sunz_sec_coord[0], azid_coord[0], satz_sec_coord[0]]
smax = [sunz_sec_coord[-1], azid_coord[-1], satz_sec_coord[-1]]
orders = [
len(sunz_sec_coord), len(azid_coord), len(satz_sec_coord)]
- f_3d_grid = atleast_2d(raylwvl.ravel())
+ f_3d_grid = np.atleast_2d(rayleigh_refl.ravel())
- if HAVE_DASK and isinstance(smin[0], Array):
- # compute all of these at the same time before passing to the interpolator
- # otherwise they are computed separately
- smin, smax, orders, f_3d_grid = da.compute(smin, smax, orders, f_3d_grid)
minterp = MultilinearInterpolator(smin, smax, orders)
minterp.set_values(f_3d_grid)
+ interp_points2 = np.vstack((sunzsec.ravel(), 180 - azidiff.ravel(), satzsec.ravel()))
+ res = minterp(interp_points2)
+ res *= 100
+ return res.reshape(sunzsec.shape)
- if HAVE_DASK:
- ipn = map_blocks(self._do_interp, minterp, sunzsec, azidiff,
- satzsec, dtype=raylwvl.dtype, chunks=azidiff.chunks)
- else:
- ipn = self._do_interp(minterp, sunzsec, azidiff, satzsec)
+ def get_reflectance(self, sun_zenith, sat_zenith, azidiff,
+ band_name_or_wavelength, redband=None):
+ """Get the reflectance from the three sun-sat angles."""
+ # if the user gave us non-dask arrays we'll use the dask
+ # version of the algorithm but return numpy arrays back
+ compute = da is not None and not isinstance(sun_zenith, da.Array)
- LOG.debug("Time - Interpolation: {0:f}".format(time.time() - tic))
+ wvl, band_name = self._get_effective_wavelength_and_band_name(band_name_or_wavelength)
+ try:
+ rayleigh_refl = _get_wavelength_adjusted_lut_rayleigh_reflectance(
+ self.reflectance_lut_filename, wvl)
+ except ValueError:
+ LOG.warning("Effective wavelength for band %s outside "
+ "nominal 400-800 nm range!", str(band_name))
+ LOG.info("Setting the rayleigh/aerosol reflectance contribution to zero!")
+ repr_arr = sun_zenith if redband is None else redband
+ zeros_like = np.zeros_like if isinstance(repr_arr, np.ndarray) else da.zeros_like
+ res = zeros_like(repr_arr)
+ else:
+ res = _map_blocks_or_direct_call(self._interp_rayleigh_refl_by_angles,
+ sun_zenith, sat_zenith, azidiff, rayleigh_refl,
+ self.reflectance_lut_filename,
+ meta=np.array((), dtype=rayleigh_refl.dtype),
+ dtype=rayleigh_refl.dtype,
+ chunks=getattr(azidiff, "chunks", None))
- ipn *= 100
- res = ipn
if redband is not None:
- res = where(redband < 20., res,
- (1 - (redband - 20) / 80) * res)
+ res = _map_blocks_or_direct_call(self._relax_rayleigh_refl_correction_where_cloudy,
+ redband, res,
+ meta=np.array((), dtype=res.dtype),
+ dtype=res.dtype,
+ chunks=getattr(res, "chunks", None))
- res = clip(nan_to_num(res), 0, 100)
+ res = np.clip(np.nan_to_num(res), 0, 100)
if compute:
res = res.compute()
-
return res
+ @staticmethod
+ def _relax_rayleigh_refl_correction_where_cloudy(redband, rayleigh_refl):
+ return np.where(redband < 20., rayleigh_refl,
+ (1 - (redband - 20) / 80) * rayleigh_refl)
+
@staticmethod
def reduce_rayleigh_highzenith(zenith, rayref, thresh_zen, maxzen, strength):
"""Reduce the Rayleigh correction amount at high zenith angles.
+
This linearly scales the Rayleigh reflectance, `rayref`, for solar or satellite zenith angles, `zenith`,
above a threshold angle, `thresh_zen`. Between `thresh_zen` and `maxzen` the Rayleigh reflectance will
be linearly scaled, from one at `thresh_zen` to zero at `maxzen`.
"""
LOG.info("Reducing Rayleigh effect at high zenith angles.")
- factor = 1. - strength * where(zenith < thresh_zen, 0, (zenith - thresh_zen) / (maxzen - thresh_zen))
+ factor = 1. - strength * np.where(zenith < thresh_zen, 0, (zenith - thresh_zen) / (maxzen - thresh_zen))
# For low zenith factor can be greater than one, so we need to clip it into a sensible range.
- factor = clip(factor, 0, 1)
+ factor = np.clip(factor, 0, 1)
return rayref * factor
-def get_reflectance_lut_from_file(filename):
+def _get_rsr_wavelength_from_band_name(platform_name, sensor, band_name):
+ try:
+ rsr = RelativeSpectralResponse(platform_name, sensor)
+ except OSError:
+ LOG.exception(
+ "No spectral responses for this platform and sensor: %s %s", platform_name, sensor)
+ msg = ("Can't get effective wavelength for band %s on platform %s and sensor %s" %
+ (str(band_name), platform_name, sensor))
+ raise KeyError(msg)
+
+ wvl = rsr.rsr[band_name]['det-1']['wavelength']
+ resp = rsr.rsr[band_name]['det-1']['response']
+ cwvl = get_central_wave(wvl, resp, weight=1. / wvl**4)
+ return cwvl
+
+
+def _get_wavelength_adjusted_lut_rayleigh_reflectance(lut_filename, wvl):
+ with h5py.File(lut_filename, 'r') as h5f:
+ rayleigh_refl = h5f["reflectance"]
+ wvl_coord = h5f["wavelengths"][:]
+ if not wvl_coord.min() < wvl < wvl_coord.max():
+ raise ValueError("Wavelength out of range for available LUT wavelengths")
+ wavelength_index, wavelength_factor = _get_wavelength_index_and_factor(wvl_coord, wvl)
+ raylwvl = (wavelength_factor * rayleigh_refl[wavelength_index - 1, :, :, :] +
+ (1 - wavelength_factor) * rayleigh_refl[wavelength_index, :, :, :])
+ return raylwvl
+
+
+def _get_wavelength_index_and_factor(wvl_coord, wvl):
+ wavelength_index = np.searchsorted(wvl_coord, wvl)
+ wvl1 = wvl_coord[wavelength_index - 1]
+ wvl2 = wvl_coord[wavelength_index]
+ wavelength_factor = (wvl2 - wvl) / (wvl2 - wvl1)
+ return wavelength_index, wavelength_factor
+
+
+def get_reflectance_lut_from_file(lut_filename):
"""Get reflectance LUT.
Read the Look-Up Tables from file with reflectances as a function of
wavelength, satellite zenith secant, azimuth difference angle, and sun
- zenith secant
+ zenith secant.
"""
- h5f = h5py.File(filename, 'r')
-
- tab = h5f['reflectance']
- wvl = h5f['wavelengths']
- azidiff = h5f['azimuth_difference']
- satellite_zenith_secant = h5f['satellite_zenith_secant']
- sun_zenith_secant = h5f['sun_zenith_secant']
-
- if HAVE_DASK:
- tab = from_array(tab, chunks=(10, 10, 10, 10))
- wvl = wvl[:] # no benefit to dask-ifying this
- azidiff = from_array(azidiff, chunks=(1000,))
- satellite_zenith_secant = from_array(satellite_zenith_secant,
- chunks=(1000,))
- sun_zenith_secant = from_array(sun_zenith_secant,
- chunks=(1000,))
- else:
+ with h5py.File(lut_filename, 'r') as h5f:
+ azidiff = h5f['azimuth_difference']
+ satellite_zenith_secant = h5f['satellite_zenith_secant']
+ sun_zenith_secant = h5f['sun_zenith_secant']
+
# load all of the data we are going to use in to memory
- tab = tab[:]
- wvl = wvl[:]
azidiff = azidiff[:]
satellite_zenith_secant = satellite_zenith_secant[:]
sun_zenith_secant = sun_zenith_secant[:]
- h5f.close()
- return tab, wvl, azidiff, satellite_zenith_secant, sun_zenith_secant
+ return azidiff, satellite_zenith_secant, sun_zenith_secant
def check_and_download(**kwargs):
=====================================
pyspectral/tests/data.py
=====================================
@@ -268,4 +268,4 @@ TEST_RAYLEIGH_LUT = np.array([[[[0.08674087, 0.09136106, 0.09747749, 0.104837
TEST_RAYLEIGH_AZID_COORD = np.array([100., 110., 120., 130., 140., 150., 160.], dtype='float32')
TEST_RAYLEIGH_SATZ_COORD = np.array([1., 1.25, 1.5, 1.75, 2., 2.25], dtype='float32')
TEST_RAYLEIGH_SUNZ_COORD = np.array([1., 1.25, 1.5, 1.75, 2., 2.25, 2.5, 2.75], dtype='float32')
-TEST_RAYLEIGH_WVL_COORD = np.array([440., 445.], dtype='float32')
+TEST_RAYLEIGH_WVL_COORD = np.array([631., 636.], dtype='float32')
=====================================
pyspectral/tests/test_rayleigh.py
=====================================
@@ -21,37 +21,32 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Unittest for the rayleigh correction utilities."""
-
+import contextlib
import os
-import sys
import numpy as np
import dask.array as da
+import h5py
+import pytest
from pyspectral import rayleigh
-from pyspectral.rayleigh import BandFrequencyOutOfRange
from pyspectral.tests.data import (
TEST_RAYLEIGH_LUT,
TEST_RAYLEIGH_AZID_COORD,
TEST_RAYLEIGH_SUNZ_COORD,
TEST_RAYLEIGH_SATZ_COORD,
TEST_RAYLEIGH_WVL_COORD)
-from pyspectral.utils import RSR_DATA_VERSION
+from pyspectral.utils import ATM_CORRECTION_LUT_VERSION
-import unittest
from unittest.mock import patch
-TEST_RAYLEIGH_RESULT1 = np.array([10.40727436, 8.69775471], dtype='float32')
-TEST_RAYLEIGH_RESULT2 = np.array([9.71696059, 8.51415689], dtype='float32')
-TEST_RAYLEIGH_RESULT3 = np.array([5.61532271, 8.69267476], dtype='float32')
-TEST_RAYLEIGH_RESULT4 = np.array([0.0, 8.69775471], dtype='float32')
+TEST_RAYLEIGH_RESULT1 = np.array([10.339923, 8.64748], dtype='float32')
+TEST_RAYLEIGH_RESULT2 = np.array([9.653559, 8.464997], dtype='float32')
+TEST_RAYLEIGH_RESULT3 = np.array([5.488735, 8.533125], dtype='float32')
+TEST_RAYLEIGH_RESULT4 = np.array([0.0, 8.64748], dtype='float32')
TEST_RAYLEIGH_RESULT_R1 = np.array([16.66666667, 20.83333333, 25.], dtype='float32')
TEST_RAYLEIGH_RESULT_R2 = np.array([0., 6.25, 12.5], dtype='float32')
TEST_ZENITH_ANGLES_RESULTS = np.array([68.67631374, 68.67631374, 32., 0.])
-# Mock some modules, so we don't need them for tests.
-
-# sys.modules['pyresample'] = MagicMock()
-
class RelativeSpectralResponseTestData(object):
"""Create the class instance to hold the RSR test data."""
@@ -87,288 +82,214 @@ class RelativeSpectralResponseTestData(object):
self.rsr[chname]['det-1']['response'] = ch3_resp
-class TestRayleighDask(unittest.TestCase):
+ at pytest.fixture
+def fake_lut_hdf5(tmp_path):
+ """Create a fake LUT HDF5 file and necessary mocks to use it."""
+ aerosol_type = "marine_clean_aerosol"
+ base_dir = tmp_path / aerosol_type
+ base_dir.mkdir()
+ _create_fake_lut_version_file(base_dir, aerosol_type)
+ for atm_type in ("midlatitude_summer", "subarctic_winter", "tropical", "us-standard"):
+ _create_fake_lut_hdf5_file(base_dir, atm_type)
+ fake_config = {
+ "rsr_dir": str(tmp_path),
+ "rayleigh_dir": str(tmp_path),
+ "download_from_internet": False,
+ }
+ with patch('pyspectral.rayleigh.get_config', lambda: fake_config), \
+ patch('pyspectral.utils.get_config', lambda: fake_config):
+ yield None
+
+
+def _create_fake_lut_version_file(base_dir, aerosol_type):
+ lut_version = ATM_CORRECTION_LUT_VERSION[aerosol_type]["version"]
+ lutfiles_path = str(base_dir / ATM_CORRECTION_LUT_VERSION[aerosol_type]["filename"])
+ with open(lutfiles_path, "w") as version_file:
+ version_file.write(lut_version)
+
+
+def _create_fake_lut_hdf5_file(base_dir, atm_type) -> str:
+ filename = str(base_dir / f"rayleigh_lut_{atm_type}.h5")
+ with h5py.File(filename, "w") as h5f:
+ h5f["reflectance"] = TEST_RAYLEIGH_LUT
+ h5f["wavelengths"] = TEST_RAYLEIGH_WVL_COORD
+ h5f["azimuth_difference"] = TEST_RAYLEIGH_AZID_COORD
+ h5f["sun_zenith_secant"] = TEST_RAYLEIGH_SUNZ_COORD
+ h5f["satellite_zenith_secant"] = TEST_RAYLEIGH_SATZ_COORD
+ return filename
+
+
+def _create_rayleigh():
+ rayl = rayleigh.Rayleigh('NOAA-20', 'viirs', atmosphere='midlatitude summer')
+ return rayl
+
+
+ at contextlib.contextmanager
+def mocked_rsr():
+ """Mock the RSR class used by the Rayleigh class with fake data."""
+ with patch('pyspectral.rayleigh.RelativeSpectralResponse') as mymock:
+ instance = mymock.return_value
+ instance.rsr = RelativeSpectralResponseTestData().rsr
+ instance.unit = '1e-6 m'
+ instance.si_scale = 1e-6
+ yield mymock
+
+
+class TestRayleighDask:
"""Class for testing pyspectral.rayleigh - with dask-arrays as input."""
- def setUp(self):
- """Set up the test."""
- self.cwvl = 0.4440124
- self.rsr = RelativeSpectralResponseTestData()
- self._res1 = da.from_array(TEST_RAYLEIGH_RESULT1, chunks=2)
- self._res2 = da.from_array(TEST_RAYLEIGH_RESULT2, chunks=2)
- self._res3 = da.from_array(TEST_RAYLEIGH_RESULT3, chunks=2)
-
- self.rayl = da.from_array(TEST_RAYLEIGH_LUT, chunks=(10, 10, 10, 10))
- self.wvl_coord = TEST_RAYLEIGH_WVL_COORD
- self.azid_coord = da.from_array(TEST_RAYLEIGH_AZID_COORD, chunks=(1000,))
- self.sunz_sec_coord = da.from_array(TEST_RAYLEIGH_SUNZ_COORD,
- chunks=(1000,))
- self.satz_sec_coord = da.from_array(TEST_RAYLEIGH_SATZ_COORD,
- chunks=(1000,))
-
- # mymock:
- with patch('pyspectral.rayleigh.RelativeSpectralResponse') as mymock:
- instance = mymock.return_value
- instance.rsr = RelativeSpectralResponseTestData().rsr
- instance.unit = '1e-6 m'
- instance.si_scale = 1e-6
-
- self.viirs_rayleigh = rayleigh.Rayleigh('NOAA-20', 'viirs', atmosphere='midlatitude summer')
-
- @patch('os.path.exists')
- @patch('pyspectral.utils.download_luts')
- @patch('pyspectral.rayleigh.get_reflectance_lut_from_file')
- @patch('pyspectral.rsr_reader.RelativeSpectralResponse.'
- '_get_rsr_data_version')
- @patch('pyspectral.rayleigh.Rayleigh.get_effective_wavelength')
- def test_dask_cliping_angles_with_nans(self, get_effective_wvl,
- get_rsr_version, get_reflectance_lut_from_file,
- download_luts, exists):
- """Test the cliping of angles outside coordinate range - with nan's in input."""
- get_reflectance_lut_from_file.return_value = (self.rayl, self.wvl_coord, self.azid_coord,
- self.satz_sec_coord, self.sunz_sec_coord)
- download_luts.return_code = None
- exists.return_code = True
- get_rsr_version.return_code = RSR_DATA_VERSION
- get_effective_wvl.return_value = self.cwvl
-
+ def test_dask_clipping_angles_with_nans(self):
+ """Test the clipping of angles outside coordinate range - with nan's in input."""
+ from pyspectral.rayleigh import _clip_angles_inside_coordinate_range
zenith_angle = da.array([79., 69., 32., np.nan])
- result = self.viirs_rayleigh._clip_angles_inside_coordinate_range(zenith_angle, 2.75)
-
+ result = _clip_angles_inside_coordinate_range(zenith_angle, 2.75)
np.testing.assert_allclose(result, TEST_ZENITH_ANGLES_RESULTS)
- @patch('os.path.exists')
- @patch('pyspectral.utils.download_luts')
- @patch('pyspectral.rayleigh.get_reflectance_lut_from_file')
- @patch('pyspectral.rsr_reader.RelativeSpectralResponse.'
- '_get_rsr_data_version')
- @patch('pyspectral.rayleigh.Rayleigh.get_effective_wavelength')
- def test_get_reflectance_dask_redband_outside_clip(self, get_effective_wvl,
- get_rsr_version, get_reflectance_lut_from_file,
- download_luts, exists):
+ def test_get_reflectance_dask_redband_outside_clip(self, fake_lut_hdf5):
"""Test getting the reflectance correction with dask inputs - using red band reflections outside 20-100."""
- get_reflectance_lut_from_file.return_value = (self.rayl, self.wvl_coord, self.azid_coord,
- self.satz_sec_coord, self.sunz_sec_coord)
- download_luts.return_code = None
- exists.return_code = True
- get_rsr_version.return_code = RSR_DATA_VERSION
- get_effective_wvl.return_value = self.cwvl
-
sun_zenith = da.array([67., 32.])
sat_zenith = da.array([45., 18.])
azidiff = da.array([150., 110.])
redband_refl = da.array([108., -0.5])
- refl_corr = self.viirs_rayleigh.get_reflectance(sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ viirs_rayl = _create_rayleigh()
+ with mocked_rsr():
+ refl_corr = viirs_rayl.get_reflectance(sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
np.testing.assert_allclose(refl_corr, TEST_RAYLEIGH_RESULT4)
- self.assertIsInstance(refl_corr, da.Array)
-
- @patch('os.path.exists')
- @patch('pyspectral.utils.download_luts')
- @patch('pyspectral.rayleigh.get_reflectance_lut_from_file')
- @patch('pyspectral.rsr_reader.RelativeSpectralResponse.'
- '_get_rsr_data_version')
- @patch('pyspectral.rayleigh.Rayleigh.get_effective_wavelength')
- def test_get_reflectance_dask(self, get_effective_wvl,
- get_rsr_version, get_reflectance_lut_from_file,
- download_luts, exists):
- """Test getting the reflectance correction with dask inputs."""
- get_reflectance_lut_from_file.return_value = (self.rayl, self.wvl_coord, self.azid_coord,
- self.satz_sec_coord, self.sunz_sec_coord)
- download_luts.return_code = None
- exists.return_code = True
- get_rsr_version.return_code = RSR_DATA_VERSION
- get_effective_wvl.return_value = self.cwvl
+ assert isinstance(refl_corr, da.Array)
+ def test_get_reflectance_dask(self, fake_lut_hdf5):
+ """Test getting the reflectance correction with dask inputs."""
sun_zenith = da.array([67., 32.])
sat_zenith = da.array([45., 18.])
azidiff = da.array([150., 110.])
redband_refl = da.array([14., 5.])
- refl_corr = self.viirs_rayleigh.get_reflectance(sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ rayl = _create_rayleigh()
+ with mocked_rsr():
+ refl_corr = rayl.get_reflectance(sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
np.testing.assert_allclose(refl_corr, TEST_RAYLEIGH_RESULT1)
- self.assertIsInstance(refl_corr, da.Array)
+ assert isinstance(refl_corr, da.Array)
sun_zenith = da.array([60., 20.])
sat_zenith = da.array([49., 26.])
azidiff = da.array([140., 130.])
redband_refl = da.array([12., 8.])
- refl_corr = self.viirs_rayleigh.get_reflectance(sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ with mocked_rsr():
+ refl_corr = rayl.get_reflectance(sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
np.testing.assert_allclose(refl_corr, TEST_RAYLEIGH_RESULT2)
- self.assertIsInstance(refl_corr, da.Array)
-
- @patch('os.path.exists')
- @patch('pyspectral.utils.download_luts')
- @patch('pyspectral.rayleigh.get_reflectance_lut_from_file')
- @patch('pyspectral.rsr_reader.RelativeSpectralResponse.'
- '_get_rsr_data_version')
- @patch('pyspectral.rayleigh.Rayleigh.get_effective_wavelength')
- def test_get_reflectance_numpy_dask(self, get_effective_wvl,
- get_rsr_version, get_reflectance_lut_from_file,
- download_luts, exists):
- """Test getting the reflectance correction with dask inputs."""
- get_reflectance_lut_from_file.return_value = (self.rayl, self.wvl_coord, self.azid_coord,
- self.satz_sec_coord, self.sunz_sec_coord)
- download_luts.return_code = None
- exists.return_code = True
- get_rsr_version.return_code = RSR_DATA_VERSION
- get_effective_wvl.return_value = self.cwvl
+ assert isinstance(refl_corr, da.Array)
+ def test_get_reflectance_numpy_dask(self, fake_lut_hdf5):
+ """Test getting the reflectance correction with dask inputs."""
sun_zenith = np.array([67., 32.])
sat_zenith = np.array([45., 18.])
azidiff = np.array([150., 110.])
redband_refl = np.array([14., 5.])
- refl_corr = self.viirs_rayleigh.get_reflectance(sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ rayl = _create_rayleigh()
+ with mocked_rsr():
+ refl_corr = rayl.get_reflectance(sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
np.testing.assert_allclose(refl_corr, TEST_RAYLEIGH_RESULT1)
- self.assertIsInstance(refl_corr, np.ndarray)
+ assert isinstance(refl_corr, np.ndarray)
sun_zenith = np.array([60., 20.])
sat_zenith = np.array([49., 26.])
azidiff = np.array([140., 130.])
redband_refl = np.array([12., 8.])
- refl_corr = self.viirs_rayleigh.get_reflectance(sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ with mocked_rsr():
+ refl_corr = rayl.get_reflectance(sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
np.testing.assert_allclose(refl_corr, TEST_RAYLEIGH_RESULT2)
- self.assertIsInstance(refl_corr, np.ndarray)
-
-
-class TestRayleigh(unittest.TestCase):
+ assert isinstance(refl_corr, np.ndarray)
+
+ def test_get_reflectance_wvl_outside_range(self, fake_lut_hdf5):
+ """Test getting the reflectance correction with wavelength outside correction range."""
+ with mocked_rsr() as rsr_obj:
+ rsr_obj.side_effect = IOError("No rsr data in pyspectral for this platform and sensor")
+ sun_zenith = da.from_array([50., 10.])
+ sat_zenith = da.from_array([39., 16.])
+ azidiff = da.from_array([170., 110.])
+ redband_refl = da.from_array([29., 12.])
+ rayl = rayleigh.Rayleigh('UFO', 'unknown', atmosphere='midlatitude summer')
+
+ # we gave a direct wavelength so the RSR object shouldn't be needed
+ refl_corr = rayl.get_reflectance(sun_zenith, sat_zenith, azidiff, 1.2, redband_refl)
+ np.testing.assert_allclose(refl_corr, 0)
+ assert isinstance(refl_corr, da.Array)
+ assert rsr_obj.not_called()
+
+
+class TestRayleigh:
"""Class for testing pyspectral.rayleigh."""
- def setUp(self):
- """Set up the test."""
- self.cwvl = 0.4440124
- self.rsr = RelativeSpectralResponseTestData()
- self._res1 = da.from_array(TEST_RAYLEIGH_RESULT1, chunks=2)
- self._res2 = da.from_array(TEST_RAYLEIGH_RESULT2, chunks=2)
- self._res3 = da.from_array(TEST_RAYLEIGH_RESULT3, chunks=2)
-
- self.rayl = TEST_RAYLEIGH_LUT
- self.wvl_coord = TEST_RAYLEIGH_WVL_COORD
- self.azid_coord = TEST_RAYLEIGH_AZID_COORD
- self.sunz_sec_coord = TEST_RAYLEIGH_SUNZ_COORD
- self.satz_sec_coord = TEST_RAYLEIGH_SATZ_COORD
-
- # mymock:
- with patch('pyspectral.rayleigh.RelativeSpectralResponse') as mymock:
- instance = mymock.return_value
- instance.rsr = RelativeSpectralResponseTestData().rsr
- instance.unit = '1e-6 m'
- instance.si_scale = 1e-6
-
- self.viirs_rayleigh = rayleigh.Rayleigh('NOAA-20', 'viirs', atmosphere='midlatitude summer')
-
- def test_get_effective_wavelength(self):
+ def test_get_effective_wavelength_and_band_name_with_floats(self, fake_lut_hdf5):
"""Test getting the effective wavelength."""
- # mymock:
- with patch('pyspectral.rayleigh.RelativeSpectralResponse') as mymock:
- instance = mymock.return_value
- instance.rsr = RelativeSpectralResponseTestData().rsr
-
- this = rayleigh.Rayleigh('Himawari-8', 'ahi')
- with self.assertRaises(BandFrequencyOutOfRange):
- this.get_effective_wavelength(0.9)
-
- # Only ch3 (~0.63) testdata implemented yet...
- ewl = this.get_effective_wavelength(0.65)
- self.assertAlmostEqual(ewl, 0.6356167)
-
- # mymock:
- with patch('pyspectral.rayleigh.RelativeSpectralResponse') as mymock:
- instance = mymock.side_effect = IOError(
- 'Fake that there is no spectral response file...')
-
- this = rayleigh.Rayleigh('Himawari-8', 'ahi')
- ewl = this.get_effective_wavelength(0.7)
- self.assertEqual(ewl, 0.7)
- ewl = this.get_effective_wavelength(0.9)
- self.assertEqual(ewl, 0.9)
- ewl = this.get_effective_wavelength(0.455)
- self.assertEqual(ewl, 0.455)
-
- @patch('os.path.exists')
- @patch('pyspectral.utils.download_luts')
- def test_rayleigh_init(self, download_luts, exists):
+ this = rayleigh.Rayleigh('Himawari-8', 'ahi')
+ # Only ch3 (~0.63) testdata implemented yet...
+ ewl, band_name = this._get_effective_wavelength_and_band_name(0.65)
+ np.testing.assert_allclose(ewl, 650) # 635.6167)
+ assert isinstance(band_name, str)
+
+ this = rayleigh.Rayleigh('Himawari-8', 'ahi')
+ ewl, band_name = this._get_effective_wavelength_and_band_name(0.7)
+ assert ewl == 700.0
+ assert isinstance(band_name, str)
+ ewl, band_name = this._get_effective_wavelength_and_band_name(0.9)
+ assert ewl == 900.0
+ assert isinstance(band_name, str)
+ ewl, band_name = this._get_effective_wavelength_and_band_name(0.455)
+ assert ewl == 455.0
+ assert isinstance(band_name, str)
+
+ def test_rayleigh_init(self, fake_lut_hdf5):
"""Test creating the Rayleigh object."""
- download_luts.return_code = None
- exists.return_code = True
-
- # mymock:
with patch('pyspectral.rayleigh.RelativeSpectralResponse') as mymock:
instance = mymock.return_value
instance.rsr = RelativeSpectralResponseTestData().rsr
- with self.assertRaises(AttributeError):
- this = rayleigh.Rayleigh('Himawari-8', 'ahi', atmosphere='unknown')
+ with pytest.raises(AttributeError):
+ rayleigh.Rayleigh('Himawari-8', 'ahi', atmosphere='unknown')
- with self.assertRaises(AttributeError):
- this = rayleigh.Rayleigh('Himawari-8', 'ahi', aerosol_type='unknown')
+ with pytest.raises(AttributeError):
+ rayleigh.Rayleigh('Himawari-8', 'ahi', aerosol_type='unknown')
this = rayleigh.Rayleigh('Himawari-8', 'ahi', atmosphere='subarctic winter')
- self.assertTrue(os.path.basename(this.reflectance_lut_filename).endswith('subarctic_winter.h5'))
- self.assertTrue(this.sensor == 'ahi')
+ assert os.path.basename(this.reflectance_lut_filename).endswith('subarctic_winter.h5')
+ assert this.sensor == 'ahi'
this = rayleigh.Rayleigh('NOAA-19', 'avhrr/3', atmosphere='tropical')
- self.assertTrue(this.sensor == 'avhrr3')
-
- @patch('os.path.exists')
- @patch('pyspectral.utils.download_luts')
- @patch('pyspectral.rayleigh.get_reflectance_lut_from_file')
- @patch('pyspectral.rsr_reader.RelativeSpectralResponse.'
- '_get_rsr_data_version')
- @patch('pyspectral.rayleigh.Rayleigh.get_effective_wavelength')
- def test_cliping_angles_with_nans(self, get_effective_wvl,
- get_rsr_version, get_reflectance_lut_from_file,
- download_luts, exists):
- """Test the cliping of angles outside coordinate range - with nan's in input."""
- get_reflectance_lut_from_file.return_value = (self.rayl, self.wvl_coord, self.azid_coord,
- self.satz_sec_coord, self.sunz_sec_coord)
- download_luts.return_code = None
- exists.return_code = True
- get_rsr_version.return_code = RSR_DATA_VERSION
- get_effective_wvl.return_value = self.cwvl
+ assert this.sensor == 'avhrr3'
+ def test_clipping_angles_with_nans(self):
+ """Test the clipping of angles outside coordinate range - with nan's in input."""
+ from pyspectral.rayleigh import _clip_angles_inside_coordinate_range
zenith_angle = np.array([79., 69., 32., np.nan])
- result = self.viirs_rayleigh._clip_angles_inside_coordinate_range(zenith_angle, 2.75)
-
+ result = _clip_angles_inside_coordinate_range(zenith_angle, 2.75)
np.testing.assert_allclose(result, TEST_ZENITH_ANGLES_RESULTS)
- def test_rayleigh_reduction(self):
+ def test_rayleigh_reduction(self, fake_lut_hdf5):
"""Test the code that reduces Rayleigh correction for high zenith angles."""
-
# Test the Rayleigh reduction code
sun_zenith = np.array([70., 65., 60.])
in_rayleigh = [50, 50, 50]
# Test case where no reduction is done.
- retv = self.viirs_rayleigh.reduce_rayleigh_highzenith(sun_zenith, in_rayleigh, 70., 90., 1)
- self.assertTrue(np.allclose(retv, in_rayleigh))
+ retv = rayleigh.Rayleigh.reduce_rayleigh_highzenith(sun_zenith, in_rayleigh, 70., 90., 1)
+ np.testing.assert_allclose(retv, in_rayleigh)
# Test case where moderate reduction is performed.
- retv = self.viirs_rayleigh.reduce_rayleigh_highzenith(sun_zenith, in_rayleigh, 30., 90., 1)
- self.assertTrue(np.allclose(retv, TEST_RAYLEIGH_RESULT_R1))
+ retv = rayleigh.Rayleigh.reduce_rayleigh_highzenith(sun_zenith, in_rayleigh, 30., 90., 1)
+ np.testing.assert_allclose(retv, TEST_RAYLEIGH_RESULT_R1)
# Test case where extreme reduction is performed.
- retv = self.viirs_rayleigh.reduce_rayleigh_highzenith(sun_zenith, in_rayleigh, 30., 90., 1.5)
- self.assertTrue(np.allclose(retv, TEST_RAYLEIGH_RESULT_R2))
-
- @patch('pyspectral.rayleigh.HAVE_DASK', False)
- @patch('os.path.exists')
- @patch('pyspectral.utils.download_luts')
- @patch('pyspectral.rayleigh.get_reflectance_lut_from_file')
- @patch('pyspectral.rsr_reader.RelativeSpectralResponse._get_rsr_data_version')
- @patch('pyspectral.rayleigh.Rayleigh.get_effective_wavelength')
- def test_get_reflectance_redband_outside_clip(self, get_effective_wvl,
- get_rsr_version, get_reflectance_lut_from_file,
- download_luts, exists):
- """Test getting the reflectance correction - using red band reflections outside 20 to 100."""
- get_reflectance_lut_from_file.return_value = (self.rayl, self.wvl_coord, self.azid_coord,
- self.satz_sec_coord, self.sunz_sec_coord)
- download_luts.return_code = None
- exists.return_code = True
- get_rsr_version.return_code = RSR_DATA_VERSION
- get_effective_wvl.return_value = self.cwvl
+ retv = rayleigh.Rayleigh.reduce_rayleigh_highzenith(sun_zenith, in_rayleigh, 30., 90., 1.5)
+ np.testing.assert_allclose(retv, TEST_RAYLEIGH_RESULT_R2)
+ @patch('pyspectral.rayleigh.da', None)
+ def test_get_reflectance_redband_outside_clip(self, fake_lut_hdf5):
+ """Test getting the reflectance correction - using red band reflections outside 20 to 100."""
sun_zenith = np.array([67., 32.])
sat_zenith = np.array([45., 18.])
azidiff = np.array([150., 110.])
redband_refl = np.array([100., 20.])
- refl_corr1 = self.viirs_rayleigh.get_reflectance(
- sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ rayl = _create_rayleigh()
+ with mocked_rsr():
+ refl_corr1 = rayl.get_reflectance(
+ sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
np.testing.assert_allclose(refl_corr1, TEST_RAYLEIGH_RESULT4)
@@ -377,71 +298,94 @@ class TestRayleigh(unittest.TestCase):
rints_high = rng.integers(low=100, high=200, size=2).astype('float')
redband_refl = np.array([rints_high[0], rints_low[0]])
- refl_corr2 = self.viirs_rayleigh.get_reflectance(
- sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ with mocked_rsr():
+ refl_corr2 = rayl.get_reflectance(
+ sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
redband_refl = np.array([rints_high[1], rints_low[1]])
- refl_corr3 = self.viirs_rayleigh.get_reflectance(
- sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ with mocked_rsr():
+ refl_corr3 = rayl.get_reflectance(
+ sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
+ assert isinstance(refl_corr1, np.ndarray)
+ assert isinstance(refl_corr2, np.ndarray)
np.testing.assert_allclose(refl_corr1, refl_corr2)
np.testing.assert_allclose(refl_corr2, refl_corr3)
- @patch('pyspectral.rayleigh.HAVE_DASK', False)
- @patch('os.path.exists')
- @patch('pyspectral.utils.download_luts')
- @patch('pyspectral.rayleigh.get_reflectance_lut_from_file')
- @patch('pyspectral.rsr_reader.RelativeSpectralResponse._get_rsr_data_version')
- @patch('pyspectral.rayleigh.Rayleigh.get_effective_wavelength')
- def test_get_reflectance(self, get_effective_wvl,
- get_rsr_version, get_reflectance_lut_from_file, download_luts, exists):
+ @patch('pyspectral.rayleigh.da', None)
+ def test_get_reflectance(self, fake_lut_hdf5):
"""Test getting the reflectance correction."""
- get_reflectance_lut_from_file.return_value = (self.rayl, self.wvl_coord, self.azid_coord,
- self.satz_sec_coord, self.sunz_sec_coord)
- download_luts.return_code = None
- exists.return_code = True
- get_rsr_version.return_code = RSR_DATA_VERSION
- get_effective_wvl.return_value = self.cwvl
-
sun_zenith = np.array([67., 32.])
sat_zenith = np.array([45., 18.])
azidiff = np.array([150., 110.])
redband_refl = np.array([14., 5.])
- refl_corr = self.viirs_rayleigh.get_reflectance(
- sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ rayl = _create_rayleigh()
+ with mocked_rsr():
+ refl_corr = rayl.get_reflectance(
+ sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
np.testing.assert_allclose(refl_corr, TEST_RAYLEIGH_RESULT1)
sun_zenith = np.array([60., 20.])
sat_zenith = np.array([49., 26.])
azidiff = np.array([140., 130.])
redband_refl = np.array([12., 8.])
- refl_corr = self.viirs_rayleigh.get_reflectance(
- sun_zenith, sat_zenith, azidiff, 'M2', redband_refl)
+ with mocked_rsr():
+ refl_corr = rayl.get_reflectance(
+ sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
+ assert isinstance(refl_corr, np.ndarray)
np.testing.assert_allclose(refl_corr, TEST_RAYLEIGH_RESULT2)
- @patch('pyspectral.rayleigh.HAVE_DASK', False)
- @patch('os.path.exists')
- @patch('pyspectral.utils.download_luts')
- @patch('pyspectral.rayleigh.get_reflectance_lut_from_file')
- def test_get_reflectance_no_rsr(self, get_reflectance_lut_from_file, download_luts, exists):
+ @patch('pyspectral.rayleigh.da', None)
+ def test_get_reflectance_no_rsr(self, fake_lut_hdf5):
"""Test getting the reflectance correction, simulating that we have no RSR data."""
- get_reflectance_lut_from_file.return_value = (self.rayl, self.wvl_coord, self.azid_coord,
- self.satz_sec_coord, self.sunz_sec_coord)
- download_luts.return_code = None
- exists.return_code = True
+ with mocked_rsr() as rsr_obj:
+ rsr_obj.side_effect = IOError("No rsr data in pyspectral for this platform and sensor")
+ sun_zenith = np.array([50., 10.])
+ sat_zenith = np.array([39., 16.])
+ azidiff = np.array([170., 110.])
+ redband_refl = np.array([29., 12.])
+ ufo = rayleigh.Rayleigh('UFO', 'unknown', atmosphere='midlatitude summer')
+ with pytest.raises(KeyError):
+ ufo.get_reflectance(sun_zenith, sat_zenith, azidiff, 'ch3', redband_refl)
- with patch('pyspectral.rayleigh.RelativeSpectralResponse') as mymock:
- instance = mymock.return_value
- mymock.side_effect = IOError("No rsr data in pyspectral for this platform and sensor")
- instance.rsr = None
- instance.unit = '1e-6 m'
- instance.si_scale = 1e-6
+ @patch('pyspectral.rayleigh.da', None)
+ def test_get_reflectance_float_wavelength(self, fake_lut_hdf5):
+ """Test getting the reflectance correction."""
+ with mocked_rsr() as rsr_obj:
+ rsr_obj.side_effect = IOError("No rsr data in pyspectral for this platform and sensor")
sun_zenith = np.array([50., 10.])
sat_zenith = np.array([39., 16.])
azidiff = np.array([170., 110.])
redband_refl = np.array([29., 12.])
- ufo = rayleigh.Rayleigh('UFO', 'unknown')
+ # rayl = _create_rayleigh()
+ rayl = rayleigh.Rayleigh('UFO', 'unknown', atmosphere='midlatitude summer')
- refl_corr = ufo.get_reflectance(sun_zenith, sat_zenith, azidiff, 0.441, redband_refl)
+ # we gave a direct wavelength so the RSR object shouldn't be needed
+ refl_corr = rayl.get_reflectance(sun_zenith, sat_zenith, azidiff, 0.634, redband_refl)
np.testing.assert_allclose(refl_corr, TEST_RAYLEIGH_RESULT3)
+ assert isinstance(refl_corr, np.ndarray)
+ assert rsr_obj.not_called()
+
+ @patch('pyspectral.rayleigh.da', None)
+ def test_get_reflectance_wvl_outside_range(self, fake_lut_hdf5):
+ """Test getting the reflectance correction with wavelength outside correction range."""
+ with mocked_rsr() as rsr_obj:
+ rsr_obj.side_effect = IOError("No rsr data in pyspectral for this platform and sensor")
+ sun_zenith = np.array([50., 10.])
+ sat_zenith = np.array([39., 16.])
+ azidiff = np.array([170., 110.])
+ redband_refl = np.array([29., 12.])
+ rayl = rayleigh.Rayleigh('UFO', 'unknown', atmosphere='midlatitude summer')
+
+ # we gave a direct wavelength so the RSR object shouldn't be needed
+ refl_corr = rayl.get_reflectance(sun_zenith, sat_zenith, azidiff, 1.2, redband_refl)
+ np.testing.assert_allclose(refl_corr, 0)
+ assert isinstance(refl_corr, np.ndarray)
+ assert rsr_obj.not_called()
+
+ def test_get_reflectance_no_lut(self, fake_lut_hdf5):
+ """Test that missing a LUT causes an exception.."""
+ # test LUT doesn't have a subartic_summer file
+ with pytest.raises(IOError):
+ rayleigh.Rayleigh('UFO', 'unknown', atmosphere='subarctic summer')
=====================================
pyspectral/utils.py
=====================================
@@ -1,12 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
-# Copyright (c) 2014-2021 Pytroll developers
+# Copyright (c) 2014-2022 Pytroll developers
#
-# Author(s):
-#
-# Adam.Dybbroe <adam.dybbroe at smhi.se>
-# Panu Lahtinen <panu.lahtinen at fmi.fi>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -25,7 +21,6 @@
import os
import logging
-import tempfile
import numpy as np
from pyspectral.config import get_config
from pyspectral.bandnames import BANDNAMES
@@ -70,10 +65,10 @@ INSTRUMENTS = {'NOAA-19': 'avhrr/3',
'MTG-I1': 'fci'
}
-HTTP_PYSPECTRAL_RSR = "https://zenodo.org/record/5797957/files/pyspectral_rsr_data.tgz"
+HTTP_PYSPECTRAL_RSR = "https://zenodo.org/record/6026563/files/pyspectral_rsr_data.tgz"
RSR_DATA_VERSION_FILENAME = "PYSPECTRAL_RSR_VERSION"
-RSR_DATA_VERSION = "v1.0.17"
+RSR_DATA_VERSION = "v1.0.18"
ATM_CORRECTION_LUT_VERSION = {}
ATM_CORRECTION_LUT_VERSION['antarctic_aerosol'] = {'version': 'v1.0.1',
@@ -116,22 +111,12 @@ for atype in AEROSOL_TYPES:
url = "{prefix}_{name}.tgz".format(prefix=URL_PREFIX, name=name)
HTTPS_RAYLEIGH_LUTS[atype] = url
-CONF = get_config()
-LOCAL_RSR_DIR = CONF.get('rsr_dir')
-LOCAL_RAYLEIGH_DIR = CONF.get('rayleigh_dir')
-
-try:
- os.makedirs(LOCAL_RSR_DIR)
-except OSError:
- if not os.path.isdir(LOCAL_RSR_DIR):
- raise
-RAYLEIGH_LUT_DIRS = {}
-for sub_dir_name in HTTPS_RAYLEIGH_LUTS:
- dirname = os.path.join(LOCAL_RAYLEIGH_DIR, sub_dir_name)
- RAYLEIGH_LUT_DIRS[sub_dir_name] = dirname
-
-TB2RAD_DIR = CONF.get('tb2rad_dir', tempfile.gettempdir())
+def get_rayleigh_lut_dir(aerosol_type):
+ """Get the rayleight LUT directory for the specified aerosol type."""
+ conf = get_config()
+ local_rayleigh_dir = conf.get('rayleigh_dir')
+ return os.path.join(local_rayleigh_dir, aerosol_type)
def convert2wavenumber(rsr):
@@ -296,7 +281,9 @@ def download_rsr(**kwargs):
except ImportError:
TQDM_LOADED = False
- dest_dir = kwargs.get('dest_dir', LOCAL_RSR_DIR)
+ config = get_config()
+ local_rsr_dir = config.get('rsr_dir')
+ dest_dir = kwargs.get('dest_dir', local_rsr_dir)
dry_run = kwargs.get('dry_run', False)
LOG.info("Download RSR files and store in directory %s", dest_dir)
@@ -344,14 +331,13 @@ def download_luts(**kwargs):
aerosol_types = HTTPS_RAYLEIGH_LUTS.keys()
chunk_size = 1024 * 1024 # 1 MB
-
for subname in aerosol_types:
LOG.debug('Aerosol type: %s', subname)
http = HTTPS_RAYLEIGH_LUTS[subname]
LOG.debug('URL = %s', http)
- subdir_path = RAYLEIGH_LUT_DIRS[subname]
+ subdir_path = get_rayleigh_lut_dir(subname)
try:
LOG.debug('Create directory: %s', subdir_path)
if not dry_run:
=====================================
rsr_convert_scripts/README.rst
=====================================
@@ -172,9 +172,13 @@ the pyspectral.yaml file:
filename: J1_VIIRS_Detector_RSR_V2/J1_VIIRS_RSR_DNBLGS_Detector_Fused_V2S.txt
bands: [DNB]
+.. code::
+
+ %> python msu_gs_reader.py
-Adam Dybbroe
-Sat Dec 1 17:39:48 2018
+Converts RSRs for the MSU-GS sensor aboard Electro-L N2. RSRs were retrieved from the NWP-SAF.
+Filenames look like:
+``rtcoef_electro-l_2_msugs_srf_ch01.txt``
.. code::
@@ -185,3 +189,6 @@ for FY-3B come as ``.prn`` text files for each channel (ex. ``ch1.prn``). For
FY-3C they come as ``.txt`` text files for channels 1, 2, 6, 7, 8, 9, and 10
only with names like ``FY3C_VIRR_CH01.txt``.
+PyTroll developers
+
+
=====================================
rsr_convert_scripts/msu_gs_reader.py
=====================================
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2022 Pytroll developers
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""Read the Electro-L N2 MSU-GS spectral response functions. Data from the NWPSAF:
+https://nwp-saf.eumetsat.int/downloads/rtcoef_rttov13/ir_srf/rtcoef_electro-l_2_msugs_srf.html
+
+"""
+
+import os
+import numpy as np
+from pyspectral.utils import convert2hdf5 as tohdf5
+from pyspectral.raw_reader import InstrumentRSR
+import logging
+
+LOG = logging.getLogger(__name__)
+
+MSUGS_BAND_NAMES = ['ch1', 'ch2', 'ch3', 'ch4', 'ch5',
+ 'ch6', 'ch7', 'ch8', 'ch9', 'ch10']
+
+#: Default time format
+_DEFAULT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
+
+#: Default log format
+_DEFAULT_LOG_FORMAT = '[%(levelname)s: %(asctime)s : %(name)s] %(message)s'
+
+
+class MsugsRSR(InstrumentRSR):
+ """Container for the Electro-L N2 MSU-GS relative spectral response data"""
+
+ def __init__(self, bandname, platform_name):
+
+ super(MsugsRSR, self).__init__(
+ bandname, platform_name, MSUGS_BAND_NAMES)
+
+ self.instrument = 'msu-gs'
+ self._get_options_from_config()
+ self._get_bandfilenames()
+
+ LOG.debug("Filenames: %s", str(self.filenames))
+ if self.filenames[bandname] and os.path.exists(self.filenames[bandname]):
+ self.requested_band_filename = self.filenames[bandname]
+ self._load()
+
+ else:
+ LOG.warning("Couldn't find an existing file for this band: %s",
+ str(self.bandname))
+
+ # To be compatible with VIIRS....
+ self.filename = self.requested_band_filename
+
+ self.unit = 'micrometer'
+ self.wavespace = 'wavelength'
+
+ def _load(self, scale=10000.0):
+ """Load the MSU-GS RSR data for the band requested"""
+ data = np.genfromtxt(self.requested_band_filename,
+ unpack=True,
+ names=['wavenumber',
+ 'response'],
+ skip_header=4)
+
+ # Data are wavenumbers in cm-1:
+ wavelength = 1. / data[0][::-1] * scale
+ response = data[1][::-1]
+
+ # The real MSU-GS likely has multiple detectors. However, for now
+ # we store the single rsr available in the NWPSAF coefficient files:
+ self.rsr = {'wavelength': wavelength, 'response': response}
+
+
+def main():
+ """Main"""
+ for platform_name in ['Electro-L-N2', ]:
+ tohdf5(MsugsRSR, platform_name, MSUGS_BAND_NAMES)
+
+
+if __name__ == "__main__":
+ import sys
+
+ LOG = logging.getLogger('msu_gs_rsr')
+ handler = logging.StreamHandler(sys.stderr)
+
+ formatter = logging.Formatter(fmt=_DEFAULT_LOG_FORMAT,
+ datefmt=_DEFAULT_TIME_FORMAT)
+ handler.setFormatter(formatter)
+ handler.setLevel(logging.DEBUG)
+ LOG.setLevel(logging.DEBUG)
+ LOG.addHandler(handler)
+
+ main()
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyspectral/-/compare/1ffc6d5d5cb68a946180aca04df1a9a8ee464025...9694907e0969dc620db4d5a619da239eab6de643
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyspectral/-/compare/1ffc6d5d5cb68a946180aca04df1a9a8ee464025...9694907e0969dc620db4d5a619da239eab6de643
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20220306/6d047499/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list