StormCast-CONUS Score-Based Data Assimilation¶
Running StormCast-CONUS with guided diffusion posterior sampling to assimilate observations.
This example demonstrates how to use the StormCast-CONUS generative model with score-based data assimilation (SDA) over the Continental United States. Sparse in-situ surface observations from NOAA Global Historical Climatology Network Hourly (GHCNh) are assimilated at each forecast step using diffusion posterior sampling (DPS) guidance. Two forecasts from the central United States are run to illustrate the impact of data assimilation: one without observations and one with GHCNh surface station data.
In this example you will learn:
- How to load StormCast-CONUS and configure SDA parameters
- Fetching HRRR initial conditions and GHCNh surface observations
- Running the model iteratively with and without observation assimilation
- Comparing assimilated and non-assimilated forecasts
Set Up¶
This example requires the following components:
- Prognostic Model: StormCast-CONUS
earth2studio.models.px.StormCastCONUSconfigured with SDA parameters. - Datasource (state): HRRR analysis
earth2studio.data.HRRR. - Datasource (obs): NOAA GHCNh surface stations
earth2studio.data.GHCNHourly.
StormCast-CONUS extends the StormCast generative architecture to the full CONUS HRRR domain at 3 km resolution. When an observation DataFrame is passed to each generator step, diffusion posterior sampling (DPS) steers the denoising trajectory toward the observed values.
import os
os.makedirs("outputs", exist_ok=True)
from dotenv import load_dotenv
load_dotenv() # TODO: make common example prep function
from datetime import timedelta
import numpy as np
import torch
from loguru import logger
from tqdm import tqdm
logger.remove()
logger.add(lambda msg: tqdm.write(msg, end=""), colorize=True)
from earth2studio.data import HRRR, GHCNHourly, fetch_data
from earth2studio.models.px import StormCastCONUS
from earth2studio.utils.coords import map_coords
# Load the default model package
package = StormCastCONUS.load_default_package()
# Configure SDA: sda_std_obs is the assumed normalised observation noise std per
# variable (lower = trust observations more).
# sda_gamma is the DPS step-size scaling (lower = stronger guidance from obs).
#
# By default the example runs on the central-US subdomain to reduce GPU memory
# requirements. For the full CONUS domain, comment out hrrr_lat_lim and
# hrrr_lon_lim below (requires ~200 GB VRAM for SDA).
hrrr_lat_lim = (273, 785)
hrrr_lon_lim = (579, 1219)
model = StormCastCONUS.load_model(
package,
hrrr_lat_lim=hrrr_lat_lim, # comment out for full CONUS domain
hrrr_lon_lim=hrrr_lon_lim, # comment out for full CONUS domain
num_diffusion_steps=18,
num_sda_diffusion_steps=96,
sda_std_obs=0.15,
sda_gamma=1e-3,
)
model = model.to("cuda:0")
hrrr = HRRR()
Console output9 lines
/__w/earth2studio/earth2studio/.venv/lib/python3.13/site-packages/torch/cuda/__init__.py:64: FutureWarning: The pynvml package is deprecated. Please install nvidia-ml-py instead. If you did not install pynvml directly, please report this to the maintainers of the package that installed pynvml for you.
import pynvml # type: ignore[import]
WARNING[XFORMERS]: xFormers can't load C++/CUDA extensions. xFormers was built for:
PyTorch 2.10.0+cu128 with CUDA 1208 (you have 2.13.0+cu130)
Python 3.10.19 (you have 3.13.13)
Please reinstall xformers (see https://github.com/facebookresearch/xformers#installing-xformers)
Memory-efficient attention, SwiGLU, sparse and more won't be available.
Set XFORMERS_MORE_DETAILS=1 for more details
CuPy distance computation test failed with error: cuVS >= 24.12 or pylibraft < 24.12 should be installed to use this featureFetch Initial Conditions¶
Pull HRRR analysis data for 17 April 2026, a date that saw a significant tornado outbreak across the central United States, and align it to the model's coordinate system.
init_time = np.array([np.datetime64("2026-04-17T18:00")])
x, coords = fetch_data(
hrrr,
time=init_time,
variable=model.variables,
lead_time=np.array([np.timedelta64(0, "h")]),
device="cuda:0",
)
x, coords = map_coords(x, coords, model.input_coords())
Console output19 lines
Fetching HRRR data: 0%| | 0/99 [00:00<?, ?it/s]
Fetching HRRR data: 1%| | 1/99 [00:00<00:46, 2.10it/s]
Fetching HRRR data: 2%|โ | 2/99 [00:00<00:25, 3.77it/s]
Fetching HRRR data: 4%|โ | 4/99 [00:00<00:12, 7.38it/s]
Fetching HRRR data: 8%|โ | 8/99 [00:00<00:06, 14.10it/s]
Fetching HRRR data: 12%|โโ | 12/99 [00:00<00:04, 18.82it/s]
Fetching HRRR data: 15%|โโ | 15/99 [00:01<00:04, 19.08it/s]
Fetching HRRR data: 18%|โโ | 18/99 [00:01<00:04, 17.05it/s]
Fetching HRRR data: 20%|โโ | 20/99 [00:01<00:05, 15.34it/s]
Fetching HRRR data: 22%|โโโ | 22/99 [00:01<00:05, 14.81it/s]
Fetching HRRR data: 24%|โโโ | 24/99 [00:01<00:06, 12.46it/s]
Fetching HRRR data: 27%|โโโ | 27/99 [00:02<00:05, 12.87it/s]
Fetching HRRR data: 30%|โโโ | 30/99 [00:02<00:05, 12.28it/s]
Fetching HRRR data: 37%|โโโโ | 37/99 [00:02<00:04, 14.37it/s]
Fetching HRRR data: 47%|โโโโโ | 47/99 [00:03<00:03, 16.34it/s]
Fetching HRRR data: 57%|โโโโโโ | 56/99 [00:03<00:01, 22.10it/s]
Fetching HRRR data: 70%|โโโโโโโ | 69/99 [00:03<00:01, 28.53it/s]
Fetching HRRR data: 82%|โโโโโโโโโ | 81/99 [00:04<00:00, 34.77it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 99/99 [00:04<00:00, 24.07it/s]Run Without Observations¶
Step the model forward 6 hours without any observations. This uses the
standard EDM diffusion sampler, equivalent to running StormCast-CONUS as a
pure generative forecast model. We store only the 10-m zonal wind (u10m)
used for comparison.
nsteps = 6
plot_var = "u10m"
plot_vars = ["u10m", "v10m", "t2m"]
var_idx = list(model.variables).index(plot_var)
np.random.seed(42)
torch.manual_seed(42)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(42)
no_obs_fields = []
gen = model.create_generator(x.clone(), coords.copy())
x_cur, c_cur = next(gen) # prime the generator, yields initial state (lead_time = 0 h)
for step in tqdm(range(nsteps), desc="No-obs forecast"):
logger.info(f"Running no-obs forecast step {step + 1}/{nsteps}")
x_cur, c_cur = gen.send(None) # advance one hour without observations
no_obs_fields.append(x_cur[0, 0, var_idx].cpu().numpy())
gen.close()
no_obs_fields = np.stack(no_obs_fields) # (nsteps, H, W)
Console output106 lines
No-obs forecast: 0%| | 0/6 [00:00<?, ?it/s]
2026-08-15 05:39:35.671 | INFO | __main__:<module>:16 - Running no-obs forecast step 1/6
No-obs forecast: 0%| | 0/6 [00:00<?, ?it/s]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:10, 2.31it/s]
Fetching GFS data: 8%|โ | 2/26 [00:00<00:06, 3.51it/s]
Fetching GFS data: 19%|โโ | 5/26 [00:00<00:02, 8.69it/s]
Fetching GFS data: 35%|โโโโ | 9/26 [00:00<00:01, 14.70it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:00<00:00, 26.32it/s]
No-obs forecast: 17%|โโ | 1/6 [00:05<00:29, 5.88s/it]
2026-08-15 05:39:41.555 | INFO | __main__:<module>:16 - Running no-obs forecast step 2/6
No-obs forecast: 17%|โโ | 1/6 [00:05<00:29, 5.88s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:05, 4.30it/s]
Fetching GFS data: 19%|โโ | 5/26 [00:00<00:02, 7.74it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:00<00:00, 34.76it/s]
No-obs forecast: 33%|โโโโ | 2/6 [00:11<00:21, 5.47s/it]
2026-08-15 05:39:46.741 | INFO | __main__:<module>:16 - Running no-obs forecast step 3/6
No-obs forecast: 33%|โโโโ | 2/6 [00:11<00:21, 5.47s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:06, 3.79it/s]
Fetching GFS data: 12%|โโ | 3/26 [00:00<00:02, 9.08it/s]
Fetching GFS data: 19%|โโ | 5/26 [00:00<00:01, 12.00it/s]
Fetching GFS data: 35%|โโโโ | 9/26 [00:00<00:00, 19.08it/s]
Fetching GFS data: 92%|โโโโโโโโโโ| 24/26 [00:00<00:00, 55.45it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:01<00:00, 21.04it/s]
No-obs forecast: 50%|โโโโโ | 3/6 [00:16<00:16, 5.55s/it]
2026-08-15 05:39:52.389 | INFO | __main__:<module>:16 - Running no-obs forecast step 4/6
No-obs forecast: 50%|โโโโโ | 3/6 [00:16<00:16, 5.55s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:04, 5.20it/s]
Fetching GFS data: 15%|โโ | 4/26 [00:00<00:01, 15.28it/s]
Fetching GFS data: 27%|โโโ | 7/26 [00:00<00:01, 13.86it/s]
Fetching GFS data: 42%|โโโโโ | 11/26 [00:00<00:00, 19.33it/s]
Fetching GFS data: 92%|โโโโโโโโโโ| 24/26 [00:00<00:00, 47.10it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:00<00:00, 33.83it/s]
No-obs forecast: 67%|โโโโโโโ | 4/6 [00:21<00:10, 5.43s/it]
2026-08-15 05:39:57.626 | INFO | __main__:<module>:16 - Running no-obs forecast step 5/6
No-obs forecast: 67%|โโโโโโโ | 4/6 [00:21<00:10, 5.43s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:05, 4.66it/s]
Fetching GFS data: 19%|โโ | 5/26 [00:00<00:01, 18.43it/s]
Fetching GFS data: 31%|โโโ | 8/26 [00:00<00:00, 21.29it/s]
Fetching GFS data: 77%|โโโโโโโโ | 20/26 [00:00<00:00, 53.29it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:01<00:00, 18.59it/s]
No-obs forecast: 83%|โโโโโโโโโ | 5/6 [00:27<00:05, 5.59s/it]
2026-08-15 05:40:03.502 | INFO | __main__:<module>:16 - Running no-obs forecast step 6/6
No-obs forecast: 83%|โโโโโโโโโ | 5/6 [00:27<00:05, 5.59s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:06, 3.90it/s]
Fetching GFS data: 15%|โโ | 4/26 [00:00<00:01, 12.17it/s]
Fetching GFS data: 23%|โโโ | 6/26 [00:00<00:01, 13.89it/s]
Fetching GFS data: 69%|โโโโโโโ | 18/26 [00:00<00:00, 44.20it/s]
Fetching GFS data: 92%|โโโโโโโโโโ| 24/26 [00:01<00:00, 17.34it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:01<00:00, 16.62it/s]
No-obs forecast: 100%|โโโโโโโโโโ| 6/6 [00:33<00:00, 5.74s/it]
No-obs forecast: 100%|โโโโโโโโโโ| 6/6 [00:33<00:00, 5.64s/it]Fetch Observations and Plot Station Locations¶
Fetch GHCNh surface observations covering the model domain. The bounding box is derived from the model's lat/lon grid so it automatically adjusts when running on a subdomain. We visualise the station network before running the assimilation forecast.
# Model lon is in [0, 360); convert to [-180, 180) for the western-hemisphere
# CONUS domain. GHCNHourly.get_stations_bbox accepts either convention.
lat_min = float(model.lat.min())
lat_max = float(model.lat.max())
lon_min = float(model.lon.min()) - 360.0
lon_max = float(model.lon.max()) - 360.0
stations = GHCNHourly.get_stations_bbox((lat_min, lon_min, lat_max, lon_max))
ghcn = GHCNHourly(
stations=stations, time_tolerance=timedelta(minutes=15), verbose=False
)
import cartopy
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
# Fetch a sample observation at the initial time to retrieve station positions.
# GHCNh returns lon in [0, 360); shift to [-180, 180) for PlateCarree plots.
sample_df = ghcn(init_time, plot_vars)
station_lats = sample_df["lat"].values
station_lons = sample_df["lon"].values - 360.0
plt.close("all")
fig, ax = plt.subplots(subplot_kw={"projection": ccrs.PlateCarree()}, figsize=(8, 6))
ax.set_extent(
[lon_min - 2, lon_max + 2, lat_min - 2, lat_max + 2], crs=ccrs.PlateCarree()
)
ax.add_feature(
cartopy.feature.STATES.with_scale("50m"), linewidth=0.5, edgecolor="black"
)
ax.add_feature(cartopy.feature.LAND, facecolor="lightyellow")
ax.gridlines(draw_labels=True, linewidth=0.3, alpha=0.5)
ax.scatter(
station_lons,
station_lats,
s=20,
marker="x",
transform=ccrs.PlateCarree(),
zorder=3,
)
ax.set_title("GHCNh Station Locations - StormCast-CONUS Domain")
plt.savefig("outputs/01_ghcn_stations.jpg", dpi=150, bbox_inches="tight")

Run Inference With Streaming Observations¶
At each forecast step the next valid time is determined from the current generator state, GHCNh observations are fetched for that time, and the observation DataFrame is sent to the generator. Zero-value wind reports (likely anemometer failure) are filtered out before assimilation.
np.random.seed(42)
torch.manual_seed(42)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(42)
obs_fields = []
gen = model.create_generator(x.clone(), coords.copy())
x_cur, c_cur = next(gen) # prime the generator, yields initial state
for step in tqdm(range(nsteps), desc="Obs forecast"):
# Target valid time is one step ahead of the current generator state
valid_time = np.array(
[c_cur["time"][0] + c_cur["lead_time"][0] + np.timedelta64(1, "h")]
)
obs_df = ghcn(valid_time, plot_vars)
# Drop zero-value wind reports (common anemometer failure mode)
if len(obs_df) > 0:
is_wind = obs_df["variable"].str.startswith(("u", "v"))
zero_wind = is_wind & (obs_df["observation"].abs() < 1e-5)
obs_df = obs_df[~zero_wind]
obs = obs_df if len(obs_df) > 0 else None
logger.info(
f"Step {step + 1}/{nsteps} โ {len(obs_df) if obs is not None else 0} obs"
)
x_cur, c_cur = gen.send(obs) # advance one hour with observations
obs_fields.append(x_cur[0, 0, var_idx].cpu().numpy())
gen.close()
obs_fields = np.stack(obs_fields) # (nsteps, H, W)
Console output68 lines
Obs forecast: 0%| | 0/6 [00:00<?, ?it/s]
2026-08-15 05:41:48.751 | INFO | __main__:<module>:24 - Step 1/6 โ 392 obs
Obs forecast: 0%| | 0/6 [00:14<?, ?it/s]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:11, 2.15it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:00<00:00, 54.94it/s]
Obs forecast: 17%|โโ | 1/6 [01:16<06:20, 76.02s/it]
2026-08-15 05:43:04.187 | INFO | __main__:<module>:24 - Step 2/6 โ 392 obs
Obs forecast: 17%|โโ | 1/6 [01:30<06:20, 76.02s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:10, 2.28it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:00<00:00, 56.90it/s]
Obs forecast: 33%|โโโโ | 2/6 [02:38<05:18, 79.65s/it]
2026-08-15 05:44:26.533 | INFO | __main__:<module>:24 - Step 3/6 โ 392 obs
Obs forecast: 33%|โโโโ | 2/6 [02:52<05:18, 79.65s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:11, 2.23it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:00<00:00, 57.82it/s]
Obs forecast: 50%|โโโโโ | 3/6 [04:09<04:15, 85.02s/it]
2026-08-15 05:45:58.076 | INFO | __main__:<module>:24 - Step 4/6 โ 392 obs
Obs forecast: 50%|โโโโโ | 3/6 [04:23<04:15, 85.02s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:12, 2.01it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:00<00:00, 51.66it/s]
Obs forecast: 67%|โโโโโโโ | 4/6 [05:35<02:50, 85.49s/it]
2026-08-15 05:47:23.786 | INFO | __main__:<module>:24 - Step 5/6 โ 392 obs
Obs forecast: 67%|โโโโโโโ | 4/6 [05:49<02:50, 85.49s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:12, 1.93it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:00<00:00, 49.90it/s]
Obs forecast: 83%|โโโโโโโโโ | 5/6 [06:59<01:24, 84.84s/it]
2026-08-15 05:48:48.630 | INFO | __main__:<module>:24 - Step 6/6 โ 392 obs
Obs forecast: 83%|โโโโโโโโโ | 5/6 [07:14<01:24, 84.84s/it]
Fetching GFS data: 0%| | 0/26 [00:00<?, ?it/s]
Fetching GFS data: 4%|โ | 1/26 [00:00<00:10, 2.33it/s]
Fetching GFS data: 100%|โโโโโโโโโโ| 26/26 [00:00<00:00, 58.18it/s]
Obs forecast: 100%|โโโโโโโโโโ| 6/6 [08:32<00:00, 87.52s/it]
Obs forecast: 100%|โโโโโโโโโโ| 6/6 [08:32<00:00, 85.38s/it]Post Processing¶
Compare the two forecasts. The top row shows the baseline (no observations), the middle row shows the assimilation forecast with station locations overlaid, and the bottom row shows the signed difference (assimilated โ baseline).
plt.close("all")
# HRRR Lambert Conformal projection
projection = ccrs.LambertConformal(
central_longitude=262.5,
central_latitude=38.5,
standard_parallels=(38.5, 38.5),
globe=ccrs.Globe(semimajor_axis=6371229, semiminor_axis=6371229),
)
fig, axes = plt.subplots(
3,
nsteps,
subplot_kw={"projection": projection},
figsize=(4 * nsteps, 8),
)
fig.subplots_adjust(wspace=0.02, hspace=0.08, left=0.1, right=0.9)
vmin, vmax = -10, 10
for step in range(nsteps):
lead_hr = step + 1
no_obs_field = no_obs_fields[step]
obs_field = obs_fields[step]
diff_field = obs_field - no_obs_field
# Row 0: No-obs forecast
ax = axes[0, step]
im0 = ax.pcolormesh(
model.lon,
model.lat,
no_obs_field,
transform=ccrs.PlateCarree(),
cmap="PRGn",
vmin=vmin,
vmax=vmax,
)
ax.add_feature(
cartopy.feature.STATES.with_scale("50m"),
linewidth=0.5,
edgecolor="black",
zorder=2,
)
ax.set_title(f"+{lead_hr}h")
# Row 1: With-obs forecast + station locations
ax = axes[1, step]
im1 = ax.pcolormesh(
model.lon,
model.lat,
obs_field,
transform=ccrs.PlateCarree(),
cmap="PRGn",
vmin=vmin,
vmax=vmax,
)
ax.scatter(
station_lons,
station_lats,
s=8,
facecolors="none",
edgecolors="black",
linewidths=0.8,
transform=ccrs.PlateCarree(),
zorder=3,
)
ax.add_feature(
cartopy.feature.STATES.with_scale("50m"),
linewidth=0.5,
edgecolor="black",
zorder=2,
)
# Row 2: Difference (assimilated โ baseline)
ax = axes[2, step]
im2 = ax.pcolormesh(
model.lon,
model.lat,
diff_field,
transform=ccrs.PlateCarree(),
cmap="RdBu_r",
vmin=-3,
vmax=3,
)
ax.add_feature(
cartopy.feature.STATES.with_scale("50m"),
linewidth=0.5,
edgecolor="black",
zorder=2,
)
for row_label, ax_row in zip(["No Obs", "Obs", "Difference"], axes[:, 0]):
ax_row.text(
-0.07,
0.5,
row_label,
va="bottom",
ha="center",
rotation="vertical",
rotation_mode="anchor",
fontsize=12,
transform=ax_row.transAxes,
)
plt.colorbar(im0, ax=axes[0, -1], shrink=0.6, label=f"{plot_var} (m/s)")
plt.colorbar(im1, ax=axes[1, -1], shrink=0.6, label=f"{plot_var} (m/s)")
plt.colorbar(im2, ax=axes[2, -1], shrink=0.6, label=f"{plot_var} (m/s)")
plt.tight_layout()
plt.savefig("outputs/01_stormcast_conus_sda_comparison.jpg", dpi=150)

Ground Truth Comparison¶
Fetch HRRR analysis at each valid forecast time and compute the absolute error of both the no-obs and assimilation forecasts. This shows whether assimilation improves accuracy relative to the actual analysis.
truth_x, truth_coords = fetch_data(
hrrr,
time=init_time,
variable=np.array([plot_var]),
lead_time=np.array([np.timedelta64(h + 1, "h") for h in range(nsteps)]),
device="cpu",
)
truth_x, truth_coords = map_coords(
truth_x,
truth_coords,
{"hrrr_y": model.hrrr_y, "hrrr_x": model.hrrr_x},
)
# truth_x shape: (time=1, lead_time=nsteps, variable=1, H, W)
truth_fields = truth_x[0, :, 0].numpy() # (nsteps, H, W)
no_obs_err = np.abs(no_obs_fields - truth_fields)
obs_err = np.abs(obs_fields - truth_fields)
Console output23 lines
Fetching HRRR data: 0%| | 0/1 [00:00<?, ?it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 2.10it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 2.09it/s]
Fetching HRRR data: 0%| | 0/1 [00:00<?, ?it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 2.11it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 2.10it/s]
Fetching HRRR data: 0%| | 0/1 [00:00<?, ?it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 4.38it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 4.37it/s]
Fetching HRRR data: 0%| | 0/1 [00:00<?, ?it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 5.62it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 5.61it/s]
Fetching HRRR data: 0%| | 0/1 [00:00<?, ?it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 3.60it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 3.60it/s]
Fetching HRRR data: 0%| | 0/1 [00:00<?, ?it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 5.54it/s]
Fetching HRRR data: 100%|โโโโโโโโโโ| 1/1 [00:00<00:00, 5.53it/s]Plot absolute errors between the StormCast-CONUS predictions and HRRR analysis ground truth. In later time-steps the assimilated forecast typically shows lower errors near the observation stations.
plt.close("all")
fig, axes = plt.subplots(
2,
nsteps,
subplot_kw={"projection": projection},
figsize=(4 * nsteps, 6),
)
fig.subplots_adjust(wspace=0.02, hspace=0.08, left=0.1, right=0.9)
err_max = 5
for step in range(nsteps):
lead_hr = step + 1
# Row 0: No-obs absolute error
ax = axes[0, step]
im0 = ax.pcolormesh(
model.lon,
model.lat,
no_obs_err[step],
transform=ccrs.PlateCarree(),
cmap="viridis",
vmin=0,
vmax=err_max,
)
ax.add_feature(
cartopy.feature.STATES.with_scale("50m"),
linewidth=0.5,
edgecolor="grey",
zorder=2,
)
ax.set_title(f"+{lead_hr}h")
# Row 1: Obs absolute error
ax = axes[1, step]
im1 = ax.pcolormesh(
model.lon,
model.lat,
obs_err[step],
transform=ccrs.PlateCarree(),
cmap="viridis",
vmin=0,
vmax=err_max,
)
ax.add_feature(
cartopy.feature.STATES.with_scale("50m"),
linewidth=0.5,
edgecolor="grey",
zorder=2,
)
for row_label, ax_row in zip(["|No Obs โ Truth|", "|Obs โ Truth|"], axes[:, 0]):
ax_row.text(
-0.07,
0.5,
row_label,
va="bottom",
ha="center",
rotation="vertical",
rotation_mode="anchor",
fontsize=12,
transform=ax_row.transAxes,
)
plt.colorbar(im0, ax=axes[0, -1], shrink=0.6, label=f"|ฮ{plot_var}| (m/s)")
plt.colorbar(im1, ax=axes[1, -1], shrink=0.6, label=f"|ฮ{plot_var}| (m/s)")
plt.tight_layout()
plt.savefig("outputs/01_stormcast_conus_sda_gt_comparison.jpg", dpi=150)
