近期文章反响不错,阅读量仿佛这夏日的温度节节攀升
言归正传,Skyborn 0.4 大幅强化了 垂直插值 与 水平网格化 工具链,新增 / 重写的函数覆盖了气象数据后处理的常见场景,该库作者在用了geocat的插值函数后感慨实在难用,于是重写了:
vinth2p 家族BilinearRegridder / ConservativeRegridder / NearestRegridder本教程使用 ERA5 (2023-08-02 ~ 2023-08-05) 再分析数据,逐个演示 8 个核心函数 + 3 个 Regridder 类的实战用法。
确保 Skyborn 升级到 0.4以上:
%pip install skyborn -U --quiet
Note: you may need to restart the kernel to use updated packages.
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
import skyborn as skb
import skyborn.interp as si
print(f"Skyborn version: {skb.__version__}")
print(f"xarray version : {xr.__version__}")
# 字体配置 (中文 + 公式)
plt.rcParams['font.family'] = 'DejaVu Sans'
plt.rcParams['axes.unicode_minus'] = False
Skyborn version: 0.4.1
xarray version : 2024.3.0
skyborn.interp 模块全景Skyborn 0.40 把所有插值/网格化函数集中到 skyborn.interp 子模块下,按功能可分为三大类:
函数 | 用途 | 输入坐标 → 输出坐标 |
|---|---|---|
interp_pressure_1d | 一维气压剖面重采样 | pressure → pressure |
interp_to_isentropic | 等熵面分析 | pressure → potential temperature (θ) |
interp_hybrid_to_pressure | 模式数据 → 等压面 | hybrid σ → pressure |
interp_sigma_to_hybrid | sigma → hybrid | σ → hybrid σ |
函数 / 类 | 用途 | 入口风格 |
|---|---|---|
interp_multidim | 经/纬度网格双线性插值 | xarray 高级接口(C-order) |
Grid + BilinearRegridder | 双线性重网格 | 底层数组(column-major) |
Grid + ConservativeRegridder | 守恒型重网格 | 适合通量类变量 |
Grid + NearestRegridder | 最近邻 | 离散 / 类别型数据 |
regrid_dataset | 一次性 Dataset 重网格 | 自动检测维度 |
rgrid2rcm / rcm2rgrid | 规则网格 ↔ 曲线网格 (WRF/NARR) | NCL 风格 |
delta_pressure_hybrid / pressure_at_hybrid_levels —— 混合坐标层厚 / 层压计算grid_to_triple / triple_to_grid —— 网格 ↔ 散点互转nearest_neighbor_indices —— 球面最近邻索引(Haversine)本教程使用 ERA5 2023-08 月初 4 天 / 6 小时间隔 / 37 个等压层 / 0.25° 数据,覆盖范围 10°N–70°N, 90°E–180°E。
关键变量:u, v, w, z, t, r, q
DATA_FILE = '/home/mw/input/era58091/ERA5-2023-08_pl.nc'
ds_raw = xr.open_dataset(DATA_FILE)
print('坐标:', list(ds_raw.coords))
print('变量:', list(ds_raw.data_vars))
print(f"时间: {ds_raw.time.values[0]} → {ds_raw.time.values[-1]} (n={ds_raw.sizes['time']})")
print(f"层次 (hPa): {ds_raw.level.values}")
print(f"latitude: {ds_raw.latitude.values[0]} → {ds_raw.latitude.values[-1]} (n={ds_raw.sizes['latitude']})")
print(f"longitude: {ds_raw.longitude.values[0]} → {ds_raw.longitude.values[-1]} (n={ds_raw.sizes['longitude']})")
坐标: ['longitude', 'latitude', 'level', 'time']
变量: ['z', 'r', 't', 'q', 'u', 'v', 'w']
时间: 2023-08-02T00:00:00.000000000 → 2023-08-05T16:00:00.000000000 (n=20)
层次 (hPa): [ 1 2 3 5 7 10 20 30 50 70 100 125 150 175
200 225 250 300 350 400 450 500 550 600 650 700 750 775
800 825 850 875 900 925 950 975 1000]
latitude: 70.0 → 10.0 (n=241)
longitude: 90.0 → 180.0 (n=361)
⚠️ 重要预处理:纬度方向
ERA5 的 latitude 默认是 由北向南递减 (70 → 10)。
Skyborn 部分网格化器(特别是 ConservativeRegridder)要求纬度 严格递增,因此我们先翻转:
# 反转 latitude (10 -> 70 递增)
ds = ds_raw.isel(latitude=slice(None, None, -1))
print(f"反转后 latitude: {ds.latitude.values[0]} → {ds.latitude.values[-1]}")
# 抽取分析时刻
ds_t0 = ds.isel(time=0)
print(f"\n分析时刻: {ds_t0.time.values}")
反转后 latitude: 10.0 → 70.0
分析时刻: 2023-08-02T00:00:00.000000000
interp_pressure_1d场景:把高分辨率的 37 层 ERA5 重采样到自定义气压层(例如 GCM 标准 17 层)。
特点:
method='linear'(线性)和 method='log'(对数压力,默认且推荐)# 取出 (40°N, 120°E) 的 u/T 单点剖面
loc = dict(latitude=40, longitude=120, method='nearest')
u_prof = ds_t0['u'].sel(**loc)
t_prof = ds_t0['t'].sel(**loc)
src_p_Pa = ds_t0.level.values.astype('float64') * 100# hPa -> Pa
print(f"源压力层数: {len(src_p_Pa)} (从 {src_p_Pa.min()/100:.0f} 到 {src_p_Pa.max()/100:.0f} hPa)")
# 目标压力层 (Pa) - 17 层经典 GCM 标准
tgt_p_Pa = np.array([100000, 92500, 85000, 70000, 50000, 40000, 30000,
25000, 20000, 15000, 10000, 7000, 5000, 3000, 2000, 1000, 500], dtype='float64')
print(f"目标压力层数: {len(tgt_p_Pa)} (从 {tgt_p_Pa.min()/100:.0f} 到 {tgt_p_Pa.max()/100:.0f} hPa)")
# 对数压力插值
u_new = si.interp_pressure_1d(
data=u_prof.values,
source_pressure=src_p_Pa,
target_pressure=tgt_p_Pa,
method='log'
)
t_new = si.interp_pressure_1d(
data=t_prof.values,
source_pressure=src_p_Pa,
target_pressure=tgt_p_Pa,
method='log'
)
print(f"\n插值后形状: u_new={u_new.shape}, t_new={t_new.shape}")
print("\n部分结果对比 (40°N, 120°E):")
for p_hpa in [1000, 850, 500, 250, 100, 50, 10]:
idx = np.where(tgt_p_Pa == p_hpa * 100)[0]
if len(idx) > 0:
i = idx[0]
print(f' {p_hpa:4d} hPa u = {u_new[i]:6.2f} m/s T = {t_new[i]:6.2f} K')
源压力层数: 37 (从 1 到 1000 hPa)
目标压力层数: 17 (从 5 到 1000 hPa)
插值后形状: u_new=(17,), t_new=(17,)
部分结果对比 (40°N, 120°E):
1000 hPa u = 4.18 m/s T = 300.22 K
850 hPa u = 9.59 m/s T = 292.61 K
500 hPa u = 14.08 m/s T = 271.27 K
250 hPa u = 14.56 m/s T = 237.22 K
100 hPa u = -0.38 m/s T = 203.65 K
50 hPa u = -7.90 m/s T = 211.42 K
10 hPa u = -15.30 m/s T = 230.29 K
绘图:原始 vs. 重采样 剖面对比
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
# (a) u 剖面
ax = axes[0]
ax.plot(u_prof.values, src_p_Pa/100, 'o-', color='lightgray', ms=5, lw=1.2, label='ERA5 (37 levels)')
ax.plot(u_new, tgt_p_Pa/100, 's-', color='C0', ms=7, lw=1.5, label='Interpolated (17 levels)')
ax.set_xlabel('U wind (m/s)')
ax.set_ylabel('Pressure (hPa)')
ax.set_yscale('log')
ax.invert_yaxis()
ax.set_yticks([1000, 850, 700, 500, 300, 200, 100, 50, 10])
ax.get_yaxis().set_major_formatter(plt.matplotlib.ticker.ScalarFormatter())
ax.set_title('U wind profile @ (40°N, 120°E)')
ax.axvline(0, color='k', lw=0.5)
ax.legend()
ax.grid(alpha=0.3)
# (b) T 剖面
ax = axes[1]
ax.plot(t_prof.values, src_p_Pa/100, 'o-', color='lightgray', ms=5, lw=1.2, label='ERA5 (37 levels)')
ax.plot(t_new, tgt_p_Pa/100, 's-', color='C3', ms=7, lw=1.5, label='Interpolated (17 levels)')
ax.set_xlabel('Temperature (K)')
ax.set_ylabel('Pressure (hPa)')
ax.set_yscale('log')
ax.invert_yaxis()
ax.set_yticks([1000, 850, 700, 500, 300, 200, 100, 50, 10])
ax.get_yaxis().set_major_formatter(plt.matplotlib.ticker.ScalarFormatter())
ax.set_title('Temperature profile @ (40°N, 120°E)')
ax.legend()
ax.grid(alpha=0.3)
plt.suptitle('Skyborn 0.40 · interp_pressure_1d (log-pressure)', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

output
interp_to_isentropic ⭐场景:把等压面数据映射到 等熵面(potential temperature, θ),用于天气分析中的等熵面诊断(IPV、上滑下降运动、对流层顶折叠等)。
位温公式:
API:
si.interp_to_isentropic(
data, # xarray.DataArray 待插值变量
temperature, # T (K)
pressure, # p (Pa) - 注意单位!
theta_levels, # 目标 θ 数组 (K)
lev_dim='level',
p0=100000.0,
kappa=0.2854,
)
我们将 ERA5 的 u 风插值到 7 个等熵面:280, 300, 320, 340, 360, 380, 400 K。
# 取 t0 时刻整层数据
t = ds_t0['t'] # (level, lat, lon) 单位 K
u = ds_t0['u'] # (level, lat, lon)
v = ds_t0['v'] # (level, lat, lon)
# pressure 必须广播到 (level, lat, lon)
pressure_3d = (ds_t0.level * 100).broadcast_like(t) # hPa -> Pa
print(f"T shape : {t.shape}")
print(f"p shape : {pressure_3d.shape}")
print(f"p range : {float(pressure_3d.min())} - {float(pressure_3d.max())} Pa")
# 目标等熵面
theta_levels = np.array([280, 300, 315, 320, 330, 340, 350, 360, 380, 400], dtype='float64')
# 插值
u_theta = si.interp_to_isentropic(
data=u, temperature=t, pressure=pressure_3d,
theta_levels=theta_levels, lev_dim='level'
)
v_theta = si.interp_to_isentropic(
data=v, temperature=t, pressure=pressure_3d,
theta_levels=theta_levels, lev_dim='level'
)
# 同时把气压本身也插值到等熵面 -> 用于诊断该 θ 面在哪个高度
p_on_theta = si.interp_to_isentropic(
data=pressure_3d, temperature=t, pressure=pressure_3d,
theta_levels=theta_levels, lev_dim='level'
)
print(f"\nu_theta : shape={u_theta.shape}, dims={u_theta.dims}")
print(f"v_theta : shape={v_theta.shape}")
print(f"p_on_theta : shape={p_on_theta.shape} unit: Pa")
T shape : (37, 241, 361)
p shape : (37, 241, 361)
p range : 100.0 - 100000.0 Pa
u_theta : shape=(10, 241, 361), dims=('theta', 'latitude', 'longitude')
v_theta : shape=(10, 241, 361)
p_on_theta : shape=(10, 241, 361) unit: Pa
绘图:320 K 等熵面上的风场 + 等熵面对应的气压(高度)
theta_lvl = 320 # K
u320 = u_theta.sel(theta=theta_lvl)
v320 = v_theta.sel(theta=theta_lvl)
p320_hPa = p_on_theta.sel(theta=theta_lvl) / 100# Pa -> hPa
speed320 = np.sqrt(u320**2 + v320**2)
fig, axes = plt.subplots(1, 2, figsize=(15, 5.5))
# (a) 320 K 风速 + 流线
ax = axes[0]
cf = ax.contourf(ds.longitude, ds.latitude, speed320,
levels=np.arange(0, 51, 5), cmap='viridis', extend='max')
# 抽稀的流线
ax.streamplot(ds.longitude.values, ds.latitude.values,
u320.values, v320.values,
density=2.0, color='white', linewidth=0.6, arrowsize=0.8)
ax.set_title(f'{theta_lvl} K isentropic surface · wind speed & streamlines')
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
plt.colorbar(cf, ax=ax, label='Wind speed (m/s)')
# (b) 320 K 等熵面对应的压力高度
ax = axes[1]
cf2 = ax.contourf(ds.longitude, ds.latitude, p320_hPa,
levels=np.arange(150, 851, 50), cmap='RdYlBu_r', extend='both')
cs = ax.contour(ds.longitude, ds.latitude, p320_hPa,
levels=np.arange(200, 801, 100), colors='k', linewidths=0.6)
ax.clabel(cs, inline=True, fontsize=8, fmt='%d')
ax.set_title(f'{theta_lvl} K isentropic surface · pressure (hPa)')
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
plt.colorbar(cf2, ax=ax, label='Pressure (hPa)')
plt.suptitle('Skyborn 0.40 · interp_to_isentropic', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
print(f'\n320 K 等熵面平均气压: {float(p320_hPa.mean()):.1f} hPa')
print(f'320 K 等熵面气压范围: {float(p320_hPa.min()):.1f} → {float(p320_hPa.max()):.1f} hPa')

output
320 K 等熵面平均气压: 541.4 hPa
320 K 等熵面气压范围: 278.0 → 708.5 hPa
物理解读:
interp_multidim场景:在保留 xarray 元信息的情况下,把数据插值到任意经/纬度网格。
特点:
xarray.interp(双线性 / 最近邻)下例把 0.25° ERA5 → 1.0° 粗网格(典型 GCM 输出网格)。
# 取 500 hPa t0 时刻的温度
t500 = ds_t0['t'].sel(level=500)
print(f"源网格: shape={t500.shape}, dims={t500.dims}")
print(f" lat: {ds.latitude.values.min()} → {ds.latitude.values.max()}, dx={float(ds.latitude.diff('latitude').mean()):.2f}°")
print(f" lon: {ds.longitude.values.min()} → {ds.longitude.values.max()}, dx={float(ds.longitude.diff('longitude').mean()):.2f}°")
# 目标网格: 1.0° 分辨率
target_lat = np.arange(10, 71, 1.0)
target_lon = np.arange(90, 181, 1.0)
t500_1deg = si.interp_multidim(
data_in=t500,
lat_out=target_lat,
lon_out=target_lon,
method='linear',
cyclic=False,
)
print(f"\n目标网格: shape={t500_1deg.shape}, dims={t500_1deg.dims}")
print(f" lat: {target_lat[0]} → {target_lat[-1]} (n={len(target_lat)})")
print(f" lon: {target_lon[0]} → {target_lon[-1]} (n={len(target_lon)})")
print(f"\n源数据平均 T = {float(t500.mean()):.3f} K")
print(f"插值后 T = {float(t500_1deg.mean()):.3f} K (差异 < 0.1 K → 双线性几乎无信息损失)")
源网格: shape=(241, 361), dims=('latitude', 'longitude')
lat: 10.0 → 70.0, dx=0.25°
lon: 90.0 → 180.0, dx=0.25°
目标网格: shape=(61, 91), dims=('latitude', 'longitude')
lat: 10.0 → 70.0 (n=61)
lon: 90.0 → 180.0 (n=91)
源数据平均 T = 265.371 K
插值后 T = 265.336 K (差异 < 0.1 K → 双线性几乎无信息损失)
绘图:原始 vs. 1° 重采样
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
levels = np.arange(255, 285, 2)
ax = axes[0]
cf = ax.contourf(ds.longitude, ds.latitude, t500,
levels=levels, cmap='RdBu_r', extend='both')
ax.set_title(f'ERA5 0.25° · 500 hPa T ({t500.shape[0]}×{t500.shape[1]} = {t500.size} pts)')
ax.set_xlabel('Longitude'); ax.set_ylabel('Latitude')
plt.colorbar(cf, ax=ax, label='T (K)')
ax = axes[1]
cf = ax.contourf(target_lon, target_lat, t500_1deg,
levels=levels, cmap='RdBu_r', extend='both')
ax.set_title(f'Interpolated 1.0° · 500 hPa T ({t500_1deg.shape[0]}×{t500_1deg.shape[1]} = {t500_1deg.size} pts)')
ax.set_xlabel('Longitude'); ax.set_ylabel('Latitude')
plt.colorbar(cf, ax=ax, label='T (K)')
plt.suptitle('Skyborn 0.40 · interp_multidim (linear)', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
print(f'\n数据点压缩比: {t500.size/t500_1deg.size:.2f}×')

output
数据点压缩比: 15.67×
Regridder 类 ⭐Skyborn 0.40 引入了 Grid + Regridder 类 的全新设计:
Regridder(src, tgt) 构造,无限次插值不同变量 — 适合批量处理。BilinearRegridder —— 双线性,速度快、连续场首选(温度、气压、湿度)ConservativeRegridder —— 守恒型,适合通量类变量(降水、辐射、积分量)NearestRegridder —— 最近邻,适合类别型 / 离散数据(地表类型、云类型)regrid_array(field) —— column-major (lon, lat),底层数组接口,最大灵活度regrid_dataset(dataset) —— C-order,xarray 高级接口,自动检测维度下面我们演示:用三种 Regridder 把 0.25° ERA5 重网格化到 1.0°。
Grid 对象# 源网格 & 目标网格
src_grid = si.Grid(lon=ds.longitude.values, lat=ds.latitude.values)
tgt_grid = si.Grid(lon=target_lon, lat=target_lat)
print(f"源网格: {len(src_grid.lon)} × {len(src_grid.lat)} = {len(src_grid.lon)*len(src_grid.lat)} pts")
print(f"目标: {len(tgt_grid.lon)} × {len(tgt_grid.lat)} = {len(tgt_grid.lon)*len(tgt_grid.lat)} pts")
源网格: 361 × 241 = 87001 pts
目标: 91 × 61 = 5551 pts
regrid_array)底层调用要求 **输入形状为 (nlon, nlat)**(column-major / Fortran 风格)。
ERA5 数据原始形状是 (nlat, nlon),所以传入前 需要 .T 转置,输出后再 .T 转回。
# 三种 Regridder 实例化
regr_b = si.BilinearRegridder( source=src_grid, target=tgt_grid)
regr_c = si.ConservativeRegridder(source=src_grid, target=tgt_grid)
regr_n = si.NearestRegridder( source=src_grid, target=tgt_grid)
# t500 形状 (lat=241, lon=361) -> 转置为 (lon, lat) -> 插值 -> 再转置回 (lat, lon)
t500_b = regr_b.regrid_array(t500.values.T).T
t500_c = regr_c.regrid_array(t500.values.T).T
t500_n = regr_n.regrid_array(t500.values.T).T
print(f"源 t500 : shape={t500.shape}, mean={float(t500.mean()):.3f} K")
print(f"Bilinear : shape={t500_b.shape}, mean={t500_b.mean():.3f} K")
print(f"Conservative : shape={t500_c.shape}, mean={t500_c.mean():.3f} K")
print(f"Nearest : shape={t500_n.shape}, mean={t500_n.mean():.3f} K")
源 t500 : shape=(241, 361), mean=265.371 K
Bilinear : shape=(61, 91), mean=265.336 K
Conservative : shape=(61, 91), mean=265.440 K
Nearest : shape=(61, 91), mean=265.336 K
fig = plt.figure(figsize=(15, 9))
gs = fig.add_gridspec(2, 3, height_ratios=[1, 0.75])
levels = np.arange(255, 285, 2)
# (a) Source 0.25°
ax0 = fig.add_subplot(gs[0, 0])
cf = ax0.contourf(ds.longitude, ds.latitude, t500,
levels=levels, cmap='RdBu_r', extend='both')
ax0.set_title(f'Source 0.25° (n={t500.size})')
ax0.set_xlabel('Longitude'); ax0.set_ylabel('Latitude')
plt.colorbar(cf, ax=ax0, label='T (K)')
# (b) Bilinear 1.0°
ax1 = fig.add_subplot(gs[0, 1])
cf = ax1.contourf(target_lon, target_lat, t500_b,
levels=levels, cmap='RdBu_r', extend='both')
ax1.set_title(f'Bilinear 1.0° (n={t500_b.size})')
ax1.set_xlabel('Longitude'); ax1.set_ylabel('Latitude')
plt.colorbar(cf, ax=ax1, label='T (K)')
# (c) Nearest 1.0°
ax2 = fig.add_subplot(gs[0, 2])
cf = ax2.contourf(target_lon, target_lat, t500_n,
levels=levels, cmap='RdBu_r', extend='both')
ax2.set_title(f'Nearest 1.0° (n={t500_n.size})')
ax2.set_xlabel('Longitude'); ax2.set_ylabel('Latitude')
plt.colorbar(cf, ax=ax2, label='T (K)')
# (d) 纬向平均温度对比 — Conservative 的守恒优势在平均场上最明显
ax3 = fig.add_subplot(gs[1, :])
t500_zonal = t500.mean(dim='longitude')
t500_b_zonal = np.nanmean(t500_b, axis=1)
t500_c_zonal = np.nanmean(t500_c, axis=1)
t500_n_zonal = np.nanmean(t500_n, axis=1)
ax3.plot(t500_zonal, ds.latitude.values, 'k-', lw=2.2, label='Source 0.25°')
ax3.plot(t500_b_zonal, target_lat, 'o-', color='C0', ms=5, lw=1.5, label='Bilinear 1.0°')
ax3.plot(t500_c_zonal, target_lat, 's-', color='C1', ms=5, lw=1.5, label='Conservative 1.0°')
ax3.plot(t500_n_zonal, target_lat, '^-', color='C2', ms=5, lw=1.5, label='Nearest 1.0°')
ax3.set_xlabel('Zonal mean temperature (K)')
ax3.set_ylabel('Latitude (°N)')
ax3.set_title('Zonal mean T comparison · Conservative preserves integral best')
ax3.legend(loc='lower right')
ax3.grid(alpha=0.3)
ax3.set_xlim(254, 284)
fig.suptitle('Skyborn 0.40 · Regridder comparison (column-major entry)',
fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()
print('\nConservative mean vs source: ', f'{t500_c.mean():.3f} K', '(closest to source)')

output
Conservative mean vs source: 265.440 K (closest to source)
⚠️ Conservative 空间可视化提示:Skyborn 0.40 的
ConservativeRegridder在处理区域型(非全球)数据时,空间分布可能出现条带/阶梯伪影,但全场积分(均值)仍严格守恒。上图用 纬向平均曲线 替代 Conservative 的空间填色图,以突出其守恒优势。
数据类型 | 推荐方法 | 原因 |
|---|---|---|
温度、气压、位势高度、相对湿度 | Bilinear | 连续场,二次精度光滑 |
降水量、净辐射、能量通量 | Conservative | 保持全场积分总量不变 |
地表类型、植被类型、云类型 | Nearest | 离散标签不能平均 |
高分辨率 → 低分辨率(降采样) | Conservative | 等价于面元平均 |
低分辨率 → 高分辨率(升采样) | Bilinear | 平滑插值 |
interp_hybrid_to_pressure场景:CESM/CAM、WRF、ECMWF IFS 等模式输出常用 hybrid-sigma 垂直坐标,下游分析往往需要插值到 标准等压面。
hybrid-sigma 公式(formulation 1, CESM 风格):
hyam, hybm),1-D因为我们的 ERA5 已经是等压面数据,这里 用合成 CAM 风格数据 演示完整流程。
# === 合成 CAM 风格 hybrid-sigma 数据 ===
nlev = 30
nlat, nlon = 20, 30
p0 = 100000.0# Pa
# 1) hybrid 系数:高层 hybm→0 (纯等压面), 低层 hybm→1 (纯 sigma 跟随地形)
k_norm = np.arange(nlev) / (nlev - 1)
hyam = np.linspace(0.001, 0.5, nlev) + 0.05 * np.sin(np.pi * k_norm)
hyam = np.clip(hyam, 0.001, 0.5)
hybm = k_norm ** 2
# 2) 经纬度网格
lon1d = np.linspace(0, 360, nlon)
lat1d = np.linspace(-30, 30, nlat)
LON, LAT = np.meshgrid(lon1d, lat1d)
# 3) 地面气压:纬向变化 + 一个'山区'低压中心
ps = 101325 + 1500 * np.cos(np.deg2rad(LAT)) \
- 8000 * np.exp(-((LAT - 10)**2 / 100 + (LON - 180)**2 / 2000))
print(f"hyam: {hyam[0]:.3f} → {hyam[-1]:.3f} (n={nlev})")
print(f"hybm: {hybm[0]:.3f} → {hybm[-1]:.3f}")
print(f"ps : {ps.min():.0f} → {ps.max():.0f} Pa (地形最低气压点 ~ {ps.min()/100:.0f} hPa)")
hyam: 0.001 → 0.500 (n=30)
hybm: 0.000 → 1.000
ps : 95036 → 102824 Pa (地形最低气压点 ~ 950 hPa)
# === 合成 T(lev, lat, lon) ===
# 标准大气:T(p) = 288 - 6.5 K/km × z, z 从测高公式反算
levels_p_3d = hyam[:, None, None] * p0 + hybm[:, None, None] * ps[None, :, :]
# 简单的 T 模型: 高空冷, 低空暖, 加经向梯度
T_synth = 273 - 70 * (1 - levels_p_3d/101325) - 0.3 * (np.abs(LAT)[None, :, :])
print(f"T_synth range: {T_synth.min():.1f} → {T_synth.max():.1f} K")
print(f" 最低温在 nlev顶部 (~{(hyam[0]*p0 + hybm[0]*ps.mean())/100:.0f} hPa)")
print(f" 最高温在 nlev底部 (~{(hyam[-1]*p0 + hybm[-1]*ps.mean())/100:.0f} hPa)")
# 包装为 xarray
hyam_da = xr.DataArray(hyam, dims=['lev'], coords={'lev': np.arange(nlev)})
hybm_da = xr.DataArray(hybm, dims=['lev'], coords={'lev': np.arange(nlev)})
ps_da = xr.DataArray(ps, dims=['lat', 'lon'],
coords={'lat': lat1d, 'lon': lon1d})
T_da = xr.DataArray(T_synth, dims=['lev', 'lat', 'lon'],
coords={'lev': np.arange(nlev), 'lat': lat1d, 'lon': lon1d})
print("\nT_da:")
print(T_da)
T_synth range: 194.1 → 308.1 K
最低温在 nlev顶部 (~1 hPa)
最高温在 nlev底部 (~1523 hPa)
T_da:
<xarray.DataArray (lev: 30, lat: 20, lon: 30)> Size: 144kB
array([[[194.06908463, 194.06908463, 194.06908463, ..., 194.06908463,
194.06908463, 194.06908463],
[195.01645305, 195.01645305, 195.01645305, ..., 195.01645305,
195.01645305, 195.01645305],
[195.96382147, 195.96382147, 195.96382147, ..., 195.96382147,
195.96382147, 195.96382147],
...,
[195.96382147, 195.96382147, 195.96382147, ..., 195.96382147,
195.96382147, 195.96382147],
[195.01645305, 195.01645305, 195.01645305, ..., 195.01645305,
195.01645305, 195.01645305],
[194.06908463, 194.06908463, 194.06908463, ..., 194.06908463,
194.06908463, 194.06908463]],
[[195.71558615, 195.71558615, 195.71558615, ..., 195.71558615,
195.71558615, 195.71558615],
[196.66298689, 196.66298689, 196.66298689, ..., 196.66298689,
196.66298689, 196.66298689],
[197.61038429, 197.61038429, 197.61038429, ..., 197.61038429,
197.61038429, 197.61038429],
...
[295.76210406, 295.7621035 , 295.76209946, ..., 295.76209946,
295.7621035 , 295.76210406],
[294.79201543, 294.79201522, 294.79201367, ..., 294.79201367,
294.79201522, 294.79201543],
[293.81930906, 293.81930899, 293.81930851, ..., 293.81930851,
293.81930899, 293.81930906]],
[[299.43974999, 299.43974999, 299.43974999, ..., 299.43974999,
299.43974999, 299.43974999],
[300.41429855, 300.41429855, 300.41429855, ..., 300.41429855,
300.41429855, 300.41429855],
[301.38603908, 301.38603908, 301.38603908, ..., 301.38603908,
301.38603908, 301.38603908],
...,
[301.386039 , 301.3860384 , 301.38603406, ..., 301.38603406,
301.3860384 , 301.386039 ],
[300.41429852, 300.41429829, 300.41429663, ..., 300.41429663,
300.41429829, 300.41429852],
[299.43974998, 299.43974991, 299.43974939, ..., 299.43974939,
299.43974991, 299.43974998]]])
Coordinates:
* lev (lev) int64 240B 0 1 2 3 4 5 6 7 8 9 ... 21 22 23 24 25 26 27 28 29
* lat (lat) float64 160B -30.0 -26.84 -23.68 -20.53 ... 23.68 26.84 30.0
* lon (lon) float64 240B 0.0 12.41 24.83 37.24 ... 335.2 347.6 360.0
# === 调用 interp_hybrid_to_pressure ===
new_p_levels = np.array([100000, 92500, 85000, 70000, 50000, 30000,
20000, 10000, 5000, 1000], dtype='float64')
T_p = si.interp_hybrid_to_pressure(
data=T_da,
ps=ps_da,
hyam=hyam_da,
hybm=hybm_da,
p0=p0,
new_levels=new_p_levels,
lev_dim='lev',
method='linear',
extrapolate=False, # 设 True 可启用 ECMWF 地下外推
)
print(f"T_p shape: {T_p.shape}, dims: {T_p.dims}")
print(f"T_p coords: {list(T_p.coords)}")
print()
for p in [100000, 50000, 10000, 1000]:
print(f" {p/100:5.0f} hPa : mean T = {float(T_p.sel(plev=p).mean()):6.2f} K")
T_p shape: (10, 20, 30), dims: ('plev', 'lat', 'lon')
T_p coords: ['lat', 'lon', 'plev']
1000 hPa : mean T = 267.35 K
500 hPa : mean T = 232.81 K
100 hPa : mean T = 205.17 K
10 hPa : mean T = 198.95 K
# === 绘图:500 hPa T 在 hybrid 与 pressure 坐标上 ===
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# (a) hybrid 第 15 层 T (该层 sigma 跟随地形, T 反映地形)
k_mid = 15
ax = axes[0]
cf = ax.contourf(lon1d, lat1d, T_da.isel(lev=k_mid), cmap='RdBu_r', levels=20)
ax.set_title(f'Hybrid layer k={k_mid} · T (受地形影响)')
ax.set_xlabel('Longitude'); ax.set_ylabel('Latitude')
plt.colorbar(cf, ax=ax, label='T (K)')
# (b) 50000 Pa T (插值后, 标准等压面)
ax = axes[1]
cf = ax.contourf(lon1d, lat1d, T_p.sel(plev=50000), cmap='RdBu_r', levels=20)
ax.set_title('After interp_hybrid_to_pressure · 500 hPa T (无地形)')
ax.set_xlabel('Longitude'); ax.set_ylabel('Latitude')
plt.colorbar(cf, ax=ax, label='T (K)')
plt.suptitle('Skyborn 0.40 · interp_hybrid_to_pressure',
fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
/tmp/ipykernel_79/1734801367.py:21: UserWarning: Glyph 21463 (\N{CJK UNIFIED IDEOGRAPH-53D7}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/tmp/ipykernel_79/1734801367.py:21: UserWarning: Glyph 22320 (\N{CJK UNIFIED IDEOGRAPH-5730}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/tmp/ipykernel_79/1734801367.py:21: UserWarning: Glyph 24418 (\N{CJK UNIFIED IDEOGRAPH-5F62}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/tmp/ipykernel_79/1734801367.py:21: UserWarning: Glyph 24433 (\N{CJK UNIFIED IDEOGRAPH-5F71}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/tmp/ipykernel_79/1734801367.py:21: UserWarning: Glyph 21709 (\N{CJK UNIFIED IDEOGRAPH-54CD}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/tmp/ipykernel_79/1734801367.py:21: UserWarning: Glyph 26080 (\N{CJK UNIFIED IDEOGRAPH-65E0}) missing from font(s) DejaVu Sans.
plt.tight_layout()
/opt/conda/lib/python3.11/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 21463 (\N{CJK UNIFIED IDEOGRAPH-53D7}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
/opt/conda/lib/python3.11/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 22320 (\N{CJK UNIFIED IDEOGRAPH-5730}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
/opt/conda/lib/python3.11/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 24418 (\N{CJK UNIFIED IDEOGRAPH-5F62}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
/opt/conda/lib/python3.11/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 24433 (\N{CJK UNIFIED IDEOGRAPH-5F71}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
/opt/conda/lib/python3.11/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 21709 (\N{CJK UNIFIED IDEOGRAPH-54CD}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
/opt/conda/lib/python3.11/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 26080 (\N{CJK UNIFIED IDEOGRAPH-65E0}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)

output
实战提示:
p0=1, hyam=ap 即可让本函数兼容。extrapolate=True 后,函数会用 ECMWF 地下外推方案(需要传 t_bot, phi_sfc),适合处理高原下边界。delta_pressure_hybrid / pressure_at_hybrid_levels 是配套辅助函数,可计算混合层层厚和层压(用于积分诊断)。# 一维气压剖面重采样
si.interp_pressure_1d(
data, source_pressure, target_pressure,
method='log', # 'log' (默认, 推荐) 或 'linear'
extrapolate=False,
missing_value=np.nan,
)
# 等熵面 (potential temperature) 插值
si.interp_to_isentropic(
data, temperature, pressure, # T (K), p (Pa)
theta_levels, # K
lev_dim='level',
p0=100000.0,
kappa=0.2854, # R_d/c_p
extrapolate=False,
output_dim='theta',
)
# Hybrid-sigma → pressure
si.interp_hybrid_to_pressure(
data, ps, hyam, hybm, # 见上方公式
p0=100000.0,
new_levels=..., # Pa, 1D array
lev_dim=None, # 自动检测
method='linear',
extrapolate=False, # True 启用 ECMWF 地下外推
t_bot=None, phi_sfc=None, # 配合 extrapolate=True 使用
)
### 9.2 水平插值
si.interp_multidim(
data_in, lat_out, lon_out,
lat_in=None, lon_in=None, # xarray 输入时自动
cyclic=False, # 经度循环边界
missing_val=None,
method='linear', # 'linear' 或 'nearest'
fill_value=np.nan,
)
# 现代 Regridder 类
src = si.Grid(lon=..., lat=...) # lat 必须递增
tgt = si.Grid(lon=..., lat=...)
regridder = si.BilinearRegridder(source=src, target=tgt)
# 或 ConservativeRegridder / NearestRegridder
# Column-major 入口 (lon, lat)
out_arr = regridder.regrid_array(field) # field shape: (nlon, nlat) 或 (..., nlon, nlat)
# xarray 入口 (Dataset)
out_ds = regridder.regrid_dataset(ds, lon_dim='longitude', lat_dim='latitude')
问题 | 检查 |
|---|---|
ConservativeRegridder 报 Array is not increasing | latitude 必须严格递增,需 ds.isel(latitude=slice(None,None,-1)) 翻转 |
插值结果全 NaN | 检查 pressure 单位(Pa vs hPa)、extrapolate 是否打开 |
等熵面输出错乱 | 确认 temperature 是 K、pressure 是 Pa、二者维度一致 |
regrid_array shape mismatch | 输入必须是 (nlon, nlat),xarray 数据要 .T |
Skyborn 0.40 的 interp 模块为气象数据后处理提供了 一站式现代化工具链:
垂直插值
interp_pressure_1d — 单点剖面快速重采样interp_to_isentropic — 经典等熵面分析(涡度、PV、对流层顶折叠)interp_hybrid_to_pressure — 模式输出 → 等压面(CAM/IFS/WRF 通吃)水平插值
interp_multidim — 简单 xarray 双线性 / 最近邻BilinearRegridder / ConservativeRegridder / NearestRegridder — 预计算权重的工业级方案regrid_array) + C-order (regrid_dataset),兼顾性能与易用最佳实践
Regridder 类预计算权重interp_multidim 或 regrid_dataset如果觉得对你有用就点个赞吧