{"article":{"slug":"do-we-really-need-bigger-ai-weather-forecasting-models-to-improve-resolution","title":"Do we really need bigger AI weather forecasting models to improve resolution?","subtitle":null,"summary":"Emma Scharfmann shows that instead of building a bigger AI weather model you can train a small downscaler on top of an existing global one: trained in about an hour, it sharpens AIFS-ENS temperature forecasts over France from ~28 km to 5.5 km and cuts station-measured error by 14% RMSE and 21% CRPS over the first two days.","content_type":"tutorial","language":"en","canonical_url":"https://huggingface.co/blog/EmmaScharfmann/downscaling-a-weather-forecasting-model","author":{"name":"Emma Scharfmann","url":"https://huggingface.co/EmmaScharfmann","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Hugging Face","url":"https://huggingface.co/blog","listing_slug":null,"listing":null},"topics":[{"name":"Machine Learning","slug":"machine-learning","url":"https://listedarticles.com/topics/machine-learning"},{"name":"AI","slug":"ai","url":"https://listedarticles.com/topics/ai"},{"name":"Science","slug":"science","url":"https://listedarticles.com/topics/science"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":3914,"reading_minutes":17,"published_at":"2026-10-20T00:00:00.000Z","added_at":"2026-10-27T08:16:51.745Z","updated_at":"2026-10-27T08:16:51.745Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/do-we-really-need-bigger-ai-weather-forecasting-models-to-improve-resolution","markdown_url":"https://listedarticles.com/articles/do-we-really-need-bigger-ai-weather-forecasting-models-to-improve-resolution.md","example":false,"citation":"Emma Scharfmann, Hugging Face. \"Do we really need bigger AI weather forecasting models to improve resolution?.\" 20 Oct 2026. https://huggingface.co/blog/EmmaScharfmann/downscaling-a-weather-forecasting-model (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://huggingface.co/blog/EmmaScharfmann/downscaling-a-weather-forecasting-model"},"body_markdown":"# Do we really need bigger AI weather forecasting models to improve resolution?\n\n*Emma Scharfmann, with Aritra Roy Gosthipaty and Kashif Rasul · Hugging Face community article · October 20, 2026*\n\n![](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/pAwGOcpd7D2ggtkLfiILD.gif)\n*Applying the downscaler we trained on AIFS-ENS's output to improve the forecast resolution from ~ 28km to 5.5km. Ensemble mean of the forecast over France, valid Wednesday 13 August 2025 at 12:00 UTC (+60 h from initialisation). Ocean grid points are masked.*\n\n## TL;DR\n\nIn this blog, we want to share something research and AI have taught us: **you don’t always need a bigger model; often you need to adapt the model to your task. The same idea applies to AI weather forecasting.** If you need forecasts at a higher resolution, you can take an existing global model and train a small downscaler on top of it to learn the local details, like the effects of orography or coastlines. We trained one in about an hour to downscale [AIFS-ENS](https://huggingface.co/ecmwf/aifs-ens-1.0) temperature forecasts over France from ~28 km to 5.5 km. Against [Météo-France weather stations](https://meteo.data.gouv.fr), it reduced the error by 14% in RMSE and 21% in CRPS over the first two days, and by 9% and 14% over ten days (as shown in the [demo space](https://emmascharfmann-downscaling-demo.hf.space)).\n\n## Table of Contents\n\n- [Intro](#intro)\n- [What is a downscaler & demo](#what-is-a-downscaler)\n- [Downscaler components](#downscaler-components)\n- [Running a downscaler for temperature over France](#running-a-downscaler-for-temperature-over-france)\n- [Training the downscaler](#training-the-downscaler)\n- [Conclusion and some notes worth sharing](#conclusion-and-some-notes-worth-sharing)\n- [References](#references)\n\n## Intro\n\nAI weather models are very good at forecasting large-scale weather patterns. But they see the world as a grid, often at 0.25° resolution (roughly equivalent to a grid of 28km in central Europe), where each cell holds a single value for temperature, wind, and every other variable. Anything smaller than a cell gets averaged out, so local effects from valleys, mountains, islands, or coastlines are lost.\n\nBig labs are tackling this by building models that work at finer resolution. [Google DeepMind's WeatherNext 3](https://blog.google/innovation-and-ai/models-and-research/google-deepmind/introducing-weathernext-3/) for example, which is unfortunately not open source, forecasts surface temperature and humidity at around 5 km (~0.05°). But building and running such models require a huge amount of compute and storage, and engineering resources that only a few organizations have.\n\nA cheaper way to do this is to train a downscaler (i.e. an AI model that predicts at a local scale starting from data at a larger scale) on top of an existing model. A downscaler learns local details to predict what happens within a cell, and therefore improves the resolution. And it runs in seconds, on top of an existing model forecast. In this post, we explain how a downscaler works, then we show how to run, train and evaluate one on weather-station observations for 2m temperature over France.\n\n## What is a downscaler?\n\nA weather forecasting model predicts the weather at a specific resolution. A downscaler is an ML model that takes a low resolution forecast and fills in the local details. The downscaler learns these local details from static information such as elevation and the coastline. For example, it learns that within a coarse cell, a mountaintop is colder than the valley below and the coast is milder than inland. Once trained, it can be plugged on top of a coarse forecast to produce a high-resolution forecast at low cost.\n\nYou can try out the downscaler we trained on AIFS-ENS (ECMWF) [[1]](#references) and [GEFS (NOAA)](https://www.ncei.noaa.gov/products/weather-climate-models/global-ensemble-forecast) forecasts and see how it compares against Météo-France temperature observations in our [demo space](https://huggingface.co/spaces/EmmaScharfmann/downscaling-demo) below.\n\nLike all ML models, downscalers are not magical. So, before training one, check that what you want to improve is something a downscaler can improve. Below is our summary of what a downscaler can and cannot improve.\n\n| What a downscaler can improve | What a downscaler can't improve |\n| --- | --- |\n| Add the patterns due to the terrain (valleys, mountains, coastlines) | Fix global errors in the coarse model: if the coarse is too warm in one region, the downscaler will not fix it |\n| Produce a single or an ensemble of fine fields from one coarse field | Add details that the inputs do not contain (it cannot add the effect of mountains without an input that identifies them, like the elevation) |\n| Work behind any coarse model | Reduce the uncertainty of the large-scale forecast |\n| Run in seconds, once trained | Exceed the quality of the fine field used during training |\n\n## Downscaler components\n\n### Training\n\n[![downscaler_training_diagram](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/0ITKanV5JU4TE_BxQsUMg.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/0ITKanV5JU4TE_BxQsUMg.png)\n\nDuring training, we show the model the same weather at two resolutions with some additional details (static data). It learns what the coarse version systematically misses by predicting the error between the coarse and fine weather.\n\n**Two datasets**: the downscaler needs a coarse dataset and a fine dataset covering the same place and times. We used ERA5 [[2]](#references) at 0.25° for the coarse dataset and CERRA [[3]](#references) at 5.5 km for the fine dataset. For each time step, the coarse field is interpolated onto the fine grid. The model learns to predict the difference between this interpolated field and the fine field.\n\n**Static data**: In addition to the field to predict, the downscaler needs additional information to infer what is happening within the cell. These fields are static and therefore easy to learn from. We add the season and the time of day, so the model can also learn how that correction varies through the day and through the year. In our example, we’re using the elevation of the fine-scale field, the height difference between the real surface and the coarse model’s surface, the land fraction and the distance to the sea of the cells, the longitude/latitude and the season and time of day as static data.\n\n**Loss function**: We made the choice in our example of building a downscaler for an ensemble model. So, we use fair CRPS[[4]](#references), which scores an ensemble rather than a single field. It rewards being close to the truth and having realistic spread. Note that the loss in our example is computed over land only, since the target reanalysis produces sea surface temperature differently from the land temperature.\n\n**Noise**: For one large-scale state, there are several possible fine-scale states. So instead of asking for one prediction, we train the downscaler to predict multiple outputs. Each forward pass draws fresh noise and produces a different fine field. Therefore, the downscaler produces an ensemble of forecasts from one coarse field.\n\n**Downscaler**: The model that learns to predict the difference between the coarse field and the fine field from the coarse field and the static data. In our example, we train a [U-Net](https://huggingface.co/docs/diffusers/en/api/models/unet) with about 2M parameters, which took around an hour to train on a GPU.\n\n### Inference\n\nOnce trained, the downscaler can be added on top of any forecast model (AI-based or physics-based) which outputs the same variable at the same resolution and covers the same region as the training data. In our example, that means that our downscaler can be used with any model producing 2m temperature at 0.25° over France. In this blog post, we apply it to AIFS-ENS. It could equally be applied to other 0.25° models such as [Microsoft’s Aurora](https://huggingface.co/microsoft/aurora), [GEFS (NOAA)](https://www.ncei.noaa.gov/products/weather-climate-models/global-ensemble-forecast) or [NVIDIA’s Atlas](https://huggingface.co/nvidia/atlas-era5). Note that one of our static inputs (the height difference) is frozen against ERA5’s terrain. That is right for AIFS-ENS, which is initialised from ERA5, but not GEFS, which runs on its own. This may be part of why the downscaler performs less well on GEFS (see the demo space).\n\n[![downscaler inference diagram](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/nrYkMy6CfIv91mUhlJx5W.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/nrYkMy6CfIv91mUhlJx5W.png)\n\n**Input data**: the downscaler takes the coarse ensemble forecast as input, as well as the same static data used during training, which is stored inside the model. Each coarse member is interpolated onto the fine grid before the forward pass. In our example, ten members from [AIFS-ENS](https://huggingface.co/ecmwf/aifs-ens-1.0) at 0.25° are used as input.\n\n**Downscaler**: it takes the interpolated coarse field, the static data and a draw of noise, then predicts the correction to add to the coarse field. Since the noise is drawn on each pass, running the same member several times gives multiple fine fields.\n\n**Output**: the model outputs multiple members at fine resolution. In our example, each of the ten coarse members yields four samples, giving forty members at 5.5 km. Note that in our example, running the downscaler takes only a few seconds per forecast.\n\n## Running a downscaler for temperature over France\n\nThis section walks you through how to use the downscaler we trained to improve the resolution of the temperature predicted by a model (here [AIFS-ENS 0.25°](https://huggingface.co/ecmwf/aifs-ens-1.0))) over France. You can follow the steps below or run this [tutorial](https://github.com/EmmaScharfmann/weather-forecast-model-downscaler/blob/main/tutorial_inference.ipynb).\n\n0. Set up\n   Clone the repo and install dependencies.\n\n```\ngit clone https://github.com/EmmaScharfmann/weather-forecast-model-downscaler\ncd weather-forecast-model-downscaler\nuv sync\n```\n\nThe forecasts are fetched from [the Earthmover Marketplace](https://app.earthmover.io/marketplace/6a10729e8a69e6bcaeae0f90),\nwhich needs a [free account](https://app.earthmover.io/auth/login) and a free subscription to the AIFS-ENS dataset. Subscribing creates a repository under your organisation. Put the repo name in `FORECASTS` below, then authenticate once.\nWe also recommend having a free Hugging Face account ([sign up here](https://huggingface.co/join)) to download the model's weights and data.\n\n```\nuv run arraylake auth login # or export ARRAYLAKE_TOKEN=ema_xxxxxxxxxxxx\nuv run hf auth login # or export HF_TOKEN=hf_xxxxxxxxxxxx\n```\n\nThen, import the packages and set the config.\n\nSee packages and config \n\n```\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport xarray as xr\nfrom huggingface_hub import hf_hub_download\n\nfrom downscale.constants import COARSE_CROP_MARGIN_DEG, ZERO_C\nfrom downscale.downscaler import Downscaler\nfrom downscale.evaluation import at_valid_times, lead_scores, stack_members, station_rmse\nfrom downscale.fetch import get_target\nfrom downscale.inference import downscale_forecast, save_downscaled\nfrom downscale.plots import map_grid, maps, native_cells, station_maps\nfrom downscale.regions import parse_region\nfrom downscale.sources import (\n    available_inits, fetch_forecast, forecast_box, open_forecasts, save_forecast, store_name,\n)\nfrom downscale.station_interp import curvilinear_for\nfrom downscale.stations import load_published_stations\n\nMODEL_REPO = \"EmmaScharfmann/france-temperature-downscaler\"\nFORECASTS = \"<earthmover-org>/ecmwf-aifs-ens-forecast-subscription\"   # your Earthmover subscription\nFORECASTS_2020 = \"hf://datasets/EmmaScharfmann/aifs-ens-france-aug2020\"\nCERRA_REPO = \"EmmaScharfmann/cerra-france-t2m-2014-2020\"\nSTATIONS = \"EmmaScharfmann/france-stations\"   # Météo-France, Aug 2020, Aug 2025, Jan-Jun-Jul 2026\n\nMEMBERS = 10          # AIFS-ENS members to downscale  (out of 51)\nMAX_LEAD_HOURS = 240\nSAMPLES = 4           # members out of the downscaler (total 4 x 10  = 40)\nEVAL_SAMPLES = 1     # both ensembles keep 10 members, and downscaling is 4x faster\nBOX = forecast_box(parse_region(\"france\"), COARSE_CROP_MARGIN_DEG)\nOUT = Path(\"outputs/tutorial\")   # every forecast is fetched and downscaled once, then read from here\n```\n\n1. Fetch the coarse forecast\n\n```\narchive = open_forecasts(FORECASTS)\ninits = available_inits(archive)\n\nINIT = pd.Timestamp(\"2025-08-11 00:00\")\ncoarse = fetch_forecast(archive, INIT, BOX, members=MEMBERS, max_lead_hours=MAX_LEAD_HOURS)\n```\n\n2. Run the downscaler\n\nThe model needs a fine grid as input, so the coarse field has to be interpolated first. The static data are part of the model's checkpoint on the Hub. Here, we choose to predict 4 fine members from the coarse field.\n\n```\nmodel = Downscaler.load(hf_hub_download(MODEL_REPO, \"model.pt\"))\nLAND = xr.DataArray(model.static[\"land_sea_mask\"] > 0.5, dims=(\"y\", \"x\"))\n\ndownscaled = downscale_forecast(model, coarse, samples=SAMPLES)\nfine = stack_members(downscaled)  \n\nsave_forecast(coarse, OUT / \"forecasts\")\nsave_downscaled(downscaled, OUT / \"downscaled\" / store_name(INIT))\n```\n\n`fine` is the output of the downscaler: its shape is `(time, lead_time, member, y, x)`, with 40 members (10 coarse members × 4 samples), and it holds the temperature in kelvin over France at a resolution of 5.5 km. On a GPU, the downscaler takes seconds to run.\n\nWe can plot the coarse forecast and the downscaled forecast:\n\n```\nLEAD = 60   # hours: 12 UTC on 13 August\nk = int(np.flatnonzero(coarse.lead_time.values == np.timedelta64(LEAD, \"h\"))[0])\nvalid = pd.Timestamp(downscaled.valid_time.values[0, k])\n\nraw_mean = native_cells(coarse.isel(time=0, lead_time=k).mean(\"ensemble\") - ZERO_C, model.lat, model.lon, LAND.values)\nfine_mean = fine.isel(time=0, lead_time=k).mean(\"member\").where(LAND).values - ZERO_C\ninterp_mean = downscaled.t2m_interp.isel(time=0, lead_time=k).mean(\"ensemble\").where(LAND).values - ZERO_C\nelevation = np.where(LAND, model.static[\"cerra_orography\"], np.nan)\n\nLABELS = {\n    \"title\": f\"Ensemble mean 2m temperature, valid {valid:%d %B %Y %H} UTC (+{LEAD} h)\",\n    \"coarse\": \"(a) AIFS-ENS, 0.25° (~28 km)\",\n    \"downscaled\": \"(b) AIFS-ENS + downscaler, ~5.5 km (~0.05°)\",\n    \"added\": \"(c) Correction added by the downscaler: (b) minus (a)\",\n    \"elevation\": \"(d) Elevation of the 5.5 km grid\",\n    \"temperature_unit\": \"2m temperature (°C)\",\n    \"correction_unit\": \"correction (K)\",\n    \"elevation_unit\": \"elevation (m)\",\n}\nlow, high = np.nanpercentile(np.concatenate([raw_mean.ravel(), fine_mean.ravel()]), [1, 99])\nfig = map_grid([\n    dict(title=LABELS[\"coarse\"], lon=model.lon, lat=model.lat, values=raw_mean,\n         vmin=low, vmax=high, label=LABELS[\"temperature_unit\"]),\n    dict(title=LABELS[\"downscaled\"], lon=model.lon, lat=model.lat, values=fine_mean,\n         vmin=low, vmax=high, label=LABELS[\"temperature_unit\"]),\n    dict(title=LABELS[\"added\"], lon=model.lon, lat=model.lat, values=fine_mean - interp_mean,\n         cmap=\"RdBu_r\", vmin=-6, vmax=6, label=LABELS[\"correction_unit\"]),\n    dict(title=LABELS[\"elevation\"], lon=model.lon, lat=model.lat, values=elevation,\n         cmap=\"terrain\", vmin=0, vmax=3000, label=LABELS[\"elevation_unit\"]),\n], title=LABELS[\"title\"])\n```\n\n[![Output of the code above: Predictions of the temperature over France: a) AIFS, b) AIFS + downscaler, c) downscaler, d) orography over France](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/ZA-Ci4hLLMJ9z0aTvGTZ3.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/ZA-Ci4hLLMJ9z0aTvGTZ3.png)\n\nWe see on panel b) that the resolution is improved everywhere but it is especially obvious in the mountains. On panel c) and d), we see that the main corrections are added where the orography is (Massif Central, Alps, Pyrenees) and on the coast (especially in Brittany).\n\n3. Compare forecasts against CERRA\n\nCERRA is what the model learned to reproduce during the training. Comparing against it shows how much the model actually learned.\n\n Expand to see the details of the evaluation code\n\n```\ndef run(archive, init):\n    \"\"\"The coarse and downscaled forecasts of one initialisation, computed once and saved.\"\"\"\n    name = store_name(init)\n    path = OUT / \"downscaled\" / name\n    if not path.exists():\n        coarse = fetch_forecast(archive, init, BOX, members=MEMBERS, max_lead_hours=MAX_LEAD_HOURS)\n        save_forecast(coarse, OUT / \"forecasts\")\n        save_downscaled(downscale_forecast(model, coarse, samples=EVAL_SAMPLES), path)\n    return xr.open_zarr(OUT / \"forecasts\" / name)[\"t2m\"], xr.open_zarr(path)\n\ndef score(downscaled, stations, cerra=None, short_leads=48):\n    \"\"\"Score one downscaled run against the stations and, when given, against CERRA.\"\"\"\n    downscaled = downscaled.isel(sample=slice(0, EVAL_SAMPLES))   # the run from section c has more\n    forecasts = {\n        \"AIFS-ENS\": downscaled.t2m_interp.rename(ensemble=\"member\").load(),\n        \"AIFS-ENS + downscaler\": stack_members(downscaled).load(),\n    }\n    read = curvilinear_for(model.lat, model.lon, stations).apply\n    observed = at_valid_times(stations.t2m, downscaled.valid_time)          # (time, lead_time, station)\n    out = {\"stations\": {}, \"means\": {}}\n    short = downscaled.lead_time <= np.timedelta64(short_leads, \"h\")\n    for name, forecast in forecasts.items():\n        at = read(forecast)\n        out[\"stations\"][name] = lead_scores(at, observed)\n        out[\"means\"][name] = at.mean(\"member\").sel(lead_time=short).values\n    out[\"observed\"] = observed.sel(lead_time=short).values\n    if cerra is not None:\n        truth = at_valid_times(cerra.t2m, downscaled.valid_time)             # (time, lead_time, y, x)\n        out[\"cerra\"] = {name: lead_scores(f, truth.where(LAND)) for name, f in forecasts.items()}\n        at = read(truth)                                                     # CERRA at the stations\n        out[\"stations\"][\"CERRA\"] = lead_scores(at, observed)                  # same valid times: the floor\n        out[\"means\"][\"CERRA\"] = at.sel(lead_time=short).values\n    return out\n```\n\n```\narchive_2020 = open_forecasts(FORECASTS_2020)\ninits_2020 = available_inits(archive_2020)\ncerra, _ = get_target(inits_2020[0], inits_2020[-1] + pd.Timedelta(hours=MAX_LEAD_HOURS), repo=CERRA_REPO,\n                      load=True)   # about 30 MB, read once rather than streamed\nstations_2020 = load_published_stations(STATIONS, start=\"2020-08-01\", end=\"2020-08-31\")\nprint(f\"{len(inits_2020)} initialisations, {inits_2020[0]:%d %b} to {inits_2020[-1]:%d %b %Y}; \"\n      f\"{stations_2020.sizes['station']} stations\")\n\nruns_2020 = {init: run(archive_2020, init) for init in inits_2020}\nresults_2020 = [score(downscaled_2020, stations_2020, cerra) for _, downscaled_2020 in runs_2020.values()]\n```\n\n```\ncoarse_first, first = runs_2020[inits_2020[0]]\nk = int(np.flatnonzero(first.lead_time.values == np.timedelta64(36, \"h\"))[0])\nvalid_2020 = pd.Timestamp(first.valid_time.values[0, k])\n\nraw = native_cells(coarse_first.isel(time=0, lead_time=k).mean(\"ensemble\").load() - ZERO_C,\n                   model.lat, model.lon, LAND.values)\ninterp = first.t2m_interp.isel(time=0, lead_time=k).mean(\"ensemble\").where(LAND).values - ZERO_C\ndown = stack_members(first).isel(time=0, lead_time=k).mean(\"member\").where(LAND).values - ZERO_C\ntruth = cerra.t2m.sel(time=valid_2020).where(LAND).values - ZERO_C\n\nwhen = f\"valid {valid_2020:%d %b %Y %H} UTC (+36 h)\"\n```\n\n```\nfig = maps([\n    (\"AIFS-ENS - coarse dataset, resolution 0.25° (~28 km)\", model.lon, model.lat, raw),\n    (\"CERRA - fine dataset, resolution: 5.5 km (~0.05°)\", model.lon, model.lat, truth),\n    (\"AIFS-ENS + downscaler - resolution: 5.5 km (~0.05°)\", model.lon, model.lat, down),\n], width=4.4, title=f\"Ensemble mean 2m temperature, {when}\")\n```\n\n[![Output of the code above: AIFS, CERRA, AIFS + downscaler temperature forecasts over France](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/GJR5m0QUEwR99jJwAQlQw.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/GJR5m0QUEwR99jJwAQlQw.png)\n\n```\nfig = maps([\n    (\"AIFS-ENS minus CERRA\", model.lon, model.lat, interp - truth),\n    (\"AIFS-ENS + downscaler minus CERRA\", model.lon, model.lat, down - truth),\n], cmap=\"RdBu_r\", vmin=-5, vmax=5, label=\"K\", title=f\"Error against CERRA, {when}\")\nfor name, error in [(\"AIFS-ENS\", interp - truth), (\"AIFS-ENS + downscaler\", down - truth)]:\n    print(f\"{name:24s} RMSE against CERRA over land {np.sqrt(np.nanmean(error ** 2)):.2f} K\")\n```\n\n[![Output of the code above: AIFS and AIFS + downscaler's errors against CERRA](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/8qGyWgdOUFwcccqE2dnMm.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/8qGyWgdOUFwcccqE2dnMm.png)\n\nAbove, we plot the coarse forecast (AIFS-ENS), the fine field (CERRA) and AIFS-ENS + downscaler on summer 2020 (which was not used for training). We see that the error of AIFS-ENS against CERRA is very high in the mountains, which is pretty well corrected by our downscaler.\n\n4. Compare forecasts against observations\n\nWe used observations from Météo-France weather stations to evaluate the model against an independent ground truth. Downloading the station data takes around 1 hour for 1 month, so we cached the stations' data on the Hub ([EmmaScharfmann/france-stations](https://huggingface.co/datasets/EmmaScharfmann/france-stations)) to save some time: `load_published_stations` in the evaluation code above reads them from there.\n\n```\n# Ensemble means and observations at every station, first 48 h of every run\nnames = (\"AIFS-ENS\", \"AIFS-ENS + downscaler\")\nfields = {name: np.concatenate([r[\"means\"][name].reshape(-1, r[\"means\"][name].shape[-1]) for r in results_2020])\n          for name in names}\nobserved = np.concatenate([r[\"observed\"].reshape(-1, r[\"observed\"].shape[-1]) for r in results_2020])\nrmse_2020 = {name: station_rmse(fields[name], observed) for name in names}\nfig = station_maps(stations_2020.longitude.values, stations_2020.latitude.values, rmse_2020,\n                   title=\"RMSE against each station, first 48 h, August 2020\")\n```\n\n[![Output of the code above: AIFS and AIFS + downscaler errors against weather stations](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/a3bC91w8BqwxbflIg1MsG.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/a3bC91w8BqwxbflIg1MsG.png)\n\nAgain, we see that the downscaler corrects pretty well the error in the mountains (Alps, Massif Central, Pyrenees) and on the coast (Brittany, Normandy), as we expect it to do.\n\nYou can run more forecast and evaluation against observations on our [demo space](https://emmascharfmann-downscaling-demo.hf.space).\n\n## Training the downscaler\n\nYou can train your own downscaler (you can change the data, resolution, variable, region...) following what we did to train our own downscaler. We will walk you through the main steps in this blog, but you can find a complete tutorial [here](https://github.com/EmmaScharfmann/weather-forecast-model-downscaler/blob/main/training_tutorial.ipynb).\n\n0. Set up\n\nYou need to clone the [github repo](https://github.com/EmmaScharfmann/weather-forecast-model-downscaler) and log into your Earthmover free account to fetch the coarse field (ERA5 in the tutorial). Then, you need to adapt the configuration for the training (number of epochs, steps per epochs, starting and ending dates of the data for training and validation).\n\n1. Building the training pair\n\nYou need to build pairs of coarse field - fine field. In our tutorial, we're using ERA5 for the coarse field and CERRA for the fine field from 2014 to 2019 for the training and 2020 for the evaluation. ERA5 is fetched from Earthmover while CERRA is cached on the Hub. Then, the static data (at the resolution of the fine field) are added to the pairs: orography, longitude/latitude, land-sea mask, time of the year/day and distance to the coast. The model has to learn to predict the difference between the two maps below:\n\n[![ERA5 and CERRA 2m temperature](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/TBU85cL1eIyOV_BIURZrQ.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/TBU85cL1eIyOV_BIURZrQ.png)\n\n2. Training the model\n\nWe train a small U-net to learn to predict the difference between the coarse and the fine field.\nRandom noise is added to predict a probabilistic correction. A CRPS loss is used to train the ensemble downscaler. 2014-2019 is used for training. We train the model for 20 epochs.\n\n![training and testing loss](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/X8ueIzdRrA3Th91yZRPCh.png)\n\n[![AIFS, AIFS + downscaler, CERRA 2m temperature](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/l1CxqeZEQmFihZX9_hyRR.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/l1CxqeZEQmFihZX9_hyRR.png)\n\n3. Evaluating the model\n\nWe evaluate the model against CERRA (year 2020). We plot the difference between the coarse model (ERA5) and ERA5 + downscaler. As expected, we see that local errors in the mountains or the coast are corrected by the downscaler while global errors of the coarse model are not corrected by the downscaler (error in Normandy).\n\n[![error against CERRA for ERA5 and ERA5 + downscaler](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/pO-VFptdAHeZxQAZooWrT.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/pO-VFptdAHeZxQAZooWrT.png)\n\nWe can also plot the power spectrum of the forecasts. The power spectrum shows how much detail a field has at each scale. The right panel shows the same, but divided by CERRA, so 1 means “as detailed as CERRA”.\n\nERA5 runs out of detail below ~100 km. A downscaled member follows CERRA, so the model adds the right amount of detail at the right scales. The ensemble mean is slightly smoother than the single member as it averages the details that are uncertain.\n\n[![Power spectrum of the 2m temperature for ERA5, ERA5 + downscaler and CERRA.](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/Fd49u7H81o6hJTFI9OKRa.png)](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/Fd49u7H81o6hJTFI9OKRa.png)\n\n## Conclusion and some notes worth sharing\n\nTraining a downscaler takes a GPU and a couple of hours. It does not make a forecast better globally, but it makes it (way) better locally. Comparing against CERRA tells us whether the downscaler learned what it was trained to learn, comparing against stations tells us whether the final forecast is good. Scoring CERRA itself against stations gives an upper bound on what the downscaler can achieve. And our downscaler gets surprisingly close to that bound (as shown in the figure below)!\n\n![](https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/U-WnecGiC-k5lK02fyM67.png)\n*RMSE by station elevation and by time of day for AIFS-ENS, AIFS-ENS + downscaler and CERRA, August 2020, all lead times. From the [demo space](https://emmascharfmann-downscaling-demo.hf.space).*\n\n**Notes worth sharing**\n\n- This is a demonstration, not a robust evaluation. We chose temperature because it is a smooth variable, summer somewhat arbitrarily (we started out looking at heatwaves and drifted towards training a downscaler), and AIFS-ENS because its forecasts were available on Earthmover and it runs fast (0.5 s per 6-hour step on a GPU).\n- The power spectrum was our most useful diagnostic. It showed that noise drawn at a single scale piles up spread at that wavelength, and changing the scale only moves the excess. Drawing noise at several scales reduces the problem but doesn't fully solve it.\n- All errors have a strong daily cycle, because of effects that CERRA, AIFS-ENS and the downscaler all struggle to predict, such as inversions at night and surface heating in the afternoon.\n- Only about 10–30% of the difference between the coarse and fine fields is predictable from our inputs. The rest would need more inputs, such as wind.\n\n**Next steps**: we'd like to try precipitation and wind, temporal conditioning instead of treating each time step independently, and a GNN trained directly on station observations instead of a reanalysis. We'd love your feedback on what was useful and what we should do next, in the comments or by email. The [model](https://huggingface.co/EmmaScharfmann/france-temperature-downscaler), [code](https://github.com/EmmaScharfmann/weather-forecast-model-downscaler) and [demo](https://emmascharfmann-downscaling-demo.hf.space) are all available.\n\n**Acknowledgements**: thanks a lot to [Quentin Nicolas](https://qnicolas.github.io/) for sharing his climate expertise and for the many comments that made this post clearer and more accurate.\n\n## References\n\n1. Lang, S., Alexe, M., Clare, M.C.A. et al. AIFS-CRPS: ensemble forecasting using a model trained with a loss function based on the continuous ranked probability score. npj Artif. Intell. 2, 18 (2026). <https://doi.org/10.1038/s44387-026-00073-7>\n2. Hersbach, H., Bell, B., Berrisford, P., et al. (2020). The ERA5 global reanalysis. *Q. J. R. Meteorol. Soc.*, 146(730), 1999–2049. <https://doi.org/10.1002/qj.3803>\n3. Ridal, M., Bazile, E., Le Moigne, P., et al. (2024). CERRA, the Copernicus European Regional Reanalysis system. *Q. J. R. Meteorol. Soc.*, 150. <https://doi.org/10.1002/qj.4764>\n4. Ferro, C. A. T. (2014). Fair scores for ensemble forecasts. *Q. J. R. Meteorol. Soc.*, 140, 1917–1923. <https://doi.org/10.1002/qj.2270>\n\n---\n\n*Originally published at [Hugging Face](https://huggingface.co/blog/EmmaScharfmann/downscaling-a-weather-forecasting-model).*\n","body_html":"<h1 id=\"do-we-really-need-bigger-ai-weather-forecasting-models-to-improv\">Do we really need bigger AI weather forecasting models to improve resolution?</h1>\n<p><em>Emma Scharfmann, with Aritra Roy Gosthipaty and Kashif Rasul · Hugging Face community article · October 20, 2026</em></p>\n<figure><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/pAwGOcpd7D2ggtkLfiILD.gif\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p><em>Applying the downscaler we trained on AIFS-ENS&#39;s output to improve the forecast resolution from ~ 28km to 5.5km. Ensemble mean of the forecast over France, valid Wednesday 13 August 2025 at 12:00 UTC (+60 h from initialisation). Ocean grid points are masked.</em></p>\n<h2 id=\"tl-dr\">TL;DR</h2>\n<p>In this blog, we want to share something research and AI have taught us: <strong>you don’t always need a bigger model; often you need to adapt the model to your task. The same idea applies to AI weather forecasting.</strong> If you need forecasts at a higher resolution, you can take an existing global model and train a small downscaler on top of it to learn the local details, like the effects of orography or coastlines. We trained one in about an hour to downscale <a href=\"https://huggingface.co/ecmwf/aifs-ens-1.0\" rel=\"nofollow ugc noopener\">AIFS-ENS</a> temperature forecasts over France from ~28 km to 5.5 km. Against <a href=\"https://meteo.data.gouv.fr\" rel=\"nofollow ugc noopener\">Météo-France weather stations</a>, it reduced the error by 14% in RMSE and 21% in CRPS over the first two days, and by 9% and 14% over ten days (as shown in the <a href=\"https://emmascharfmann-downscaling-demo.hf.space\" rel=\"nofollow ugc noopener\">demo space</a>).</p>\n<h2 id=\"table-of-contents\">Table of Contents</h2>\n<ul><li><a href=\"#intro\">Intro</a></li><li><a href=\"#what-is-a-downscaler\">What is a downscaler &amp; demo</a></li><li><a href=\"#downscaler-components\">Downscaler components</a></li><li><a href=\"#running-a-downscaler-for-temperature-over-france\">Running a downscaler for temperature over France</a></li><li><a href=\"#training-the-downscaler\">Training the downscaler</a></li><li><a href=\"#conclusion-and-some-notes-worth-sharing\">Conclusion and some notes worth sharing</a></li><li><a href=\"#references\">References</a></li></ul>\n<h2 id=\"intro\">Intro</h2>\n<p>AI weather models are very good at forecasting large-scale weather patterns. But they see the world as a grid, often at 0.25° resolution (roughly equivalent to a grid of 28km in central Europe), where each cell holds a single value for temperature, wind, and every other variable. Anything smaller than a cell gets averaged out, so local effects from valleys, mountains, islands, or coastlines are lost.</p>\n<p>Big labs are tackling this by building models that work at finer resolution. <a href=\"https://blog.google/innovation-and-ai/models-and-research/google-deepmind/introducing-weathernext-3/\" rel=\"nofollow ugc noopener\">Google DeepMind&#39;s WeatherNext 3</a> for example, which is unfortunately not open source, forecasts surface temperature and humidity at around 5 km (~0.05°). But building and running such models require a huge amount of compute and storage, and engineering resources that only a few organizations have.</p>\n<p>A cheaper way to do this is to train a downscaler (i.e. an AI model that predicts at a local scale starting from data at a larger scale) on top of an existing model. A downscaler learns local details to predict what happens within a cell, and therefore improves the resolution. And it runs in seconds, on top of an existing model forecast. In this post, we explain how a downscaler works, then we show how to run, train and evaluate one on weather-station observations for 2m temperature over France.</p>\n<h2 id=\"what-is-a-downscaler\">What is a downscaler?</h2>\n<p>A weather forecasting model predicts the weather at a specific resolution. A downscaler is an ML model that takes a low resolution forecast and fills in the local details. The downscaler learns these local details from static information such as elevation and the coastline. For example, it learns that within a coarse cell, a mountaintop is colder than the valley below and the coast is milder than inland. Once trained, it can be plugged on top of a coarse forecast to produce a high-resolution forecast at low cost.</p>\n<p>You can try out the downscaler we trained on AIFS-ENS (ECMWF) <a href=\"#references\">[1]</a> and <a href=\"https://www.ncei.noaa.gov/products/weather-climate-models/global-ensemble-forecast\" rel=\"nofollow ugc noopener\">GEFS (NOAA)</a> forecasts and see how it compares against Météo-France temperature observations in our <a href=\"https://huggingface.co/spaces/EmmaScharfmann/downscaling-demo\" rel=\"nofollow ugc noopener\">demo space</a> below.</p>\n<p>Like all ML models, downscalers are not magical. So, before training one, check that what you want to improve is something a downscaler can improve. Below is our summary of what a downscaler can and cannot improve.</p>\n<div class=\"table-wrap\"><table><thead><tr><th>What a downscaler can improve</th><th>What a downscaler can&#39;t improve</th></tr></thead><tbody><tr><td>Add the patterns due to the terrain (valleys, mountains, coastlines)</td><td>Fix global errors in the coarse model: if the coarse is too warm in one region, the downscaler will not fix it</td></tr><tr><td>Produce a single or an ensemble of fine fields from one coarse field</td><td>Add details that the inputs do not contain (it cannot add the effect of mountains without an input that identifies them, like the elevation)</td></tr><tr><td>Work behind any coarse model</td><td>Reduce the uncertainty of the large-scale forecast</td></tr><tr><td>Run in seconds, once trained</td><td>Exceed the quality of the fine field used during training</td></tr></tbody></table></div>\n<h2 id=\"downscaler-components\">Downscaler components</h2>\n<h3 id=\"training\">Training</h3>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/0ITKanV5JU4TE_BxQsUMg.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/0ITKanV5JU4TE_BxQsUMg.png\" alt=\"downscaler_training_diagram\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>During training, we show the model the same weather at two resolutions with some additional details (static data). It learns what the coarse version systematically misses by predicting the error between the coarse and fine weather.</p>\n<p><strong>Two datasets</strong>: the downscaler needs a coarse dataset and a fine dataset covering the same place and times. We used ERA5 <a href=\"#references\">[2]</a> at 0.25° for the coarse dataset and CERRA <a href=\"#references\">[3]</a> at 5.5 km for the fine dataset. For each time step, the coarse field is interpolated onto the fine grid. The model learns to predict the difference between this interpolated field and the fine field.</p>\n<p><strong>Static data</strong>: In addition to the field to predict, the downscaler needs additional information to infer what is happening within the cell. These fields are static and therefore easy to learn from. We add the season and the time of day, so the model can also learn how that correction varies through the day and through the year. In our example, we’re using the elevation of the fine-scale field, the height difference between the real surface and the coarse model’s surface, the land fraction and the distance to the sea of the cells, the longitude/latitude and the season and time of day as static data.</p>\n<p><strong>Loss function</strong>: We made the choice in our example of building a downscaler for an ensemble model. So, we use fair CRPS<a href=\"#references\">[4]</a>, which scores an ensemble rather than a single field. It rewards being close to the truth and having realistic spread. Note that the loss in our example is computed over land only, since the target reanalysis produces sea surface temperature differently from the land temperature.</p>\n<p><strong>Noise</strong>: For one large-scale state, there are several possible fine-scale states. So instead of asking for one prediction, we train the downscaler to predict multiple outputs. Each forward pass draws fresh noise and produces a different fine field. Therefore, the downscaler produces an ensemble of forecasts from one coarse field.</p>\n<p><strong>Downscaler</strong>: The model that learns to predict the difference between the coarse field and the fine field from the coarse field and the static data. In our example, we train a <a href=\"https://huggingface.co/docs/diffusers/en/api/models/unet\" rel=\"nofollow ugc noopener\">U-Net</a> with about 2M parameters, which took around an hour to train on a GPU.</p>\n<h3 id=\"inference\">Inference</h3>\n<p>Once trained, the downscaler can be added on top of any forecast model (AI-based or physics-based) which outputs the same variable at the same resolution and covers the same region as the training data. In our example, that means that our downscaler can be used with any model producing 2m temperature at 0.25° over France. In this blog post, we apply it to AIFS-ENS. It could equally be applied to other 0.25° models such as <a href=\"https://huggingface.co/microsoft/aurora\" rel=\"nofollow ugc noopener\">Microsoft’s Aurora</a>, <a href=\"https://www.ncei.noaa.gov/products/weather-climate-models/global-ensemble-forecast\" rel=\"nofollow ugc noopener\">GEFS (NOAA)</a> or <a href=\"https://huggingface.co/nvidia/atlas-era5\" rel=\"nofollow ugc noopener\">NVIDIA’s Atlas</a>. Note that one of our static inputs (the height difference) is frozen against ERA5’s terrain. That is right for AIFS-ENS, which is initialised from ERA5, but not GEFS, which runs on its own. This may be part of why the downscaler performs less well on GEFS (see the demo space).</p>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/nrYkMy6CfIv91mUhlJx5W.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/nrYkMy6CfIv91mUhlJx5W.png\" alt=\"downscaler inference diagram\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p><strong>Input data</strong>: the downscaler takes the coarse ensemble forecast as input, as well as the same static data used during training, which is stored inside the model. Each coarse member is interpolated onto the fine grid before the forward pass. In our example, ten members from <a href=\"https://huggingface.co/ecmwf/aifs-ens-1.0\" rel=\"nofollow ugc noopener\">AIFS-ENS</a> at 0.25° are used as input.</p>\n<p><strong>Downscaler</strong>: it takes the interpolated coarse field, the static data and a draw of noise, then predicts the correction to add to the coarse field. Since the noise is drawn on each pass, running the same member several times gives multiple fine fields.</p>\n<p><strong>Output</strong>: the model outputs multiple members at fine resolution. In our example, each of the ten coarse members yields four samples, giving forty members at 5.5 km. Note that in our example, running the downscaler takes only a few seconds per forecast.</p>\n<h2 id=\"running-a-downscaler-for-temperature-over-france\">Running a downscaler for temperature over France</h2>\n<p>This section walks you through how to use the downscaler we trained to improve the resolution of the temperature predicted by a model (here <a href=\"https://huggingface.co/ecmwf/aifs-ens-1.0\" rel=\"nofollow ugc noopener\">AIFS-ENS 0.25°</a>)) over France. You can follow the steps below or run this <a href=\"https://github.com/EmmaScharfmann/weather-forecast-model-downscaler/blob/main/tutorial_inference.ipynb\" rel=\"nofollow ugc noopener\">tutorial</a>.</p>\n<ol start=\"0\"><li><p>Set up</p><p> Clone the repo and install dependencies.</p></li></ol>\n<pre><code>git clone https://github.com/EmmaScharfmann/weather-forecast-model-downscaler\ncd weather-forecast-model-downscaler\nuv sync</code></pre>\n<p>The forecasts are fetched from <a href=\"https://app.earthmover.io/marketplace/6a10729e8a69e6bcaeae0f90\" rel=\"nofollow ugc noopener\">the Earthmover Marketplace</a>,\nwhich needs a <a href=\"https://app.earthmover.io/auth/login\" rel=\"nofollow ugc noopener\">free account</a> and a free subscription to the AIFS-ENS dataset. Subscribing creates a repository under your organisation. Put the repo name in <code>FORECASTS</code> below, then authenticate once.\nWe also recommend having a free Hugging Face account (<a href=\"https://huggingface.co/join\" rel=\"nofollow ugc noopener\">sign up here</a>) to download the model&#39;s weights and data.</p>\n<pre><code>uv run arraylake auth login # or export ARRAYLAKE_TOKEN=ema_xxxxxxxxxxxx\nuv run hf auth login # or export HF_TOKEN=hf_xxxxxxxxxxxx</code></pre>\n<p>Then, import the packages and set the config.</p>\n<p>See packages and config </p>\n<pre><code>from pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport xarray as xr\nfrom huggingface_hub import hf_hub_download\n\nfrom downscale.constants import COARSE_CROP_MARGIN_DEG, ZERO_C\nfrom downscale.downscaler import Downscaler\nfrom downscale.evaluation import at_valid_times, lead_scores, stack_members, station_rmse\nfrom downscale.fetch import get_target\nfrom downscale.inference import downscale_forecast, save_downscaled\nfrom downscale.plots import map_grid, maps, native_cells, station_maps\nfrom downscale.regions import parse_region\nfrom downscale.sources import (\n    available_inits, fetch_forecast, forecast_box, open_forecasts, save_forecast, store_name,\n)\nfrom downscale.station_interp import curvilinear_for\nfrom downscale.stations import load_published_stations\n\nMODEL_REPO = &quot;EmmaScharfmann/france-temperature-downscaler&quot;\nFORECASTS = &quot;&lt;earthmover-org&gt;/ecmwf-aifs-ens-forecast-subscription&quot;   # your Earthmover subscription\nFORECASTS_2020 = &quot;hf://datasets/EmmaScharfmann/aifs-ens-france-aug2020&quot;\nCERRA_REPO = &quot;EmmaScharfmann/cerra-france-t2m-2014-2020&quot;\nSTATIONS = &quot;EmmaScharfmann/france-stations&quot;   # Météo-France, Aug 2020, Aug 2025, Jan-Jun-Jul 2026\n\nMEMBERS = 10          # AIFS-ENS members to downscale  (out of 51)\nMAX_LEAD_HOURS = 240\nSAMPLES = 4           # members out of the downscaler (total 4 x 10  = 40)\nEVAL_SAMPLES = 1     # both ensembles keep 10 members, and downscaling is 4x faster\nBOX = forecast_box(parse_region(&quot;france&quot;), COARSE_CROP_MARGIN_DEG)\nOUT = Path(&quot;outputs/tutorial&quot;)   # every forecast is fetched and downscaled once, then read from here</code></pre>\n<ol><li>Fetch the coarse forecast</li></ol>\n<pre><code>archive = open_forecasts(FORECASTS)\ninits = available_inits(archive)\n\nINIT = pd.Timestamp(&quot;2025-08-11 00:00&quot;)\ncoarse = fetch_forecast(archive, INIT, BOX, members=MEMBERS, max_lead_hours=MAX_LEAD_HOURS)</code></pre>\n<ol start=\"2\"><li>Run the downscaler</li></ol>\n<p>The model needs a fine grid as input, so the coarse field has to be interpolated first. The static data are part of the model&#39;s checkpoint on the Hub. Here, we choose to predict 4 fine members from the coarse field.</p>\n<pre><code>model = Downscaler.load(hf_hub_download(MODEL_REPO, &quot;model.pt&quot;))\nLAND = xr.DataArray(model.static[&quot;land_sea_mask&quot;] &gt; 0.5, dims=(&quot;y&quot;, &quot;x&quot;))\n\ndownscaled = downscale_forecast(model, coarse, samples=SAMPLES)\nfine = stack_members(downscaled)  \n\nsave_forecast(coarse, OUT / &quot;forecasts&quot;)\nsave_downscaled(downscaled, OUT / &quot;downscaled&quot; / store_name(INIT))</code></pre>\n<p><code>fine</code> is the output of the downscaler: its shape is <code>(time, lead_time, member, y, x)</code>, with 40 members (10 coarse members × 4 samples), and it holds the temperature in kelvin over France at a resolution of 5.5 km. On a GPU, the downscaler takes seconds to run.</p>\n<p>We can plot the coarse forecast and the downscaled forecast:</p>\n<pre><code>LEAD = 60   # hours: 12 UTC on 13 August\nk = int(np.flatnonzero(coarse.lead_time.values == np.timedelta64(LEAD, &quot;h&quot;))[0])\nvalid = pd.Timestamp(downscaled.valid_time.values[0, k])\n\nraw_mean = native_cells(coarse.isel(time=0, lead_time=k).mean(&quot;ensemble&quot;) - ZERO_C, model.lat, model.lon, LAND.values)\nfine_mean = fine.isel(time=0, lead_time=k).mean(&quot;member&quot;).where(LAND).values - ZERO_C\ninterp_mean = downscaled.t2m_interp.isel(time=0, lead_time=k).mean(&quot;ensemble&quot;).where(LAND).values - ZERO_C\nelevation = np.where(LAND, model.static[&quot;cerra_orography&quot;], np.nan)\n\nLABELS = {\n    &quot;title&quot;: f&quot;Ensemble mean 2m temperature, valid {valid:%d %B %Y %H} UTC (+{LEAD} h)&quot;,\n    &quot;coarse&quot;: &quot;(a) AIFS-ENS, 0.25° (~28 km)&quot;,\n    &quot;downscaled&quot;: &quot;(b) AIFS-ENS + downscaler, ~5.5 km (~0.05°)&quot;,\n    &quot;added&quot;: &quot;(c) Correction added by the downscaler: (b) minus (a)&quot;,\n    &quot;elevation&quot;: &quot;(d) Elevation of the 5.5 km grid&quot;,\n    &quot;temperature_unit&quot;: &quot;2m temperature (°C)&quot;,\n    &quot;correction_unit&quot;: &quot;correction (K)&quot;,\n    &quot;elevation_unit&quot;: &quot;elevation (m)&quot;,\n}\nlow, high = np.nanpercentile(np.concatenate([raw_mean.ravel(), fine_mean.ravel()]), [1, 99])\nfig = map_grid([\n    dict(title=LABELS[&quot;coarse&quot;], lon=model.lon, lat=model.lat, values=raw_mean,\n         vmin=low, vmax=high, label=LABELS[&quot;temperature_unit&quot;]),\n    dict(title=LABELS[&quot;downscaled&quot;], lon=model.lon, lat=model.lat, values=fine_mean,\n         vmin=low, vmax=high, label=LABELS[&quot;temperature_unit&quot;]),\n    dict(title=LABELS[&quot;added&quot;], lon=model.lon, lat=model.lat, values=fine_mean - interp_mean,\n         cmap=&quot;RdBu_r&quot;, vmin=-6, vmax=6, label=LABELS[&quot;correction_unit&quot;]),\n    dict(title=LABELS[&quot;elevation&quot;], lon=model.lon, lat=model.lat, values=elevation,\n         cmap=&quot;terrain&quot;, vmin=0, vmax=3000, label=LABELS[&quot;elevation_unit&quot;]),\n], title=LABELS[&quot;title&quot;])</code></pre>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/ZA-Ci4hLLMJ9z0aTvGTZ3.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/ZA-Ci4hLLMJ9z0aTvGTZ3.png\" alt=\"Output of the code above: Predictions of the temperature over France: a) AIFS, b) AIFS + downscaler, c) downscaler, d) orography over France\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>We see on panel b) that the resolution is improved everywhere but it is especially obvious in the mountains. On panel c) and d), we see that the main corrections are added where the orography is (Massif Central, Alps, Pyrenees) and on the coast (especially in Brittany).</p>\n<ol start=\"3\"><li>Compare forecasts against CERRA</li></ol>\n<p>CERRA is what the model learned to reproduce during the training. Comparing against it shows how much the model actually learned.</p>\n<p> Expand to see the details of the evaluation code</p>\n<pre><code>def run(archive, init):\n    &quot;&quot;&quot;The coarse and downscaled forecasts of one initialisation, computed once and saved.&quot;&quot;&quot;\n    name = store_name(init)\n    path = OUT / &quot;downscaled&quot; / name\n    if not path.exists():\n        coarse = fetch_forecast(archive, init, BOX, members=MEMBERS, max_lead_hours=MAX_LEAD_HOURS)\n        save_forecast(coarse, OUT / &quot;forecasts&quot;)\n        save_downscaled(downscale_forecast(model, coarse, samples=EVAL_SAMPLES), path)\n    return xr.open_zarr(OUT / &quot;forecasts&quot; / name)[&quot;t2m&quot;], xr.open_zarr(path)\n\ndef score(downscaled, stations, cerra=None, short_leads=48):\n    &quot;&quot;&quot;Score one downscaled run against the stations and, when given, against CERRA.&quot;&quot;&quot;\n    downscaled = downscaled.isel(sample=slice(0, EVAL_SAMPLES))   # the run from section c has more\n    forecasts = {\n        &quot;AIFS-ENS&quot;: downscaled.t2m_interp.rename(ensemble=&quot;member&quot;).load(),\n        &quot;AIFS-ENS + downscaler&quot;: stack_members(downscaled).load(),\n    }\n    read = curvilinear_for(model.lat, model.lon, stations).apply\n    observed = at_valid_times(stations.t2m, downscaled.valid_time)          # (time, lead_time, station)\n    out = {&quot;stations&quot;: {}, &quot;means&quot;: {}}\n    short = downscaled.lead_time &lt;= np.timedelta64(short_leads, &quot;h&quot;)\n    for name, forecast in forecasts.items():\n        at = read(forecast)\n        out[&quot;stations&quot;][name] = lead_scores(at, observed)\n        out[&quot;means&quot;][name] = at.mean(&quot;member&quot;).sel(lead_time=short).values\n    out[&quot;observed&quot;] = observed.sel(lead_time=short).values\n    if cerra is not None:\n        truth = at_valid_times(cerra.t2m, downscaled.valid_time)             # (time, lead_time, y, x)\n        out[&quot;cerra&quot;] = {name: lead_scores(f, truth.where(LAND)) for name, f in forecasts.items()}\n        at = read(truth)                                                     # CERRA at the stations\n        out[&quot;stations&quot;][&quot;CERRA&quot;] = lead_scores(at, observed)                  # same valid times: the floor\n        out[&quot;means&quot;][&quot;CERRA&quot;] = at.sel(lead_time=short).values\n    return out</code></pre>\n<pre><code>archive_2020 = open_forecasts(FORECASTS_2020)\ninits_2020 = available_inits(archive_2020)\ncerra, _ = get_target(inits_2020[0], inits_2020[-1] + pd.Timedelta(hours=MAX_LEAD_HOURS), repo=CERRA_REPO,\n                      load=True)   # about 30 MB, read once rather than streamed\nstations_2020 = load_published_stations(STATIONS, start=&quot;2020-08-01&quot;, end=&quot;2020-08-31&quot;)\nprint(f&quot;{len(inits_2020)} initialisations, {inits_2020[0]:%d %b} to {inits_2020[-1]:%d %b %Y}; &quot;\n      f&quot;{stations_2020.sizes[&#39;station&#39;]} stations&quot;)\n\nruns_2020 = {init: run(archive_2020, init) for init in inits_2020}\nresults_2020 = [score(downscaled_2020, stations_2020, cerra) for _, downscaled_2020 in runs_2020.values()]</code></pre>\n<pre><code>coarse_first, first = runs_2020[inits_2020[0]]\nk = int(np.flatnonzero(first.lead_time.values == np.timedelta64(36, &quot;h&quot;))[0])\nvalid_2020 = pd.Timestamp(first.valid_time.values[0, k])\n\nraw = native_cells(coarse_first.isel(time=0, lead_time=k).mean(&quot;ensemble&quot;).load() - ZERO_C,\n                   model.lat, model.lon, LAND.values)\ninterp = first.t2m_interp.isel(time=0, lead_time=k).mean(&quot;ensemble&quot;).where(LAND).values - ZERO_C\ndown = stack_members(first).isel(time=0, lead_time=k).mean(&quot;member&quot;).where(LAND).values - ZERO_C\ntruth = cerra.t2m.sel(time=valid_2020).where(LAND).values - ZERO_C\n\nwhen = f&quot;valid {valid_2020:%d %b %Y %H} UTC (+36 h)&quot;</code></pre>\n<pre><code>fig = maps([\n    (&quot;AIFS-ENS - coarse dataset, resolution 0.25° (~28 km)&quot;, model.lon, model.lat, raw),\n    (&quot;CERRA - fine dataset, resolution: 5.5 km (~0.05°)&quot;, model.lon, model.lat, truth),\n    (&quot;AIFS-ENS + downscaler - resolution: 5.5 km (~0.05°)&quot;, model.lon, model.lat, down),\n], width=4.4, title=f&quot;Ensemble mean 2m temperature, {when}&quot;)</code></pre>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/GJR5m0QUEwR99jJwAQlQw.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/GJR5m0QUEwR99jJwAQlQw.png\" alt=\"Output of the code above: AIFS, CERRA, AIFS + downscaler temperature forecasts over France\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<pre><code>fig = maps([\n    (&quot;AIFS-ENS minus CERRA&quot;, model.lon, model.lat, interp - truth),\n    (&quot;AIFS-ENS + downscaler minus CERRA&quot;, model.lon, model.lat, down - truth),\n], cmap=&quot;RdBu_r&quot;, vmin=-5, vmax=5, label=&quot;K&quot;, title=f&quot;Error against CERRA, {when}&quot;)\nfor name, error in [(&quot;AIFS-ENS&quot;, interp - truth), (&quot;AIFS-ENS + downscaler&quot;, down - truth)]:\n    print(f&quot;{name:24s} RMSE against CERRA over land {np.sqrt(np.nanmean(error ** 2)):.2f} K&quot;)</code></pre>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/8qGyWgdOUFwcccqE2dnMm.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/8qGyWgdOUFwcccqE2dnMm.png\" alt=\"Output of the code above: AIFS and AIFS + downscaler&#39;s errors against CERRA\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>Above, we plot the coarse forecast (AIFS-ENS), the fine field (CERRA) and AIFS-ENS + downscaler on summer 2020 (which was not used for training). We see that the error of AIFS-ENS against CERRA is very high in the mountains, which is pretty well corrected by our downscaler.</p>\n<ol start=\"4\"><li>Compare forecasts against observations</li></ol>\n<p>We used observations from Météo-France weather stations to evaluate the model against an independent ground truth. Downloading the station data takes around 1 hour for 1 month, so we cached the stations&#39; data on the Hub (<a href=\"https://huggingface.co/datasets/EmmaScharfmann/france-stations\" rel=\"nofollow ugc noopener\">EmmaScharfmann/france-stations</a>) to save some time: <code>load_published_stations</code> in the evaluation code above reads them from there.</p>\n<pre><code># Ensemble means and observations at every station, first 48 h of every run\nnames = (&quot;AIFS-ENS&quot;, &quot;AIFS-ENS + downscaler&quot;)\nfields = {name: np.concatenate([r[&quot;means&quot;][name].reshape(-1, r[&quot;means&quot;][name].shape[-1]) for r in results_2020])\n          for name in names}\nobserved = np.concatenate([r[&quot;observed&quot;].reshape(-1, r[&quot;observed&quot;].shape[-1]) for r in results_2020])\nrmse_2020 = {name: station_rmse(fields[name], observed) for name in names}\nfig = station_maps(stations_2020.longitude.values, stations_2020.latitude.values, rmse_2020,\n                   title=&quot;RMSE against each station, first 48 h, August 2020&quot;)</code></pre>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/a3bC91w8BqwxbflIg1MsG.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/a3bC91w8BqwxbflIg1MsG.png\" alt=\"Output of the code above: AIFS and AIFS + downscaler errors against weather stations\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>Again, we see that the downscaler corrects pretty well the error in the mountains (Alps, Massif Central, Pyrenees) and on the coast (Brittany, Normandy), as we expect it to do.</p>\n<p>You can run more forecast and evaluation against observations on our <a href=\"https://emmascharfmann-downscaling-demo.hf.space\" rel=\"nofollow ugc noopener\">demo space</a>.</p>\n<h2 id=\"training-the-downscaler\">Training the downscaler</h2>\n<p>You can train your own downscaler (you can change the data, resolution, variable, region...) following what we did to train our own downscaler. We will walk you through the main steps in this blog, but you can find a complete tutorial <a href=\"https://github.com/EmmaScharfmann/weather-forecast-model-downscaler/blob/main/training_tutorial.ipynb\" rel=\"nofollow ugc noopener\">here</a>.</p>\n<ol start=\"0\"><li>Set up</li></ol>\n<p>You need to clone the <a href=\"https://github.com/EmmaScharfmann/weather-forecast-model-downscaler\" rel=\"nofollow ugc noopener\">github repo</a> and log into your Earthmover free account to fetch the coarse field (ERA5 in the tutorial). Then, you need to adapt the configuration for the training (number of epochs, steps per epochs, starting and ending dates of the data for training and validation).</p>\n<ol><li>Building the training pair</li></ol>\n<p>You need to build pairs of coarse field - fine field. In our tutorial, we&#39;re using ERA5 for the coarse field and CERRA for the fine field from 2014 to 2019 for the training and 2020 for the evaluation. ERA5 is fetched from Earthmover while CERRA is cached on the Hub. Then, the static data (at the resolution of the fine field) are added to the pairs: orography, longitude/latitude, land-sea mask, time of the year/day and distance to the coast. The model has to learn to predict the difference between the two maps below:</p>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/TBU85cL1eIyOV_BIURZrQ.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/TBU85cL1eIyOV_BIURZrQ.png\" alt=\"ERA5 and CERRA 2m temperature\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<ol start=\"2\"><li>Training the model</li></ol>\n<p>We train a small U-net to learn to predict the difference between the coarse and the fine field.\nRandom noise is added to predict a probabilistic correction. A CRPS loss is used to train the ensemble downscaler. 2014-2019 is used for training. We train the model for 20 epochs.</p>\n<figure><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/X8ueIzdRrA3Th91yZRPCh.png\" alt=\"training and testing loss\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/l1CxqeZEQmFihZX9_hyRR.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/l1CxqeZEQmFihZX9_hyRR.png\" alt=\"AIFS, AIFS + downscaler, CERRA 2m temperature\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<ol start=\"3\"><li>Evaluating the model</li></ol>\n<p>We evaluate the model against CERRA (year 2020). We plot the difference between the coarse model (ERA5) and ERA5 + downscaler. As expected, we see that local errors in the mountains or the coast are corrected by the downscaler while global errors of the coarse model are not corrected by the downscaler (error in Normandy).</p>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/pO-VFptdAHeZxQAZooWrT.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/pO-VFptdAHeZxQAZooWrT.png\" alt=\"error against CERRA for ERA5 and ERA5 + downscaler\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>We can also plot the power spectrum of the forecasts. The power spectrum shows how much detail a field has at each scale. The right panel shows the same, but divided by CERRA, so 1 means “as detailed as CERRA”.</p>\n<p>ERA5 runs out of detail below ~100 km. A downscaled member follows CERRA, so the model adds the right amount of detail at the right scales. The ensemble mean is slightly smoother than the single member as it averages the details that are uncertain.</p>\n<p><a href=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/Fd49u7H81o6hJTFI9OKRa.png\" rel=\"nofollow ugc noopener\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/Fd49u7H81o6hJTFI9OKRa.png\" alt=\"Power spectrum of the 2m temperature for ERA5, ERA5 + downscaler and CERRA.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<h2 id=\"conclusion-and-some-notes-worth-sharing\">Conclusion and some notes worth sharing</h2>\n<p>Training a downscaler takes a GPU and a couple of hours. It does not make a forecast better globally, but it makes it (way) better locally. Comparing against CERRA tells us whether the downscaler learned what it was trained to learn, comparing against stations tells us whether the final forecast is good. Scoring CERRA itself against stations gives an upper bound on what the downscaler can achieve. And our downscaler gets surprisingly close to that bound (as shown in the figure below)!</p>\n<figure><img src=\"https://cdn-uploads.huggingface.co/production/uploads/6a2f0f8a8aec62a63aae7447/U-WnecGiC-k5lK02fyM67.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p><em>RMSE by station elevation and by time of day for AIFS-ENS, AIFS-ENS + downscaler and CERRA, August 2020, all lead times. From the <a href=\"https://emmascharfmann-downscaling-demo.hf.space\" rel=\"nofollow ugc noopener\">demo space</a>.</em></p>\n<p><strong>Notes worth sharing</strong></p>\n<ul><li>This is a demonstration, not a robust evaluation. We chose temperature because it is a smooth variable, summer somewhat arbitrarily (we started out looking at heatwaves and drifted towards training a downscaler), and AIFS-ENS because its forecasts were available on Earthmover and it runs fast (0.5 s per 6-hour step on a GPU).</li><li>The power spectrum was our most useful diagnostic. It showed that noise drawn at a single scale piles up spread at that wavelength, and changing the scale only moves the excess. Drawing noise at several scales reduces the problem but doesn&#39;t fully solve it.</li><li>All errors have a strong daily cycle, because of effects that CERRA, AIFS-ENS and the downscaler all struggle to predict, such as inversions at night and surface heating in the afternoon.</li><li>Only about 10–30% of the difference between the coarse and fine fields is predictable from our inputs. The rest would need more inputs, such as wind.</li></ul>\n<p><strong>Next steps</strong>: we&#39;d like to try precipitation and wind, temporal conditioning instead of treating each time step independently, and a GNN trained directly on station observations instead of a reanalysis. We&#39;d love your feedback on what was useful and what we should do next, in the comments or by email. The <a href=\"https://huggingface.co/EmmaScharfmann/france-temperature-downscaler\" rel=\"nofollow ugc noopener\">model</a>, <a href=\"https://github.com/EmmaScharfmann/weather-forecast-model-downscaler\" rel=\"nofollow ugc noopener\">code</a> and <a href=\"https://emmascharfmann-downscaling-demo.hf.space\" rel=\"nofollow ugc noopener\">demo</a> are all available.</p>\n<p><strong>Acknowledgements</strong>: thanks a lot to <a href=\"https://qnicolas.github.io/\" rel=\"nofollow ugc noopener\">Quentin Nicolas</a> for sharing his climate expertise and for the many comments that made this post clearer and more accurate.</p>\n<h2 id=\"references\">References</h2>\n<ol><li>Lang, S., Alexe, M., Clare, M.C.A. et al. AIFS-CRPS: ensemble forecasting using a model trained with a loss function based on the continuous ranked probability score. npj Artif. Intell. 2, 18 (2026). <a href=\"https://doi.org/10.1038/s44387-026-00073-7\" rel=\"nofollow ugc noopener\">https://doi.org/10.1038/s44387-026-00073-7</a></li><li>Hersbach, H., Bell, B., Berrisford, P., et al. (2020). The ERA5 global reanalysis. <em>Q. J. R. Meteorol. Soc.</em>, 146(730), 1999–2049. <a href=\"https://doi.org/10.1002/qj.3803\" rel=\"nofollow ugc noopener\">https://doi.org/10.1002/qj.3803</a></li><li>Ridal, M., Bazile, E., Le Moigne, P., et al. (2024). CERRA, the Copernicus European Regional Reanalysis system. <em>Q. J. R. Meteorol. Soc.</em>, 150. <a href=\"https://doi.org/10.1002/qj.4764\" rel=\"nofollow ugc noopener\">https://doi.org/10.1002/qj.4764</a></li><li>Ferro, C. A. T. (2014). Fair scores for ensemble forecasts. <em>Q. J. R. Meteorol. Soc.</em>, 140, 1917–1923. <a href=\"https://doi.org/10.1002/qj.2270\" rel=\"nofollow ugc noopener\">https://doi.org/10.1002/qj.2270</a></li></ol>\n<hr />\n<p><em>Originally published at <a href=\"https://huggingface.co/blog/EmmaScharfmann/downscaling-a-weather-forecasting-model\" rel=\"nofollow ugc noopener\">Hugging Face</a>.</em></p>","headings":[{"level":1,"text":"Do we really need bigger AI weather forecasting models to improve resolution?","id":"do-we-really-need-bigger-ai-weather-forecasting-models-to-improv"},{"level":2,"text":"TL;DR","id":"tl-dr"},{"level":2,"text":"Table of Contents","id":"table-of-contents"},{"level":2,"text":"Intro","id":"intro"},{"level":2,"text":"What is a downscaler?","id":"what-is-a-downscaler"},{"level":2,"text":"Downscaler components","id":"downscaler-components"},{"level":3,"text":"Training","id":"training"},{"level":3,"text":"Inference","id":"inference"},{"level":2,"text":"Running a downscaler for temperature over France","id":"running-a-downscaler-for-temperature-over-france"},{"level":2,"text":"Training the downscaler","id":"training-the-downscaler"},{"level":2,"text":"Conclusion and some notes worth sharing","id":"conclusion-and-some-notes-worth-sharing"},{"level":2,"text":"References","id":"references"}]}}