Skip to content

Commit f8daa85

Browse files
committed
move delay_minutes to metamodel
1 parent a8122e0 commit f8daa85

File tree

12 files changed

+55
-72
lines changed

12 files changed

+55
-72
lines changed

src/nwp_consumer/internal/entities/modelmetadata.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ class ModelMetadata:
6565
to the repository.
6666
"""
6767

68+
delay_minutes: int
69+
"""The model delay in minutes.
70+
71+
Note that this can be overwritten by using `with_delay_minutes`.
72+
"""
73+
6874
chunk_count_overrides: dict[str, int] = dataclasses.field(default_factory=dict)
6975
"""Mapping of dimension names to the desired number of chunks in that dimension.
7076
@@ -201,6 +207,10 @@ def with_running_hours(self, hours: list[int]) -> "ModelMetadata":
201207
"""Returns metadata for the given model with the given running hours."""
202208
return dataclasses.replace(self, running_hours=hours)
203209

210+
def with_delay_minutes(self, delay_minutes: int) -> "ModelMetadata":
211+
"""Returns metadata for the given model with the given delay minutes."""
212+
return dataclasses.replace(self, delay_minutes=delay_minutes)
213+
204214
def with_max_step(self, max_step: int) -> "ModelMetadata":
205215
"""Returns metadata for the given model with the given max step."""
206216
return dataclasses.replace(
@@ -254,6 +264,7 @@ class Models:
254264
longitude=[float(f"{lon / 10:.2f}") for lon in range(-1800, 1800 + 1, 1)],
255265
),
256266
running_hours=[0, 6, 12, 18],
267+
delay_minutes=(60 * 26), # 1 day, plus leeway
257268
)
258269
"""ECMWF's High Resolution Integrated Forecast System."""
259270

@@ -274,6 +285,7 @@ class Models:
274285
longitude=[v / 10 for v in range(-1800, 1800, 1)],
275286
),
276287
running_hours=[0, 12],
288+
delay_minutes=(60 * 26), # 1 day, plus leeway
277289
)
278290
"""Summary statistics from ECMWF's Ensemble Forecast System."""
279291

@@ -303,6 +315,7 @@ class Models:
303315
longitude=[v / 10 for v in range(-1800, 1800, 1)],
304316
),
305317
running_hours=[0, 6, 12, 18],
318+
delay_minutes=(60 * 26), # 1 day, plus leeway
306319
)
307320
"""Full ensemble data from ECMWF's Ensemble Forecast System."""
308321

@@ -335,6 +348,7 @@ class Models:
335348
longitude=[float(lon) for lon in range(-180, 180 + 1, 1)],
336349
),
337350
running_hours=[0, 6, 12, 18],
351+
delay_minutes=(60 * 5), # 5 hours
338352
)
339353
"""NCEP's Global Forecast System."""
340354

@@ -372,6 +386,7 @@ class Models:
372386
# TODO: Change to -180 -> 180
373387
),
374388
running_hours=[0, 6, 12, 18],
389+
delay_minutes=(60 * 24 * 7) + (60 * 12), # 7.5 days
375390
)
376391
"""MetOffice's Unified Model, in the Global configuration, at a resolution of 17km."""
377392

@@ -405,6 +420,7 @@ class Models:
405420
],
406421
),
407422
running_hours=[0, 12],
423+
delay_minutes=300,
408424
)
409425
"""MetOffice's Unified Model, in the Global configuration, at a resolution of 10km."""
410426

@@ -438,6 +454,7 @@ class Models:
438454
x_osgb=[int(x) for x in np.arange(start=-239000, stop=857000, step=2000)],
439455
),
440456
running_hours=list(range(0, 24, 6)),
457+
delay_minutes=120,
441458
)
442459
"""MetOffice's Unified Model in the UKV configuration, at a resolution of 2km"""
443460

@@ -468,5 +485,6 @@ class Models:
468485
x_laea=[int(x) for x in np.arange(start=-576000, stop=332000 + 2000, step=2000)],
469486
),
470487
running_hours=list(range(0, 24, 3)), # Only first 12 steps available for hourly runs
488+
delay_minutes=120,
471489
)
472490
"""MetOffice's Unified Model in the UKV configuration, at a resolution of 2km"""

src/nwp_consumer/internal/entities/repometadata.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,6 @@ class RawRepositoryMetadata:
4040
but rather are defined by pre-selected agreements with the provider.
4141
"""
4242

43-
delay_minutes: int
44-
"""The approximate model delay in minutes.
45-
46-
This delay is the time between the running of the model and the time
47-
at which the data is actually available."""
48-
4943
required_env: list[str]
5044
"""Environment variables required for usage."""
5145

@@ -65,18 +59,23 @@ class RawRepositoryMetadata:
6559
available_models: dict[str, ModelMetadata]
6660
"""A dictionary of available models and their metadata."""
6761

68-
def determine_latest_it_from(self, t: dt.datetime, running_hours: list[int]) -> dt.datetime:
62+
def determine_latest_it_from(
63+
self, t: dt.datetime,
64+
running_hours: list[int],
65+
delay_minutes:int) -> dt.datetime:
6966
"""Determine the latest available initialization time from a given time.
7067
7168
Args:
7269
t: The time from which to determine the latest initialization time.
7370
running_hours: A list of hours at which the model runs each day.
71+
delay_minutes: The delay in minutes after the initialization time
72+
before data is available.
7473
7574
Returns:
7675
The latest available initialization time prior to the given time.
7776
"""
7877
it = (
79-
t.replace(minute=0, second=0, microsecond=0) - dt.timedelta(minutes=self.delay_minutes)
78+
t.replace(minute=0, second=0, microsecond=0) - dt.timedelta(minutes=delay_minutes)
8079
).replace(minute=0)
8180
while it.hour not in running_hours:
8281
it -= dt.timedelta(hours=1)

src/nwp_consumer/internal/entities/test_modelmetadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def test_with_region(self) -> None:
2727
longitude=[float(f"{lon / 10:.2f}") for lon in range(-1800, 1800 + 1, 1)],
2828
),
2929
running_hours=[0, 6, 12, 18],
30+
delay_minutes=60,
3031
)
3132

3233
@dataclasses.dataclass

src/nwp_consumer/internal/entities/test_repometadata.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ class TestRawRepositoryMetadata(unittest.TestCase):
1313
name="test",
1414
is_archive=False,
1515
is_order_based=False,
16-
delay_minutes=55,
1716
required_env=["TEST"],
1817
optional_env={"TEST": "test"},
1918
max_connections=1,
@@ -50,7 +49,9 @@ class TestCase:
5049

5150
for test in tests:
5251
with self.subTest(name=test.name):
53-
result = self.metadata.determine_latest_it_from(test.t, [0, 6, 12, 18])
52+
result = self.metadata.determine_latest_it_from(test.t,
53+
[0, 6, 12, 18],
54+
delay_minutes=60)
5455
self.assertEqual(result, test.expected)
5556

5657

src/nwp_consumer/internal/repositories/raw_repositories/ceda.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ def repository() -> entities.RawRepositoryMetadata:
117117
name="CEDA",
118118
is_archive=True,
119119
is_order_based=False,
120-
delay_minutes=(60 * 24 * 7) + (60 * 12), # 7.5 days
121120
max_connections=20,
122121
required_env=["CEDA_USER", "CEDA_PASS"],
123122
optional_env={},

src/nwp_consumer/internal/repositories/raw_repositories/ecmwf_mars.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,6 @@ def repository() -> entities.RawRepositoryMetadata:
211211
name="ECMWF-MARS",
212212
is_archive=True,
213213
is_order_based=False,
214-
delay_minutes=(60 * 26), # 1 day, plus leeway
215214
max_connections=20,
216215
required_env=[
217216
"ECMWF_API_KEY",

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ def repository() -> entities.RawRepositoryMetadata:
6767
name="ECMWF-Realtime-S3",
6868
is_archive=False,
6969
is_order_based=True,
70-
delay_minutes=(60 * 7), # 7 hours
7170
max_connections=100,
7271
required_env=[
7372
"ECMWF_REALTIME_S3_ACCESS_KEY",
@@ -81,14 +80,16 @@ def repository() -> entities.RawRepositoryMetadata:
8180
},
8281
postprocess_options=entities.PostProcessOptions(),
8382
available_models={
84-
"default": entities.Models.ECMWF_HRES_IFS_0P1DEGREE.with_region("uk-north60"),
85-
"hres-ifs-uk": entities.Models.ECMWF_HRES_IFS_0P1DEGREE.with_region("uk-north60"),
83+
"default": entities.Models.ECMWF_HRES_IFS_0P1DEGREE.with_region("uk-north60").
84+
with_delay_minutes(60 * 7),
85+
"hres-ifs-uk": entities.Models.ECMWF_HRES_IFS_0P1DEGREE.with_region("uk-north60").
86+
with_delay_minutes(60 * 7),
8687
"hres-ifs-india": entities.Models.ECMWF_HRES_IFS_0P1DEGREE.with_region(
8788
"india",
88-
).with_chunk_count_overrides({"variable": 1}),
89+
).with_chunk_count_overrides({"variable": 1}).with_delay_minutes(60 * 7),
8990
"hres-ifs-nl": entities.Models.ECMWF_HRES_IFS_0P1DEGREE.with_region(
9091
"nl",
91-
).with_max_step(84),
92+
).with_max_step(84).with_delay_minutes(60 * 7),
9293
},
9394
)
9495

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

Lines changed: 15 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -123,41 +123,6 @@
123123

124124
log = logging.getLogger("nwp-consumer")
125125

126-
class AvailableModel:
127-
"""Class to hold information about an available model."""
128-
name: str
129-
delay_minute: int
130-
model: entities.ModelMetadata
131-
132-
def __init__(self, name: str, delay_minute: int, model: entities.ModelMetadata) -> None:
133-
"""Create a new instance."""
134-
self.name = name
135-
self.delay_minute = delay_minute
136-
self.model = model
137-
138-
# set up the available models
139-
available_models = []
140-
available_models.append(AvailableModel(name="default",
141-
delay_minute=300,
142-
model=entities.Models.MO_UM_GLOBAL_10KM.with_region("india")))
143-
available_models.append(AvailableModel(name="um-global-10km-india",
144-
delay_minute=300,
145-
model=entities.Models.MO_UM_GLOBAL_10KM.with_region("india")))
146-
available_models.append(AvailableModel(name="um-global-10km-uk",
147-
delay_minute=300,
148-
model=entities.Models.MO_UM_GLOBAL_10KM.with_region("uk")))
149-
available_models.append(AvailableModel(name="um-ukv-2km",
150-
delay_minute=120,
151-
model=entities.Models.MO_UM_UKV_2KM_LAEA))
152-
153-
# get all model names
154-
available_models_names = [m.name for m in available_models]
155-
# get dict of delay minutes by model name
156-
delay_minutes_dict = {m.name: m.delay_minute for m in available_models}
157-
# get dict of model metadata by model name
158-
models_dict = {m.name: m.model for m in available_models}
159-
160-
161126

162127
class MetOfficeDatahubRawRepository(ports.RawRepository):
163128
"""Repository implementation for data from MetOffice's DataHub service."""
@@ -188,24 +153,34 @@ def __init__(self, order_id: str, api_key: str) -> None:
188153
@override
189154
def repository() -> entities.RawRepositoryMetadata:
190155

191-
requested_model: str = get_requested_model_name()
192-
193156
return entities.RawRepositoryMetadata(
194157
name="MetOffice-Weather-Datahub",
195158
is_archive=False,
196159
is_order_based=True,
197-
delay_minutes=delay_minutes_dict[requested_model],
198160
max_connections=10,
199161
required_env=["METOFFICE_API_KEY", "METOFFICE_ORDER_ID"],
200162
optional_env={"METOFFICE_DATASPEC": "1.1.0"},
201163
postprocess_options=entities.PostProcessOptions(),
202-
available_models=models_dict,
164+
available_models={
165+
"default": entities.Models.MO_UM_GLOBAL_10KM.with_region("india"),
166+
"um-global-10km-india": entities.Models.MO_UM_GLOBAL_10KM.with_region("india"),
167+
"um-global-10km-uk": entities.Models.MO_UM_GLOBAL_10KM.with_region("uk"),
168+
"um-ukv-2km": entities.Models.MO_UM_UKV_2KM_LAEA,
169+
},
203170
)
204171

205172
@staticmethod
206173
@override
207174
def model() -> entities.ModelMetadata:
208-
requested_model: str = get_requested_model_name()
175+
requested_model: str = os.getenv("MODEL", default="default")
176+
if requested_model not in MetOfficeDatahubRawRepository.repository().available_models:
177+
log.warn(
178+
f"Unknown model '{requested_model}' requested, falling back to default. "
179+
"MetOffice Datahub repository only supports "
180+
f"'{list(MetOfficeDatahubRawRepository.repository().available_models.keys())}'. "
181+
"Ensure MODEL environment variable is set to a valid model name.",
182+
)
183+
requested_model = "default"
209184
return MetOfficeDatahubRawRepository.repository().available_models[requested_model]
210185

211186
@classmethod
@@ -542,16 +517,3 @@ def _convert_ukv(path: pathlib.Path) -> ResultE[list[xr.DataArray]]:
542517
)
543518

544519
return Success([da])
545-
546-
547-
def get_requested_model_name() -> str:
548-
"""Get the requested model name from the environment variable."""
549-
requested_model: str = os.getenv("MODEL", default="default")
550-
if requested_model not in available_models_names:
551-
log.warn(
552-
f"Unknown model '{requested_model}' requested, falling back to default. "
553-
f"MetOffice Datahub repository only supports '{available_models_names}'. "
554-
"Ensure MODEL environment variable is set to a valid model name.",
555-
)
556-
requested_model = "default"
557-
return requested_model

src/nwp_consumer/internal/repositories/raw_repositories/noaa_s3.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ def repository() -> entities.RawRepositoryMetadata:
5555
name="NOAA-GFS-S3",
5656
is_archive=False,
5757
is_order_based=False,
58-
delay_minutes=(60 * 5), # 5 hours
5958
max_connections=100,
6059
required_env=[],
6160
optional_env={},

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def test__download(self) -> None:
3636

3737
self.assertIsInstance(dl_result, Success, msg=f"{dl_result!s}")
3838

39+
@patch.dict(os.environ, {"MODEL": "um-global-10km-india"}, clear=True)
3940
def test_convert(self) -> None:
4041
"""Test the _convert method."""
4142

@@ -92,6 +93,8 @@ class TestCase:
9293
else:
9394
self.assertIsInstance(region_result, Success, msg=f"{region_result}")
9495

96+
97+
9598
@patch.dict(os.environ, {"MODEL": "um-ukv-2km"}, clear=True)
9699
def test_convert_ukv(self) -> None:
97100
@dataclasses.dataclass

0 commit comments

Comments
 (0)