人为排放数据文件这件事,很多做 WRF-Chem 的人第一次上手都会卡住:模式跑起来了,气象场也对得上,但一开化学就发现地面 NOx 浓度低得离谱,或者 PM2.5 的空间分布明显往海上飘。折腾半天回头查,问题往往不在 namelist,也不在化学机制,而是出在最上游那份wrfchemi文件上——单位错了、时间戳差了几个小时、物种映射漏了几项,都足够让后面的结果失真。上一期把清单选型和机制对齐的框架铺开了,这一篇专门解决一个动作:怎么把一份原始人为排放清单,变成模式能直接读的wrfchemi_d01_*文件。内容偏工程实操,从字段盘点、单位换算、重网格化到输出校验都会走一遍,中间会附上我自己踩过的坑和一套上线前的自检清单。适合已经能跑通 WRF-Chem 基础算例、正准备换自己研究区域清单的朋友,也适合被别人的wrfchemi文件坑过、想搞清楚它内部长什么样的同学。
1. 先把思路捋顺:人为排放文件到底在解决什么问题
1.1 从清单到模式:一份排放数据要跨过哪几道坎
原始清单和模式输入,中间隔着的不只是格式差异,而是两套完全不同的世界观。清单是“统计口径”的产物:某个行政区、某个行业部门在一年里烧了多少煤、跑了多少车,最后折算成一个面源通量。模式要的是“网格口径”的产物:在某个投影下的某个格点上,某个化学物种在某个小时内的摩尔数或质量。这两者之间至少有四道坎要跨。
第一道是空间基准的坎。清单常见的是规则经纬网格或者行政单元,模式用的是投影后的兰勃特、墨卡托网格,两者格点不重合、面积不相等。直接最近邻取样,山区和海岸线附近会错得很难看,因为一个源清单格点被硬塞进一个模式格点,通量总量就变了。正确做法是面积加权重网格化(conservative regridding),保证守恒。
第二道是时间基准的坎。年排放量要拆到小时,靠的是时间分配曲线(月变化、周变化、日变化)。这套曲线不同清单给的形式不一样,有的是月度因子表,有的是逐时权重文件。拆完之后还要注意时区——清单给的当地时间得转成 UTC,因为 WRF 内部一律用 UTC。
第三道是物种基准的坎。清单报的是 NOx、NMVOC、PM2.5 这种“总量物种”,模式要的是 NO、NO2、ALD、ETH、OL2、TOL 这种“机制物种”。NOx 拆成 NO 和 NO2 有固定比例(常见 9:1 上下,具体看清单文档),NMVOC 拆成十几个机制物种就复杂得多,要靠物种谱(speciation profile)。
第四道是单位基准的坎。清单可能是 ton/year/grid、kg/m²/s、mol/km²/hr 中的任何一种,模式要的单位取决于化学机制和emiss_opt的设定。这一步换算错了,结果可能差六个数量级,而且不会有任何报错提示你。
这四道坎里,前三道错了还能从空间分布上看出端倪,第四道错了往往只能靠和观测对比才能发现。所以我习惯把单位换算放到流程最前面先算清楚,而不是留到最后。
1.2 三条技术路线的取舍:官方工具、第三方工具还是自己写脚本
到具体实现层面,常见有三条路。
第一条是官方预处理工具链。这类工具用 Fortran 写成,读清单原始格式或中间 NetCDF,按 namelist 配置做投影变换、物种映射、时间插值,直接吐出wrfchemi。优点是省事、边界条件处理成熟、化学机制映射表是现成的;缺点是编译依赖多(NetCDF、MPI、有时还要额外的插值库),namelist 字段含义靠读源码猜,报错信息极其吝啬——经常只给你一句“读取失败”,剩下的全靠自己查。
第二条是专门的排放映射工具。这类工具抽象层次更高一些,核心思路是“输入文件 + 映射表 → 输出文件”,映射表用文本描述哪个变量映射到哪个物种、用什么单位、乘什么系数。配置比第一条路清爽,适合批量处理多个物种。缺点是遇到清单格式特别怪的时候,还是得自己先转一道中间格式。
第三条是自己写脚本。用 Python 的 xarray、netCDF4、ESMF 的 Python 接口或 xESMF 做重网格化,逻辑完全透明,每一步都能打印出来核对。缺点是什么都得自己搭,包括守恒重网格、时间维度拼接、变量属性写入。
我的实际选择是混合路线:空间重网格和物种映射用脚本做,因为这两步最需要中间结果可视化核对;时间维度拼接和最终文件组装也用脚本,因为要频繁调试时间戳。官方工具链我只用来做交叉验证——同一份清单跑两遍,比对输出的量级是否一致,一致了才敢往下用。这不是说官方工具不好,而是它太“黑箱”,出问题时排查成本高,不适合第一次接手新清单的场景。
1.3 时间戳与化学机制:两个最容易在上游就埋雷的地方
先说时间戳。wrfchemi_d01_2020-01-01_00:00:00这种命名的文件,里面装的是该时刻起算的一个时间步的排放,还是该时刻对应的小时平均排放?这个语义在模式源码里是有明确定义的,但文档里往往一笔带过。我见过不止一个算例,作者把“小时平均”当成“瞬时值”去填,结果所有排放整体平移了一个小时,日变化峰值位置全错。验证方法很简单:故意造一份只在 12 时非零的测试文件,跑一天,看模式输出的排放诊断量峰值出现在哪一小时,对上了就说明语义理解正确。
再说化学机制。emiss_opt的取值和化学机制是绑定的,选错了不但物种对不上,程序还可能读完文件后默默把不认识的变量忽略掉,一句警告都不给。不同版本的模式里同一个编号对应的机制可能还不一样,所以我强烈建议:动手做文件之前,先翻一下自己手上这版源码里的排放驱动模块,把编号和机制、以及它期望的变量名清单抄下来,做成一张表贴在屏幕上。这一步花十分钟,能省掉后面两天的排查。
常见的机制与物种命名大致是这个路子(具体以你本地版本为准):
| 机制类型 | 典型无机物种 | 典型 VOC 物种 | 气溶胶物种 |
|---|---|---|---|
| RADM2 系 | E_SO2、E_NO、E_NO2、E_CO、E_NH3 | E_ETH、E_HC3、E_HC5、E_HC8、E_OL2、E_OLT、E_XYL、E_ALD、E_ORA2 | E_PM_10、E_PM25I、E_PM25J、E_BC、E_OC |
| MOZART 系 | E_SO2、E_NO、E_CO、E_NH3 | 按碳键数细分的烷烃、烯烃、芳香烃变量 | 分档的细颗粒与粗颗粒 |
| CBMZ 系 | 与 RADM2 接近 | 按 CBMZ 的物种缩写命名 | 分档气溶胶 |
一个血泪教训:不同机制对同一物种的摩尔质量假设可能不同,尤其是把“以 NO 计的 NOx”映射成 E_NO 和 E_NO2 两个变量时,两份文件加起来的氮原子总量是否等于原始 NOx,一定要自己算一遍验证。
2. 原料盘点:人为排放清单的字段与规格
2.1 清单里到底有什么:部门分类与物种映射
拿到一份清单,第一件事不是急着读进程序,而是把它的元数据翻个底朝天。至少要确认四件事:覆盖的时间范围、空间分辨率、包含的物种清单、以及部门分类体系(常见的是按行业大类分,比如能源、工业、交通、居民、农业、溶剂使用等)。
部门分类为什么重要?因为时间分配曲线和垂直分配曲线都是按部门给的。交通源的日变化是双峰(早晚高峰),居民源是早晚做饭取暖的双峰但时间点不同,工业源基本平稳,农业源有强季节性。如果你把全部部门合并成一个总量再套统一的日变化曲线,日变化的相位就废了。所以正确的中间数据结构应该是“物种 × 部门 × 网格 × 月份(或小时)”的多维数组,而不是一上来就求和。
物种映射这块,最容易出问题的是 NMVOC。清单给的通常是一个总量,要靠物种谱拆。物种谱的来源一般是清单自带的、或者是文献里的区域化谱。这里有个实操细节:不同物种谱的基准不一样,有的按质量分数给,有的按摩尔分数给,混用会直接错。另外物种谱里常常有一项“其他”,占比还不小(10%~30% 很常见),这部分要么映射到某个代表物种,要么按比例分摊到其余物种上,两种处理对臭氧生成的影响不一样,做敏感性分析时值得留意。
气溶胶这边,清单一般给 PM10 和 PM2.5 的总量,有时给 BC、OC 的份额。模式需要的是分档的 E_PM_10、E_PM25I(细颗粒的不可溶部分)、E_PM25J(可溶部分)、E_BC、E_OC。PM2.5 到 I/J 的拆分没有物理上的唯一答案,常见做法是按固定比例分,或者根据排放源类型分(燃烧源偏 I,二次生成前体偏 J)。这个选择要在文档里写清楚,不然复现性没法保证。
2.2 网格、单位与时间分辨率的三重对齐
网格对齐这部分,重点不是“能不能插值”,而是“插完总量对不对”。举个具体场景:清单是 0.1° 规则经纬网格,目标区域是 3 km 的兰勃特投影网格。高纬地区经向格距随纬度收缩,如果直接用双线性插值,总量会系统性偏大或偏小,偏差大小取决于区域纬度范围。用面积加权守恒插值,总量可以做到机器精度级别的守恒。
单位对齐要做一张完整的换算表,把清单里出现的每一种单位都过一遍。最容易漏的是“每网格”和“每平方米”的差别——有些清单的 ton/year 是每个格点的总量,有些是 ton/year/km²,两者的含义完全不同。分辨方法很简单:拿人口密集区和沙漠区对比一下数值,如果沙漠区也有可观的数值且不随网格面积变化,那多半是“每网格”;如果数值大致和面积成反比,那就是密度单位。这个检查我每次都做,因为清单文档经常写得含糊。
时间分辨率对齐要注意闰年和月份天数。年排放量除以 8760 小时是常规做法,但如果你的模拟跨了 2 月,用“年总量 / 12 / 当月天数 / 24”和“年总量 / 8760”得到的 2 月小时值会差 2% 左右。这个差异对短期个例影响不大,但对年均浓度评估是有意义的,建议按月份天数做归一化,一年内总量仍然守恒。
2.3 单位换算的完整算式:一步都不能省
这一步我把公式全部写出来,因为现场调试时经常需要手算核对。
场景一:清单单位是 ton/year/grid,目标单位是 mol/km²/hr。
设某格点年排放量为 $E$(ton/year),物种摩尔质量为 $M$(g/mol),格点面积为 $A$(km²)。
- 换算成克:$E \times 10^6$ g/year
- 换算成摩尔:$E \times 10^6 / M$ mol/year
- 换算成每小时:$E \times 10^6 / (M \times 8760)$ mol/hr
- 除以面积:$E \times 10^6 / (M \times 8760 \times A)$ mol/km²/hr
代入一组数字感受一下:$E = 100$ ton/year 的 SO2,$M = 64$ g/mol,$A = 9$ km²(3 km × 3 km)。
$100 \times 10^6 / (64 \times 8760 \times 9) \approx 19.8$ mol/km²/hr。这个量级是合理的,一座中型城市的 SO2 排放折算下来大致就在这个范围。
场景二:清单单位是 kg/m²/s,目标单位是 μg/m²/s。
这个简单,乘 $10^9$。但要小心有些清单给的是 kg/m²/year,那就还要除以 $3.15 \times 10^7$ 秒。
场景三:目标单位是 mol/km²/hr,从 μg/m²/s 反推。
$1\ \mu g/m^2/s = 10^{-6}\ g/m^2/s = 10^{-6}/M\ mol/m^2/s = 10^{-6} \times 10^6 / M\ mol/km^2/s = 1/M\ mol/km^2/s = 3600/M\ mol/km^2/hr$。
也就是说,从 μg/m²/s 转 mol/km²/hr,系数是 $3600/M$。对 SO2(M=64)就是 56.25;对 NO(M=30)就是 120。这两个系数我在脚本里做成了常量表,省得每次重推。
强调一点:摩尔质量要用模式内部实际采用的原子量,而不是你自己查的精确值。有些机制用 N=14、S=32 这种整数近似,你按 14.0067 算出来的结果会有千分之几的偏差,虽然小,但在做通量对比验证时会造成对不上的困扰。
2.4 缺测值、负值与海洋格点的处理
原始清单里出现负值是常事,尤其是经过二次处理的清单,差值运算会留下负数。这些值必须清零,不能保留——模式读到负排放会直接产生负浓度,或者在取对数、开方的地方崩掉。
缺测值要分情况。陆地格点的缺测一般意味着“无数据”,处理上有两种策略:填零(保守,但会低估)或者用邻近格点填充(激进,但可能引入虚假热点)。我的做法是看缺测比例,低于 1% 直接填零并在日志里记下来,高于 5% 就说明清单覆盖有问题,得回头找源头。
海洋格点处理起来更微妙。人为排放理论上在海上应该是零,但清单因为网格粗,海岸线附近的格点往往是“陆海混合格点”,带着一部分陆地排放。如果你按掩膜直接把海洋格点清零,沿海城市的排放会被削掉一块。更好的做法是用高分辨率的陆地掩膜做面积加权,把混合格点里的陆地部分按比例保留下来。这个细节对沿海区域的臭氧模拟影响不小,我在一个沿海算例上对比过,处理和不处理,近地面 NOx 能差 15% 以上。
还有一类特殊值:某些清单会把船舶、航空这类移动源单独列出来,落在港口和机场格点上,量级很大。这部分要不要算进人为面源,取决于你的研究目的,但一定要在文档里明确,不然别人接手会一头雾水。
3. 实操:从原始清单到 wrfchemi 文件的全流程
3.1 环境准备与目录规划
目录结构我建议一开始就定死,不要边做边改:
emis_work/ ├── 00_raw/ 原始清单,只读,不许改 ├── 01_cut/ 裁剪到目标区域后的中间文件 ├── 02_regrid/ 重网格化到模式网格 ├── 03_spec/ 物种拆分与单位换算后 ├── 04_hourly/ 逐时化 └── 05_final/ 最终 wrfchemi 文件这种分层的好处是每一步都能单独重跑。比如发现物种谱用错了,只需要从 03 往后重跑,不用把重网格几十分钟的计算再来一遍。原始目录设为只读,是为了防止手滑把源文件覆盖掉——我干过一次,重新下载花了半天。
Python 环境这边,核心依赖就三个:xarray管多维数据、netCDF4管读写、xESMF或ESMF管守恒重网格。如果不想装 ESMF(编译确实麻烦),可以用xesmf的 wheel 包,或者退一步用pyresample做面积加权。版本上没什么特殊要求,但要注意xarray新版对dask的依赖有变化,装之前先建个干净的虚拟环境。
环境变量这块,NETCDF4相关的路径在某些系统上会干扰库的加载,如果 import 报错,先检查LD_LIBRARY_PATH里有没有混进别的 NetCDF 版本。这个问题很隐蔽,报错信息通常指向一个完全无关的函数。
3.2 清单裁剪与重网格化:守恒是底线
裁剪这一步看着简单,坑不少。目标区域用经纬度矩形裁剪时,边界要留出一圈余量(我一般留 1°~2°),因为后续重网格时边界格点需要邻域信息,裁得太紧会导致边缘出现人为的低值带。
重网格化的核心是生成权重矩阵。以 xESMF 为例,大致流程是这样:
import xarray as xr import xesmf as xe # 源网格:清单的规则经纬网格 ds_src = xr.open_dataset("01_cut/so2_year.nc") # 目标网格:从 wrfout 或 geo_em 里取经纬度 ds_dst = xr.open_dataset("geo_em.d01.nc") dst_grid = xr.Dataset({ "lat": (["y", "x"], ds_dst.XLAT_M.squeeze().values), "lon": (["y", "x"], ds_dst.XLONG_M.squeeze().values), }) # 关键:用 conservative 方法,且声明周期性 regridder = xe.Regridder( ds_src, dst_grid, method="conservative", periodic=False, filename="weights_so2.nc", # 缓存权重,重复用 reuse_weights=False, ) dst = regridder(ds_src["emis"])method="conservative"是必须的,bilinear和nearest_s2d都会破坏总量守恒。缓存权重文件很重要——同一套源网格和目标网格,权重只算一次,后面每个物种直接reuse_weights=True复用,能省掉大量时间。
重网格完成后,立刻做守恒检查,这是整个流程里我认为最有价值的一步:
| 检查项 | 方法 | 合格判据 |
|---|---|---|
| 全局总量守恒 | 源场求和 vs 目标场求和 | 相对偏差 < 1e-6 |
| 空间分布合理 | 画两张图对比 | 热点位置一致,无边缘异常带 |
| 极值合理性 | 统计 max/min | 目标场 max 不超过源场 max 的 2 倍 |
| 无效值 | 检查 NaN 和负值 | 数量为 0 |
如果总量偏差在 1e-3 量级而不是 1e-6,通常是目标网格超出了源网格覆盖范围,边缘格点被填了缺测。这时候要么扩大裁剪范围,要么在重网格后对边缘做填充处理。
3.3 物种拆分与单位换算的实现要点
物种拆分我用一个外部配置文件驱动,不把映射关系写死在代码里。配置大概长这样:
NOx: target: [E_NO, E_NO2] split: [0.9, 0.1] # 质量分数,NO:NO2 molar_mass: [30, 46] SO2: target: [E_SO2] split: [1.0] molar_mass: [64] NMVOC: target: [E_ETH, E_HC3, E_HC5, E_HC8, E_OL2, E_OLT, E_XYL, E_ALD, E_ORA2] split: [0.04, 0.28, 0.12, 0.09, 0.05, 0.06, 0.08, 0.10, 0.18] molar_mass: [28, 44, 72, 114, 42, 56, 106, 30, 60]配置文件的好处是改起来不用碰代码,也方便把物种谱的来源和版本写进注释。这里要注意split的和必须是 1.0(或在“其他”项上显式留出余量),加总不为 1 的时候一定要报错而不是静默归一化,因为静默归一化会掩盖物种谱本身的录入错误。
单位换算那一段,我习惯把每个物种的换算系数先算出来打印一遍,人工扫一眼量级:
def factor_ton_year_grid_to_mol_km2_hr(molar_mass, area_km2): """ton/year/grid -> mol/km2/hr""" return 1e6 / (molar_mass * 8760.0 * area_km2) for sp, mm in [("SO2", 64), ("NO", 30), ("NO2", 46), ("CO", 28)]: f = factor_ton_year_grid_to_mol_km2_hr(mm, 9.0) print(f"{sp:>4s} M={mm:>3d} factor={f:.4e}")打印出来大致是 SO2 1.98e-1、NO 4.23e-1、NO2 2.76e-1、CO 4.53e-1 这个量级。如果哪个数突然冒出 1e-8 或者 1e+5,基本可以确定单位理解错了。这一步花不到一分钟,但拦住过我好几次错误。
关于 NOx 的拆分,再补一句:质量分数 0.9/0.1 换成摩尔比不是 9:1,因为 NO 和 NO2 的摩尔质量不同。如果你的清单给的是摩尔量,拆分比例要重算。这个细节在交叉验证时会导致约 10% 的氮原子总量偏差,很容易被误判成清单问题。
3.4 时间维度的拼接与小时化
小时化这部分,数据结构的设计决定了后面顺不顺利。我建议中间结果保持“物种 × 小时 × 纬度 × 经度”的四维结构,先把一年 8760 小时全展开(内存不够就按月分块),最后再按模式需要的格式切。
时间分配曲线的应用顺序是:年总量 → 月因子 → 周因子 → 日因子 → 归一化。归一化这一步不能省,因为各套因子的和不一定等于 1,不归一化会导致年总量漂移。具体公式:
$$E_{h} = E_{year} \cdot \frac{f_{m} \cdot f_{d} \cdot f_{h}}{\sum_{m,d,h} f_{m} \cdot f_{d} \cdot f_{h}}$$
分母是全年所有小时的因子乘积之和,算一次存起来就行。这个写法保证了全年加总严格等于年排放量,不会因为因子表的舍入误差累积。
时区转换要单独强调。清单的日变化曲线通常按当地时给,而 WRF 要 UTC。转换时不是简单加几个小时,还要考虑夏令时——如果清单所在区域实行夏令时,而你的因子表没有考虑,夏季会整体偏一小时。处理办法是查清楚清单的因子表基准,然后在代码里显式写一个时区偏移常量,注释里写清楚依据。
文件命名和时间戳语义要对上。如果模式配置的是逐文件读(每个小时一个文件),命名格式是wrfchemi_d01_YYYY-MM-DD_HH:00:00;如果是时间序列单文件模式,那就是一个wrfchemi_d01带时间维。两种模式我建议先做后者调试,因为单文件容易检查时间维是否正确,确认无误后再拆成逐文件。
3.5 输出文件的变量命名、属性与单位校验
最终文件的变量属性是很多人忽略的地方。units、description、stagger、MemoryOrder这些属性,模式读的时候不一定全用,但units在有些版本里会被读进来做一致性检查,写错了可能触发警告甚至静默换算。我的习惯是每个变量都写全属性:
da = ds["E_SO2"] da.attrs.update({ "FieldType": 104, "MemoryOrder": "XY", "description": "anthropogenic emission of SO2", "units": "mol km-2 hr-1", "stagger": "", "coordinates": "XLONG XLAT XTIME", })维度的顺序也要注意,模式期望的一般是(Time, emis_alm, south_north, west_east),其中emis_alm那个维度长度是 1。这个维度名在不同版本里可能叫emissions_zdim或emis_alm,从已有的示例文件里ncdump -h看一眼最稳妥,照抄维度名和顺序,别自己发明。
写完之后的校验,我做三件事。第一,ncdump -h看头信息,确认维度、变量、属性齐全。第二,把文件读回来和中间结果逐变量比对总量,防止写入时维度错位(这种错误最隐蔽,因为文件能正常打开)。第三,拿一个已知的简单场景手算验证——比如构造一个单格点、单物种、恒定排放的测试文件,跑一步模式,看输出的排放诊断量是否等于预期值。第三步最费事,但也是唯一能确认“模式真的按你以为的方式在读”的方法。
4. 排错实录:那些跑完才发现错的地方
4.1 现象与原因速查表
下面这张表是我这几年攒下来的,按“现象 → 可能原因 → 排查动作”组织,出问题时可以照着走一遍。
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 模式启动即报错,提示找不到排放文件 | 文件名日期格式与模拟时段不匹配 | 检查start_*时间与文件名的年月日时是否逐一对上 |
| 文件能读,但浓度场全为零 | 变量名与机制不匹配,被静默忽略 | 对照源码里的变量清单逐一核对;用一个非零测试文件验证 |
| 浓度量级整体偏小 3~6 个数量级 | 单位换算系数错(常见是漏了面积或时间) | 手算一个格点的系数,与脚本输出对比 |
| 沿海城市排放明显偏低 | 海洋掩膜处理过度,混合格点被清零 | 改用面积加权掩膜,检查海岸线附近格点值 |
| 日变化峰值时间整体偏移 | 时区未转 UTC,或时间戳语义理解错误 | 构造单小时非零文件做冲击测试 |
| 山区出现异常高值带 | 重网格用了最近邻,格点面积不匹配 | 换 conservative 方法,做总量守恒检查 |
| 气溶胶总量对但组分不对 | PM2.5 到 I/J 的拆分比例用错 | 检查拆分配置,确认与机制定义一致 |
| 模拟中途崩溃在排放读取处 | 文件中存在 NaN 或极大值 | 全字段统计 min/max/NaN 数量 |
表格只是起点,真正花时间的是定位到具体哪一步出的问题。我的经验是从最终浓度往回倒推:先在模式里打开排放诊断输出,看模式认为的排放是多少,和你写入文件的值比;不一致就说明读取环节有问题,一致就说明写入环节没问题,往上游查。这个二分法能砍掉一半排查时间。
4.2 三个隐蔽的坑
第一个坑:垂直维度被忽略了。人为排放文件里那个长度为 1 的维度,容易被误认为是可有可无的。但在某些配置下(比如带烟羽抬升的模块),模式会按这个维度做垂直分配,如果维度名写错或者长度不对,程序可能读到了但分配逻辑走偏,结果是地面排放全对、高空排放全错。表现是近地面浓度正常,但高空输送和光化学都异常。这个问题极难从浓度场上直接看出来,只能靠检查文件结构。
第二个坑:物种在机制里存在,但不在排放映射表里。比如某些机制有 NH3 的化学反应,但排放模块的默认映射里没有 NH3 这一项,你写进去了也不会被用。反过来,你写了机制里根本没有的变量,也不会报错。所以核对必须双向:机制要什么,文件给什么,两边都要对得上。我习惯做一个集合差运算,把“文件有但机制不要”和“机制要但文件没有”都列出来,逐个确认是有意为之还是漏了。
第三个坑:闰年与跨年边界。模拟跨年时,第 366 天的文件和下一年的第 1 天文件如果时间基准没统一,会出现重复或缺失一小时。这个错误在年均统计里几乎看不出来,但在做逐时对比时会突然出现一个诡异的跳变。处理方法是在生成阶段就用pandas.date_range生成完整时间索引,按索引逐个填,不手工推算日期。
还有一个不算坑但很烦人的点:某些文件系统对文件名里的冒号支持不好,
wrfchemi_d01_2020-01-01_00:00:00这种命名在特定环境下会出问题。遇到读写异常,先试着把冒号换掉看看,虽然模式默认命名带冒号,但很多版本支持自定义格式。
4.3 上线前的一套自检清单
这套清单我现在每次做新清单都会走一遍,大概二十分钟,能拦住 90% 的低级错误。
结构层面:文件名与模拟时段逐小时对齐,无缺漏;维度名、顺序与示例文件一致;每个变量属性完整;时间维的units属性写法正确(hours since ...这类)。
数值层面:全局总量与源清单一致(相对偏差 < 1e-6);无 NaN、无负值;各物种最大值量级合理(和文献里的城市排放强度对得上);海洋格点接近零但不全为零(保留混合格点)。
物理层面:画一张全年平均的空间分布图,看热点是否落在预期的城市和工业区;画一张日变化曲线,看峰值时间是否符合交通/居民源的规律;挑三个格点(城市中心、郊区、远郊)看梯度是否合理。
交叉验证:用另一条技术路线(官方工具或直接用示例文件替换单个物种)跑一遍,对比两者输出的量级差异。差异在 10% 以内可以接受,超过就要查原因。
最后分享一个小技巧:把这个自检流程写成一个脚本,每次生成完自动跑一遍,输出一份 Markdown 报告。看起来是多花时间做工具,但实际用起来,它让我从“每次都要手动检查半天还担心漏项”变成了“看一眼报告就放心”。这份报告还有一个额外好处——投稿时审稿人问排放数据怎么处理的,直接把报告和配置文件的版本号附上去,比写一大段文字描述清楚得多。