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()"