只要做空气质量数值模拟,WRF-CMAQ 这个名字就是绕不开的一道坎。CMAQ 是核心的化学传输模型,负责把污染物从排放、输送到化学转化、沉降的全过程算出来,WRF 则在前端提供逐时气象驱动场,外加一份可靠的排放源清单,三者合在一起,才能支撑起一次 PM2.5 重污染个例复盘,或者一个城市的臭氧污染成因分析。这篇文章,是我对照 CMAQ 用户指南第 13 章(UG_ch13)做的梳理,把模型架构、模块分工、实操流程和常见坑串成一条线。适合刚接触模型的研究生、想从气象转做空气质量的工程师,以及工作中需要看懂模拟结果的环境咨询从业者。看完之后,你脑子里应该能立起 WRF 到 MCIP 再到 CCTM 再到后处理这条完整链路,知道每个环节的输入输出是什么,也知道真跑起来会把时间丢在哪些地方。
1. WRF-CMAQ 到底是什么:气象场、排放源与化学机制组成的三角联动
1.1 这不是一个黑箱子,而是一条生产流水线
先说结论:WRF-CMAQ 不是单个执行程序,而是一套多段式流水线。WRF 输出气象场,MCIP 把气象场翻译成 CMAQ 能读的化学接口文件,排放处理模块把各种格式的源清单变成网格化的机制物种,最后 CCTM(Chemical Transport Model)在三维网格上求解化学传输方程,输出每个网格逐时的污染物浓度。任何一段断了,后面全部白跑。
这条流水线每一步的文件流转关系很明确。WRF 跑完会生成 wrfout_d01、wrfout_d02 这类文件,MCIP 读取它们,输出 METCRO3D、METDOT3D、METBDY3D、METCRO2D 等一组接口文件;排放处理输出的是包含各化学物种排放率的网格文件;CCTM 同时读取这两类数据,再配上初始条件 ICON 和边界条件 BCON,最后生成 CCTM_CONC 浓度文件、CCTM_DRYDEP 干沉降文件和 CCTM_WETDEP 湿沉降文件。后处理要做的就是从这些输出里提取变量、对比站点、画图。
很多人被 CMQA 的各种文件后缀搞晕,其实就是没想清楚“谁产生文件、谁消费文件”这条关系链。我在帮师弟排查问题时发现,他卡在不知道 CMAQ 项目目录里 build、scripts、data 三大文件夹各自的职责,于是拿到模型之后的第一件事不是编译,而是对着目录结构发呆。记住一条原则:CMAQ 的模块化设计本来就是让你可以分头折腾的,气象、排放、化学机制各自独立处理,最后在 CCTM 汇合。所以理解流水线关系,比记住每个程序的名字更重要。
1.2 为什么气象场质量几乎决定了模拟的成败
气象场对空气质量模拟的影响,怎么说都不为过。边界层高度从 500 米涨到 1500 米,污染物垂直扩散空间放大了三倍,地面浓度立刻能掉一半;风向偏了 20 度,污染带就从城市中心偏到郊区;风速估算差个 30%,区域输送的路径和到达时间就完全不同。降水更是气态污染物的“清洗剂”,一场中等强度的雨可以把硝酸盐和硫酸盐湿沉降掉很大一部分。这些气象过程的模拟误差,会直接传导到化学过程和浓度输出里。
我在实际案例里对比过不同气象驱动方案下的 PM2.5 模拟结果,同一套排放清单、同一个 CMAQ 版本,仅仅因为 WRF 参数化方案不同,日均浓度可以相差 30% 到 50%。所以老手拿到一份模拟结果,第一反应不是调化学机制,而是先看气象场跑得对不对:风场有没有明显偏差、边界层日变化是否合理、降水落区和实况差多少。气象场不行,后面 CMQA 算得再精细,也只是在一个错误的基础上构建另一个错误。这一点毕业论文里可能不会教你,但实际项目里几乎每次都会遇到。
2. 三个核心模块谁管谁:WRF、MCIP 与 CCTM 的职责边界
2.1 CCTM 内部到底在算什么:一个网格里的“浓度收支”
如果把一个网格想象成一个水池,CCTM 算的就是“某种污染物浓度在这个水池里怎么变”。进水的项是水平输送从周围网格带进来的污染物和本地排放,出水的项是水平输送带走的部分和垂直方向扩散出边界层的部分,水池内部同时进行着气相化学反应,有的物种在生成、有的物种在消耗,底部还有干沉降不断移走物质,天上下雨时湿沉降又清掉一批。一天之内这十几个过程同时运行,CCTM 用算子分裂的方法把它们逐个处理,每一步都尽量保证质量守恒。
CCTM 支持的化学机制主要有 CB(碳键机制)和 SAPRC 两大系列。CB6r3 是当前很常见的碳键机制版本,把几百上千种 VOCs 按照碳键结构归纳成几十个物种,从而把大气化学简化成可以数值求解的方程系统。气溶胶模块 ae6、ae7 则负责描述硫酸盐、硝酸盐、铵盐、黑碳、有机气溶胶的生成、增长和清除。选择机制时,排放处理里的物种映射必须与 CCTM 的机制保持一致,不然模型启动阶段就会报错。这个坑在 4.3 节我会专门展开。
通俗地说,CCTM 的核心是在每个网格、每一层、每个时间步里同时处理平流、扩散、化学反应、排放、沉降,空间上覆盖整个模拟域,时间上通常是逐小时输出。所以它对计算资源的要求很高,也是整条流水线里最耗时的一环。实际跑一个三层嵌套、7 天案例,CCTM 转起来动辄就是十几个小时甚至几天,跑之前做好资源规划和文件备份很有必要。
2.2 MCIP 和排放清单:连接气象与化学的两座桥
MCIP 是 Meteorology-Chemistry Interface Processor 的缩写,它的工作说白了就是把 WRF 的气象输出“翻译”成 CMAQ 需要的气象驱动。WRF 输出的 wrfout 里虽然包含温度、风、湿等基本要素,但 CMAQ 的气相化学、气溶胶和沉降模块需要的远不止这些,它还需要行星边界层高度、摩擦速度、Monin-Obukhov 长度、感热通量、潜热通量,以及 2 米温度、2 米比湿、10 米风这类近地面参数。MCIP 会从 wrfout 里抽取出基础变量,经过诊断计算和垂直插值后,生成 CMQA 能直接读取的接口文件。
MCIP 输出的文件再强调一次:METCRO3D 是各层三维气象场,METDOT3D 是水平风在格点上而不是格心上的分量,METBDY3D 是边界气象场,METCRO2D 是地表二维参数。命名看起来绕,但用途各不相同。如果 MCIP 处理时网格设置出了问题,比如投影参数和 WRF 不一致,CCTM 通常会在启动阶段或运行中途直接报错,而且报错信息不会明说“你的 MCIP 错了”,只会提示找不到某些变量或文件读取出错,排查起来很费劲。
另一座桥是排放清单处理。这一步是 CMAQ 整条链路里最容易出偏差的环节。一份原始清单可能是吨/年、吨/月、千克/小时这些五花八门的单位,你得统一单位并拆成逐小时排放率;NOx 要按比例拆成 NO 和 NO2,VOCs 要按照化学机制里定义的物种一一对应;不同行业、不同地区要用不同的时间分配曲线,比如交通源在工作日和周末的高峰时刻完全不同。这个环节处理不好,CCTM 运行再稳定,模拟浓度也和实测对不上。
3. 从零跑通一个 7 天个例模拟:WRF、MCIP、排放与 CCTM 实操记录
3.1 环境准备与版本选型
先说环境。CMAQ 和 WRF 都是典型的 Linux 下数值模型,常用环境是 CentOS/Rocky Linux 或 Ubuntu 服务器,编译器可以用 Intel ifort 也可以 gfortran。需要提前装好的底层库包括 netCDF-C、netCDF-Fortran、MPI(OpenMPI 或 MPICH)、zlib、HDF5,以及最关键的 I/O API 库。I/O API 是 CMAQ 读写 netCDF 文件的底层依赖,编译时必须与后续 CCTM 使用同一套编译器和环境变量,否则跑起来会出现文件读写不匹配的怪问题。
版本选型建议:CMAQ 5.3.3 是目前社区文档和讨论最多、最稳的一个版本,适合新手起步;CMAQ 5.4 对气溶胶机制做了明显更新,新增了更多有机气溶胶物种,机制更全但编译和调试的资料相对少一些。WRF 用 4.3 或 4.4 以上都没有问题,关键是确认与 MCIP 的接口兼容。我个人的习惯是不追最新版本,等社区的坑填得差不多了再升级。毕竟模型是工具,不是用来折腾编译器的。
| 版本组合 | 优点 | 缺点 |
|---|---|---|
| WRF 4.3 + CMAQ 5.3.3 | 社区资料多、坑少、编译顺利 | 机制和过程代表性稍旧 |
| WRF 4.4 + CMAQ 5.4 | 气溶胶机制更新、结果物理过程更全 | 编译与调试资料少、算得更慢 |
| WRF 4.6 + CMAQ 5.4+ | 气象场精度更高 | 依赖库版本要求高、环境配置复杂 |
3.2 WRF 配置要点:嵌套域、时间步长和 CFL 约束
WRF 部分很多人已经很熟,我挑和 CMAQ 强相关的地方说。做城市尺度模拟,一般设置三层嵌套:d01 覆盖整个区域(水平分辨率 27 km),d02 覆盖省份或重点区域(9 km),d03 覆盖目标城市(3 km)。namelist.wps 里的 geogrid 设置和 namelist.input 里的 dx、dy、e_we、e_sn 必须完全对齐,嵌套关系要满足子域边界落在父域网格线上,否则 metgrid 阶段就会警告甚至出错。
时间步长是 WRF 最容易翻车的设置。3 km 网格用 60 秒是比较稳的组合,9 km 网格可以用 180 秒左右。很多人为了省时间,3 km 域直接把 dt 拉到 90 秒甚至 120 秒,平时没事,一旦遇到强对流或大风天气,立刻触发 CFL 条件不满足的报错,之前算的内容全白费。CFL 条件是数值稳定性的底线,核心意思是每个时间步内信息传播的距离不能超过一个网格。所以水平分辨率越高,dt 就得越小,这个关系没法绕过。
气象驱动数据一般用 GFS 或者 FNL 再分析场,FNL 分辨率更高,模拟效果通常更好一些。namelist.input 里的 num_metgrid_levels 要和 WPS 处理时保持一致,别随手填一个数字,否则 real 阶段会报出非常奇葩的垂直层不匹配错误。跑完 WRF 之后,先检查 wrfout 文件里的风速、温度、降水是否合理,再进下一步 MCIP,省得后面木已成舟才发现气象场就是歪的。
3.3 MCIP 处理与排放清单准备
MCIP 的 namelist 里最关键的三项:WRF 输入路径、输出路径、网格范围。网格范围必须从 WRF 的 namelist 里原样复制,尤其是投影参数,如果是兰伯特投影,中心经度、两条标准纬线、起始点经纬度一个都不能错。MCIP 处理完成后,先检查生成文件的时间范围是否覆盖你要的模拟时段,再检查文件里有没有缺测或异常值。这一段如果错了,CCTM 的报错会很莫名其妙。
排放清单准备这一步,新手最容易卡住。如果手里有已经网格化的排放数据,比如常见的 MEIC 清单或者其他区域清单产品,就需要做两个关键动作:时间分配和化学物种映射。时间分配解决“一天 24 小时的排放怎么分布”的问题,不同源类别用不同曲线;化学物种映射解决“清单里的 CO、NMHC、NOx 怎么对应到 CB6r3 机制里的几十个物种”的问题。这一层处理做完,才输出 CCTM 直接可读的网格排放文件。
如果只有源源清单(比如点源坐标和年排放量),则还需要做空间分配,也就是根据人口、路网、土地利用等代理变量,把源排放分摊到每个网格。这个环节工作量很大,也很容易出系统性偏差。我第一次做河北某城市的案例时,把交通源全部堆到了高速公路上,结果主城区模拟浓度明显偏低,街道尺度的空间分布完全失真。现在我的习惯是先跑通流程,用现成的网格清单把链路串起来,之后再细化空间分配,分步骤推进比一步到位靠谱得多。
3.4 CCTM 运行参数与结果输出
CCTM 的运行脚本一般是 run.cctm.* 这样的脚本,里面最关键的几个变量是机制名称、模拟起始时间、运行天数、边界条件和初始条件。机制名称要和排放清单的物种映射一致,比如都用 CB6r3_ae6_aq。边界条件如果做短期的区域模拟,用 CMAQ 自带的默认 profile 也够,但背景浓度给低了会导致模拟值系统性偏低,臭氧的背景值默认给 20 ppb 左右比较常见,具体要看模拟区域和季节。初始条件可以从前一天模拟结果里读,也可以让模型自己 ramp-up 几天后进入稳定状态。
并行计算设置上,单节点多核可以用 NPCOL 和 NPROW 控制进程网格。比如分配 36 个核,可以设 NPCOL=6、NPROW=6,实测下来行数和列数越接近,通信开销越小,整体效率越高。跑起来之后记得保存运行日志,我一般用 tee 同时输出到屏幕和文件,方便出错后回溯。CCTM 跑完会在输出目录下生成 CCTM_CONC_v53_gmt_* 之类的文件,可以用日志文件确认结束状态,不要只看文件是否存在。
CCTM 输出的是 I/O API 格式的 netCDF 文件,里面包括 O3、NO2、PM2.5、PM10、SO2、CO 等常规物种浓度,单位通常是 ppm 或者 ug/m3。如果只想要一次性的结果,直接读 CCTM_CONC 就行。如果要做长期评估,还需要把逐小时结果汇总成日均值或日最大 8 小时臭氧均值,这是后处理里最基本的动作。
3.5 后处理与验证:用监测站点数据说话
后处理的第一步是提取模拟值,跟国控站监测数据做对比。Python 里用 netCDF4 库读 CMAQ 输出文件,找到离站点最近的网格点,把逐小时模拟序列拉出来,再和监测时间序列画在一张图上。常用的统计指标包括平均偏差 MB、归一化平均偏差 NMB、归一化平均误差 NME、相关系数 R、均方根误差 RMSE。论文里最常放的表就是这几项,不同污染物版本的验收口径不一样,但思路都差不多:模拟值和站点值时间变化趋势要基本一致,量级上不能偏太多。
画图工具上,空间分布图可以用 Python 的 Cartopy 或 Basemap,站点时间序列用 Matplotlib 就够了。分析场景比较灵活的时候,也可以用美国环保署开源的 VERDI 工具,它能直接读 CMAQ 输出文件并做切片、差值和多情景对比,交互式探索时比写脚本方便得多。我的习惯是:探索性分析用 VERDI,正式出图出表用 Python 脚本,前者省时间,后者可复现、可交给流程自动化。
4. 常见问题与排查技巧实录
4.1 CFL 报错:时间步长怎么调才不翻车
CFL 报错是 WRF 阶段最常见的噩梦。典型症状是运行到中途突然跳出“CFL violated”或者计时器报错的提示,然后任务终止。原因要么是时间步长太大,要么是模拟区域地形过于复杂导致局地风速极值。处理方式最直接的就是把 dt 减半,3 km 域从 60 秒减到 30 秒,9 km 域从 180 秒减到 90 秒,如果问题依旧再继续往下减。还有一种情况是垂直层设置太密集导致近地面层风场梯度大,此时还需要适当调整垂直坐标拉伸系数。
经验上,dt 要同时参考水平分辨率和天气形势来定。夏季强对流多发时段,3 km 域用 60 秒都未必保险,遇到大范围雷暴天气直接降到 40 秒更稳妥。反正多算几个小时比中途崩溃重新跑要划算。如果你发现每次崩都在同一个时段,那基本可以确定是那个时段里有强风或强对流过程,针对性地减小 dt 就能解决,完全不用改物理方案。
4.2 模拟浓度整体偏低:先从排放清单找原因
模拟浓度系统性偏低的排查顺序,第一永远看排放清单。很多新手的案例里,把吨/年的总量直接当成克/秒去用,数量级差了上千倍,模拟浓度能高才奇怪。第二看时间分配有没有真正生效,如果排放文件里每个小时都是同一个值,说明时间分配这一步没有做或者代码路径没走对。第三看边界条件给的背景浓度是否合理,臭氧背景值给太低,整个模拟区域就像泡在没有污染物的空气里一样。
我在实际项目中遇到过一种隐蔽情况:排放清单空间范围和模拟域不完全重叠,导致部分区域排放为 0。那种情况模拟图会非常醒目地出现一块干净得像风景区一样的区域,而监测站恰好落在那块区域,对比结果自然惨不忍睹。检查方法是把排放文件画出来,和模拟域边界核对,确保每个网格都有值。排放是浓度的源头,源头错了,后面调化学参数和扩散参数都是白费力气。
4.3 化学机制物种不匹配:启动即报错的元凶
CCTM 启动时如果报 species not in file、找不到某些变量,或者读取 CAM_ABFLAG 之类文件时出错,多半是排放清单和 CCTM 的化学机制不一致。比如排放处理时用的是 CB05 机制物种,CCTM 却选了 CB6r3,两者在 VOC 物种的拆分上差别很大,很多物种对不上号。解决办法是统一机制,要么排放处理跟着 CCTM 走,要么 CCTM 换回和排放处理一样的机制。
排查技巧:先看 CCTM 日志里第一类报错发生在哪个文件读取阶段,再回去查排放文件里的变量列表。netCDF 文件里的变量名是可以直接列出来的,比如用 Python 的 netCDF4 库把变量名打印出来,一眼就能看出有没有 CB6r3 需要的物种。这个排查顺序能省下大量时间。我曾经因为排放文件里缺了异戊二烯的一个中间物种,让模型在启动后十分钟才崩,日志又没有明确提示,折腾了两天才定位到物种映射文件漏了一行。
4.4 嵌套网格偏移与输出为空
嵌套网格对不齐,是 CMAQ 里一个非常“鬼”的问题。典型表现是模拟出的浓度场在嵌套边界处出现明显断层,或者整个浓度场相对监测站点有系统性偏移。原因几乎都出在 MCIP 或者 CCTM 的网格定义和 WRF 不一致,尤其是投影参数里中心经度或标准纬线抄错一位小数,图层放大了以后偏移就很明显。排查方法是把 CMAQ 域的经纬度网格画出来,叠到 WRF 地形图上,手动检查边界是否重合。别嫌这个步骤麻烦,它比任何日志都直观。
还有一种“输出为空”的情况。CCTM 跑完,CCTM_CONC 文件也生成了,但里面没有数据,或者跑到一半日志就停住。优先检查磁盘空间,很多大内存模型跑到最后一刻因为磁盘满了才静默失败;其次检查运行日志结尾有没有 ERROR 关键字,别只看“completed”字样。真实项目里,输出被中断比编译失败更难发现,因为所有前置文件都好好的,只有结果文件缺失,这时候回溯日志、确认每个任务结束状态就非常重要。
我个人在实际操作中最深的体会是:WRF-CMAQ 的上手曲线不是陡,而是长。前期的编译和配置确实磨人,但它很少考验智商,更多是考验耐心和条理。另外一个坚持了很久的习惯是,每个版本的 namelist、runscript、编译记录都单独备份,目录名带上日期和备注。三个月后回看模拟结果,如果当时的配置已经找不到了,你基本就得从头再来。做模型测试时也别指望一步到位,先跑通、再跑好,用单层 3 km 域跑通一个 7 天案例,远比一上来就铺三层嵌套然后卡在排放数据处理上有价值。把这条流水线的框架刻在脑子里之后,剩下的就是一个个细节去磨,磨到最后你会发现,CMAQ 的运行逻辑其实非常规整,报错也大多有迹可循。