From 06c6e4ed5258ffdf68e29185794586ea48acdb40 Mon Sep 17 00:00:00 2001 From: Niall Siegenheim Date: Thu, 25 Jun 2026 19:03:08 +0200 Subject: [PATCH 1/4] feat(models): replace ecmwf_aifs025_ensemble with hosted aifs_ens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The open-meteo AIFS ensemble (`ecmwf_aifs025_ensemble`) has been retired in jua-core and replaced by the hosted ClickHouse model `aifs_ens` (AIFS ENS from ECMWF Open Data). The query engine no longer accepts the old id (returns 422 "not a valid Model"), which is why the SDK functional tests began failing for `ecmwf_aifs025_ensemble`. Swap the enum entry and its `_MODEL_META_INFO`: - `Models.AIFS_ENS = "aifs_ens"` in the **With Grid Access** block (next to AIFS). Unlike the old open-meteo model, the hosted model is a ClickHouse grid model, so grid/bbox slices are supported — not point-only. - `has_grid_access=True`, `has_statistics=True` (51-member ensemble). 0.25 degree grid (1440x720) matches the default num_lats/num_lons. - `has_forecast_file_access=False`: served from the query engine (ClickHouse), not the Jua zarr archive — same as the other externally-sourced CH models (ICON, NOAA GFS, EC IFS). - 360 forecasted hours, uniform 6-hourly cadence (0-360h, 61 steps). - Drop `forecast_name_mapping` — the enum value equals the API name. The SDK applies no client-side restriction gating, so aifs_ens is fully accessible (point + grid + statistics); access is governed purely by the server-side `allowed_models` entitlement (granted in the jua-core migration). The old id is intentionally NOT aliased: the hosted model does not serve `geopotential_at_pressure_level_50000Pa`, so silently re-pointing would 422. Verification: - jua-core config: libraries/jua-query-v2/src/jua_query_v2/configs/forecasts/models/aifs_ens.yaml - grid: "720x1440", is_ensemble_model: true - all 11 YAML variables already present in Variables enum - just check-commit passes (lint + 202 tests) --- src/jua/weather/_model_meta.py | 8 +++++--- src/jua/weather/models.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/jua/weather/_model_meta.py b/src/jua/weather/_model_meta.py index 353df12..007f9d1 100644 --- a/src/jua/weather/_model_meta.py +++ b/src/jua/weather/_model_meta.py @@ -234,11 +234,13 @@ class ModelMetaInfo: special=((1, 0, 78),), ), ) -_MODEL_META_INFO[Models.ECMWF_AIFS_ENSEMBLE] = ModelMetaInfo( - forecast_name_mapping="ecmwf_aifs025_ensemble", +_MODEL_META_INFO[Models.AIFS_ENS] = ModelMetaInfo( + has_grid_access=True, full_forecasted_hours=360, - has_forecast_file_access=False, has_statistics=True, + # 0.25 degree grid (1440x720 after south-pole drop) — matches the default + # num_lats=720 / num_lons=1440, so no override needed. + temporal_resolution=TemporalResolution(base=6), ) _MODEL_META_INFO[Models.ECMWF_IFS_ENSEMBLE] = ModelMetaInfo( full_forecasted_hours=360, diff --git a/src/jua/weather/models.py b/src/jua/weather/models.py index c0086a9..43cd1fe 100644 --- a/src/jua/weather/models.py +++ b/src/jua/weather/models.py @@ -30,6 +30,7 @@ class Models(str, Enum): EPT2_HELIOS = "ept2_1_helios" EPT2_EUROPA = "ept2_1_europa" AIFS = "aifs" + AIFS_ENS = "aifs_ens" AURORA = "aurora" ECMWF_IFS_SINGLE = "ecmwf_ifs_single" ICON_EU = "icon_eu" @@ -37,7 +38,6 @@ class Models(str, Enum): NOAA_GFS_SINGLE = "noaa_gfs_single" # Without Grid Access - ECMWF_AIFS_ENSEMBLE = "ecmwf_aifs025_ensemble" ECMWF_IFS_ENSEMBLE = "ecmwf_ens" GFS_GLOBAL_ENSEMBLE = "gfs_global_ensemble" GFS_GLOBAL_SINGLE = "gfs_global_single" From d6a653d346a738e6fc2ded72b47d63c332a2a833 Mon Sep 17 00:00:00 2001 From: Niall Siegenheim Date: Thu, 25 Jun 2026 19:19:55 +0200 Subject: [PATCH 2/4] test(models): assert aifs_ens grid access and old id removal Lock in that the hosted aifs_ens model exposes grid access + statistics (not point-only) and that the retired open-meteo ecmwf_aifs025_ensemble id is gone. --- tests/weather/test_aifs_ens_meta.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/weather/test_aifs_ens_meta.py diff --git a/tests/weather/test_aifs_ens_meta.py b/tests/weather/test_aifs_ens_meta.py new file mode 100644 index 0000000..5acb19f --- /dev/null +++ b/tests/weather/test_aifs_ens_meta.py @@ -0,0 +1,27 @@ +"""Regression tests for the hosted AIFS ENS model metadata. + +AIFS ENS replaced the retired open-meteo ``ecmwf_aifs025_ensemble``. Unlike the +old point-only open-meteo model, the hosted ClickHouse model exposes full grid +access plus ensemble statistics, so it must not regress back into the +"point queries only" behaviour gated by ``has_grid_access``. +""" + +from jua.weather._model_meta import get_model_meta_info +from jua.weather.models import Models + + +def test_aifs_ens_is_fully_accessible() -> None: + meta = get_model_meta_info(Models.AIFS_ENS) + # Hosted ClickHouse grid model: grid/bbox slices must be allowed, not just points. + assert meta.has_grid_access is True + # 51-member ensemble: statistics available. + assert meta.has_statistics is True + # 0.25 degree global grid (1440x720 after south-pole drop). + assert (meta.num_lats, meta.num_lons) == (720, 1440) + # 0-360h forecast horizon. + assert meta.full_forecasted_hours == 360 + + +def test_retired_open_meteo_aifs_ensemble_is_removed() -> None: + assert not hasattr(Models, "ECMWF_AIFS_ENSEMBLE") + assert "ecmwf_aifs025_ensemble" not in {m.value for m in Models} From bcfe515eb6ca422abd2a6a3ec261f4fa5fd9854f Mon Sep 17 00:00:00 2001 From: Niall Siegenheim Date: Thu, 25 Jun 2026 21:57:32 +0200 Subject: [PATCH 3/4] refactor(models): keep ecmwf_aifs025_ensemble; add aifs_ens additively Make this PR purely additive (expand phase) so it can merge fully green. Removing the old open-meteo ecmwf_aifs025_ensemble is deferred to a separate contract-phase PR, after the backend that serves aifs_ens is deployed. Net effect vs main: add Models.AIFS_ENS (grid access + statistics) and its meta; the existing ECMWF_AIFS_ENSEMBLE entry is retained. Drop the old-id-removed assertion from the test accordingly. --- src/jua/weather/_model_meta.py | 6 ++++++ src/jua/weather/models.py | 1 + tests/weather/test_aifs_ens_meta.py | 14 +++++--------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/jua/weather/_model_meta.py b/src/jua/weather/_model_meta.py index 007f9d1..f0bd2cc 100644 --- a/src/jua/weather/_model_meta.py +++ b/src/jua/weather/_model_meta.py @@ -242,6 +242,12 @@ class ModelMetaInfo: # num_lats=720 / num_lons=1440, so no override needed. temporal_resolution=TemporalResolution(base=6), ) +_MODEL_META_INFO[Models.ECMWF_AIFS_ENSEMBLE] = ModelMetaInfo( + forecast_name_mapping="ecmwf_aifs025_ensemble", + full_forecasted_hours=360, + has_forecast_file_access=False, + has_statistics=True, +) _MODEL_META_INFO[Models.ECMWF_IFS_ENSEMBLE] = ModelMetaInfo( full_forecasted_hours=360, has_forecast_file_access=False, diff --git a/src/jua/weather/models.py b/src/jua/weather/models.py index 43cd1fe..4ee9c45 100644 --- a/src/jua/weather/models.py +++ b/src/jua/weather/models.py @@ -38,6 +38,7 @@ class Models(str, Enum): NOAA_GFS_SINGLE = "noaa_gfs_single" # Without Grid Access + ECMWF_AIFS_ENSEMBLE = "ecmwf_aifs025_ensemble" ECMWF_IFS_ENSEMBLE = "ecmwf_ens" GFS_GLOBAL_ENSEMBLE = "gfs_global_ensemble" GFS_GLOBAL_SINGLE = "gfs_global_single" diff --git a/tests/weather/test_aifs_ens_meta.py b/tests/weather/test_aifs_ens_meta.py index 5acb19f..270e11f 100644 --- a/tests/weather/test_aifs_ens_meta.py +++ b/tests/weather/test_aifs_ens_meta.py @@ -1,9 +1,10 @@ """Regression tests for the hosted AIFS ENS model metadata. -AIFS ENS replaced the retired open-meteo ``ecmwf_aifs025_ensemble``. Unlike the -old point-only open-meteo model, the hosted ClickHouse model exposes full grid -access plus ensemble statistics, so it must not regress back into the -"point queries only" behaviour gated by ``has_grid_access``. +The hosted ClickHouse ``aifs_ens`` model is added alongside the existing +open-meteo ``ecmwf_aifs025_ensemble`` (additive / expand phase). Unlike the +point-only open-meteo model, the hosted model exposes full grid access plus +ensemble statistics, so it must not regress into the "point queries only" +behaviour gated by ``has_grid_access``. """ from jua.weather._model_meta import get_model_meta_info @@ -20,8 +21,3 @@ def test_aifs_ens_is_fully_accessible() -> None: assert (meta.num_lats, meta.num_lons) == (720, 1440) # 0-360h forecast horizon. assert meta.full_forecasted_hours == 360 - - -def test_retired_open_meteo_aifs_ensemble_is_removed() -> None: - assert not hasattr(Models, "ECMWF_AIFS_ENSEMBLE") - assert "ecmwf_aifs025_ensemble" not in {m.value for m in Models} From c93ca14d2ab25fa0b53630b7f02734fa28c78cb6 Mon Sep 17 00:00:00 2001 From: Niall Siegenheim Date: Fri, 26 Jun 2026 10:28:13 +0200 Subject: [PATCH 4/4] test(functional): use a valid init time for aifs_ens specific-forecast test test_specific_forecast falls back to DEFAULT_FORECAST_DATE (2025-10-20) for models without an override. aifs_ens (ECMWF Open Data) only has data from mid-2025 onward and is sparse at the edges, so 2025-10-20 returns 400 "init_time does not exist". Add a MODEL_SPECIFIC_FORECAST_DATES override of 2026-03-03 (verified present in production ClickHouse), mirroring the existing EPT2_REASONING / ICON_EU recent-model overrides. Fixes the only failing functional test on the AIFS_ENS addition; metadata and latest-forecast functional tests already pass against the deployed API. --- tests/functional/test_forecasts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/functional/test_forecasts.py b/tests/functional/test_forecasts.py index 57cb0dd..62a200e 100644 --- a/tests/functional/test_forecasts.py +++ b/tests/functional/test_forecasts.py @@ -36,6 +36,10 @@ MODEL_SPECIFIC_FORECAST_DATES = { Models.EPT2_REASONING: datetime(2025, 11, 23, 0, 0, 0), Models.ICON_EU: datetime(2026, 2, 9, 0, 0, 0), + # AIFS ENS (ECMWF Open Data) backfill starts mid-2025 and is sparse at the + # edges; the default 2025-10-20 isn't present. Use a verified mid-history + # init time. + Models.AIFS_ENS: datetime(2026, 3, 3, 0, 0, 0), } SOLAR_ONLY_MODELS = {Models.EPT2_HELIOS}