首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >代码实战 | skyborn插值函数快速入门

代码实战 | skyborn插值函数快速入门

作者头像
用户11172986
发布2026-05-29 12:17:12
发布2026-05-29 12:17:12
150
举报
文章被收录于专栏:气python风雨气python风雨

代码实战 | skyborn插值函数快速入门

近期文章反响不错,阅读量仿佛这夏日的温度节节攀升

言归正传,Skyborn 0.4 大幅强化了 垂直插值水平网格化 工具链,新增 / 重写的函数覆盖了气象数据后处理的常见场景,该库作者在用了geocat的插值函数后感慨实在难用,于是重写了:

  • 现代化等熵面 / 混合坐标 / Sigma 重映射:扩展 vinth2p 家族
  • C-order 与 column-major 双入口:兼顾 xarray 高级接口与底层数组性能
  • 三种重网格化器BilinearRegridder / ConservativeRegridder / NearestRegridder
  • 优化 ECMWF 处理逻辑:更新混合 / Sigma 辅助函数

本教程使用 ERA5 (2023-08-02 ~ 2023-08-05) 再分析数据,逐个演示 8 个核心函数 + 3 个 Regridder 类的实战用法

一、安装与导入

确保 Skyborn 升级到 0.4以上:

代码语言:javascript
复制
%pip install skyborn -U --quiet
代码语言:javascript
复制
Note: you may need to restart the kernel to use updated packages.
代码语言:javascript
复制
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
代码语言:javascript
复制
Skyborn version: 0.4.1
xarray version : 2024.3.0

二、skyborn.interp 模块全景

Skyborn 0.40 把所有插值/网格化函数集中到 skyborn.interp 子模块下,按功能可分为三大类:

2.1 垂直插值(vertical interpolation)

函数

用途

输入坐标 → 输出坐标

interp_pressure_1d

一维气压剖面重采样

pressure → pressure

interp_to_isentropic

等熵面分析

pressure → potential temperature (θ)

interp_hybrid_to_pressure

模式数据 → 等压面

hybrid σ → pressure

interp_sigma_to_hybrid

sigma → hybrid

σ → hybrid σ

2.2 水平插值(horizontal interpolation)

函数 / 类

用途

入口风格

interp_multidim

经/纬度网格双线性插值

xarray 高级接口(C-order)

Grid + BilinearRegridder

双线性重网格

底层数组(column-major)

Grid + ConservativeRegridder

守恒型重网格

适合通量类变量

Grid + NearestRegridder

最近邻

离散 / 类别型数据

regrid_dataset

一次性 Dataset 重网格

自动检测维度

rgrid2rcm / rcm2rgrid

规则网格 ↔ 曲线网格 (WRF/NARR)

NCL 风格

2.3 辅助函数

  • delta_pressure_hybrid / pressure_at_hybrid_levels —— 混合坐标层厚 / 层压计算
  • grid_to_triple / triple_to_grid —— 网格 ↔ 散点互转
  • nearest_neighbor_indices —— 球面最近邻索引(Haversine)

三、数据准备:ERA5 再分析

本教程使用 ERA5 2023-08 月初 4 天 / 6 小时间隔 / 37 个等压层 / 0.25° 数据,覆盖范围 10°N–70°N, 90°E–180°E

关键变量u, v, w, z, t, r, q

代码语言:javascript
复制
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']})")
代码语言:javascript
复制
坐标: ['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)要求纬度 严格递增,因此我们先翻转:

代码语言:javascript
复制
# 反转 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}")
代码语言:javascript
复制
反转后 latitude: 10.0 → 70.0

分析时刻: 2023-08-02T00:00:00.000000000

四、垂直插值(一):interp_pressure_1d

场景:把高分辨率的 37 层 ERA5 重采样到自定义气压层(例如 GCM 标准 17 层)。

特点

  • 单点剖面(1D)
  • 支持 method='linear'(线性)和 method='log'(对数压力,默认且推荐
  • 输入/输出气压单位必须一致(这里统一为 Pa)
代码语言:javascript
复制
# 取出 (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')
代码语言:javascript
复制
源压力层数: 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. 重采样 剖面对比

代码语言:javascript
复制
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
output

output

五、垂直插值(二):interp_to_isentropic

场景:把等压面数据映射到 等熵面(potential temperature, θ),用于天气分析中的等熵面诊断(IPV、上滑下降运动、对流层顶折叠等)。

位温公式

API

代码语言:javascript
复制
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。

代码语言:javascript
复制
# 取 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")
代码语言:javascript
复制
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 等熵面上的风场 + 等熵面对应的气压(高度)

代码语言:javascript
复制
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
output

output

代码语言:javascript
复制
320 K 等熵面平均气压: 541.4 hPa
320 K 等熵面气压范围: 278.0 → 708.5 hPa

物理解读

  • 左图 风速:320 K 在中纬度位于对流层中-上部,可以看到副热带急流的明显结构(>40 m/s 强风区)。
  • 右图 等熵面气压:在副热带气压高(~300 hPa),向北气压降低(~400-500 hPa)—— 这正是等熵面 向极向下倾斜 的天气学事实。

六、水平插值(一):interp_multidim

场景:在保留 xarray 元信息的情况下,把数据插值到任意经/纬度网格。

特点

  • 高级 xarray 接口(C-order)
  • 自动处理任意维度顺序
  • 底层基于 xarray.interp(双线性 / 最近邻)
  • 默认不外推,越界返回 NaN

下例把 0.25° ERA5 → 1.0° 粗网格(典型 GCM 输出网格)。

代码语言:javascript
复制
# 取 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 → 双线性几乎无信息损失)")
代码语言:javascript
复制
源网格: 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° 重采样

代码语言:javascript
复制
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
output

output

代码语言:javascript
复制
数据点压缩比: 15.67×

七、水平插值(二):现代化 Regridder 类 ⭐

Skyborn 0.40 引入了 Grid + Regridder 的全新设计:

  1. 预计算权重,重复使用:一次 Regridder(src, tgt) 构造,无限次插值不同变量 — 适合批量处理。
  2. 三种方法
    • BilinearRegridder —— 双线性,速度快、连续场首选(温度、气压、湿度)
    • ConservativeRegridder —— 守恒型,适合通量类变量(降水、辐射、积分量)
    • NearestRegridder —— 最近邻,适合类别型 / 离散数据(地表类型、云类型)
  3. 双入口设计 ⭐ 0.40 重点:
    • regrid_array(field) —— column-major (lon, lat),底层数组接口,最大灵活度
    • regrid_dataset(dataset) —— C-order,xarray 高级接口,自动检测维度

下面我们演示:用三种 Regridder 把 0.25° ERA5 重网格化到 1.0°。

7.1 构造 Grid 对象

代码语言:javascript
复制
# 源网格 & 目标网格
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")
代码语言:javascript
复制
源网格: 361 × 241 = 87001 pts
目标:   91 × 61 = 5551 pts

7.2 三种 Regridder:column-major 入口(regrid_array

底层调用要求 **输入形状为 (nlon, nlat)**(column-major / Fortran 风格)。 ERA5 数据原始形状是 (nlat, nlon),所以传入前 需要 .T 转置,输出后再 .T 转回。

代码语言:javascript
复制
# 三种 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")
代码语言:javascript
复制
源 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

7.3 三种方法对比绘图

代码语言:javascript
复制
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
output

output

代码语言:javascript
复制
Conservative mean vs source:  265.440 K (closest to source)

⚠️ Conservative 空间可视化提示:Skyborn 0.40 的 ConservativeRegridder 在处理区域型(非全球)数据时,空间分布可能出现条带/阶梯伪影,但全场积分(均值)仍严格守恒。上图用 纬向平均曲线 替代 Conservative 的空间填色图,以突出其守恒优势。

7.4 方法选择参考

数据类型

推荐方法

原因

温度、气压、位势高度、相对湿度

Bilinear

连续场,二次精度光滑

降水量、净辐射、能量通量

Conservative

保持全场积分总量不变

地表类型、植被类型、云类型

Nearest

离散标签不能平均

高分辨率 → 低分辨率(降采样)

Conservative

等价于面元平均

低分辨率 → 高分辨率(升采样)

Bilinear

平滑插值

八、模式数据后处理:interp_hybrid_to_pressure

场景:CESM/CAM、WRF、ECMWF IFS 等模式输出常用 hybrid-sigma 垂直坐标,下游分析往往需要插值到 标准等压面

hybrid-sigma 公式(formulation 1, CESM 风格):

  • , :hybrid 系数(hyam, hybm),1-D
  • :地面气压 (Pa),2-D
  • Pa(参考气压)

因为我们的 ERA5 已经是等压面数据,这里 用合成 CAM 风格数据 演示完整流程。

代码语言:javascript
复制
# === 合成 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)")
代码语言:javascript
复制
hyam: 0.001 → 0.500  (n=30)
hybm: 0.000 → 1.000
ps  : 95036 → 102824 Pa  (地形最低气压点 ~ 950 hPa)
代码语言:javascript
复制
# === 合成 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)
代码语言:javascript
复制
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
代码语言:javascript
复制
# === 调用 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")
代码语言:javascript
复制
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
代码语言:javascript
复制
# === 绘图: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()
代码语言:javascript
复制
/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
output

output

实战提示

  1. CESM/CAM 用 formulation 1 — 上面示例可直接套用。
  2. ECMWF IFS 用 formulation 2: 只需把 p0=1, hyam=ap 即可让本函数兼容。
  3. 启用 extrapolate=True 后,函数会用 ECMWF 地下外推方案(需要传 t_bot, phi_sfc),适合处理高原下边界。
  4. delta_pressure_hybrid / pressure_at_hybrid_levels 是配套辅助函数,可计算混合层层厚和层压(用于积分诊断)。

九、参数速查表

9.1 垂直插值

代码语言:javascript
复制
# 一维气压剖面重采样


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 水平插值

xarray 高级接口 - 自动维度

代码语言:javascript
复制

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')  

9.3 数据预处理 Checklist

问题

检查

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 — 预计算权重的工业级方案
  • 双入口设计:column-major (regrid_array) + C-order (regrid_dataset),兼顾性能与易用

最佳实践

  1. 大数据集 / 反复重网格化 → 使用 Regridder 类预计算权重
  2. 一次性 xarray Dataset 处理 → 用 interp_multidimregrid_dataset
  3. 通量类变量(降水、辐射)→ 必须用 Conservative
  4. 等熵面诊断时 务必检查单位:T (K), p (Pa), θ (K)

如果觉得对你有用就点个赞吧

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 气python风雨 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 代码实战 | skyborn插值函数快速入门
    • 一、安装与导入
    • 二、skyborn.interp 模块全景
      • 2.1 垂直插值(vertical interpolation)
      • 2.2 水平插值(horizontal interpolation)
      • 2.3 辅助函数
    • 三、数据准备:ERA5 再分析
    • 四、垂直插值(一):interp_pressure_1d
    • 五、垂直插值(二):interp_to_isentropic ⭐
    • 六、水平插值(一):interp_multidim
    • 七、水平插值(二):现代化 Regridder 类 ⭐
      • 7.1 构造 Grid 对象
      • 7.2 三种 Regridder:column-major 入口(regrid_array)
      • 7.3 三种方法对比绘图
      • 7.4 方法选择参考
    • 八、模式数据后处理:interp_hybrid_to_pressure
    • 九、参数速查表
      • 9.1 垂直插值
  • xarray 高级接口 - 自动维度
    • 9.3 数据预处理 Checklist
    • 十、小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档