Emma Scharfmann
add logs and fix plot
8ae4133
Raw
History Blame Contribute Delete
5.82 kB
"""
aifs.plot
=========
Cartopy-based helpers for visualising AIFS forecast output on a global map.
All functions return ``matplotlib.figure.Figure`` objects so they work in
both notebooks (``plt.show()``) and scripts (``fig.savefig(...)``).
Quickstart
----------
from aifs.plot import plot_field, plot_field_sequence
# Single map
fig = plot_field(state, "2t", title="2-m Temperature β€” T+6h")
fig.savefig("t2m_T+6.png", dpi=150)
# Multi-panel sequence
fig = plot_field_sequence(states, "2t", max_steps=4)
fig.savefig("t2m_sequence.png", dpi=150)
"""
from __future__ import annotations
import warnings
import numpy as np
warnings.filterwarnings("ignore", category=UserWarning)
# ── Variable metadata ─────────────────────────────────────────────────────────
#: Variables that can be extracted from forecast state dicts
PLOTTABLE = [
"2t", "msl", "sp", "tcw", "10u", "10v", "swh", "mwp",
"t_850", "t_500", "u_850", "v_850", "z_500", "q_700",
]
_CMAP = {
"2t": "RdBu_r", "t_850": "RdBu_r", "t_500": "RdBu_r",
"msl": "viridis", "sp": "viridis",
"10u": "RdBu", "10v": "RdBu",
"u_850": "RdBu", "v_850": "RdBu",
"swh": "Blues", "mwp": "Blues", "tcw": "Blues",
"z_500": "plasma", "q_700": "YlGn",
}
_UNITS = {
"2t": "K", "t_850": "K", "t_500": "K",
"msl": "Pa", "sp": "Pa", "z_500": "mΒ²/sΒ²",
"10u": "m/s", "10v": "m/s", "u_850": "m/s", "v_850": "m/s",
"swh": "m", "mwp": "s", "tcw": "kg/mΒ²", "q_700": "kg/kg",
}
_LONG_NAME = {
"2t": "2-m Temperature",
"msl": "Mean Sea-Level Pressure",
"sp": "Surface Pressure",
"tcw": "Total Column Water",
"10u": "10-m U Wind",
"10v": "10-m V Wind",
"swh": "Significant Wave Height",
"mwp": "Mean Wave Period",
"t_850": "Temperature at 850 hPa",
"t_500": "Temperature at 500 hPa",
"u_850": "U Wind at 850 hPa",
"v_850": "V Wind at 850 hPa",
"z_500": "Geopotential at 500 hPa",
"q_700": "Specific Humidity at 700 hPa",
}
# ── Grid coordinate extraction ────────────────────────────────────────────────
def _get_latlons(state: dict) -> tuple[np.ndarray, np.ndarray]:
"""
Return (lats, lons) for the grid the forecast was run on.
The anemoi tensor handler injects ``state["latitudes"]`` and
``state["longitudes"]`` from the checkpoint metadata before the first
inference step, and these are propagated to every output state via
``new_states = input_states.copy()``. We read them directly β€” no
separate grid-geometry lookup needed.
Longitudes are returned in the range [0, 360) as stored by anemoi;
callers that need [-180, 180) should call ``_to_180(lons)``.
"""
lats = state.get("latitudes")
lons = state.get("longitudes")
if lats is None or lons is None:
raise KeyError(
"State dict does not contain 'latitudes'/'longitudes'. "
"Make sure you are passing a state returned by run_forecast() "
"and have not stripped those keys."
)
lats = np.asarray(lats).ravel()
lons = np.asarray(lons).ravel()
if len(lats) < 3 or len(lons) < 3:
raise ValueError(
f"Grid has only {len(lats)} points β€” expected ~542 080 for N320. "
"The state latitudes/longitudes may be corrupt."
)
return lats, lons
def _to_180(lons: np.ndarray) -> np.ndarray:
"""Normalise longitudes from [0, 360) to [-180, 180) for Cartopy."""
return np.where(lons > 180, lons - 360, lons)
def _extract_field(state: dict, variable: str) -> np.ndarray | None:
"""Pull ``variable`` out of ``state["fields"]``, return None if missing."""
return state.get("fields", {}).get(variable)
# ── Public API ────────────────────────────────────────────────────────────────
def plot_field(
state: dict,
variable: str,
) -> "matplotlib.figure.Figure":
"""
Plot a single forecast field on a global map.
Parameters
----------
state:
One element from the list returned by :func:`aifs.forecast.run_forecast`.
variable:
Short name of the field to plot (e.g. ``"2t"``, ``"msl"``).
See :data:`PLOTTABLE` for supported names.
Returns
-------
matplotlib.figure.Figure
"""
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.tri as tri
data = _extract_field(state, variable)
if data is None:
raise KeyError(
f"Variable '{variable}' not found in forecast state. "
f"Available: {sorted(state.get('fields', {}).keys())}"
)
units = _UNITS.get(variable, "")
lname = _LONG_NAME.get(variable, variable)
dt = state.get("date", "")
lats, lons = _get_latlons(state)
lons_plot = _to_180(lons)
fig, ax = plt.subplots(figsize=(11, 6), subplot_kw={"projection": ccrs.PlateCarree()})
ax.coastlines()
ax.add_feature(cfeature.BORDERS, linestyle=":")
triangulation = tri.Triangulation(lons_plot, lats)
contour = ax.tricontourf(triangulation, data, levels=20, transform=ccrs.PlateCarree(), cmap="RdBu_r")
cbar = fig.colorbar(contour, ax=ax, orientation="vertical", shrink=0.7, label=variable)
cbar.set_label(f"{lname} [{units}]", fontsize=10)
plt.title(variable .format(dt))
fig.tight_layout()
plt.show()
return fig