[Git][debian-gis-team/pyspectral][master] 7 commits: New upstream version 0.13.0+ds
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Sat Dec 2 20:49:04 GMT 2023
Antonio Valentino pushed to branch master at Debian GIS Project / pyspectral
Commits:
14cabd37 by Antonio Valentino at 2023-12-02T15:18:46+00:00
New upstream version 0.13.0+ds
- - - - -
46dfd9e1 by Antonio Valentino at 2023-12-02T15:18:48+00:00
Update upstream source from tag 'upstream/0.13.0+ds'
Update to upstream version '0.13.0+ds'
with Debian dir 54be938a8f953bdcf20939bee62d4734c9140557
- - - - -
8d93b0a1 by Antonio Valentino at 2023-12-02T15:19:31+00:00
New upstream release
- - - - -
ab8dc36b by Antonio Valentino at 2023-12-02T15:26:48+00:00
Regresh all patches
- - - - -
950718b7 by Antonio Valentino at 2023-12-02T15:30:29+00:00
Fix python3-pyspectral.lintian-overrides
- - - - -
f55d9f84 by Antonio Valentino at 2023-12-02T15:31:55+00:00
Typo
- - - - -
76573dab by Antonio Valentino at 2023-12-02T20:46:52+00:00
Fix lintian warning
- - - - -
28 changed files:
- CHANGELOG.md
- CHANGELOG_RSR_DATA.rst
- continuous_integration/environment.yaml
- debian/changelog
- debian/control
- debian/patches/0002-Fix-install-data.patch
- debian/python3-pyspectral-doc.docs
- debian/python3-pyspectral.lintian-overrides
- debian/rules
- doc/37_reflectance.rst
- doc/rad_definitions.rst
- doc/usage.rst
- pyspectral/bandnames.py
- pyspectral/blackbody.py
- pyspectral/config.py
- pyspectral/etc/pyspectral.yaml
- pyspectral/near_infrared_reflectance.py
- pyspectral/radiance_tb_conversion.py
- pyspectral/solar.py
- pyspectral/tests/test_blackbody.py
- pyspectral/tests/test_rad_tb_conversions.py
- pyspectral/tests/test_rayleigh.py
- pyspectral/tests/test_reflectance.py
- pyspectral/tests/test_solarflux.py
- pyspectral/tests/unittest_helpers.py
- pyspectral/utils.py
- + rsr_convert_scripts/epic_reader.py
- setup.py
Changes:
=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,26 @@
+## Version <v0.13.0> (2023/11/27)
+
+### Issues Closed
+
+* [Issue 205](https://github.com/pytroll/pyspectral/issues/205) - Near infrared reflectance calculation does not preserve input dtype ([PR 206](https://github.com/pytroll/pyspectral/pull/206) by [@pnuu](https://github.com/pnuu))
+* [Issue 201](https://github.com/pytroll/pyspectral/issues/201) - blackbody functions not dask friendly, trigger early dask computation
+
+In this release 2 issues were closed.
+
+### Pull Requests Merged
+
+#### Features added
+
+* [PR 208](https://github.com/pytroll/pyspectral/pull/208) - Update the solar spectrum code to use the included reference spectra
+* [PR 207](https://github.com/pytroll/pyspectral/pull/207) - Remove pkg_resources usage in config handling
+* [PR 206](https://github.com/pytroll/pyspectral/pull/206) - Return 3.x um reflectance with the same dtype as the NIR data ([205](https://github.com/pytroll/pyspectral/issues/205))
+* [PR 204](https://github.com/pytroll/pyspectral/pull/204) - Bump up python versions
+* [PR 203](https://github.com/pytroll/pyspectral/pull/203) - Allow dataarrays and preserve dtype in rad2temp
+* [PR 202](https://github.com/pytroll/pyspectral/pull/202) - Update the solar spectrum code to use the included reference spectra
+
+In this release 6 pull requests were closed.
+
+
## Version <v0.12.5> (2023/09/21)
### Pull Requests Merged
=====================================
CHANGELOG_RSR_DATA.rst
=====================================
@@ -2,6 +2,20 @@ Changelog for the Relative Spectral Response data
=================================================
+
+Version <v1.2.4> (Sat Oct 21 12:25:00 PM CEST 2023)
+-------------------------------------------
+
+ * Normalized RSR responses for the EPIC sensor. Now values are sclaed to be
+ between 0 and 1.
+
+
+Version <v1.2.3> (Fri Oct 20 01:58:29 2023)
+-------------------------------------------
+
+ * Added RSR file for EPIC on DSCOVR (responses not normalized)
+
+
Version <v1.2.2> (Tue Nov 15 14:44:03 2022)
-------------------------------------------
=====================================
continuous_integration/environment.yaml
=====================================
@@ -17,8 +17,6 @@ dependencies:
- behave
- netcdf4
- h5py
- - h5netcdf
- - pyhdf
- mock
- pytest
- pytest-cov
=====================================
debian/changelog
=====================================
@@ -1,7 +1,12 @@
-pyspectral (0.12.5+ds-2) UNRELEASED; urgency=medium
+pyspectral (0.13.0+ds-1) UNRELEASED; urgency=medium
+ * New upstream release.
* debian/control:
- Move the common package description to source.
+ * debian/patches:
+ - Refresh all patches.
+ * Fix d/python3-pyspectral.lintian-overrides.
+ * Fix lintian warning in documentation file.
-- Antonio Valentino <antonio.valentino at tiscali.it> Sun, 26 Nov 2023 08:26:43 +0000
=====================================
debian/control
=====================================
@@ -83,7 +83,7 @@ Depends: ${sphinxdoc:Depends},
${misc:Depends}
Suggests: python3-pyspectral,
www-browser
-Description: ${source:Synopsis} -- docuentation
+Description: ${source:Synopsis} -- documentation
${source:Extended-Description}
.
This package includes the PySpectral documentation in HTML format.
=====================================
debian/patches/0002-Fix-install-data.patch
=====================================
@@ -8,7 +8,7 @@ Forwarded: not-needed
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/setup.py b/setup.py
-index 51bebff..4bb72c6 100644
+index 739b038..cbc5f6d 100644
--- a/setup.py
+++ b/setup.py
@@ -78,8 +78,8 @@ setup(name=NAME,
@@ -21,4 +21,4 @@ index 51bebff..4bb72c6 100644
+ # 'pyspectral/data/MSG_SEVIRI_Spectral_Response_Characterisation.XLS'])],
test_suite='pyspectral.tests.suite',
tests_require=test_requires,
- python_requires='>=3.7',
+ python_requires='>=3.10',
=====================================
debian/python3-pyspectral-doc.docs
=====================================
@@ -1 +1 @@
-build/html
+.pybuild/build/html
=====================================
debian/python3-pyspectral.lintian-overrides
=====================================
@@ -1 +1 @@
-package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/pyspectral-0.12.5+ds.*-info/*]
+package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/pyspectral-*.*-info/*]
=====================================
debian/rules
=====================================
@@ -14,8 +14,9 @@ execute_after_dh_auto_build: export http_proxy=127.0.0.1:9
execute_after_dh_auto_build: export https_proxy=127.0.0.1:9
execute_after_dh_auto_build:
ifeq (,$(filter nodoc,$(DEB_BUILD_OPTIONS)))
- PYTHONPATH=. python3 -m sphinx -N -bhtml doc build/html # HTML generator
- # PYTHONPATH=. python3 -m sphinx -N -bman doc build/man # Manpage generator
+ PYTHONPATH=. python3 -m sphinx -N -bhtml doc .pybuild/build/html # HTML generator
+ # PYTHONPATH=. python3 -m sphinx -N -bman doc .pybuild/build/man # Manpage generator
+ sed -i "s:\'.\*/pyspectral/data/e490_00a.dat\':\'/usr/lib/dist-packages/pyspectral/data/e490_00a.dat\':g" .pybuild/build/html/api.html
endif
execute_after_dh_auto_install:
=====================================
doc/37_reflectance.rst
=====================================
@@ -123,9 +123,9 @@ would be:
where :math:`S(\lambda)` is the spectral solar irradiance.
>>> from pyspectral.rsr_reader import RelativeSpectralResponse
- >>> from pyspectral.solar import (SolarIrradianceSpectrum, TOTAL_IRRADIANCE_SPECTRUM_2000ASTM)
+ >>> from pyspectral.solar import SolarIrradianceSpectrum
>>> viirs = RelativeSpectralResponse('Suomi-NPP', 'viirs')
- >>> solar_irr = SolarIrradianceSpectrum(TOTAL_IRRADIANCE_SPECTRUM_2000ASTM, dlambda=0.005)
+ >>> solar_irr = SolarIrradianceSpectrum(dlambda=0.005)
>>> sflux = solar_irr.inband_solarflux(viirs.rsr['M12'])
>>> print(np.round(sflux, 7))
2.2428119
=====================================
doc/rad_definitions.rst
=====================================
@@ -156,8 +156,8 @@ TOA Solar irradiance and solar constant
First, the TOA solar irradiance in wavelength space:
- >>> from pyspectral.solar import (SolarIrradianceSpectrum, TOTAL_IRRADIANCE_SPECTRUM_2000ASTM)
- >>> solar_irr = SolarIrradianceSpectrum(TOTAL_IRRADIANCE_SPECTRUM_2000ASTM, dlambda=0.0005)
+ >>> from pyspectral.solar import SolarIrradianceSpectrum
+ >>> solar_irr = SolarIrradianceSpectrum(dlambda=0.0005)
>>> print("Solar irradiance = {}".format(round(solar_irr.solar_constant(), 3)))
Solar irradiance = 1366.091
>>> solar_irr.plot('/tmp/solar_irradiance.png')
@@ -201,8 +201,8 @@ In python code it may look like this:
>>> from pyspectral.utils import convert2wavenumber, get_central_wave
>>> seviri = RelativeSpectralResponse('Meteosat-8', 'seviri')
>>> rsr, info = convert2wavenumber(seviri.rsr)
- >>> from pyspectral.solar import (SolarIrradianceSpectrum, TOTAL_IRRADIANCE_SPECTRUM_2000ASTM)
- >>> solar_irr = SolarIrradianceSpectrum(TOTAL_IRRADIANCE_SPECTRUM_2000ASTM, dlambda=0.0005, wavespace='wavenumber')
+ >>> from pyspectral.solar import SolarIrradianceSpectrum
+ >>> solar_irr = SolarIrradianceSpectrum(dlambda=0.0005, wavespace='wavenumber')
>>> print("Solar Irradiance (SEVIRI band VIS008) = {sflux:12.6f}".format(sflux=solar_irr.inband_solarflux(rsr['VIS0.8'])))
Solar Irradiance (SEVIRI band VIS008) = 63767.908405
=====================================
doc/usage.rst
=====================================
@@ -35,9 +35,9 @@ Now, you can work with the data as you wish, make some simple plot for instance:
A simple use case:
>>> from pyspectral.rsr_reader import RelativeSpectralResponse
- >>> from pyspectral.solar import (SolarIrradianceSpectrum, TOTAL_IRRADIANCE_SPECTRUM_2000ASTM)
+ >>> from pyspectral.solar import SolarIrradianceSpectrum
>>> modis = RelativeSpectralResponse('EOS-Aqua', 'modis')
- >>> solar_irr = SolarIrradianceSpectrum(TOTAL_IRRADIANCE_SPECTRUM_2000ASTM, dlambda=0.005)
+ >>> solar_irr = SolarIrradianceSpectrum(dlambda=0.005)
>>> sflux = solar_irr.inband_solarflux(modis.rsr['20'])
>>> print("Solar flux over Band: {sflux}".format(sflux=round(sflux, 6)))
Solar flux over Band: 2.002928
=====================================
pyspectral/bandnames.py
=====================================
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-# Copyright (c) 2021-2022 Adam.Dybbroe
+# Copyright (c) 2021-2023 Pytroll developers
#
#
# This program is free software: you can redistribute it and/or modify
@@ -70,7 +70,6 @@ for chan_num in range(1, 37):
# MODIS RSR files were made before 'chX' became standard in pyspectral
BANDNAMES['modis'] = {str(chan_num): str(chan_num) for chan_num in range(1, 37)}
-
BANDNAMES['seviri'] = {'VIS006': 'VIS0.6',
'VIS008': 'VIS0.8',
'IR_016': 'NIR1.6',
=====================================
pyspectral/blackbody.py
=====================================
@@ -28,96 +28,50 @@ try:
except ImportError:
da = np
+from pyspectral.utils import use_map_blocks_on
+
LOG = logging.getLogger(__name__)
H_PLANCK = 6.62606957 * 1e-34 # SI-unit = [J*s]
K_BOLTZMANN = 1.3806488 * 1e-23 # SI-unit = [J/K]
C_SPEED = 2.99792458 * 1e8 # SI-unit = [m/s]
+PLANCK_C1 = H_PLANCK * C_SPEED / K_BOLTZMANN
+PLANCK_C2 = 2 * H_PLANCK * C_SPEED**2
+
EPSILON = 0.000001
+ at use_map_blocks_on("radiance")
def blackbody_rad2temp(wavelength, radiance):
- """Derive brightness temperatures from radiance using the Planck function.
-
- Wavelength space. Assumes SI units as input and returns
- temperature in Kelvin
-
- """
- mask = False
- if np.isscalar(radiance):
- rad = np.array([radiance, ], dtype='float64')
- else:
- rad = np.array(radiance, dtype='float64')
- if np.ma.is_masked(radiance):
- mask = radiance.mask
-
- rad = np.ma.masked_array(rad, mask=mask)
- rad = np.ma.masked_less_equal(rad, 0)
-
- if np.isscalar(wavelength):
- wvl = np.array([wavelength, ], dtype='float64')
- else:
- wvl = np.array(wavelength, dtype='float64')
+ """Derive brightness temperatures from radiance using the inverse Planck function.
- const1 = H_PLANCK * C_SPEED / K_BOLTZMANN
- const2 = 2 * H_PLANCK * C_SPEED**2
- res = const1 / (wvl * np.log(np.divide(const2, rad * wvl**5) + 1.0))
+ Args:
+ wavelength: The wavelength to use, in SI units (metre).
+ radiance: The radiance to derive temperatures from, in SI units (W/m^2 sr^-1). Scalar or arrays are accepted.
- shape = rad.shape
- resshape = res.shape
+ Returns:
+ The derived temperature in Kelvin.
- if wvl.shape[0] == 1:
- if rad.shape[0] == 1:
- return res[0]
- else:
- return res[::].reshape(shape)
- else:
- if rad.shape[0] == 1:
- return res[0, :]
- else:
- if len(shape) == 1:
- return np.reshape(res, (shape[0], resshape[1]))
- else:
- return np.reshape(res, (shape[0], shape[1], resshape[1]))
+ """
+ with np.errstate(invalid='ignore'):
+ return PLANCK_C1 / (wavelength * np.log(PLANCK_C2 / (radiance * wavelength**5) + 1.0))
+ at use_map_blocks_on("radiance")
def blackbody_wn_rad2temp(wavenumber, radiance):
- """Derive brightness temperatures from radiance using the Planck function.
+ """Derive brightness temperatures from radiance using the inverse Planck function.
+
+ Args:
+ wavenumber: The wavenumber to use, in SI units (1/metre).
+ radiance: The radiance to derive temperatures from, in SI units (W/m^2 sr^-1). Scalar or arrays are accepted.
- Wavenumber space
+ Returns:
+ The derived temperature in Kelvin.
"""
- if np.isscalar(radiance):
- radiance = np.array([radiance], dtype='float64')
- elif isinstance(radiance, (list, tuple)):
- radiance = np.array(radiance, dtype='float64')
- if np.isscalar(wavenumber):
- wavnum = np.array([wavenumber], dtype='float64')
- elif isinstance(wavenumber, (list, tuple)):
- wavnum = np.array(wavenumber, dtype='float64')
-
- const1 = H_PLANCK * C_SPEED / K_BOLTZMANN
- const2 = 2 * H_PLANCK * C_SPEED**2
- res = const1 * wavnum / np.log(
- np.divide(const2 * wavnum**3, radiance) + 1.0)
-
- shape = radiance.shape
- resshape = res.shape
-
- if wavnum.shape[0] == 1:
- if radiance.shape[0] == 1:
- return res[0]
- else:
- return res[::].reshape(shape)
- else:
- if radiance.shape[0] == 1:
- return res[0, :]
- else:
- if len(shape) == 1:
- return res.reshape((shape[0], resshape[1]))
- else:
- return res.reshape((shape[0], shape[1], resshape[1]))
+ with np.errstate(invalid='ignore'):
+ return PLANCK_C1 * wavenumber / np.log((PLANCK_C2 * wavenumber**3) / radiance + 1.0)
def planck(wave, temperature, wavelength=True):
@@ -145,24 +99,12 @@ def planck(wave, temperature, wavelength=True):
LOG.debug("Using {0} when calculating the Blackbody radiance".format(
units[(wavelength is True) - 1]))
- if np.isscalar(temperature):
- temperature = np.array([temperature, ], dtype='float64')
- elif isinstance(temperature, (list, tuple)):
- temperature = np.array(temperature, dtype='float64')
-
- shape = temperature.shape
- if np.isscalar(wave):
- wln = np.array([wave, ], dtype='float64')
- else:
- wln = np.array(wave, dtype='float64')
-
if wavelength:
- const = 2 * H_PLANCK * C_SPEED ** 2
- nom = const / wln ** 5
- arg1 = H_PLANCK * C_SPEED / (K_BOLTZMANN * wln)
+ nom = PLANCK_C2 / wave ** 5
+ arg1 = PLANCK_C1 / wave
else:
- nom = 2 * H_PLANCK * (C_SPEED ** 2) * (wln ** 3)
- arg1 = H_PLANCK * C_SPEED * wln / K_BOLTZMANN
+ nom = PLANCK_C2 * wave ** 3
+ arg1 = PLANCK_C1 * wave
with np.errstate(divide='ignore', invalid='ignore'):
# use dask functions when needed
@@ -178,7 +120,7 @@ def planck(wave, temperature, wavelength=True):
str(np.nanmax(arg2)), str(np.nanmin(arg2)))
try:
- exp_arg = np.multiply(arg1.astype('float64'), arg2.astype('float64'))
+ exp_arg = np.multiply(arg1, arg2)
except MemoryError:
LOG.warning(("Dimensions used in numpy.multiply probably reached "
"limit!\n"
@@ -197,21 +139,7 @@ def planck(wave, temperature, wavelength=True):
with np.errstate(over='ignore'):
denom = np.exp(exp_arg) - 1
- rad = nom / denom
- radshape = rad.shape
- if wln.shape[0] == 1:
- if temperature.shape[0] == 1:
- return rad[0, 0]
- else:
- return rad[:, 0].reshape(shape)
- else:
- if temperature.shape[0] == 1:
- return rad[0, :]
- else:
- if len(shape) == 1:
- return rad.reshape((shape[0], radshape[1]))
- else:
- return rad.reshape((shape[0], shape[1], radshape[1]))
+ return nom / denom
def blackbody_wn(wavenumber, temp):
=====================================
pyspectral/config.py
=====================================
@@ -21,20 +21,13 @@
import logging
import os
+from collections.abc import Mapping
from os.path import expanduser
+from pathlib import Path
import yaml
from appdirs import AppDirs
-try:
- # python 3.3+
- from collections.abc import Mapping
-except ImportError:
- # deprecated (above can't be done in 2.7)
- from collections import Mapping
-
-import pkg_resources
-
try:
from yaml import UnsafeLoader
except ImportError:
@@ -43,16 +36,7 @@ except ImportError:
LOG = logging.getLogger(__name__)
-BUILTIN_CONFIG_FILE = pkg_resources.resource_filename(__name__,
- os.path.join('etc', 'pyspectral.yaml'))
-
-
-CONFIG_FILE = os.environ.get('PSP_CONFIG_FILE')
-
-if CONFIG_FILE is not None and (not os.path.exists(CONFIG_FILE) or
- not os.path.isfile(CONFIG_FILE)):
- raise IOError(str(CONFIG_FILE) + " pointed to by the environment " +
- "variable PSP_CONFIG_FILE is not a file or does not exist!")
+BUILTIN_CONFIG_FILE = Path(__file__).resolve().parent / "etc" / "pyspectral.yaml"
def recursive_dict_update(d, u):
@@ -72,15 +56,13 @@ def recursive_dict_update(d, u):
return d
-def get_config():
- """Get the configuration from file."""
- if CONFIG_FILE is not None:
- configfile = CONFIG_FILE
- else:
- configfile = BUILTIN_CONFIG_FILE
+def get_config(config_file: str | Path = None) -> dict:
+ """Get configuration options from YAML file."""
+ if config_file is None:
+ config_file = _get_env_or_builtin_config_path()
config = {}
- with open(configfile, 'r') as fp_:
+ with open(config_file, 'r') as fp_:
config = recursive_dict_update(config, yaml.load(fp_, Loader=UnsafeLoader))
app_dirs = AppDirs('pyspectral', 'pytroll')
@@ -91,3 +73,13 @@ def get_config():
os.makedirs(config['rayleigh_dir'], exist_ok=True)
return config
+
+
+def _get_env_or_builtin_config_path() -> Path:
+ config_file = os.environ.get('PSP_CONFIG_FILE')
+ if config_file is not None and not os.path.isfile(config_file):
+ raise IOError(f"{config_file} pointed to by the environment variable "
+ f"'PSP_CONFIG_FILE' is not a file or does not exist!")
+ if config_file is None:
+ return BUILTIN_CONFIG_FILE
+ return Path(config_file)
=====================================
pyspectral/etc/pyspectral.yaml
=====================================
@@ -50,6 +50,9 @@ download_from_internet: True
# path: /home/a000680/data/SpectralResponses/modis/aqua
# tb2rad_lut_filename: /path/to/radiance/tb/lut/data/tb2rad_lut_aqua_modis_ir3.7.npz
+#DSCOVR-epic:
+# path: C:/Users/Simon/Downloads/EPIC_Filters_Original_Data.xlsx
+
# NOAA-20-viirs:
# # J1_VIIRS_RSR_M1_Detector_Fused_V2.txt
=====================================
pyspectral/near_infrared_reflectance.py
=====================================
@@ -38,7 +38,7 @@ except ImportError:
from pyspectral.config import get_config
from pyspectral.radiance_tb_conversion import RadTbConverter
-from pyspectral.solar import TOTAL_IRRADIANCE_SPECTRUM_2000ASTM, SolarIrradianceSpectrum
+from pyspectral.solar import SolarIrradianceSpectrum
from pyspectral.utils import BANDNAMES, WAVE_LENGTH, get_bandname_from_wavelength
LOG = logging.getLogger(__name__)
@@ -161,8 +161,7 @@ class Calculator(RadTbConverter):
def _get_solarflux(self):
"""Derive the in-band solar flux from rsr over the Near IR band (3.7 or 3.9 microns)."""
solar_spectrum = \
- SolarIrradianceSpectrum(TOTAL_IRRADIANCE_SPECTRUM_2000ASTM,
- dlambda=0.0005,
+ SolarIrradianceSpectrum(dlambda=0.0005,
wavespace=self.wavespace)
self.solar_flux = solar_spectrum.inband_solarflux(self.rsr[self.bandname])
@@ -243,11 +242,11 @@ class Calculator(RadTbConverter):
# Assume rsr is in microns!!!
# FIXME!
self._rad3x_t11 = self.tb2radiance(tb_therm, lut=lut)['radiance']
- thermal_emiss_one = self._rad3x_t11 * self.rsr_integral
-
+ rsr_integral = tb_therm.dtype.type(self.rsr_integral)
+ thermal_emiss_one = self._rad3x_t11 * rsr_integral
l_nir = self.tb2radiance(tb_nir, lut=lut)['radiance']
self._rad3x = l_nir.copy()
- l_nir *= self.rsr_integral
+ l_nir *= rsr_integral
if thermal_emiss_one.ravel().shape[0] < 10:
LOG.info('thermal_emiss_one = %s', str(thermal_emiss_one))
@@ -268,7 +267,8 @@ class Calculator(RadTbConverter):
self.derive_rad39_corr(tb_therm, tbco2)
LOG.info("CO2 correction applied...")
else:
- self._rad3x_correction = 1.0
+ self._rad3x_correction = np.float64(1.0)
+ self._rad3x_correction = self._rad3x_correction.astype(tb_nir.dtype)
corrected_thermal_emiss_one = thermal_emiss_one * self._rad3x_correction
nomin = l_nir - corrected_thermal_emiss_one
=====================================
pyspectral/radiance_tb_conversion.py
=====================================
@@ -201,15 +201,16 @@ class RadTbConverter(object):
scale = 1.0
if lut:
+ lut_radiance = lut['radiance'].astype(tb_.dtype)
ntb = (tb_ * self.tb_scale).astype('int16')
start = int(lut['tb'][0] * self.tb_scale)
retv = {}
- bounds = 0, lut['radiance'].shape[0] - 1
+ bounds = 0, lut_radiance.shape[0] - 1
index = (ntb - start).clip(bounds[0], bounds[1])
try:
- retv['radiance'] = index.map_blocks(self._getitem, lut['radiance'], dtype=lut['radiance'].dtype)
+ retv['radiance'] = index.map_blocks(self._getitem, lut_radiance, dtype=lut_radiance.dtype)
except AttributeError:
- retv['radiance'] = lut['radiance'][index]
+ retv['radiance'] = lut_radiance[index]
try:
retv['radiance'] = retv['radiance'].item()
except (ValueError, AttributeError):
=====================================
pyspectral/solar.py
=====================================
@@ -2,7 +2,7 @@
#
# -*- coding: utf-8 -*-
#
-# Copyright (c) 2013-2022 Pytroll developers
+# Copyright (c) 2013-2023 Pytroll developers
#
#
# This program is free software: you can redistribute it and/or modify
@@ -26,17 +26,16 @@ various instrument bands given their relative spectral response functions
"""
import logging
+from pathlib import Path
import numpy as np
-from pkg_resources import resource_filename
LOG = logging.getLogger(__name__)
# STANDARD SPECTRA from Air Mass Zero: http://rredc.nrel.gov/solar/spectra/am0/
# * 2000 ASTM Standard Extraterrestrial Spectrum Reference E-490-00
# (119.5 - 1,000,000.0 nm)
-TOTAL_IRRADIANCE_SPECTRUM_2000ASTM = resource_filename(__name__,
- 'data/e490_00a.dat')
+TOTAL_IRRADIANCE_SPECTRUM_2000ASTM = Path(__file__).resolve().parent / "data" / "e490_00a.dat"
class SolarIrradianceSpectrum(object):
@@ -48,14 +47,26 @@ class SolarIrradianceSpectrum(object):
in units of W/m^2/micron
"""
- def __init__(self, filename, **options):
+ def __init__(self, filename=TOTAL_IRRADIANCE_SPECTRUM_2000ASTM, **options):
"""Initialize the top of atmosphere solar irradiance spectrum object from file.
+ By default, this will use the following spectra:
+ 2000 ASTM Standard Extraterrestrial Spectrum Reference E-490-00
+
+ To use a different spectra, specify the `filename` when initialising the class.
+
Input:
- filename: Filename of the solar irradiance spectrum
- dlambda:
- Delta wavelength: the step in wavelength defining the resolution on
- which to integrate/convolute.
+ filename: Filename of the solar irradiance spectrum (default: 2000 ASTM)
+ options:
+ dlambda:
+ Delta wavelength: the step in wavelength defining the resolution on
+ which to integrate/convolute.
+ Default is 0.005 if 'wavespace' is 'wavelength' and 2.0 if 'wavenumber'.
+ wavespace:
+ It is possible to specify if the solar irradiance spectrum should
+ be given in terms of wavelength (default) or in terms of
+ wavenumber. If the latter is desired 'wavespace' should be set to
+ 'wavenumber'.
"""
self.wavelength = None
=====================================
pyspectral/tests/test_blackbody.py
=====================================
@@ -19,12 +19,13 @@
"""Unit testing the Blackbody/Plack radiation derivation."""
-import unittest
-
+import dask
+import dask.array as da
import numpy as np
+import pytest
from pyspectral.blackbody import blackbody, blackbody_rad2temp, blackbody_wn, blackbody_wn_rad2temp
-from pyspectral.tests.unittest_helpers import assertNumpyArraysEqual
+from pyspectral.tests.unittest_helpers import ComputeCountingScheduler
RAD_11MICRON_300KELVIN = 9573176.935507433
RAD_11MICRON_301KELVIN = 9714686.576498277
@@ -36,92 +37,65 @@ WN_RAD_11MICRON_301KELVIN = 0.00117547716523
__unittest = True
-class CustomScheduler(object):
- """Custom dask scheduler that raises an exception if dask is computed too many times."""
-
- def __init__(self, max_computes=1):
- """Set starting and maximum compute counts."""
- self.max_computes = max_computes
- self.total_computes = 0
-
- def __call__(self, dsk, keys, **kwargs):
- """Compute dask task and keep track of number of times we do so."""
- import dask
- self.total_computes += 1
- if self.total_computes > self.max_computes:
- raise RuntimeError("Too many dask computations were scheduled: {}".format(self.total_computes))
- return dask.get(dsk, keys, **kwargs)
-
-
-class TestBlackbody(unittest.TestCase):
+class TestBlackbody:
"""Unit testing the blackbody function."""
def test_blackbody(self):
"""Calculate the blackbody radiation from wavelengths and temperatures."""
wavel = 11. * 1E-6
- black = blackbody((wavel, ), [300., 301])
- self.assertEqual(black.shape[0], 2)
- self.assertAlmostEqual(black[0], RAD_11MICRON_300KELVIN)
- self.assertAlmostEqual(black[1], RAD_11MICRON_301KELVIN)
-
- temp1 = blackbody_rad2temp(wavel, black[0])
- self.assertAlmostEqual(temp1, 300.0, 4)
- temp2 = blackbody_rad2temp(wavel, black[1])
- self.assertAlmostEqual(temp2, 301.0, 4)
+ black = blackbody(wavel, 300.)
+ assert black == pytest.approx(RAD_11MICRON_300KELVIN)
+ temp1 = blackbody_rad2temp(wavel, black)
+ assert temp1 == pytest.approx(300.0, 4)
- black = blackbody(13. * 1E-6, 200.)
- self.assertTrue(np.isscalar(black))
+ black = blackbody(wavel, 301.)
+ assert black == pytest.approx(RAD_11MICRON_301KELVIN)
+ temp2 = blackbody_rad2temp(wavel, black)
+ assert temp2 == pytest.approx(301.0, 4)
tb_therm = np.array([[300., 301], [299, 298], [279, 286]])
- black = blackbody((10. * 1E-6, 11.e-6), tb_therm)
- self.assertIsInstance(black, np.ndarray)
+ black = blackbody(10. * 1E-6, tb_therm)
+ assert isinstance(black, np.ndarray)
- tb_therm = np.array([[300., 301], [0., 298], [279, 286]])
- black = blackbody((10. * 1E-6, 11.e-6), tb_therm)
- self.assertIsInstance(black, np.ndarray)
-
- def test_blackbody_dask(self):
+ def test_blackbody_dask_wave_tuple(self):
"""Calculate the blackbody radiation from wavelengths and temperatures with dask arrays."""
- import dask
- import dask.array as da
- tb_therm = da.from_array([[300., 301], [299, 298], [279, 286]], chunks=2)
- with dask.config.set(scheduler=CustomScheduler(0)):
- black = blackbody((10. * 1E-6, 11.e-6), tb_therm)
- self.assertIsInstance(black, da.Array)
-
- tb_therm = da.from_array([[300., 301], [0., 298], [279, 286]], chunks=2)
- with dask.config.set(scheduler=CustomScheduler(0)):
- black = blackbody((10. * 1E-6, 11.e-6), tb_therm)
- self.assertIsInstance(black, da.Array)
+ tb_therm = da.array([[300., 301], [299, 298], [279, 286]])
+ with dask.config.set(scheduler=ComputeCountingScheduler(0)):
+ black = blackbody(10. * 1E-6, tb_therm)
+ assert isinstance(black, da.Array)
+ assert black.dtype == np.float64
+
+ @pytest.mark.parametrize("dtype", (np.float32, np.float64, float))
+ def test_blackbody_dask_wave_array(self, dtype):
+ """Test blackbody calculations with dask arrays as inputs."""
+ tb_therm = da.array([[300., 301], [0., 298], [279, 286]], dtype=dtype)
+ with dask.config.set(scheduler=ComputeCountingScheduler(0)):
+ black = blackbody(da.array([10. * 1E-6, 11.e-6], dtype=dtype), tb_therm)
+ assert isinstance(black, da.Array)
+ assert black.dtype == dtype
def test_blackbody_wn(self):
"""Calculate the blackbody radiation from wavenumbers and temperatures."""
wavenumber = 90909.1 # 11 micron band
- black = blackbody_wn((wavenumber, ), [300., 301])
- self.assertEqual(black.shape[0], 2)
- self.assertAlmostEqual(black[0], WN_RAD_11MICRON_300KELVIN)
- self.assertAlmostEqual(black[1], WN_RAD_11MICRON_301KELVIN)
+ black = blackbody_wn(wavenumber, 300.)
+ assert black == pytest.approx(WN_RAD_11MICRON_300KELVIN)
+ temp1 = blackbody_wn_rad2temp(wavenumber, black)
+ assert temp1 == pytest.approx(300.0, 4)
- temp1 = blackbody_wn_rad2temp(wavenumber, black[0])
- self.assertAlmostEqual(temp1, 300.0, 4)
- temp2 = blackbody_wn_rad2temp(wavenumber, black[1])
- self.assertAlmostEqual(temp2, 301.0, 4)
+ black = blackbody_wn(wavenumber, 301.)
+ assert black == pytest.approx(WN_RAD_11MICRON_301KELVIN)
+ temp2 = blackbody_wn_rad2temp(wavenumber, black)
+ assert temp2 == pytest.approx(301.0, 4)
- t__ = blackbody_wn_rad2temp(wavenumber, [0.001, 0.0009])
+ t__ = blackbody_wn_rad2temp(wavenumber, np.array([0.001, 0.0009]))
expected = [290.3276916, 283.76115441]
- self.assertAlmostEqual(t__[0], expected[0])
- self.assertAlmostEqual(t__[1], expected[1])
+ np.testing.assert_allclose(t__, expected)
radiances = np.array([0.001, 0.0009, 0.0012, 0.0018]).reshape(2, 2)
t__ = blackbody_wn_rad2temp(wavenumber, radiances)
expected = np.array([290.3276916, 283.76115441,
302.4181330, 333.1414164]).reshape(2, 2)
- self.assertAlmostEqual(t__[1, 1], expected[1, 1], 5)
- self.assertAlmostEqual(t__[0, 0], expected[0, 0], 5)
- self.assertAlmostEqual(t__[0, 1], expected[0, 1], 5)
- self.assertAlmostEqual(t__[1, 0], expected[1, 0], 5)
-
- assertNumpyArraysEqual(t__, expected)
+ np.testing.assert_allclose(t__, expected)
def test_blackbody_wn_dask(self):
"""Test that blackbody rad2temp preserves dask arrays."""
@@ -129,15 +103,10 @@ class TestBlackbody(unittest.TestCase):
import dask.array as da
wavenumber = 90909.1 # 11 micron band
radiances = da.from_array([0.001, 0.0009, 0.0012, 0.0018], chunks=2).reshape(2, 2)
- with dask.config.set(scheduler=CustomScheduler(0)):
+ with dask.config.set(scheduler=ComputeCountingScheduler(0)):
t__ = blackbody_wn_rad2temp(wavenumber, radiances)
- self.assertIsInstance(t__, da.Array)
+ assert isinstance(t__, da.Array)
t__ = t__.compute()
expected = np.array([290.3276916, 283.76115441,
302.4181330, 333.1414164]).reshape(2, 2)
- self.assertAlmostEqual(t__[1, 1], expected[1, 1], 5)
- self.assertAlmostEqual(t__[0, 0], expected[0, 0], 5)
- self.assertAlmostEqual(t__[0, 1], expected[0, 1], 5)
- self.assertAlmostEqual(t__[1, 0], expected[1, 0], 5)
-
- assertNumpyArraysEqual(t__, expected)
+ np.testing.assert_allclose(t__, expected)
=====================================
pyspectral/tests/test_rad_tb_conversions.py
=====================================
@@ -19,24 +19,17 @@
"""Testing the radiance to brightness temperature conversion."""
-import sys
+import unittest
+import warnings
+from unittest.mock import patch
import numpy as np
+import pytest
-from pyspectral.radiance_tb_conversion import RadTbConverter, SeviriRadTbConverter
+from pyspectral.radiance_tb_conversion import RadTbConverter, SeviriRadTbConverter, radiance2tb
from pyspectral.utils import get_central_wave
-if sys.version_info < (2, 7):
- import unittest2 as unittest
-else:
- import unittest
-if sys.version_info < (3,):
- from mock import patch
-else:
- from unittest.mock import patch
-
-
-TEST_TBS = np.array([200., 270., 300., 302., 350.], dtype='float32')
+TEST_TBS = np.array([200., 270., 300., 302., 350.])
TRUE_RADS = np.array([856.937353205, 117420.385297,
479464.582505, 521412.9511, 2928735.18944],
@@ -45,8 +38,7 @@ TRUE_RADS_SEVIRI = np.array([2.391091e-08,
2.559173e-06,
9.797091e-06,
1.061431e-05,
- 5.531423e-05],
- dtype='float64')
+ 5.531423e-05])
TEST_RSR = {'20': {}}
TEST_RSR['20']['det-1'] = {}
@@ -231,10 +223,10 @@ class RSRTestDataModis(object):
self.rsr = TEST_RSR
-class TestSeviriConversions(unittest.TestCase):
+class TestSeviriConversions:
"""Testing the conversions between radiances and brightness temperatures."""
- def setUp(self):
+ def setup_method(self):
"""Set up."""
with patch('pyspectral.radiance_tb_conversion.RelativeSpectralResponse') as mymock:
instance = mymock.return_value
@@ -247,32 +239,38 @@ class TestSeviriConversions(unittest.TestCase):
self.sev2 = SeviriRadTbConverter('Meteosat-9', 'IR3.9')
- def test_rad2tb(self):
+ @pytest.mark.parametrize("dtype", (np.float32, np.float64, float))
+ def test_rad2tb(self, dtype):
"""Unit testing the radiance to brightness temperature conversion."""
- res = self.sev1.tb2radiance(TEST_TBS, lut=False)
- self.assertTrue(np.allclose(TRUE_RADS_SEVIRI, res['radiance']))
+ res = self.sev1.tb2radiance(TEST_TBS.astype(dtype), lut=False)
+ assert res['radiance'].dtype == dtype
+ np.testing.assert_allclose(TRUE_RADS_SEVIRI.astype(dtype), res['radiance'], atol=1e-8)
- def test_conversion_simple(self):
+ @pytest.mark.parametrize("dtype", (np.float32, np.float64, float))
+ def test_conversion_simple(self, dtype):
"""Test the conversion based on the non-linear approximation (SEVIRI).
Test the tb2radiance function to convert radiances to Tb's
using tabulated coefficients based on a non-linear approximation
"""
- retv = self.sev2.tb2radiance(TEST_TBS)
+ retv = self.sev2.tb2radiance(TEST_TBS.astype(dtype))
rads = retv['radiance']
# Units space = wavenumber (cm-1):
- tbs = self.sev2.radiance2tb(rads)
- self.assertTrue(np.allclose(TEST_TBS, tbs))
+ tbs = self.sev2.radiance2tb(rads.astype(dtype))
+ assert tbs.dtype == dtype
+ np.testing.assert_allclose(TEST_TBS.astype(dtype), tbs, rtol=1e-6)
np.random.seed()
tbs1 = 200.0 + np.random.random(50) * 150.0
- retv = self.sev2.tb2radiance(tbs1)
+ retv = self.sev2.tb2radiance(tbs1.astype(dtype))
rads = retv['radiance']
tbs = self.sev2.radiance2tb(rads)
- self.assertTrue(np.allclose(tbs1, tbs))
+ assert tbs.dtype == dtype
+ np.testing.assert_allclose(tbs1, tbs, rtol=1e-6)
- def test_conversions_methods(self):
+ @pytest.mark.parametrize("dtype", (np.float32, np.float64, float))
+ def test_conversions_methods(self, dtype):
"""Test the conversion methods.
Using the two diferent conversion methods to verify that they give
@@ -281,12 +279,14 @@ class TestSeviriConversions(unittest.TestCase):
"""
# Units space = wavenumber (cm-1):
- retv2 = self.sev2.tb2radiance(TEST_TBS)
- retv1 = self.sev1.tb2radiance(TEST_TBS)
+ retv2 = self.sev2.tb2radiance(TEST_TBS.astype(dtype))
+ retv1 = self.sev1.tb2radiance(TEST_TBS.astype(dtype))
rads1 = retv1['radiance']
rads2 = retv2['radiance']
- self.assertTrue(np.allclose(rads1, rads2))
+ assert rads1.dtype == dtype
+ assert rads2.dtype == dtype
+ np.testing.assert_allclose(rads1, rads2, atol=1e-8)
class TestRadTbConversions(unittest.TestCase):
@@ -327,33 +327,106 @@ class TestRadTbConversions(unittest.TestCase):
def test_rad2tb(self):
"""Unit testing the radiance to brightness temperature conversion."""
res = self.modis.tb2radiance(TEST_TBS, lut=False)
- self.assertTrue(np.allclose(TRUE_RADS, res['radiance']))
+ np.testing.assert_allclose(TRUE_RADS, res['radiance'], rtol=1e-5, atol=1e-8)
res = self.modis2.tb2radiance(TEST_TBS, lut=False)
- self.assertTrue(np.allclose(TRUE_RADS, res['radiance']))
+ np.testing.assert_allclose(TRUE_RADS, res['radiance'], rtol=1e-5, atol=1e-8)
rad = res['radiance']
tbs = self.modis.radiance2tb(rad)
- self.assertTrue(np.allclose(TEST_TBS, tbs, atol=0.25))
+ np.testing.assert_allclose(TEST_TBS, tbs, atol=0.25)
res = self.modis.tb2radiance(TEST_TBS, lut=False, normalized=False)
integral = self.modis.rsr_integral
- self.assertTrue(np.allclose(TRUE_RADS * integral, res['radiance']))
+ np.testing.assert_allclose(TRUE_RADS * integral, res['radiance'], rtol=1e-5, atol=1e-8)
res = self.modis.tb2radiance(237., lut=False)
- self.assertAlmostEqual(16570.579551068, res['radiance'])
+ assert res['radiance'] == pytest.approx(16570.579551068)
res = self.modis.tb2radiance(277., lut=False)
- self.assertAlmostEqual(167544.39368663222, res['radiance'])
+ assert res['radiance'] == pytest.approx(167544.39368663222)
res = self.modis.tb2radiance(1.1, lut=False)
- self.assertAlmostEqual(0.0, res['radiance'])
+ assert res['radiance'] == pytest.approx(0.0)
res = self.modis.tb2radiance(11.1, lut=False)
- self.assertAlmostEqual(0.0, res['radiance'])
+ assert res['radiance'] == pytest.approx(0.0)
res = self.modis.tb2radiance(100.1, lut=False)
- self.assertAlmostEqual(5.3940515573e-06, res['radiance'])
+ assert res['radiance'] == pytest.approx(5.3940515573e-06, abs=1e-7)
res = self.modis.tb2radiance(200.1, lut=False)
- self.assertAlmostEqual(865.09759706, res['radiance'])
+ assert res['radiance'] == pytest.approx(865.09759706)
+
+
+def test_rad2tb_types():
+ """Test radiance to brightness temperature conversion preserves shape and type."""
+ # 1d rads
+ rad = TRUE_RADS
+ central_wavelength = TEST_RSR['20']['det-1']['central_wavelength'] * 1e-6
+ tbs = radiance2tb(rad, central_wavelength)
+ np.testing.assert_allclose(tbs, TEST_TBS, atol=0.25)
+
+ # 2d rads
+ rad_2d = np.vstack((TRUE_RADS, TRUE_RADS))
+ tbs = radiance2tb(rad_2d, central_wavelength)
+ assert tbs.shape == rad_2d.shape
+
+ tbs = radiance2tb(rad_2d.astype(np.float32), central_wavelength)
+ assert tbs.dtype == np.float32
+
+
+def test_rad2tb_xarray_types():
+ """Test radiance to brightness temperature conversion works with xarray."""
+ xr = pytest.importorskip("xarray")
+
+ central_wavelength = 3.78e-6
+
+ rad_2d = np.vstack((TRUE_RADS, TRUE_RADS))
+ rad = xr.DataArray(rad_2d, dims=["y", "x"])
+ tbs = radiance2tb(rad, central_wavelength)
+ assert isinstance(tbs, xr.DataArray)
+
+ rad = xr.DataArray(rad_2d.astype(np.float32), dims=["y", "x"])
+ tbs = radiance2tb(rad, central_wavelength)
+ assert tbs.dtype == np.float32
+
+
+def test_rad2tb_does_not_warn_about_invalid_log_values():
+ """Test that radiance to temp does not warn about invalid log values."""
+ xr = pytest.importorskip("xarray")
+
+ central_wavelength = 3.78e-6
+
+ rad = xr.DataArray(np.array([-1, 2, 3]).astype(np.float32), dims=["x"])
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ tbs = radiance2tb(rad, central_wavelength)
+ assert np.isnan(tbs[0])
+
+ da = pytest.importorskip("dask.array")
+
+ rad = da.from_array(np.array([-1, 2, 3]), chunks=2).astype(np.float32)
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ tbs = da.map_blocks(radiance2tb, rad, wavelength=central_wavelength)
+ assert np.isnan(tbs[0])
+
+ rad = xr.DataArray(da.from_array(np.array([-1, 2, 3]), chunks=2).astype(np.float32), dims=["x"])
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ tbs = xr.map_blocks(radiance2tb, rad, kwargs=dict(wavelength=central_wavelength))
+ assert np.isnan(tbs[0])
+
+ rad = da.from_array(np.array([-1, 2, 3]), chunks=2).astype(np.float32)
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ tbs = radiance2tb(rad, central_wavelength)
+ assert np.isnan(tbs[0])
+
+ rad = xr.DataArray(da.from_array(np.array([-1, 2, 3]), chunks=2).astype(np.float32), dims=["x"])
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ tbs = radiance2tb(rad, central_wavelength)
+ assert np.isnan(tbs[0])
+ assert isinstance(tbs, xr.DataArray)
=====================================
pyspectral/tests/test_rayleigh.py
=====================================
@@ -219,7 +219,7 @@ class TestRayleighDask:
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()
+ rsr_obj.assert_not_called()
class TestRayleigh:
@@ -385,7 +385,7 @@ class TestRayleigh:
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()
+ rsr_obj.assert_not_called()
@patch('pyspectral.rayleigh.da', None)
def test_get_reflectance_wvl_outside_range(self, fake_lut_hdf5):
@@ -402,7 +402,7 @@ class TestRayleigh:
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()
+ rsr_obj.assert_not_called()
def test_get_reflectance_no_lut(self, fake_lut_hdf5):
"""Test that missing a LUT causes an exception.."""
=====================================
pyspectral/tests/test_reflectance.py
=====================================
@@ -203,13 +203,25 @@ class TestReflectance(unittest.TestCase):
refl = refl37.reflectance_from_tbs(sunz, tb3, tb4)
self.assertTrue(hasattr(refl, 'mask'))
+ sunz = np.array([80.], dtype=np.float32)
+ tb3 = np.array([295.], dtype=np.float32)
+ tb4 = np.array([282.], dtype=np.float32)
+ refl = refl37.reflectance_from_tbs(sunz, tb3, tb4)
+ np.testing.assert_allclose(refl, np.array([0.45249779], np.float32))
+ assert refl.dtype == np.float32
+
try:
import dask.array as da
- sunz = da.from_array([50.], chunks=10)
- tb3 = da.from_array([300.], chunks=10)
- tb4 = da.from_array([285.], chunks=10)
- refl = refl37.reflectance_from_tbs(sunz, tb3, tb4)
- self.assertTrue(hasattr(refl, 'compute'))
+ import dask.config
+
+ from pyspectral.tests.unittest_helpers import ComputeCountingScheduler
+
+ with dask.config.set(scheduler=ComputeCountingScheduler(max_computes=0)):
+ sunz = da.from_array([50.], chunks=10)
+ tb3 = da.from_array([300.], chunks=10)
+ tb4 = da.from_array([285.], chunks=10)
+ refl = refl37.reflectance_from_tbs(sunz, tb3, tb4)
+ self.assertTrue(hasattr(refl, 'compute'))
except ImportError:
pass
=====================================
pyspectral/tests/test_solarflux.py
=====================================
@@ -24,7 +24,7 @@ import unittest
import numpy as np
-from pyspectral.solar import TOTAL_IRRADIANCE_SPECTRUM_2000ASTM, SolarIrradianceSpectrum
+from pyspectral.solar import SolarIrradianceSpectrum
TEST_RSR = {}
TEST_RSR['det-1'] = {}
@@ -59,8 +59,7 @@ class TestSolarflux(unittest.TestCase):
def setUp(self):
"""Set up."""
- self.solar_irr = SolarIrradianceSpectrum(TOTAL_IRRADIANCE_SPECTRUM_2000ASTM,
- dlambda=0.005)
+ self.solar_irr = SolarIrradianceSpectrum(dlambda=0.005)
self.rsr = TEST_RSR
def test_read(self):
=====================================
pyspectral/tests/unittest_helpers.py
=====================================
@@ -28,3 +28,21 @@ def assertNumpyArraysEqual(self, other):
raise AssertionError("Shapes don't match")
if not np.allclose(self, other):
raise AssertionError("Elements don't match!")
+
+
+class ComputeCountingScheduler:
+ """Scheduler raising an exception if data are computed too many times."""
+
+ def __init__(self, max_computes=1):
+ """Set starting and maximum compute counts."""
+ self.max_computes = max_computes
+ self.total_computes = 0
+
+ def __call__(self, dsk, keys, **kwargs):
+ """Compute dask task and keep track of number of times we do so."""
+ import dask
+ self.total_computes += 1
+ if self.total_computes > self.max_computes:
+ raise RuntimeError("Too many dask computations were scheduled: "
+ "{}".format(self.total_computes))
+ return dask.get(dsk, keys, **kwargs)
=====================================
pyspectral/utils.py
=====================================
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
-# Copyright (c) 2014-2022 Pytroll developers
+# Copyright (c) 2014-2023 Pytroll developers
#
#
# This program is free software: you can redistribute it and/or modify
@@ -23,6 +23,8 @@ import logging
import os
import tarfile
import warnings
+from functools import wraps
+from inspect import getfullargspec
import numpy as np
import requests
@@ -91,17 +93,18 @@ INSTRUMENTS = {'Envisat': 'aatsr',
'NOAA-21': 'viirs',
'Suomi-NPP': 'viirs',
'FY-3B': 'virr',
- 'FY-3C': 'virr'}
+ 'FY-3C': 'virr',
+ 'DSCOVR': 'epic'}
INSTRUMENT_TRANSLATION_DASH2SLASH = {'avhrr-1': 'avhrr/1',
'avhrr-2': 'avhrr/2',
'avhrr-3': 'avhrr/3'}
-HTTP_PYSPECTRAL_RSR = "https://zenodo.org/record/7311143/files/pyspectral_rsr_data.tgz"
+HTTP_PYSPECTRAL_RSR = "https://zenodo.org/records/10029746/files/pyspectral_rsr_data.tgz"
RSR_DATA_VERSION_FILENAME = "PYSPECTRAL_RSR_VERSION"
-RSR_DATA_VERSION = "v1.2.2"
+RSR_DATA_VERSION = "v1.2.4"
ATM_CORRECTION_LUT_VERSION = {}
ATM_CORRECTION_LUT_VERSION['antarctic_aerosol'] = {'version': 'v1.0.1',
@@ -579,3 +582,35 @@ def are_instruments_identical(name1, name2):
if name1 == INSTRUMENT_TRANSLATION_DASH2SLASH.get(name2):
return True
return False
+
+
+def use_map_blocks_on(argument_to_run_map_blocks_on):
+ """Use map blocks on a given argument.
+
+ This decorator assumes only one of the arguments of the decorated function is chunked.
+ """
+ def decorator(f):
+ argspec = getfullargspec(f)
+ argument_index = argspec.args.index(argument_to_run_map_blocks_on)
+
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ array = args[argument_index]
+ chunks = getattr(array, "chunks", None)
+ if chunks is None:
+ return f(*args, **kwargs)
+ import dask.array as da
+ import xarray as xr
+ if isinstance(array, da.Array):
+ return da.map_blocks(f, *args, **kwargs)
+ elif isinstance(array, xr.DataArray):
+ new_array = array.copy()
+ new_args = list(args)
+ new_args[argument_index] = array.data
+ new_data = da.map_blocks(f, *new_args, **kwargs)
+ new_array.data = new_data
+ return new_array
+ else:
+ raise NotImplementedError(f"Don't know how to map_blocks on {type(array)}")
+ return wrapper
+ return decorator
=====================================
rsr_convert_scripts/epic_reader.py
=====================================
@@ -0,0 +1,120 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2023 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 DSCOVR-EPIC spectral response functions.
+
+Data from the NASA Goddard website:
+https://avdc.gsfc.nasa.gov/pub/DSCOVR/EPIC_Filter_Data/
+
+"""
+
+import logging
+import os
+
+import numpy as np
+import pandas as pd
+
+from pyspectral.raw_reader import InstrumentRSR
+from pyspectral.utils import convert2hdf5 as tohdf5
+
+LOG = logging.getLogger(__name__)
+
+
+EPIC_BAND_NAMES = {'B317': 'T(317.5) %',
+ 'B325': 'T(325) %',
+ 'B340': 'T(340) %',
+ 'B388': 'T(388) %',
+ 'B443': 'T(443) %',
+ 'B551': 'T(551) %',
+ 'B680': 'T(680) %',
+ 'B688': 'T(687.75) %',
+ 'B764': 'T(764) %',
+ 'B780': 'T(779.5) %'}
+
+#: 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 EpicRSR(InstrumentRSR):
+ """Container for the DSCOVR EPIC relative spectral response data."""
+
+ def __init__(self, bandname, platform_name):
+ """Initialize the EPIC RSR class."""
+ super(EpicRSR, self).__init__(
+ bandname, platform_name, EPIC_BAND_NAMES.keys())
+
+ self.instrument = 'epic'
+ self._get_options_from_config()
+
+ LOG.debug("Filename: %s", str(self.path))
+ if os.path.exists(self.path):
+ 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 EPIC relative spectral responses."""
+ df1 = pd.read_excel(self.path,
+ sheet_name='Data',
+ skiprows=4,
+ engine='openpyxl')
+ # Remove empty row from the data
+ df1.drop(df1.index[0], inplace=True)
+ # Column names don't match band names - so we use a dict to find correct
+ # columns. We also need to find the column with the wavelength data.
+ # This is the column before the actual RSR data for each band.
+ col_pos = df1.columns.get_loc(EPIC_BAND_NAMES[self.bandname])
+ wvl_data = df1.iloc[:, col_pos - 1]
+ srf_data = df1.iloc[:, col_pos]
+
+ # Not all bands have an identical number of RSR points, so we need to
+ # remove NaNs from the data.
+ wvl_data.dropna(inplace=True)
+ srf_data.dropna(inplace=True)
+
+ # Data is in nanometers, so we need to convert to micrometers.
+ self.rsr = {'wavelength': np.array(wvl_data) / 1000,
+ 'response': np.array(srf_data) / np.nanmax(srf_data)}
+
+
+if __name__ == "__main__":
+ import sys
+ LOG = logging.getLogger('epic_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)
+
+ for platform_name in ['DSCOVR', ]:
+ tohdf5(EpicRSR, platform_name, list(EPIC_BAND_NAMES.keys()))
=====================================
setup.py
=====================================
@@ -82,6 +82,6 @@ setup(name=NAME,
'pyspectral/data/MSG_SEVIRI_Spectral_Response_Characterisation.XLS'])],
test_suite='pyspectral.tests.suite',
tests_require=test_requires,
- python_requires='>=3.7',
+ python_requires='>=3.10',
zip_safe=False,
)
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyspectral/-/compare/d5eaee2927a899611ca76087d60922c1fc025b5c...76573dabf83a9f4dfe9c30eeba7069b22ca95f82
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyspectral/-/compare/d5eaee2927a899611ca76087d60922c1fc025b5c...76573dabf83a9f4dfe9c30eeba7069b22ca95f82
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/20231202/86dfb25c/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list