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

Add combined GRIB reader for both SEVIRI and FCI L2 products #2717

Merged
merged 28 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
28f44c7
Add common functionality for FCI data readers
Jan 11, 2024
d3ccedc
Add reader for both SEVIRI and FCI L2 products in GRIB2 format
Jan 11, 2024
de65b63
Add EUM L2 GRIB-reader test package
Jan 11, 2024
0474fd0
Add my name to AUTHORS.md
Jan 11, 2024
1150b44
Merge pull request #1 from dnaviap/feature/eum_l2_grib_reader
dnaviap Jan 11, 2024
d8043db
Merge branch 'pytroll:main' into main
dnaviap Jan 11, 2024
767aeab
Delete eum_l2_grib.yaml and update seviri_l2_grib.yaml to avoid chang…
Jan 12, 2024
a53ddea
Add fci_l2_grib.yaml reader
Jan 12, 2024
f94c4f7
Delete seviri_l2_grib.py since eum_l2_grib.py is compatible with FCI …
Jan 12, 2024
66946ad
Delete obsolete test_seviri_l2_grib.py
Jan 12, 2024
e7009de
Merge pull request #2 from dnaviap/feature/eum_l2_grib_refactor
dnaviap Jan 12, 2024
5bbb421
Refactor duplicate code in tests
May 8, 2024
486b3a6
Correct for RSS data
May 8, 2024
c6322fa
Modify fci_base doc-string
May 8, 2024
ed5213b
Fix end_time computation, optimize SEVIRI imports and fix code style …
strandgren Jun 13, 2024
de9a73e
Merge with remote main
strandgren Jun 13, 2024
164bcba
Add tests for end_time.
strandgren Jun 13, 2024
98fcca2
Merge pull request #3 from strandgren/feature_combined_eum_grib_reader
dnaviap Jun 24, 2024
241bbc0
Merge branch 'pytroll:main' into main
dnaviap Jun 24, 2024
c3efc55
Add fci base test
Jun 24, 2024
6a7ba7f
Merge pull request #4 from dnaviap/feature/add-fci-base-test
dnaviap Jun 24, 2024
291d288
Merge branch 'pytroll:main' into main
dnaviap Jun 24, 2024
12f1860
Merge branch 'pytroll:main' into main
dnaviap Jul 1, 2024
54acda0
Adapt to use pytest instead of unittest
strandgren Oct 14, 2024
b263479
Update tests to use pytest instead of unittest
strandgren Oct 14, 2024
966855e
Merge branch 'main' into main
dnaviap Oct 15, 2024
eba7964
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 16, 2024
ba30733
Remove unused imports
strandgren Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions satpy/readers/eum_l2_grib.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"""

import logging
from datetime import timedelta

import dask.array as da
import numpy as np
Expand All @@ -33,7 +32,7 @@
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
from satpy.readers.seviri_base import PLATFORM_DICT, REPEAT_CYCLE_DURATION, REPEAT_CYCLE_DURATION_RSS
dnaviap marked this conversation as resolved.
Show resolved Hide resolved
from satpy.readers.seviri_base import calculate_area_extent as seviri_calculate_area_extent
from satpy.utils import get_legacy_chunk_size

Expand Down Expand Up @@ -75,10 +74,8 @@ def start_time(self):
@property
def end_time(self):
"""Return the sensing end time."""
if self.sensor == "seviri":
return self.start_time + timedelta(minutes=REPEAT_CYCLE_DURATION)
elif self.sensor == "fci":
return self.filename_info["end_time"]
delta = REPEAT_CYCLE_DURATION_RSS if self._ssp_lon == 9.5 else REPEAT_CYCLE_DURATION
return self.start_time + delta
dnaviap marked this conversation as resolved.
Show resolved Hide resolved

def get_area_def(self, dataset_id):
"""Return the area definition for a dataset."""
Expand Down Expand Up @@ -249,10 +246,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))
Expand Down
2 changes: 1 addition & 1 deletion satpy/readers/fci_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
def calculate_area_extent(area_dict):
"""Calculate the area extent seen by MTG FCI instrument.

Since the center of the FCI L2 grid is located at the interface between the pixels, there are equally many
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).

Expand Down
2 changes: 2 additions & 0 deletions satpy/readers/seviri_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@

REPEAT_CYCLE_DURATION = 15

REPEAT_CYCLE_DURATION_RSS = 5

C1 = 1.19104273e-5
C2 = 1.43877523

Expand Down
101 changes: 37 additions & 64 deletions satpy/tests/reader_tests/test_eum_l2_grib.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,132 +70,136 @@
ec_.codes_grib_new_from_file.side_effect = lambda fh: next(fake_gid_generator)
self.ec_ = ec_

def common_checks(self, mock_file, dataset_id):
"""Commmon checks for fci and seviri data."""
# 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

@unittest.skipIf(sys.platform.startswith("win"), "'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(self, da_, xr_):
"""Test the reading of data from the product."""
from satpy.readers.eum_l2_grib import REPEAT_CYCLE_DURATION, EUML2GribFileHandler
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.eum_l2_grib.ec", self.ec_):
self.ec_.codes_get_values.return_value = np.ones(1000*1200)
self.ec_.codes_get.side_effect = lambda gid, key: FAKE_SEVIRI_MESSAGE[key]
self.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)

# 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
self.common_checks(mock_file, dataset_id)

# Checks the basic data reading
assert REPEAT_CYCLE_DURATION == 15
dnaviap marked this conversation as resolved.
Show resolved Hide resolved

# 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.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.)
self.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()"

Check warning on line 202 in satpy/tests/reader_tests/test_eum_l2_grib.py

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ New issue: Large Method

Test_EUML2GribFileHandler.test_seviri_data_reading has 71 lines, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.

@unittest.skipIf(sys.platform.startswith("win"), "'eccodes' not supported on Windows")
@mock.patch("satpy.readers.eum_l2_grib.xr")
Expand Down Expand Up @@ -224,38 +228,7 @@

dataset_id = make_dataid(name="dummmy", resolution=2000)

# 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
self.common_checks(mock_file, dataset_id)

# Checks the correct execution of the _get_global_attributes and _get_metadata_from_msg functions
attributes = self.reader._get_attributes()
Expand Down