Skip to content

Commit b898264

Browse files
committed
feat(repositories): Enable working with eu stream
1 parent 3272c0a commit b898264

4 files changed

Lines changed: 118 additions & 9 deletions

File tree

‎src/nwp_consumer/internal/repositories/raw_repositories/ecmwf_realtime.py‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
time of 2024-10-25 00:00 and a target time of 2024-10-26 01:00 (step of 25 hours).
2222
2323
The file contents is specific to the order agreed with the data provider.
24-
For the order that OCF has created, there are four distinct datasets.
25-
This is because OCF has ordered two separate regions and 17 variables,
26-
which are split across two datasets.
24+
OCF orders a single west-europe region alongside a separate india region,
25+
and the variables are split across multiple datasets within each file.
26+
Datasets that overlap the requested model region are cropped to it, which
27+
allows building a sub-region store (e.g. nl) from the larger west-europe order.
2728
2829
Also, some of the data contains larger steps than we are interested in due
2930
to necessities in the order creation process.
@@ -91,6 +92,9 @@ def repository() -> entities.RawRepositoryMetadata:
9192
"hres-ifs-nl": entities.Models.ECMWF_HRES_IFS_0P1DEGREE.with_region(
9293
"nl",
9394
).with_max_step(84).with_delay_minutes(60 * 7),
95+
"hres-ifs-west-europe": entities.Models.ECMWF_HRES_IFS_0P1DEGREE.with_region(
96+
"west-europe",
97+
).with_delay_minutes(60 * 7),
9498
},
9599
)
96100

@@ -273,14 +277,22 @@ def _convert(path: pathlib.Path) -> ResultE[list[xr.DataArray]]:
273277
expected_lats = ECMWFRealTimeS3RawRepository.model().expected_coordinates.latitude
274278
expected_steps = ECMWFRealTimeS3RawRepository.model().expected_coordinates.step
275279

280+
north, west, south, east = \
281+
ECMWFRealTimeS3RawRepository.model().expected_coordinates.nwse()
282+
276283
for i, ds in enumerate(dss):
277-
# ECMWF Realtime provides all regions in one set of datasets,
278-
# so distinguish via their coordinates
284+
# ECMWF Realtime provides multiple orders in one set of datasets
285+
# (e.g. west-europe and india), so distinguish via their coordinates.
286+
# Datasets that overlap the requested region are kept and cropped to it,
287+
# which allows a sub-region store (e.g. nl) to be built from the
288+
# larger west-europe order.
289+
ds_lons = ds.coords["longitude"].values
290+
ds_lats = ds.coords["latitude"].values
279291
step = np.timedelta64(ds.coords["step"].values, "h").astype(int) # type: ignore[arg-type]
280292
is_relevant_dataset_predicate: bool = (
281293
(expected_lons is not None and expected_lats is not None)
282-
and (expected_lons[0] <= max(ds.coords["longitude"].values) <= expected_lons[-1])
283-
and (expected_lats[-1] <= max(ds.coords["latitude"].values) <= expected_lats[0])
294+
and (min(ds_lons) <= expected_lons[-1] and max(ds_lons) >= expected_lons[0])
295+
and (min(ds_lats) <= expected_lats[0] and max(ds_lats) >= expected_lats[-1])
284296
and (expected_steps[0] <= step <= expected_steps[-1])
285297
)
286298
if not is_relevant_dataset_predicate:
@@ -311,6 +323,15 @@ def _convert(path: pathlib.Path) -> ResultE[list[xr.DataArray]]:
311323
.sortby(variables=["step", "variable", "longitude"])
312324
.sortby(variables="latitude", ascending=False)
313325
)
326+
# Crop to the requested model region, since the order may cover
327+
# a larger extent (e.g. cropping nl from the west-europe order).
328+
# Round coordinates to the store's 4 d.p. precision first, else
329+
# floating-point grid-edge differences drop boundary points and
330+
# misalign the write with chunk boundaries.
331+
da = da.assign_coords(
332+
latitude=[float(f"{v:.4f}") for v in da["latitude"].values],
333+
longitude=[float(f"{v:.4f}") for v in da["longitude"].values],
334+
).sel(latitude=slice(north, south), longitude=slice(west, east))
314335

315336
except Exception as e:
316337
return Failure(

‎src/nwp_consumer/internal/repositories/raw_repositories/mo_datahub.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ def repository() -> entities.RawRepositoryMetadata:
168168
"um-global-10km-nl": entities.Models.MO_UM_GLOBAL_10KM\
169169
.with_region("nl")\
170170
.with_extra_parameters([entities.Parameter.PRESSURE_MSL]),
171+
"um-global-10km-west-europe": entities.Models.MO_UM_GLOBAL_10KM\
172+
.with_region("west-europe")\
173+
.with_extra_parameters([entities.Parameter.PRESSURE_MSL]),
171174
"um-ukv-2km": entities.Models.MO_UM_UKV_2KM_LAEA,
172175
},
173176
)
@@ -403,6 +406,18 @@ def _convert_global(path: pathlib.Path) -> ResultE[list[xr.DataArray]]:
403406
da = da.sortby(variables=["step", "variable", "longitude"])
404407
da = da.sortby(variables="latitude", ascending=False)
405408

409+
# The order covers the full west-europe extent, so crop to the
410+
# requested model region. Round coordinates to the store's 4 d.p.
411+
# precision first, else floating-point grid-edge differences drop
412+
# boundary points and misalign the write with chunk boundaries.
413+
# This is a no-op when the model region matches the order bounds.
414+
north, west, south, east = \
415+
MetOfficeDatahubRawRepository.model().expected_coordinates.nwse()
416+
da = da.assign_coords(
417+
latitude=[float(f"{v:.4f}") for v in da["latitude"].values],
418+
longitude=[float(f"{v:.4f}") for v in da["longitude"].values],
419+
).sel(latitude=slice(north, south), longitude=slice(west, east))
420+
406421
except Exception as e:
407422
return Failure(
408423
ValueError(f"Error processing DataArray for path '{path}'. Error context: {e}"),

‎src/nwp_consumer/internal/repositories/raw_repositories/test_ecmwf_realtime.py‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from returns.result import Failure, ResultE, Success
1010

11-
from ...entities import NWPDimensionCoordinateMap, Parameter
11+
from ...entities import Models, NWPDimensionCoordinateMap, Parameter
1212
from .ecmwf_realtime import ECMWFRealTimeS3RawRepository
1313

1414
if TYPE_CHECKING:
@@ -170,3 +170,35 @@ class TestCase:
170170
@patch.dict(os.environ, {"MODEL": "hres-ifs-india"}, clear=True)
171171
def test_convert_india(self) -> None:
172172
return self.test__convert()
173+
174+
def test_convert_crops_to_region(self) -> None:
175+
"""Test that _convert crops datasets to the requested model region.
176+
177+
The test grib contains a uk dataset (lon -12..3, lat 48..60); requesting
178+
a smaller sub-region should crop the output down to it, mimicking cropping
179+
e.g. nl from the larger west-europe order.
180+
"""
181+
cropped_model = dataclasses.replace(
182+
Models.ECMWF_HRES_IFS_0P1DEGREE,
183+
name="hres-ifs_uk-sub",
184+
expected_coordinates=Models.ECMWF_HRES_IFS_0P1DEGREE.expected_coordinates.crop(
185+
north=55, west=-8, south=50, east=0,
186+
).unwrap(),
187+
)
188+
189+
with patch.object(
190+
ECMWFRealTimeS3RawRepository,
191+
"model",
192+
staticmethod(lambda: cropped_model),
193+
):
194+
result = ECMWFRealTimeS3RawRepository._convert(
195+
path=pathlib.Path(__file__).parent.absolute()
196+
/ "test_gribs"
197+
/ "test_ECMWFRealtime_HRES-IFS_10u_20241104T00_S60.grib",
198+
)
199+
self.assertIsInstance(result, Success, msg=f"{result!s}")
200+
for da in result.unwrap():
201+
self.assertGreaterEqual(float(da.latitude.min()), 50)
202+
self.assertLessEqual(float(da.latitude.max()), 55)
203+
self.assertGreaterEqual(float(da.longitude.min()), -8)
204+
self.assertLessEqual(float(da.longitude.max()), 0)

‎src/nwp_consumer/internal/repositories/raw_repositories/test_mo_datahub.py‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from returns.result import Failure, ResultE, Success
99

10-
from ...entities import NWPDimensionCoordinateMap, Parameter
10+
from ...entities import Models, NWPDimensionCoordinateMap, Parameter
1111
from .mo_datahub import MetOfficeDatahubRawRepository
1212

1313

@@ -95,6 +95,47 @@ class TestCase:
9595

9696

9797

98+
def test_convert_crops_to_region(self) -> None:
99+
"""Test that _convert_global crops the data to the requested model region.
100+
101+
The order provides a larger extent (here, the india test grib), which
102+
should be cropped down to a smaller requested region.
103+
"""
104+
cropped_model = dataclasses.replace(
105+
Models.MO_UM_GLOBAL_10KM,
106+
name="um-global_india-sub",
107+
expected_coordinates=Models.MO_UM_GLOBAL_10KM.expected_coordinates.crop(
108+
north=20, west=70, south=10, east=90,
109+
).unwrap(),
110+
)
111+
112+
with patch.object(
113+
MetOfficeDatahubRawRepository,
114+
"model",
115+
staticmethod(lambda: cropped_model),
116+
):
117+
result = MetOfficeDatahubRawRepository._convert_global(
118+
path=pathlib.Path(__file__).parent.absolute()
119+
/ "test_gribs"
120+
/ "test_MODatahub_UM-Global_t2m_20241120T00_S00.grib",
121+
)
122+
self.assertIsInstance(result, Success, msg=f"{result!s}")
123+
da = result.unwrap()[0]
124+
self.assertGreaterEqual(float(da.latitude.min()), 10)
125+
self.assertLessEqual(float(da.latitude.max()), 20)
126+
self.assertGreaterEqual(float(da.longitude.min()), 70)
127+
self.assertLessEqual(float(da.longitude.max()), 90)
128+
# Output must be a contiguous subset of the cropped region store
129+
region_result = NWPDimensionCoordinateMap.from_xarray(da).bind(
130+
dataclasses.replace(
131+
cropped_model.expected_coordinates,
132+
init_time=[dt.datetime(2024, 11, 20, 0, tzinfo=dt.UTC)],
133+
variable=[Parameter.TEMPERATURE_SL],
134+
step=[0],
135+
).determine_region,
136+
)
137+
self.assertIsInstance(region_result, Success, msg=f"{region_result!s}")
138+
98139
@patch.dict(os.environ, {"MODEL": "um-ukv-2km"}, clear=True)
99140
def test_convert_ukv(self) -> None:
100141
@dataclasses.dataclass

0 commit comments

Comments
 (0)