Completed
Pull Request — develop (#33)
by
unknown
27s
created

Rayleigh.get_reflectance()   C

Complexity

Conditions 7

Size

Total Lines 84

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
cc 7
c 7
b 1
f 0
dl 0
loc 84
rs 5.3611

1 Method

Rating   Name   Duplication   Size   Complexity  
A Rayleigh._do_interp() 0 6 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
4
# Copyright (c) 2016-2018 Pytroll
5
6
# Author(s):
7
8
#   Adam.Dybbroe <[email protected]>
9
#   Martin Raspaud <[email protected]>
10
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
15
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
# GNU General Public License for more details.
20
21
# You should have received a copy of the GNU General Public License
22
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
24
"""Atmospheric correction of shortwave imager bands in the wavelength range 400
25
to 800 nm
26
27
"""
28
29
import logging
30
import os
31
from six import integer_types
32
33
import h5py
34
import numpy as np
35
import dask.array as da
36
from scipy.interpolate import interpn
37
38
from pyspectral.rsr_reader import RelativeSpectralResponse
39
from pyspectral.utils import (INSTRUMENTS, RAYLEIGH_LUT_DIRS,
40
                              AEROSOL_TYPES, ATMOSPHERES,
41
                              BANDNAMES,
42
                              download_luts, get_central_wave,
43
                              get_bandname_from_wavelength)
44
45
LOG = logging.getLogger(__name__)
46
47
WITH_CYTHON = True
48
try:
49
    from geotiepoints.multilinear import MultilinearInterpolator
50
except ImportError:
51
    LOG.warning(
52
        "Couldn't import fast multilinear interpolation with Cython.")
53
    LOG.warning("Check your geotiepoints installation!")
54
    WITH_CYTHON = False
55
56
57
class BandFrequencyOutOfRange(Exception):
58
59
    """Exception when the band frequency is out of the visible range"""
60
    pass
61
62
63
class Rayleigh(object):
64
65
    """Container for the atmospheric correction of satellite imager short
66
    wave bands. Removing background contributions of Rayleigh scattering of
67
    molecules and Mie scattering and absorption by aerosols.
68
69
    """
70
71
    def __init__(self, platform_name, sensor, **kwargs):
72
        self.platform_name = platform_name
73
        self.sensor = sensor
74
        self.coeff_filename = None
75
76
        atm_type = kwargs.get('atmosphere', 'us-standard')
77
        if atm_type not in ATMOSPHERES:
78
            raise AttributeError('Atmosphere type not supported! ' +
79
                                 'Need to be one of {}'.format(str(ATMOSPHERES)))
80
81
        aerosol_type = kwargs.get('aerosol_type', 'marine_clean_aerosol')
82
83
        if aerosol_type not in AEROSOL_TYPES:
84
            raise AttributeError('Aerosol type not supported! ' +
85
                                 'Need to be one of {0}'.format(str(AEROSOL_TYPES)))
86
87
        rayleigh_dir = RAYLEIGH_LUT_DIRS[aerosol_type]
88
89
        if atm_type not in ATMOSPHERES.keys():
90
            LOG.error("Atmosphere type %s not supported", atm_type)
91
92
        LOG.info("Atmosphere chosen: %s", atm_type)
93
94
        # Try fix instrument naming
95
        instr = INSTRUMENTS.get(platform_name, sensor)
96
        if instr != sensor:
97
            sensor = instr
98
            LOG.warning("Inconsistent sensor/satellite input - " +
99
                        "sensor set to %s", sensor)
100
101
        self.sensor = sensor.replace('/', '')
102
103
        ext = atm_type.replace(' ', '_')
104
        lutname = "rayleigh_lut_{0}.h5".format(ext)
105
        self.reflectance_lut_filename = os.path.join(rayleigh_dir, lutname)
106
        if not os.path.exists(self.reflectance_lut_filename):
107
            LOG.warning(
108
                "No lut file %s on disk", self.reflectance_lut_filename)
109
            LOG.info("Will download from internet...")
110
            download_luts(aerosol_type=aerosol_type)
111
112
        if (not os.path.exists(self.reflectance_lut_filename) or
113
                not os.path.isfile(self.reflectance_lut_filename)):
114
            raise IOError('pyspectral file for Rayleigh scattering correction ' +
115
                          'does not exist! Filename = ' +
116
                          str(self.reflectance_lut_filename))
117
118
        LOG.debug('LUT filename: %s', str(self.reflectance_lut_filename))
119
        self._rayl = None
120
        self._wvl_coord = None
121
        self._azid_coord = None
122
        self._satz_sec_coord = None
123
        self._sunz_sec_coord = None
124
125
    def get_effective_wavelength(self, bandname):
126
        """Get the effective wavelength with Rayleigh scattering in mind"""
127
128
        try:
129
            rsr = RelativeSpectralResponse(self.platform_name, self.sensor)
130
        except IOError:
131
            LOG.exception(
132
                "No spectral responses for this platform and sensor: %s %s", self.platform_name, self.sensor)
133
            if isinstance(bandname, (float, integer_types)):
134
                LOG.warning(
135
                    "Effective wavelength is set to the requested band wavelength = %f", bandname)
136
                return bandname
137
            raise
138
139
        if isinstance(bandname, str):
140
            bandname = BANDNAMES.get(self.sensor, BANDNAMES['generic']).get(bandname, bandname)
141
        elif isinstance(bandname, (float, integer_types)):
142
            if not(0.4 < bandname < 0.8):
143
                raise BandFrequencyOutOfRange(
144
                    'Requested band frequency should be between 0.4 and 0.8 microns!')
145
            bandname = get_bandname_from_wavelength(self.sensor, bandname, rsr.rsr)
146
147
        wvl, resp = rsr.rsr[bandname][
148
            'det-1']['wavelength'], rsr.rsr[bandname]['det-1']['response']
149
150
        cwvl = get_central_wave(wvl, resp, weight=1. / wvl**4)
151
        LOG.debug("Band name: %s  Effective wavelength: %f", bandname, cwvl)
152
153
        return cwvl
154
155
    def get_reflectance_lut(self):
156
        """Read the LUT with reflectances as a function of wavelength, satellite zenith
157
        secant, azimuth difference angle, and sun zenith secant
158
159
        """
160
        if self._rayl is None:
161
            lut_vars = get_reflectance_lut(self.reflectance_lut_filename)
162
            self._rayl = lut_vars[0]
163
            # wvl_coord is used in a lot of non-dask functions, keep in memory
164
            self._wvl_coord = lut_vars[1].persist()
165
            self._azid_coord = lut_vars[2]
166
            self._satz_sec_coord = lut_vars[3]
167
            self._sunz_sec_coord = lut_vars[4]
168
        return self._rayl, self._wvl_coord, self._azid_coord,\
169
            self._satz_sec_coord, self._sunz_sec_coord
170
171
    def get_reflectance(self, sun_zenith, sat_zenith, azidiff, bandname,
172
                        redband=None):
173
        """Get the reflectance from the three sun-sat angles."""
174
        # Get wavelength in nm for band:
175
        wvl = self.get_effective_wavelength(bandname) * 1000.0
176
        rayl, wvl_coord, azid_coord, satz_sec_coord, sunz_sec_coord = \
177
            self.get_reflectance_lut()
178
        wvl_coord = wvl_coord.persist()
179
180
        clip_angle = da.rad2deg(da.arccos(1. / sunz_sec_coord.max()))
181
        sun_zenith = da.clip(da.asarray(sun_zenith), 0, clip_angle)
182
        sunzsec = 1. / da.cos(da.deg2rad(sun_zenith))
183
        clip_angle = da.rad2deg(da.arccos(1. / satz_sec_coord.max()))
184
        sat_zenith = da.clip(da.asarray(sat_zenith), 0, clip_angle)
185
        satzsec = 1. / da.cos(da.deg2rad(da.asarray(sat_zenith)))
186
187
        shape = sun_zenith.shape
188
189
        if not(wvl_coord.min() < wvl < wvl_coord.max()):
190
            LOG.warning(
191
                "Effective wavelength for band %s outside 400-800 nm range!",
192
                str(bandname))
193
            LOG.info(
194
                "Set the rayleigh/aerosol reflectance contribution to zero!")
195
            chunks = sun_zenith.chunks if redband is None else redband.chunks
196
            return da.zeros(shape, chunks=chunks)
197
198
        idx = np.searchsorted(wvl_coord, wvl)
199
        wvl1 = wvl_coord[idx - 1]
200
        wvl2 = wvl_coord[idx]
201
202
        fac = (wvl2 - wvl) / (wvl2 - wvl1)
203
204
        # FIXME: Can we do this right before interpolating instead?
205
        raylwvl = fac * rayl[idx - 1, :, :, :] + (1 - fac) * rayl[idx, :, :, :]
206
207
        import time
208
        tic = time.time()
209
210
        if WITH_CYTHON:
211
            smin = [sunz_sec_coord[0], azid_coord[0], satz_sec_coord[0]]
212
            smax = [sunz_sec_coord[-1], azid_coord[-1], satz_sec_coord[-1]]
213
            orders = [
214
                len(sunz_sec_coord), len(azid_coord), len(satz_sec_coord)]
215
            minterp = MultilinearInterpolator(smin, smax, orders)
216
217
            f_3d_grid = raylwvl
218
            minterp.set_values(da.atleast_2d(f_3d_grid.ravel()))
219
220
            def _do_interp(minterp, sunzsec, azidiff, satzsec):
221
                interp_points2 = np.vstack((sunzsec.ravel(),
222
                                            180 - azidiff.ravel(),
223
                                            satzsec.ravel()))
224
                res = minterp(interp_points2)
225
                return res.reshape(sunzsec.shape)
226
227
            ipn = da.map_blocks(_do_interp, minterp, sunzsec, azidiff,
228
                                satzsec, dtype=raylwvl.dtype,
229
                                chunks=azidiff.chunks)
230
        else:
231
            # FIXME: Untested
232
            def _do_interp(sunz_sec_coord, azid_coord, satz_sec_coord,
233
                           raylwvl):
234
                interp_points = np.dstack((sunzsec,
235
                                           180. - azidiff,
236
                                           satzsec))
237
238
                return interpn(
239
                    (sunz_sec_coord, azid_coord, satz_sec_coord),
240
                    raylwvl[:, :, :], interp_points)
241
            ipn = da.map_blocks(_do_interp, sunz_sec_coord, azid_coord,
242
                                satz_sec_coord, raylwvl[:, :, :],
243
                                dtype=raylwvl.dtype,
244
                                chunks=azidiff.chunks)
245
246
        LOG.debug("Time - Interpolation: {0:f}".format(time.time() - tic))
247
248
        ipn *= 100
249
        res = ipn
250
        if redband is not None:
251
            res = da.where(da.less(redband, 20.), res,
252
                           (1 - (redband - 20) / 80) * res)
253
254
        return da.clip(res, 0, 100)
255
256
257
def get_reflectance_lut(filename):
258
    """Read the LUT with reflectances as a function of wavelength, satellite
259
    zenith secant, azimuth difference angle, and sun zenith secant
260
261
    """
262
263
    h5f = h5py.File(filename, 'r')
264
265
    tab = da.from_array(h5f['reflectance'],  chunks=(10, 10, 10, 10))
266
    wvl = da.from_array(h5f['wavelengths'],  chunks=(100,))
267
    azidiff = da.from_array(h5f['azimuth_difference'], chunks=(1000,))
268
    satellite_zenith_secant = da.from_array(h5f['satellite_zenith_secant'],
269
                                            chunks=(1000,))
270
    sun_zenith_secant = da.from_array(h5f['sun_zenith_secant'],
271
                                      chunks=(1000,))
272
273
    return tab, wvl, azidiff, satellite_zenith_secant, sun_zenith_secant
274
275
276
if __name__ == "__main__":
277
    SHAPE = (1000, 3000)
278
    NDIM = SHAPE[0] * SHAPE[1]
279
    SUNZ = np.ma.arange(
280
        NDIM / 2, NDIM + NDIM / 2).reshape(SHAPE) * 60. / float(NDIM)
281
    SATZ = np.ma.arange(NDIM).reshape(SHAPE) * 60. / float(NDIM)
282
    AZIDIFF = np.ma.arange(NDIM).reshape(SHAPE) * 179.9 / float(NDIM)
283
    rfl = this.get_reflectance(SUNZ, SATZ, AZIDIFF, 'M4')
284