Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Harmonize calibration in SEVIRI readers #1457

Merged
merged 27 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3569be4
Add comprehensive calib. test for HRIT reader
sfinkens Nov 27, 2020
3fe18a3
Generalize calibration test
sfinkens Nov 30, 2020
f3e30e1
Add comprehensive calib. test for Native reader
sfinkens Nov 30, 2020
84ff1f2
Add comprehensive calib. test for netCDF reader
sfinkens Nov 30, 2020
ab0f489
Harmonize calib. of SEVIRI HRIT and Native readers
sfinkens Nov 27, 2020
0e5a01c
Separate calibration from file handler
sfinkens Nov 27, 2020
18a6c16
Update SEVIRI nc reader as well
sfinkens Nov 27, 2020
2e45029
Fix netCDF reader
sfinkens Nov 30, 2020
c188edf
Enable all tests for Native and NC readers
sfinkens Nov 30, 2020
eb6691a
Remove old calibration tests
sfinkens Dec 1, 2020
89b48ce
Add more calibration tests
sfinkens Dec 1, 2020
afb3d90
Repair calibration algorithm tests
sfinkens Dec 1, 2020
6f51d30
Move calibration algorithm to separate class
sfinkens Dec 1, 2020
d63017d
Balance level of abstraction in calibrate
sfinkens Dec 2, 2020
6b00bf0
Test data types in calibration
sfinkens Dec 2, 2020
90f3eb9
Remove duplicate thresholding
sfinkens Dec 2, 2020
ce18171
Privatize attributes of calibration classes
sfinkens Dec 2, 2020
e9fd289
Factorize creation of coefficient dictionary
sfinkens Dec 2, 2020
996cab2
Document meaning of line quality flags
sfinkens Dec 2, 2020
00a8863
Merge branch 'master' into feature-align-seviri-readers
sfinkens Dec 15, 2020
31419c0
Fix HRV coefficient selection in HRIT
sfinkens Dec 15, 2020
916b297
Improve check for GSICS availability
sfinkens Dec 15, 2020
dfb9d73
Add common base class for initialization
sfinkens Dec 15, 2020
088956f
Revert "Add common base class for initialization"
sfinkens Dec 15, 2020
9760c44
Update documentation
sfinkens Dec 15, 2020
c2bd336
Update URLs of reference documents
sfinkens Dec 15, 2020
1f7d05b
Explain why there's no calib_mode in seviri_nc
sfinkens Dec 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 23 additions & 4 deletions doc/source/readers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,10 @@ This is described in the developer guide, see :doc:`dev_guide/custom_reader`.
Implemented readers
===================

SEVIRI L1.5 data readers
------------------------

xRIT-based readers
------------------

.. automodule:: satpy.readers.hrit_base
.. automodule:: satpy.readers.seviri_base
:noindex:

SEVIRI HRIT format reader
Expand All @@ -224,6 +223,26 @@ SEVIRI HRIT format reader
.. automodule:: satpy.readers.seviri_l1b_hrit
:noindex:

SEVIRI Native format reader
^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. automodule:: satpy.readers.seviri_l1b_native
:noindex:

SEVIRI netCDF format reader
^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. automodule:: satpy.readers.seviri_l1b_nc
:noindex:


Other xRIT-based readers
------------------------

.. automodule:: satpy.readers.hrit_base
:noindex:


JMA HRIT format reader
^^^^^^^^^^^^^^^^^^^^^^

Expand Down
227 changes: 215 additions & 12 deletions satpy/readers/seviri_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,109 @@
#
# You should have received a copy of the GNU General Public License along with
# satpy. If not, see <http://www.gnu.org/licenses/>.
"""Utilities and helper classes for MSG HRIT/Native data reading.
"""Common functionality for SEVIRI L1.5 data readers.

Introduction
------------

*The Spinning Enhanced Visible and InfraRed Imager (SEVIRI) is the primary
instrument on Meteosat Second Generation (MSG) and has the capacity to observe
the Earth in 12 spectral channels.*

*Level 1.5 corresponds to image data that has been corrected for all unwanted
radiometric and geometric effects, has been geolocated using a standardised
projection, and has been calibrated and radiance-linearised.*
(From the EUMETSAT documentation)

Satpy provides the following readers for SEVIRI L1.5 data in different formats:

- Native: :mod:`satpy.readers.seviri_l1b_native`
- HRIT: :mod:`satpy.readers.seviri_l1b_hrit`
- netCDF: :mod:`satpy.readers.seviri_l1b_nc`


Calibration
-----------

This section describes how to control the calibration of SEVIRI L1.5 data.


Calibration to radiance
^^^^^^^^^^^^^^^^^^^^^^^

The SEVIRI L1.5 data readers allow for choosing between two file-internal
calibration coefficients to convert counts to radiances:

- Nominal for all channels (default)
- GSICS where available (IR currently) and nominal for the remaining
channels (VIS & HRV currently)

In order to change the default behaviour, use the ``reader_kwargs`` keyword
argument upon Scene creation::

import satpy
scene = satpy.Scene(filenames,
reader='seviri_l1b_...',
reader_kwargs={'calib_mode': 'GSICS'})
scene.load(['VIS006', 'IR_108'])

Furthermore, it is possible to specify external calibration coefficients
for the conversion from counts to radiances. External coefficients take
precedence over internal coefficients, but you can also mix internal and
external coefficients: If external calibration coefficients are specified
for only a subset of channels, the remaining channels will be calibrated
using the chosen file-internal coefficients (nominal or GSICS).

Calibration coefficients must be specified in [mW m-2 sr-1 (cm-1)-1].

In the following example we use external calibration coefficients for the
``VIS006`` & ``IR_108`` channels, and nominal coefficients for the
remaining channels::

coefs = {'VIS006': {'gain': 0.0236, 'offset': -1.20},
'IR_108': {'gain': 0.2156, 'offset': -10.4}}
scene = satpy.Scene(filenames,
reader='seviri_l1b_...',
reader_kwargs={'ext_calib_coefs': coefs})
scene.load(['VIS006', 'VIS008', 'IR_108', 'IR_120'])

In the next example we use external calibration coefficients for the
``VIS006`` & ``IR_108`` channels, GSICS coefficients where available
(other IR channels) and nominal coefficients for the rest::

coefs = {'VIS006': {'gain': 0.0236, 'offset': -1.20},
'IR_108': {'gain': 0.2156, 'offset': -10.4}}
scene = satpy.Scene(filenames,
reader='seviri_l1b_...',
reader_kwargs={'calib_mode': 'GSICS',
'ext_calib_coefs': coefs})
scene.load(['VIS006', 'VIS008', 'IR_108', 'IR_120'])


Calibration to reflectance
^^^^^^^^^^^^^^^^^^^^^^^^^^

When loading solar channels, the SEVIRI L1.5 data readers apply a correction for
the Sun-Earth distance variation throughout the year - as recommended by
the EUMETSAT document
`Conversion from radiances to reflectances for SEVIRI warm channels`_.
In the unlikely situation that this correction is not required, it can be
removed on a per-channel basis using
:func:`satpy.readers.utils.remove_earthsun_distance_correction`.


References:
MSG Level 1.5 Image Data Format Description
https://www.eumetsat.int/website/wcm/idc/idcplg?IdcService=GET_FILE&dDocName=PDF_TEN_05105_MSG_IMG_DATA&RevisionSelectionMethod=LatestReleased&Rendition=Web
- `MSG Level 1.5 Image Data Format Description`_
- `Radiometric Calibration of MSG SEVIRI Level 1.5 Image Data in Equivalent Spectral Blackbody Radiance`_

.. _Conversion from radiances to reflectances for SEVIRI warm channels:
https://www-cdn.eumetsat.int/files/2020-04/pdf_msg_seviri_rad2refl.pdf

.. _MSG Level 1.5 Image Data Format Description:
https://www-cdn.eumetsat.int/files/2020-05/pdf_ten_05105_msg_img_data.pdf

.. _Radiometric Calibration of MSG SEVIRI Level 1.5 Image Data in Equivalent Spectral Blackbody Radiance:
https://www-cdn.eumetsat.int/files/2020-04/pdf_ten_msg_seviri_rad_calib.pdf

"""

Expand Down Expand Up @@ -334,23 +432,29 @@ def images_used(self):
mpef_product_header = MpefProductHeader().get()


class SEVIRICalibrationHandler(object):
"""Calibration handler for SEVIRI HRIT- and native-formats."""
class SEVIRICalibrationAlgorithm:
"""SEVIRI calibration algorithms."""

def __init__(self, platform_id, scan_time):
"""Initialize the calibration algorithm."""
self._platform_id = platform_id
self._scan_time = scan_time

def _convert_to_radiance(self, data, gain, offset):
def convert_to_radiance(self, data, gain, offset):
"""Calibrate to radiance."""
data = data.where(data > 0)
return (data * gain + offset).clip(0.0, None)

def _erads2bt(self, data, channel_name):
"""Convert effective radiance to brightness temperature."""
cal_info = CALIB[self.platform_id][channel_name]
cal_info = CALIB[self._platform_id][channel_name]
alpha = cal_info["ALPHA"]
beta = cal_info["BETA"]
wavenumber = CALIB[self.platform_id][channel_name]["VC"]
wavenumber = CALIB[self._platform_id][channel_name]["VC"]

return (self._tl15(data, wavenumber) - beta) / alpha

def _ir_calibrate(self, data, channel_name, cal_type):
def ir_calibrate(self, data, channel_name, cal_type):
"""Calibrate to brightness temperature."""
if cal_type == 1:
# spectral radiances
Expand All @@ -364,7 +468,7 @@ def _ir_calibrate(self, data, channel_name, cal_type):
def _srads2bt(self, data, channel_name):
"""Convert spectral radiance to brightness temperature."""
a__, b__, c__ = BTFIT[channel_name]
wavenumber = CALIB[self.platform_id][channel_name]["VC"]
wavenumber = CALIB[self._platform_id][channel_name]["VC"]
temp = self._tl15(data, wavenumber)

return a__ * temp * temp + b__ * temp + c__
Expand All @@ -374,14 +478,95 @@ def _tl15(self, data, wavenumber):
return ((C2 * wavenumber) /
np.log((1.0 / data) * C1 * wavenumber ** 3 + 1.0))

def _vis_calibrate(self, data, solar_irradiance):
def vis_calibrate(self, data, solar_irradiance):
"""Calibrate to reflectance.

This uses the method described in Conversion from radiances to
reflectances for SEVIRI warm channels: https://tinyurl.com/y67zhphm
"""
reflectance = np.pi * data * 100.0 / solar_irradiance
return apply_earthsun_distance_correction(reflectance, self.start_time)
return apply_earthsun_distance_correction(reflectance, self._scan_time)


class SEVIRICalibrationHandler:
"""Calibration handler for SEVIRI HRIT-, native- and netCDF-formats.

Handles selection of calibration coefficients and calls the appropriate
calibration algorithm.
"""

def __init__(self, platform_id, channel_name, coefs, calib_mode, scan_time):
"""Initialize the calibration handler."""
self._platform_id = platform_id
self._channel_name = channel_name
self._coefs = coefs
self._calib_mode = calib_mode.upper()
self._scan_time = scan_time
self._algo = SEVIRICalibrationAlgorithm(
platform_id=self._platform_id,
scan_time=self._scan_time
)

valid_modes = ('NOMINAL', 'GSICS')
if self._calib_mode not in valid_modes:
raise ValueError(
'Invalid calibration mode: {}. Choose one of {}'.format(
self._calib_mode, valid_modes)
)

def calibrate(self, data, calibration):
"""Calibrate the given data."""
if calibration == 'counts':
res = data
elif calibration in ['radiance', 'reflectance',
'brightness_temperature']:
gain, offset = self.get_gain_offset()
res = self._algo.convert_to_radiance(
data.astype(np.float32), gain, offset
)
else:
raise ValueError(
'Invalid calibration {} for channel {}'.format(
calibration, self._channel_name
)
)

if calibration == 'reflectance':
solar_irradiance = CALIB[self._platform_id][self._channel_name]["F"]
res = self._algo.vis_calibrate(res, solar_irradiance)
elif calibration == 'brightness_temperature':
res = self._algo.ir_calibrate(
res, self._channel_name, self._coefs['radiance_type']
)

return res

def get_gain_offset(self):
"""Get gain & offset for calibration from counts to radiance.

Choices for internal coefficients are nominal or GSICS. If no
GSICS coefficients are available for a certain channel, fall back to
nominal coefficients. External coefficients take precedence over
internal coefficients.
"""
coefs = self._coefs['coefs']

# Select internal coefficients for the given calibration mode
internal_gain = coefs['NOMINAL']['gain']
internal_offset = coefs['NOMINAL']['offset']
if self._calib_mode == 'GSICS':
gsics_gain = coefs['GSICS']['gain']
gsics_offset = coefs['GSICS']['offset'] * gsics_gain
if gsics_gain != 0 and gsics_offset != 0:
# If no GSICS coefficients are available for a certain channel,
# they are set to zero in the file.
internal_gain = gsics_gain
internal_offset = gsics_offset

# Override with external coefficients, if any.
gain = coefs['EXTERNAL'].get('gain', internal_gain)
offset = coefs['EXTERNAL'].get('offset', internal_offset)
return gain, offset


def chebyshev(coefs, time, domain):
Expand Down Expand Up @@ -432,6 +617,24 @@ def calculate_area_extent(area_dict):
return tuple(aex)


def create_coef_dict(coefs_nominal, coefs_gsics, radiance_type, ext_coefs):
"""Create coefficient dictionary expected by calibration class."""
return {
'coefs': {
'NOMINAL': {
'gain': coefs_nominal[0],
'offset': coefs_nominal[1],
},
'GSICS': {
'gain': coefs_gsics[0],
'offset': coefs_gsics[1]
},
'EXTERNAL': ext_coefs
},
'radiance_type': radiance_type
}


def get_service_mode(ssp_lon):
"""Get information about whether we're dealing with data from the FES, RSS or IODC service mode."""
seviri_service_modes = {'0.0': {'name': 'FES', 'desc': 'Full Earth scanning service'},
Expand Down