diff --git a/AUTHORS.md b/AUTHORS.md
index c5f8607c85..0925a7f48d 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -7,6 +7,7 @@ The following people have made contributions to this project:
+- [Youva Aoun (YouvaEUMex)](https://github.com/YouvaEUMex)
- [Trygve Aspenes (TAlonglong)](https://github.com/TAlonglong)
- [Talfan Barnie (TalfanBarnie)](https://github.com/TalfanBarnie)
- [Jonathan Beavers (jon4than)](https://github.com/jon4than)
@@ -38,6 +39,7 @@ The following people have made contributions to this project:
- [David Hoese (djhoese)](https://github.com/djhoese)
- [Marc Honnorat (honnorat)](https://github.com/honnorat)
- [Lloyd Hughes (system123)](https://github.com/system123)
+- [Sara Hörnquist (shornqui)](https://github.com/shornqui)
- [Mikhail Itkin (mitkin)](https://github.com/mitkin)
- [Tommy Jasmin (tommyjasmin)](https://github.com/tommyjasmin)
- [Jactry Zeng](https://github.com/jactry)
@@ -47,6 +49,7 @@ The following people have made contributions to this project:
- [Janne Kotro (jkotro)](https://github.com/jkotro)
- [Ralph Kuehn (ralphk11)](https://github.com/ralphk11)
- [Panu Lahtinen (pnuu)](https://github.com/pnuu)
+- [Clement Laplace (ClementLaplace)](https://github.com/ClementLaplace)
- [Jussi Leinonen (jleinonen)](https://github.com/jleinonen) - meteoswiss
- [Thomas Leppelt (m4sth0)](https://github.com/m4sth0) - Deutscher Wetterdienst
- [Lu Liu (yukaribbba)](https://github.com/yukaribbba)
@@ -55,6 +58,7 @@ The following people have made contributions to this project:
- [Luca Merucci (lmeru)](https://github.com/lmeru)
- [Lucas Meyer (LTMeyer)](https://github.com/LTMeyer)
- [Zifeng Mo (Isotr0py)](https://github.com/Isotr0py)
+- [David Navia (dnaviap)](https://github.com/dnaviap)
- [Ondrej Nedelcev (nedelceo)](https://github.com/nedelceo)
- [Oana Nicola](https://github.com/)
- [Esben S. Nielsen (storpipfugl)](https://github.com/storpipfugl)
@@ -79,12 +83,14 @@ The following people have made contributions to this project:
- [Michael Schmutz (Graenni)](https://github.com/Graenni) - Meteotest AG
- [Hauke Schulz (observingClouds)](https://github.com/observingClouds)
- [Jakub Seidl (seidlj)](https://github.com/seidlj)
+- [Will Sharpe (wjsharpe)](https://github.com/wjsharpe)
- [Eysteinn Sigurðsson (eysteinn)](https://github.com/eysteinn)
- [Jean-Luc Shaw (jeanlucshaw)](https://github.com/jeanlucshaw)
- [Dario Stelitano (bornagain1981)](https://github.com/bornagain1981)
- [Johan Strandgren (strandgren)](https://github.com/strandgren)
- [Matias Takala (elfsprite)](https://github.com/elfsprite)
- [Taiga Tsukada (tsukada-cs)](https://github.com/tsukada-cs)
+- [Antonio Valentino](https://github.com/avalentino)
- [Christian Versloot (christianversloot)](https://github.com/christianversloot)
- [Helga Weber (helgaweb)](https://github.com/helgaweb)
- [hazbottles (hazbottles)](https://github.com/hazbottles)
diff --git a/satpy/etc/readers/fci_l2_grib.yaml b/satpy/etc/readers/fci_l2_grib.yaml
new file mode 100644
index 0000000000..cc16c77081
--- /dev/null
+++ b/satpy/etc/readers/fci_l2_grib.yaml
@@ -0,0 +1,28 @@
+reader:
+ name: fci_l2_grib
+ short_name: FCI L2 GRIB2
+ long_name: MTG FCI L2 data in GRIB2 format
+ description: Reader for EUMETSAT MTG FCI L2 files in GRIB2 format.
+ status: Nominal
+ supports_fsspec: false
+ sensors: [fci]
+ reader: !!python/name:satpy.readers.yaml_reader.GEOFlippableFileYAMLReader
+
+file_types:
+ grib_fci_clm:
+ file_reader: !!python/name:satpy.readers.eum_l2_grib.EUML2GribFileHandler
+ file_patterns:
+ - '{pflag}_{location_indicator},{data_designator},MTI{spacecraft_id:1d}+FCI-2-CLM-{subtype}-{coverage}-{subsetting}-{component1}-{component2}-{component3}-{purpose}-GRIB2_{oflag}_{originator}_{processing_time:%Y%m%d%H%M%S}_{facility_or_tool}_{environment}_{start_time:%Y%m%d%H%M%S}_{end_time:%Y%m%d%H%M%S}_{processing_mode}_{special_compression}_{disposition_mode}_{repeat_cycle_in_day:>04d}_{count_in_repeat_cycle:>04d}.bin'
+
+
+datasets:
+ cloud_mask:
+ name: cloud_mask
+ long_name: Cloud Classification
+ standard_name: cloud_classification
+ resolution: 2000
+ file_type: grib_fci_clm
+ parameter_number: 7
+ units: "1"
+ flag_values: [0, 1, 2, 3]
+ flag_meanings: ['clear sky over water','clear sky over land', 'cloudy', 'undefined' ]
diff --git a/satpy/etc/readers/seviri_l2_grib.yaml b/satpy/etc/readers/seviri_l2_grib.yaml
index 5d7a204e24..9f156c5388 100644
--- a/satpy/etc/readers/seviri_l2_grib.yaml
+++ b/satpy/etc/readers/seviri_l2_grib.yaml
@@ -14,7 +14,7 @@ file_types:
# EUMETSAT MSG SEVIRI L2 Aerosol Properties over Sea product
# https://navigator.eumetsat.int/product/EO:EUM:DAT:MSG:AES
grib_seviri_aes:
- file_reader: !!python/name:satpy.readers.seviri_l2_grib.SeviriL2GribFileHandler
+ file_reader: !!python/name:satpy.readers.eum_l2_grib.EUML2GribFileHandler
file_patterns:
- 'AESGRIBProd_{start_time:%Y%m%d%H%M%S}Z_00_{server:8s}_{spacecraft:5s}_{scan_mode:3s}_{sub_sat:5s}'
- '{spacecraft:4s}-SEVI-MSGAESE-{id1:4s}-{id2:4s}-{start_time:%Y%m%d%H%M%S}.000000000Z-{product_creation_time:%Y%m%d%H%M%S}-{ord_num:7s}'
@@ -24,7 +24,7 @@ file_types:
# EUMETSAT MSG SEVIRI L2 Cloud Mask product
# https://navigator.eumetsat.int/product/EO:EUM:DAT:MSG:CLM
grib_seviri_clm:
- file_reader: !!python/name:satpy.readers.seviri_l2_grib.SeviriL2GribFileHandler
+ file_reader: !!python/name:satpy.readers.eum_l2_grib.EUML2GribFileHandler
file_patterns:
- 'CLMEncProd_{start_time:%Y%m%d%H%M%S}Z_00_{server:8s}_{spacecraft:5s}_{scan_mode:3s}_{sub_sat:5s}'
- '{spacecraft:4s}-SEVI-MSGCLMK-{id1:4s}-{id2:4s}-{start_time:%Y%m%d%H%M%S}.000000000Z-{product_creation_time:%Y%m%d%H%M%S}-{ord_num:7s}'
@@ -34,7 +34,7 @@ file_types:
# EUMETSAT MSG SEVIRI L2 Cloud Top Height product
# https://navigator.eumetsat.int/product/EO:EUM:DAT:MSG:CTH
grib_seviri_cth:
- file_reader: !!python/name:satpy.readers.seviri_l2_grib.SeviriL2GribFileHandler
+ file_reader: !!python/name:satpy.readers.eum_l2_grib.EUML2GribFileHandler
file_patterns:
- 'CTHEncProd_{start_time:%Y%m%d%H%M%S}Z_00_{server:8s}_{spacecraft:5s}_{scan_mode:3s}_{sub_sat:5s}'
- '{spacecraft:4s}-SEVI-MSGCLTH-{id1:4s}-{id2:4s}-{start_time:%Y%m%d%H%M%S}.000000000Z-{product_creation_time:%Y%m%d%H%M%S}-{ord_num:7s}'
@@ -44,7 +44,7 @@ file_types:
# EUMETSAT MSG SEVIRI L2 Clear-Sky Reflectance Map product
# https://navigator.eumetsat.int/product/EO:EUM:DAT:MSG:CRM
grib_seviri_crm:
- file_reader: !!python/name:satpy.readers.seviri_l2_grib.SeviriL2GribFileHandler
+ file_reader: !!python/name:satpy.readers.eum_l2_grib.EUML2GribFileHandler
file_patterns:
- 'CRMEncProd_{start_time:%Y%m%d%H%M%S}Z_00_{server:8s}_{spacecraft:5s}_{scan_mode:3s}_{sub_sat:5s}'
- '{spacecraft:4s}-SEVI-MSGCRMN-{id1:4s}-{id2:4s}-{start_time:%Y%m%d%H%M%S}.000000000Z-{product_creation_time:%Y%m%d%H%M%S}-{ord_num:7s}'
@@ -54,7 +54,7 @@ file_types:
# EUMETSAT MSG SEVIRI L2 Active Fire Monitoring product
# https://navigator.eumetsat.int/product/EO:EUM:DAT:MSG:FIR
grib_seviri_fir:
- file_reader: !!python/name:satpy.readers.seviri_l2_grib.SeviriL2GribFileHandler
+ file_reader: !!python/name:satpy.readers.eum_l2_grib.EUML2GribFileHandler
file_patterns:
- 'FIREncProd_{start_time:%Y%m%d%H%M%S}Z_00_{server:8s}_{spacecraft:5s}_{scan_mode:3s}_{sub_sat:5s}'
- '{spacecraft:4s}-SEVI-MSGFIRG-{id1:4s}-{id2:4s}-{start_time:%Y%m%d%H%M%S}.000000000Z-{product_creation_time:%Y%m%d%H%M%S}-{ord_num:7s}'
@@ -65,7 +65,7 @@ file_types:
# EUMETSAT MSG SEVIRI L2 Multi-Sensor Precipitation Estimate product
# https://navigator.eumetsat.int/product/EO:EUM:DAT:MSG:MPE-GRIB
grib_seviri_mpe:
- file_reader: !!python/name:satpy.readers.seviri_l2_grib.SeviriL2GribFileHandler
+ file_reader: !!python/name:satpy.readers.eum_l2_grib.EUML2GribFileHandler
file_patterns:
- 'MPEGRIBProd_{start_time:%Y%m%d%H%M%S}Z_00_{server:8s}_{spacecraft:5s}_{scan_mode:3s}_{sub_sat:5s}'
- '{spacecraft:4s}-SEVI-MSGMPEG-{id1:4s}-{id2:4s}-{start_time:%Y%m%d%H%M%S}.000000000Z-{product_creation_time:%Y%m%d%H%M%S}-{ord_num:7s}'
@@ -75,7 +75,7 @@ file_types:
# EUMETSAT MSG SEVIRI L2 Optimal Cloud Analysis product
# https://navigator.eumetsat.int/product/EO:EUM:DAT:MSG:OCA
grib_seviri_oca:
- file_reader: !!python/name:satpy.readers.seviri_l2_grib.SeviriL2GribFileHandler
+ file_reader: !!python/name:satpy.readers.eum_l2_grib.EUML2GribFileHandler
file_patterns:
- 'OCAEncProd_{start_time:%Y%m%d%H%M%S}Z_00_{server:8s}_{spacecraft:5s}_{scan_mode:3s}_{sub_sat:5s}'
- '{spacecraft:4s}-SEVI-MSGOCAE-{id1:4s}-{id2:4s}-{start_time:%Y%m%d%H%M%S}.000000000Z-{product_creation_time:%Y%m%d%H%M%S}-{ord_num:7s}'
diff --git a/satpy/readers/seviri_l2_grib.py b/satpy/readers/eum_l2_grib.py
similarity index 77%
rename from satpy/readers/seviri_l2_grib.py
rename to satpy/readers/eum_l2_grib.py
index d178d6b716..543aa71c30 100644
--- a/satpy/readers/seviri_l2_grib.py
+++ b/satpy/readers/eum_l2_grib.py
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with satpy. If not, see .
-"""Reader for the SEVIRI L2 products in GRIB2 format.
+"""Reader for both SEVIRI and FCI L2 products in GRIB2 format.
References:
FM 92 GRIB Edition 2
@@ -31,22 +31,29 @@
from satpy.readers._geos_area import get_area_definition, get_geos_area_naming
from satpy.readers.eum_base import get_service_mode
+from satpy.readers.fci_base import calculate_area_extent as fci_calculate_area_extent
from satpy.readers.file_handlers import BaseFileHandler
-from satpy.readers.seviri_base import PLATFORM_DICT, REPEAT_CYCLE_DURATION, calculate_area_extent
+from satpy.readers.seviri_base import PLATFORM_DICT as SEVIRI_PLATFORM_DICT
+from satpy.readers.seviri_base import REPEAT_CYCLE_DURATION as SEVIRI_REPEAT_CYCLE_DURATION
+from satpy.readers.seviri_base import REPEAT_CYCLE_DURATION_RSS as SEVIRI_REPEAT_CYCLE_DURATION_RSS
+from satpy.readers.seviri_base import calculate_area_extent as seviri_calculate_area_extent
from satpy.utils import get_legacy_chunk_size
+CHUNK_SIZE = get_legacy_chunk_size()
+
try:
import eccodes as ec
except ImportError:
raise ImportError(
- "Missing eccodes-python and/or eccodes C-library installation. Use conda to install eccodes")
+ "Missing eccodes-python and/or eccodes C-library installation. Use conda to install eccodes")
-CHUNK_SIZE = get_legacy_chunk_size()
logger = logging.getLogger(__name__)
-class SeviriL2GribFileHandler(BaseFileHandler):
- """Reader class for SEVIRI L2 products in GRIB format."""
+class EUML2GribFileHandler(BaseFileHandler):
+ """Reader class for EUM L2 products in GRIB format."""
+
+ calculate_area_extent = None
def __init__(self, filename, filename_info, filetype_info):
"""Read the global attributes and prepare for dataset reading."""
@@ -54,6 +61,14 @@ def __init__(self, filename, filename_info, filetype_info):
# Turn on support for multiple fields in single GRIB messages (required for SEVIRI L2 files)
ec.codes_grib_multi_support_on()
+ if "seviri" in self.filetype_info["file_type"]:
+ self.sensor = "seviri"
+ self.PLATFORM_NAME = SEVIRI_PLATFORM_DICT[self.filename_info["spacecraft"]]
+ elif "fci" in self.filetype_info["file_type"]:
+ self.sensor = "fci"
+ self.PLATFORM_NAME = f"MTG-i{self.filename_info['spacecraft_id']}"
+ pass
+
@property
def start_time(self):
"""Return the sensing start time."""
@@ -62,14 +77,24 @@ def start_time(self):
@property
def end_time(self):
"""Return the sensing end time."""
- return self.start_time + dt.timedelta(minutes=REPEAT_CYCLE_DURATION)
+ if self.sensor == "seviri":
+ delta = SEVIRI_REPEAT_CYCLE_DURATION_RSS if self._ssp_lon == 9.5 else SEVIRI_REPEAT_CYCLE_DURATION
+ return self.start_time + dt.timedelta(minutes=delta)
+ elif self.sensor == "fci":
+ return self.filename_info["end_time"]
def get_area_def(self, dataset_id):
"""Return the area definition for a dataset."""
+ # Compute the dictionary with the area extension
+
self._area_dict["column_step"] = dataset_id["resolution"]
self._area_dict["line_step"] = dataset_id["resolution"]
- area_extent = calculate_area_extent(self._area_dict)
+ if self.sensor == "seviri":
+ area_extent = seviri_calculate_area_extent(self._area_dict)
+
+ elif self.sensor == "fci":
+ area_extent = fci_calculate_area_extent(self._area_dict)
# Call the get_area_definition function to obtain the area
area_def = get_area_definition(self._pdict, area_extent)
@@ -173,19 +198,20 @@ def _get_proj_area(self, gid):
"""
# Get name of area definition
area_naming_input_dict = {"platform_name": "msg",
- "instrument_name": "seviri",
+ "instrument_name": self.sensor,
"resolution": self._res,
}
area_naming = get_geos_area_naming({**area_naming_input_dict,
- **get_service_mode("seviri", self._ssp_lon)})
+ **get_service_mode(self.sensor, self._ssp_lon)})
# Read all projection and area parameters from the message
earth_major_axis_in_meters = self._get_from_msg(gid, "earthMajorAxis") * 1000.0 # [m]
earth_minor_axis_in_meters = self._get_from_msg(gid, "earthMinorAxis") * 1000.0 # [m]
- earth_major_axis_in_meters = self._scale_earth_axis(earth_major_axis_in_meters)
- earth_minor_axis_in_meters = self._scale_earth_axis(earth_minor_axis_in_meters)
+ if self.sensor == "seviri":
+ earth_major_axis_in_meters = self._scale_earth_axis(earth_major_axis_in_meters)
+ earth_minor_axis_in_meters = self._scale_earth_axis(earth_minor_axis_in_meters)
nr_in_radius_of_earth = self._get_from_msg(gid, "NrInRadiusOfEarth")
xp_in_grid_lengths = self._get_from_msg(gid, "XpInGridLengths")
@@ -204,14 +230,21 @@ def _get_proj_area(self, gid):
"p_id": "",
}
- # Compute the dictionary with the area extension
- area_dict = {
- "center_point": xp_in_grid_lengths,
- "north": self._nrows,
- "east": 1,
- "west": self._ncols,
- "south": 1,
- }
+ if self.sensor == "seviri":
+ # Compute the dictionary with the area extension
+ area_dict = {
+ "center_point": xp_in_grid_lengths,
+ "north": self._nrows,
+ "east": 1,
+ "west": self._ncols,
+ "south": 1,
+ }
+
+ elif self.sensor == "fci":
+ area_dict = {
+ "nlines": self._ncols,
+ "ncols": self._nrows,
+ }
return pdict, area_dict
@@ -219,10 +252,9 @@ def _get_proj_area(self, gid):
def _scale_earth_axis(data):
"""Scale Earth axis data to make sure the value matched the expected unit [m].
- The earthMinorAxis value stored in the aerosol over sea product is scaled incorrectly by a factor of 1e8. This
- method provides a flexible temporarily workaraound by making sure that all earth axis values are scaled such
- that they are on the order of millions of meters as expected by the reader. As soon as the scaling issue has
- been resolved by EUMETSAT this workaround can be removed.
+ The earthMinorAxis value stored in the MPEF aerosol over sea product prior to December 12, 2022 has the wrong
+ unit and this method provides a flexible work-around by making sure that all earth axis values are scaled such
+ that they are on the order of millions of meters as expected by the reader.
"""
scale_factor = 10 ** np.ceil(np.log10(1e6/data))
@@ -256,11 +288,9 @@ def _get_attributes(self):
"projection_longitude": self._ssp_lon
}
- attributes = {
- "orbital_parameters": orbital_parameters,
- "sensor": "seviri",
- "platform_name": PLATFORM_DICT[self.filename_info["spacecraft"]]
- }
+ attributes = {"orbital_parameters": orbital_parameters, "sensor": self.sensor,
+ "platform_name": self.PLATFORM_NAME}
+
return attributes
@staticmethod
diff --git a/satpy/readers/fci_base.py b/satpy/readers/fci_base.py
new file mode 100644
index 0000000000..c1f6fc2110
--- /dev/null
+++ b/satpy/readers/fci_base.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017-2018 Satpy developers
+#
+# This file is part of satpy.
+#
+# satpy 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.
+#
+# satpy 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
+# satpy. If not, see .
+"""Common functionality for FCI data readers."""
+from __future__ import annotations
+
+
+def calculate_area_extent(area_dict):
+ """Calculate the area extent seen by MTG FCI instrument.
+
+ Since the center of the FCI grids is located at the interface between the pixels, there are equally many
+ pixels (e.g. 5568/2 = 2784 for 2km grid) in each direction from the center points. Hence, the area extent
+ can be easily computed by simply adding and subtracting half the width and height from teh centre point (=0).
+
+ Args:
+ area_dict: A dictionary containing the required parameters
+ ncols: number of pixels in east-west direction
+ nlines: number of pixels in south-north direction
+ column_step: Pixel resulution in meters in east-west direction
+ line_step: Pixel resulution in meters in south-north direction
+ Returns:
+ tuple: An area extent for the scene defined by the lower left and
+ upper right corners
+
+ """
+ ncols = area_dict["ncols"]
+ nlines = area_dict["nlines"]
+ column_step = area_dict["column_step"]
+ line_step = area_dict["line_step"]
+
+ ll_c = (0 - ncols / 2.) * column_step
+ ll_l = (0 + nlines / 2.) * line_step
+ ur_c = (0 + ncols / 2.) * column_step
+ ur_l = (0 - nlines / 2.) * line_step
+
+ return (ll_c, ll_l, ur_c, ur_l)
diff --git a/satpy/readers/seviri_base.py b/satpy/readers/seviri_base.py
index 1a98dda098..aa224b0d6a 100644
--- a/satpy/readers/seviri_base.py
+++ b/satpy/readers/seviri_base.py
@@ -225,6 +225,8 @@
REPEAT_CYCLE_DURATION = 15
+REPEAT_CYCLE_DURATION_RSS = 5
+
C1 = 1.19104273e-5
C2 = 1.43877523
diff --git a/satpy/tests/reader_tests/test_eum_l2_grib.py b/satpy/tests/reader_tests/test_eum_l2_grib.py
new file mode 100644
index 0000000000..50c6be5398
--- /dev/null
+++ b/satpy/tests/reader_tests/test_eum_l2_grib.py
@@ -0,0 +1,302 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2019 Satpy developers
+#
+# satpy 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.
+#
+# satpy 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 satpy. If not, see .
+
+"""EUM L2 GRIB-reader test package."""
+
+import datetime
+import sys
+from unittest import mock
+
+import numpy as np
+import pytest
+
+from satpy.tests.utils import make_dataid
+
+# Dictionary to be used as fake GRIB message
+FAKE_SEVIRI_MESSAGE = {
+ "longitudeOfSubSatellitePointInDegrees": 9.5,
+ "dataDate": 20191020,
+ "dataTime": 1745,
+ "Nx": 1000,
+ "Ny": 1200,
+ "earthMajorAxis": 6400.,
+ "earthMinorAxis": 6300.,
+ "NrInRadiusOfEarth": 6.,
+ "XpInGridLengths": 500,
+ "parameterNumber": 30,
+ "missingValue": 9999,
+}
+
+FAKE_FCI_MESSAGE = {
+ "longitudeOfSubSatellitePointInDegrees": 0.0,
+ "dataDate": 20191020,
+ "dataTime": 1745,
+ "Nx": 5568,
+ "Ny": 5568,
+ "earthMajorAxis": 6378140.,
+ "earthMinorAxis": 6356755.,
+ "NrInRadiusOfEarth": 6.6107,
+ "XpInGridLengths": 2784.0,
+ "parameterNumber": 30,
+ "missingValue": 9999,
+}
+
+# List to be used as fake GID source
+FAKE_GID = [0, 1, 2, 3, None]
+
+
+@pytest.fixture
+@mock.patch("satpy.readers.eum_l2_grib.ec")
+def setup_reader(ec_):
+ """Set up the test by creating a mocked eccodes library."""
+ fake_gid_generator = (i for i in FAKE_GID)
+ ec_.codes_grib_new_from_file.side_effect = lambda fh: next(fake_gid_generator)
+ return ec_
+
+
+def common_checks(ec_, reader, mock_file, dataset_id):
+ """Commmon checks for fci and seviri data."""
+ # Checks that the codes_grib_multi_support_on function has been called
+ ec_.codes_grib_multi_support_on.assert_called()
+
+ # Restarts the id generator and clears the call history
+ fake_gid_generator = (i for i in FAKE_GID)
+ ec_.codes_grib_new_from_file.side_effect = lambda fh: next(fake_gid_generator)
+ ec_.codes_grib_new_from_file.reset_mock()
+ ec_.codes_release.reset_mock()
+
+ # Checks the correct execution of the get_dataset function with a valid parameter_number
+ valid_dataset = reader.get_dataset(dataset_id, {"parameter_number": 30})
+ # Checks the correct file open call
+ mock_file.assert_called_with("test.grib", "rb")
+ # Checks that the dataset has been created as a DataArray object
+ assert valid_dataset._extract_mock_name() == "xr.DataArray()"
+ # Checks that codes_release has been called after each codes_grib_new_from_file call
+ # (except after the last one which has returned a None)
+ assert ec_.codes_grib_new_from_file.call_count == ec_.codes_release.call_count + 1
+
+ # Restarts the id generator and clears the call history
+ fake_gid_generator = (i for i in FAKE_GID)
+ ec_.codes_grib_new_from_file.side_effect = lambda fh: next(fake_gid_generator)
+ ec_.codes_grib_new_from_file.reset_mock()
+ ec_.codes_release.reset_mock()
+
+ # Checks the correct execution of the get_dataset function with an invalid parameter_number
+ invalid_dataset = reader.get_dataset(dataset_id, {"parameter_number": 50})
+ # Checks that the function returns None
+ assert invalid_dataset is None
+ # Checks that codes_release has been called after each codes_grib_new_from_file call
+ # (except after the last one which has returned a None)
+ assert ec_.codes_grib_new_from_file.call_count == ec_.codes_release.call_count + 1
+
+
+@pytest.mark.skipif(sys.platform.startswith("win"), reason="'eccodes' not supported on Windows")
+@mock.patch("satpy.readers.eum_l2_grib.xr")
+@mock.patch("satpy.readers.eum_l2_grib.da")
+def test_seviri_data_reading(da_, xr_, setup_reader):
+ """Test the reading of data from the product."""
+ from satpy.readers.eum_l2_grib import EUML2GribFileHandler
+ from satpy.utils import get_legacy_chunk_size
+ ec_ = setup_reader
+ chunk_size = get_legacy_chunk_size()
+
+ with mock.patch("builtins.open", mock.mock_open()) as mock_file:
+ with mock.patch("satpy.readers.eum_l2_grib.ec", ec_):
+ ec_.codes_get_values.return_value = np.ones(1000 * 1200)
+ ec_.codes_get.side_effect = lambda gid, key: FAKE_SEVIRI_MESSAGE[key]
+ reader = EUML2GribFileHandler(
+ filename="test.grib",
+ filename_info={
+ "spacecraft": "MET11",
+ "start_time": datetime.datetime(year=2020, month=10, day=20,
+ hour=19, minute=45, second=0)
+ },
+ filetype_info={
+ "file_type": "seviri"
+ }
+ )
+
+ dataset_id = make_dataid(name="dummmy", resolution=3000)
+
+ common_checks(ec_, reader, mock_file, dataset_id)
+
+ # Check end_time
+ assert reader.end_time == datetime.datetime(year=2020, month=10, day=20,
+ hour=19, minute=50, second=0)
+
+ # Checks the correct execution of the _get_global_attributes and _get_metadata_from_msg functions
+ attributes = reader._get_attributes()
+ expected_attributes = {
+ "orbital_parameters": {
+ "projection_longitude": 9.5
+ },
+ "sensor": "seviri",
+ "platform_name": "Meteosat-11"
+ }
+ assert attributes == expected_attributes
+
+ # Checks the reading of an array from the message
+ reader._get_xarray_from_msg(0)
+
+ # Checks that dask.array has been called with the correct arguments
+ name, args, kwargs = da_.mock_calls[0]
+ assert np.all(args[0] == np.ones((1200, 1000)))
+ assert args[1] == chunk_size
+
+ # Checks that xarray.DataArray has been called with the correct arguments
+ name, args, kwargs = xr_.mock_calls[0]
+ assert kwargs["dims"] == ("y", "x")
+
+ # Checks the correct execution of the _get_proj_area function
+ pdict, area_dict = reader._get_proj_area(0)
+
+ expected_pdict = {
+ "a": 6400000.,
+ "b": 6300000.,
+ "h": 32000000.,
+ "ssp_lon": 9.5,
+ "nlines": 1000,
+ "ncols": 1200,
+ "a_name": "msg_seviri_rss_3km",
+ "a_desc": "MSG SEVIRI Rapid Scanning Service area definition with 3 km resolution",
+ "p_id": "",
+ }
+ assert pdict == expected_pdict
+ expected_area_dict = {
+ "center_point": 500,
+ "north": 1200,
+ "east": 1,
+ "west": 1000,
+ "south": 1,
+ }
+ assert area_dict == expected_area_dict
+
+ # Checks the correct execution of the get_area_def function
+ with mock.patch("satpy.readers.eum_l2_grib.seviri_calculate_area_extent",
+ mock.Mock(name="seviri_calculate_area_extent")) as cae:
+ with mock.patch("satpy.readers.eum_l2_grib.get_area_definition", mock.Mock()) as gad:
+ dataset_id = make_dataid(name="dummmy", resolution=400.)
+ reader.get_area_def(dataset_id)
+ # Asserts that seviri_calculate_area_extent has been called with the correct arguments
+ expected_args = ({"center_point": 500, "east": 1, "west": 1000, "south": 1, "north": 1200,
+ "column_step": 400., "line_step": 400.},)
+ name, args, kwargs = cae.mock_calls[0]
+ assert args == expected_args
+ # Asserts that get_area_definition has been called with the correct arguments
+ name, args, kwargs = gad.mock_calls[0]
+ assert args[0] == expected_pdict
+ # The second argument must be the return result of seviri_calculate_area_extent
+ assert args[1]._extract_mock_name() == "seviri_calculate_area_extent()"
+
+
+@pytest.mark.skipif(sys.platform.startswith("win"), reason="'eccodes' not supported on Windows")
+@mock.patch("satpy.readers.eum_l2_grib.xr")
+@mock.patch("satpy.readers.eum_l2_grib.da")
+def test_fci_data_reading(da_, xr_, setup_reader):
+ """Test the reading of fci data from the product."""
+ from satpy.readers.eum_l2_grib import EUML2GribFileHandler
+ from satpy.utils import get_legacy_chunk_size
+ ec_ = setup_reader
+ chunk_size = get_legacy_chunk_size()
+
+ with mock.patch("builtins.open", mock.mock_open()) as mock_file:
+ with mock.patch("satpy.readers.eum_l2_grib.ec", ec_):
+ ec_.codes_get_values.return_value = np.ones(5568 * 5568)
+ ec_.codes_get.side_effect = lambda gid, key: FAKE_FCI_MESSAGE[key]
+ reader = EUML2GribFileHandler(
+ filename="test.grib",
+ filename_info={
+ "spacecraft_id": "1",
+ "start_time": datetime.datetime(year=2020, month=10, day=20,
+ hour=19, minute=40, second=0),
+ "end_time": datetime.datetime(year=2020, month=10, day=20,
+ hour=19, minute=50, second=0)
+ },
+ filetype_info={
+ "file_type": "fci"
+ }
+ )
+
+ dataset_id = make_dataid(name="dummmy", resolution=2000)
+
+ common_checks(ec_, reader, mock_file, dataset_id)
+
+ # Check end_time
+ assert reader.end_time == datetime.datetime(year=2020, month=10, day=20,
+ hour=19, minute=50, second=0)
+
+ # Checks the correct execution of the _get_global_attributes and _get_metadata_from_msg functions
+ attributes = reader._get_attributes()
+ expected_attributes = {
+ "orbital_parameters": {
+ "projection_longitude": 0.0
+ },
+ "sensor": "fci",
+ "platform_name": "MTG-i1"
+ }
+ assert attributes == expected_attributes
+
+ # Checks the reading of an array from the message
+ reader._get_xarray_from_msg(0)
+
+ # Checks that dask.array has been called with the correct arguments
+ name, args, kwargs = da_.mock_calls[0]
+ assert np.all(args[0] == np.ones((5568, 5568)))
+ assert args[1] == chunk_size
+
+ # Checks that xarray.DataArray has been called with the correct arguments
+ name, args, kwargs = xr_.mock_calls[0]
+ assert kwargs["dims"] == ("y", "x")
+
+ # Checks the correct execution of the _get_proj_area function
+ pdict, area_dict = reader._get_proj_area(0)
+
+ expected_pdict = {
+ "a": 6378140000.0,
+ "b": 6356755000.0,
+ "h": 35785830098.0,
+ "ssp_lon": 0.0,
+ "nlines": 5568,
+ "ncols": 5568,
+ "a_name": "msg_fci_fdss_2km",
+ "a_desc": "MSG FCI Full Disk Scanning Service area definition with 2 km resolution",
+ "p_id": ""
+ }
+ assert pdict == expected_pdict
+ expected_area_dict = {
+ "nlines": 5568,
+ "ncols": 5568
+ }
+ assert area_dict == expected_area_dict
+
+ # Checks the correct execution of the get_area_def function
+ with mock.patch("satpy.readers.eum_l2_grib.fci_calculate_area_extent",
+ mock.Mock(name="fci_calculate_area_extent")) as cae:
+ with mock.patch("satpy.readers.eum_l2_grib.get_area_definition", mock.Mock()) as gad:
+ dataset_id = make_dataid(name="dummmy", resolution=2000.)
+ reader.get_area_def(dataset_id)
+ # Asserts that seviri_calculate_area_extent has been called with the correct arguments
+ expected_args = ({"nlines": 5568, "ncols": 5568,
+ "column_step": 2000., "line_step": 2000.},)
+ name, args, kwargs = cae.mock_calls[0]
+ assert args == expected_args
+ # Asserts that get_area_definition has been called with the correct arguments
+ name, args, kwargs = gad.mock_calls[0]
+ assert args[0] == expected_pdict
+ # The second argument must be the return result of seviri_calculate_area_extent
+ assert args[1]._extract_mock_name() == "fci_calculate_area_extent()"
diff --git a/satpy/tests/reader_tests/test_fci_base.py b/satpy/tests/reader_tests/test_fci_base.py
new file mode 100644
index 0000000000..eda7eee8a1
--- /dev/null
+++ b/satpy/tests/reader_tests/test_fci_base.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017 Satpy developers
+#
+# This file is part of satpy.
+#
+# satpy 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.
+#
+# satpy 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
+# satpy. If not, see .
+
+"""FCI base reader tests package."""
+
+from satpy.readers.fci_base import calculate_area_extent
+from satpy.tests.utils import make_dataid
+
+
+def test_calculate_area_extent():
+ """Test function for calculate_area_extent."""
+ dataset_id = make_dataid(name="dummy", resolution=2000.0)
+
+ area_dict = {
+ "nlines": 5568,
+ "ncols": 5568,
+ "line_step": dataset_id["resolution"],
+ "column_step": dataset_id["resolution"],
+ }
+
+ area_extent = calculate_area_extent(area_dict)
+
+ expected = (-5568000.0, 5568000.0, 5568000.0, -5568000.0)
+
+ assert area_extent == expected
diff --git a/satpy/tests/reader_tests/test_seviri_l2_grib.py b/satpy/tests/reader_tests/test_seviri_l2_grib.py
deleted file mode 100644
index d3b40d6caa..0000000000
--- a/satpy/tests/reader_tests/test_seviri_l2_grib.py
+++ /dev/null
@@ -1,182 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-#
-# Copyright (c) 2019 Satpy developers
-#
-# satpy 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.
-#
-# satpy 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 satpy. If not, see .
-
-"""SEVIRI L2 GRIB-reader test package."""
-
-import datetime
-import sys
-import unittest
-from unittest import mock
-
-import numpy as np
-
-from satpy.tests.utils import make_dataid
-
-# Dictionary to be used as fake GRIB message
-FAKE_MESSAGE = {
- "longitudeOfSubSatellitePointInDegrees": 9.5,
- "dataDate": 20191020,
- "dataTime": 1745,
- "Nx": 1000,
- "Ny": 1200,
- "earthMajorAxis": 6400.,
- "earthMinorAxis": 6300.,
- "NrInRadiusOfEarth": 6.,
- "XpInGridLengths": 500,
- "parameterNumber": 30,
- "missingValue": 9999,
-}
-
-# List to be used as fake GID source
-FAKE_GID = [0, 1, 2, 3, None]
-
-
-class Test_SeviriL2GribFileHandler(unittest.TestCase):
- """Test the SeviriL2GribFileHandler reader."""
-
- @mock.patch("satpy.readers.seviri_l2_grib.ec")
- def setUp(self, ec_):
- """Set up the test by creating a mocked eccodes library."""
- fake_gid_generator = (i for i in FAKE_GID)
- ec_.codes_grib_new_from_file.side_effect = lambda fh: next(fake_gid_generator)
- ec_.codes_get.side_effect = lambda gid, key: FAKE_MESSAGE[key]
- ec_.codes_get_values.return_value = np.ones(1000*1200)
- self.ec_ = ec_
-
- @unittest.skipIf(sys.platform.startswith("win"), "'eccodes' not supported on Windows")
- @mock.patch("satpy.readers.seviri_l2_grib.xr")
- @mock.patch("satpy.readers.seviri_l2_grib.da")
- def test_data_reading(self, da_, xr_):
- """Test the reading of data from the product."""
- from satpy.readers.seviri_l2_grib import REPEAT_CYCLE_DURATION, SeviriL2GribFileHandler
- from satpy.utils import get_legacy_chunk_size
- CHUNK_SIZE = get_legacy_chunk_size()
-
- with mock.patch("builtins.open", mock.mock_open()) as mock_file:
- with mock.patch("satpy.readers.seviri_l2_grib.ec", self.ec_):
- self.reader = SeviriL2GribFileHandler(
- filename="test.grib",
- filename_info={
- "spacecraft": "MET11",
- "start_time": datetime.datetime(year=2020, month=10, day=20,
- hour=19, minute=45, second=0)
- },
- filetype_info={}
- )
-
- dataset_id = make_dataid(name="dummmy", resolution=3000)
-
- # Checks that the codes_grib_multi_support_on function has been called
- self.ec_.codes_grib_multi_support_on.assert_called()
-
- # Restarts the id generator and clears the call history
- fake_gid_generator = (i for i in FAKE_GID)
- self.ec_.codes_grib_new_from_file.side_effect = lambda fh: next(fake_gid_generator)
- self.ec_.codes_grib_new_from_file.reset_mock()
- self.ec_.codes_release.reset_mock()
-
- # Checks the correct execution of the get_dataset function with a valid parameter_number
- valid_dataset = self.reader.get_dataset(dataset_id, {"parameter_number": 30})
- # Checks the correct file open call
- mock_file.assert_called_with("test.grib", "rb")
- # Checks that the dataset has been created as a DataArray object
- assert valid_dataset._extract_mock_name() == "xr.DataArray()"
- # Checks that codes_release has been called after each codes_grib_new_from_file call
- # (except after the last one which has returned a None)
- assert self.ec_.codes_grib_new_from_file.call_count == self.ec_.codes_release.call_count + 1
-
- # Restarts the id generator and clears the call history
- fake_gid_generator = (i for i in FAKE_GID)
- self.ec_.codes_grib_new_from_file.side_effect = lambda fh: next(fake_gid_generator)
- self.ec_.codes_grib_new_from_file.reset_mock()
- self.ec_.codes_release.reset_mock()
-
- # Checks the correct execution of the get_dataset function with an invalid parameter_number
- invalid_dataset = self.reader.get_dataset(dataset_id, {"parameter_number": 50})
- # Checks that the function returns None
- assert invalid_dataset is None
- # Checks that codes_release has been called after each codes_grib_new_from_file call
- # (except after the last one which has returned a None)
- assert self.ec_.codes_grib_new_from_file.call_count == self.ec_.codes_release.call_count + 1
-
- # Checks the basic data reading
- assert REPEAT_CYCLE_DURATION == 15
-
- # Checks the correct execution of the _get_global_attributes and _get_metadata_from_msg functions
- attributes = self.reader._get_attributes()
- expected_attributes = {
- "orbital_parameters": {
- "projection_longitude": 9.5
- },
- "sensor": "seviri",
- "platform_name": "Meteosat-11"
- }
- assert attributes == expected_attributes
-
- # Checks the reading of an array from the message
- self.reader._get_xarray_from_msg(0)
-
- # Checks that dask.array has been called with the correct arguments
- name, args, kwargs = da_.mock_calls[0]
- assert np.all(args[0] == np.ones((1200, 1000)))
- assert args[1] == CHUNK_SIZE
-
- # Checks that xarray.DataArray has been called with the correct arguments
- name, args, kwargs = xr_.mock_calls[0]
- assert kwargs["dims"] == ("y", "x")
-
- # Checks the correct execution of the _get_proj_area function
- pdict, area_dict = self.reader._get_proj_area(0)
-
- expected_pdict = {
- "a": 6400000.,
- "b": 6300000.,
- "h": 32000000.,
- "ssp_lon": 9.5,
- "nlines": 1000,
- "ncols": 1200,
- "a_name": "msg_seviri_rss_3km",
- "a_desc": "MSG SEVIRI Rapid Scanning Service area definition with 3 km resolution",
- "p_id": "",
- }
- assert pdict == expected_pdict
- expected_area_dict = {
- "center_point": 500,
- "north": 1200,
- "east": 1,
- "west": 1000,
- "south": 1,
- }
- assert area_dict == expected_area_dict
-
- # Checks the correct execution of the get_area_def function
- with mock.patch("satpy.readers.seviri_l2_grib.calculate_area_extent",
- mock.Mock(name="calculate_area_extent")) as cae:
- with mock.patch("satpy.readers.seviri_l2_grib.get_area_definition", mock.Mock()) as gad:
- dataset_id = make_dataid(name="dummmy", resolution=400.)
- self.reader.get_area_def(dataset_id)
- # Asserts that calculate_area_extent has been called with the correct arguments
- expected_args = ({"center_point": 500, "east": 1, "west": 1000, "south": 1, "north": 1200,
- "column_step": 400., "line_step": 400.},)
- name, args, kwargs = cae.mock_calls[0]
- assert args == expected_args
- # Asserts that get_area_definition has been called with the correct arguments
- name, args, kwargs = gad.mock_calls[0]
- assert args[0] == expected_pdict
- # The second argument must be the return result of calculate_area_extent
- assert args[1]._extract_mock_name() == "calculate_area_extent()"