ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FLEXPART拉格朗日粒子扩散模型在Ubuntu上的编译与运行实践

FLEXPART拉格朗日粒子扩散模型在Ubuntu上的编译与运行实践

简介:Flexpart 10.4 的源码资源包(flexpart.tar.gz),面向在 Ubuntu 18.04 下开展大气粒子轨迹模拟、需要从源码编译安装模型的研究者与开发者,尤其适合有一定 Linux 基础、希望理解依赖库构建逻辑的读者。整个压缩包约 328.8 MB,共 718 个文件;除 Fortran 源代码、makefile 和 shell 编译脚本外,还包含大量 HTML 说明文档、PNG 示意图、RST/TXT 技术文档、CSV 样例数据、f90 源码、控制文件与运行日志,以及用于测试的 GRIB 气象数据文件与 Python 辅助脚本,目录层级完整,便于对照查阅。针对源码编译前最棘手的依赖准备问题,包内相关说明覆盖 libemos、HDF5、NetCDF、eccodes、grib_api、zlib、szip、jasper 等核心依赖库的作用、版本匹配与编译顺序要点,能帮助读者理清 Flexpart 在 Ubuntu 18.04 环境中的完整编译链路,也方便后续针对特定数据格式进行二次开发。已有 608 人学习,适合需要结合系统环境完成 Flexpart 安装、调试与结果验证的研究人员。

1. flexpart 是什么:一个能逆向追污染源的拉格朗日粒子扩散模型

flexpart 这个包,在我手里是当“后悔药”用的。某模拟项目X里,监测点凌晨出现一个高浓度峰值,排放源清单怎么都对不上,我把这套拉格朗日粒子扩散模型拿到 Ubuntu 上编译跑通,用反向追踪模式把 72 小时气团轨迹追回去,才定位到问题源头。它能解决的不是“风往哪吹”这种大路问题,而是更细的一层:给定浓度观测或排放假设,反推气团从哪来、向哪去、沿途经过了哪些高度层、有没有可能被抬升到边界层以上。适合做大气污染溯源、火山灰扩散、放射性物质传输评估的从业者,新手按本文也能在 Ubuntu 上把一个最小案例跑起来。全文按我实际拆包顺序展开,先讲编译,再讲配置,最后讲跑结果和踩坑。

2. 在 Ubuntu 上编译 flexpart:先把三类依赖装齐,再动 makefile

flexpart 是 Fortran 写的,整个模型本质上是一个可执行文件加一堆输入文件。编译这步卡住的人不少,原因不是源码有问题,而是依赖没备齐。我先后在几台 Ubuntu 20.04 和 22.04 上编过,结论一致:依赖装对了,make 一次过;依赖缺了,报错信息又短又玄学。

2.1 依赖选型:gfortran、NetCDF、eccodes 三件套

flexpart 的编译需要三样东西:Fortran 编译器、NetCDF 库(带 Fortran 接口)、以及读写气象 GRIB 数据的底层库。下面这个表格是我实际使用的组合,按“够用且不容易踩坑”的标准选的。

组件选型说明
编译器gfortran开源、apt 直接装,ifort 也能编但没必要
NetCDFlibnetcdf-dev + libnetcdf-fortran-dev必须带 Fortran 头文件 netcdff.mod
GRIB 读取libeccodes-dev新版库,老的 grib_api 在新系统上容易编不过
辅助make, openblasmake 必有,openblas 可选但建议装

很多人在 NetCDF 上翻车:只装了 libnetcdf-dev,没装 libnetcdf-fortran-dev,结果 make 时找不到 netcdff.mod。这两个包的区别,前者是 C 接口,后者才是 Fortran 接口。flexpart 的源码里大量调用nf_open、nf_create这类函数,没有 Fortran 接口直接编译失败。

安装命令如下:

sudo apt update sudo apt install -y gfortran make \ libnetcdf-dev libnetcdf-fortran-dev \ libeccodes-dev libopenblas-dev

说明一下每个包的作用:gfortran 提供编译器;libnetcdf-fortran-dev 提供 netcdff.mod 和 Fortran 链接库;libeccodes-dev 是 ECMWF 编码库的 Debian 包,flexpart 读 GRIB 气象数据时靠它解析;openblas 不是必须的,但某些版本编译时链接数学库,装上能省去报错。如果你的 Ubuntu 是 18.04,可能没有 libeccodes-dev 这个包名,需要从源码编译 eccodes,或者换用 grib_api,但我在新版本上没遇到这种情况。

2.2 编译操作:解压、改 makefile 三处变量、make

拿到 flexpart.tar.gz 后,先解压看目录结构。这一步不要跳,确认包内包含 src、tools、docs 目录,再开始改配置。

tar -xzf flexpart.tar.gz cd flexpart ls -la

解压后进入源码根目录,会看到一个 makefile 或 makefile.mk。flexpart 的 makefile 逻辑很直接,顶部定义编译器、NetCDF 路径、eccodes 路径,然后按编译选项生成可执行文件。我一般只改下面这几个变量:

# 编译器 FC = gfortran # NetCDF 安装根目录 NETCDF = /usr # eccodes 安装根目录 ECCODES = /usr # 编译选项 FFLAGS = -O2 -fdefault-real-8 -ffpe-trap=invalid,zero,overflow LDFLAGS = -L$(NETCDF)/lib -lnetcdff -L$(ECCODES)/lib -leccodes

这里有个关键参数:-fdefault-real-8。flexpart 的源码假设默认 real 是 8 字节,不加这个选项,所有浮点运算都会按 4 字节处理,结果会出现大量 NaN,而且很难排查。-ffpe-trap是让浮点异常直接暴露出来,编译时不加也行,运行时能帮你更快发现数据问题。

NetCDF 和 eccodes 的路径,在 Ubuntu 上一般就是/usr,因为 apt 装的库都在 /usr/lib 和 /usr/include 里。如果你是自己编译安装到 /opt/local 这类目录,要把路径改成实际的安装位置,否则链接时找不到库文件。改完之后直接 make:

make -j4

-j4 是用 4 个并行任务编译,速度快一些。第一次编译如果机器配置低,建议先不加 -j,避免内存不够。编译过程会输出大量 Fortran 编译信息,看到 src/flexpart 这个可执行文件生成,就算成了。

2.3 验证编译结果:先跑一个默认用例

编译通过不代表能跑通。flexpart 编译完,我习惯先不急着配自己的数据,而是看它有没有内置测试用例。很多发行版源码树里带一个小规模示例数据,放在 tests 或 examples 目录下。

ls src/flexpart ./src/flexpart

不带参数直接运行,正常情况下会读取当前目录下的 pathnames 文件。如果当前目录没有这个文件,它会报错退出,提示缺少 pathnames。这个报错其实是个好消息,说明可执行文件能正常启动、Fortran 运行时没问题。如果连这个都出不来,比如提示找不到共享库,那就是链接路径没配对,用ldd src/flexpart查一下缺哪个库。

验证完这一步,编译阶段就算结束了。接下来真正花时间的是配置运行环境。

3. 跑通第一个模拟:pathnames 与 COMMAND 的配置细节

编译只是热身,flexpart 的使用门槛主要在配置。它不像普通软件有图形界面,所有控制全靠几个文本文件。我最早接触时对着文档折腾了两天才弄清楚每个文件的职责,这里把最关键的几个讲透。

3.1 运行目录文件清单:到底需要准备哪些文件

flexpart 运行时,当前目录下必须存在一组固定文件。不同版本文件数量略有差异,以最常见的源码树为例,至少需要下面这些:

文件作用
pathnames告诉你各输入输出数据放在哪个目录
COMMAND控制模拟主参数,如起止时间、粒子数、输出间隔
OUTGRID定义浓度输出的网格范围与分辨率
RELEASES定义释放源的经纬度、高度、时间窗口
available物种与沉降参数数据库

注意,运行时只认当前目录下的这几个名字,所以每次做新案例,我一般复制一个干净目录,再改里面的文件,而不是直接在源码目录里跑,避免污染原始配置。常见做法是这样组织目录:

mkdir -p /data/flexpart/case1 cd /data/flexpart/case1 cp /data/flexpart/available ./available

然后把 pathnames、COMMAND、OUTGRID、RELEASES 这四个文件按需求改好,再执行 flexpart。源码树里会带模板,先把模板复制过来改,比自己新建文件少踩很多格式坑。

3.2 pathnames:路径文件里每一行的含义

pathnames 是 flexpart 第一个读取的文件,它按固定行序告诉模型各数据目录在哪。格式很简单,但有一个隐蔽的坑:Fortran 读取时对行列位置敏感,路径后面不能有多余空格,目录层级也不能错。我常用的一个 pathnames 长这样:

/data/flexpart/input/met /data/flexpart/input/geo /data/flexpart/input/oh /data/flexpart/output

第一行是气象数据的根目录,第二行是地理数据目录,第三行是 OH 自由基浓度场数据目录,第四行是输出目录。如果你的模拟不涉及 OH 化学,第三行可以指向一个空目录,但这一行不能省略。行末如果多了一个空格,Fortran 的固定格式读取会把它当成路径的一部分,运行时直接报错找不到目录。

这个文件改完,可以用一个很简单的命令检查行尾:

cat -A pathnames

每行结尾应该是干净的$,如果看到^M或者空格$,说明文件带了 Windows 换行或多余空格。这个细节看起来不起眼,但能省掉你半小时排查时间。

3.3 COMMAND 和 RELEASES:粒子数、释放时间、输出间隔

COMMAND 文件用 Fortran namelist 格式写成,控制模拟的全局行为。我一般重点关注这几个参数:

&COMMAND PARTICLE_NUMBER = 10000 LDTEFF = .TRUE. LSUBGRID = .TRUE. LDIRD = 1 MAXAGE = 72.0 LOUTSTEP = 3600.0 LOUTAVER = .TRUE. /

PARTICLE_NUMBER 是释放的总粒子数,数值越大浓度场越平滑,但运行时间和输出文件体积也越大。LDIRD 是方向控制:1 表示正向追踪,-1 表示反向追踪,做溯源时用 -1。MAXAGE 是粒子最大存活时间,单位小时,比如 72.0 表示追踪三天。LOUTSTEP 是浓度输出时间间隔,单位秒,3600 就是每小时输出一帧。LOUTAVER 决定输出的是瞬时值还是时间平均值,做浓度图一般用 .TRUE.。

RELEASES 文件定义释放源,格式比 COMMAND 更固定。我常用的写法是一个释放组,包含时间窗口和空间范围:

1 2019-06-01 00:00 2019-06-01 02:00 0.0 72.0 1.0E+06 0.0 0.0 113.0 30.0 0.0 100.0

第一行是释放组编号;第二行和第三行是释放开始和结束时间;第四行是释放持续时间、粒子寿命上限、总释放质量、一个标志位;第五行是释放中心经度、纬度、最低高度、最高高度,单位分别是度和米。这里的经纬度我写的是某模拟项目X里用过的测试点,替换成你研究区域的位置即可。

每次改完这几个文件,我都会做一次“最小配置验证”:先只放 1000 个粒子、输出间隔拉大,跑通流程后再把粒子数和输出频率加上去。这个习惯帮我避免了很多“配置错了但跑了几小时才发现”的尴尬。

4. 运行与输出:从粒子轨迹到浓度网格,结果怎么读

配置写完,终于可以真正运行 flexpart。这一章讲输入数据怎么对齐、运行日志怎么看、以及输出文件如何解读。

4.1 气象数据与地理数据:先把输入对齐

flexpart 不是自带气象数据的,它需要你提供气象驱动场。常见做法是去全球再分析数据集下载 GRIB 格式的气象场,然后放到 pathnames 指定的目录里。下载时要注意两个事:一是时间范围要覆盖模拟窗口,二是变量要完整,至少包含风速、温度、湿度、气压和边界层高度这些核心场。

下载完先检查文件,再放入目录。我一般用 eccodes 提供的工具看一眼:

grib_ls /data/flexpart/input/met/20190601.grib

grib_ls 会列出 GRIB 文件里的所有场,包括变量名、层数、时间。这里重点看数据的 level 类型,flexpart 需要多层三维场,如果下的是单层场,运行时会报“无法插值”之类的错误。地理数据方面,flexpart 需要地表高度、土地利用类型等文件,这些一般在包内自带生成工具,或者用模型配套脚本从公开地表数据集转换。

把气象数据放好后,再检查路径:

ls /data/flexpart/input/met/ ls /data/flexpart/input/geo/

确认目录里确实有文件,不是空目录。我遇到过好多次,pathnames 指对了,但目录是空的,flexpart 一句话不报就退出,看起来像程序崩了。所以养成习惯,在运行之前先手动确认每个目录非空。

4.2 启动运行:命令行入口与运行日志

所有输入就位后,在运行目录里执行可执行文件:

cd /data/flexpart/case1 ../src/flexpart

运行时终端会滚动大量文本,包括正在读取哪个 GRIB 文件、插值进度、粒子释放信息。两个 Linux 小技巧可以帮上忙:

../src/flexpart > run.log 2>&1 tail -f run.log

把输出重定向到日志文件,再用 tail 实时看进度,这样即使终端断开,日志也保留下来。运行时间取决于粒子数、模拟时长和气象数据覆盖范围,小案例几分钟,大案例几小时。如果运行几分钟后日志不动了,按 Ctrl-C 中断,检查日志最后几行,多半是路径问题或数据时间不匹配。

4.3 输出文件解读:partposit、grid_conc 与 header

运行结束后,输出目录里会出现几类文件。最简单的是 header,记录模拟基本信息,比如坐标系参数、网格定义。核心输出是两类:一类是粒子位置文件 partposit,记录每个粒子随时间变化的坐标;一类是浓度网格文件 grid_conc,按你设置的网格输出浓度场。

partposit 是二进制格式,不能直接打开看。flexpart 源码树里带了转换工具,常见做法是到 tools 目录编译 readpartposit:

cd flexpart/tools gfortran readpartposit.f90 -o readpartposit cd /data/flexpart/output ../flexpart/tools/readpartposit < partposit > partposit.txt

转换出来的文本,一行一个粒子,列包含时间、经纬度、高度等信息。用 head 看一眼格式:

head -20 partposit.txt

grid_conc 文件如果编译时 NetCDF 接口正常,会输出为 NetCDF 格式,直接用 Python 的 xarray 读取画图很方便:

import xarray as xr import matplotlib.pyplot as plt ds = xr.open_dataset("grid_conc_20190601_00.nc") conc = ds["conc"].sum(dim="time").squeeze() fig, ax = plt.subplots(figsize=(8, 6)) mesh = conc.plot(ax=ax, cmap="YlOrRd") ax.set_title("Column-integrated concentration (ng/m3)") plt.savefig("concentration.png", dpi=150)

这段代码把整个模拟时段的浓度按时间维度求和,得到一张累计浓度分布图。对于污染溯源场景,这张图能直接告诉你高浓度覆盖了哪些区域,再叠加监测点位就能对照。

5. 避坑清单:编译与运行阶段的五条血泪经验

这一章直接写我实际踩过的坑,每条按现象、原因、解决三个步骤展开,照着排查能省不少时间。

5.1 编译报错找不到 netcdff.mod

现象:执行 make 后,gfortran 报错Error: Cannot open file 'netcdff.mod',或者提示can't find -lnetcdff。

原因:只装了 libnetcdf-dev,没装 libnetcdf-fortran-dev。前者是 C 接口的库,不包含 Fortran module 文件。

解决:

sudo apt install libnetcdf-fortran-dev

装完后检查 /usr/include 或 /usr/lib/gcc 相关目录下是否存在 netcdff.mod。如果存在但 makefile 里没指定头文件路径,在 FFLAGS 里加一行:

FFLAGS += -I/usr/include

从那以后我每次新机器编译前,都先执行apt search netcdf看一眼 Fortran 接口包是否在列,再开始动 makefile。

5.2 运行秒退但无任何报错信息

现象:在运行目录执行./flexpart,终端只输出一两行,进程就退出,没有明确的 error 信息。

原因:最常见的两个。一是 pathnames 里某个路径末尾多了一个空格,Fortran 固定格式读取把空格当成了路径的一部分;二是某个数据目录是空的,flexpart 检查不到文件直接退出。

解决:先cat -A pathnames确认行尾干净,再逐个ls检查目录内容,最后在 makefile 里加-g -fbacktrace重新编译一遍。flexpart 的日志本身比较简略,编译时加回溯选项能定位到具体是哪个文件读取失败。这个坑我连续遇到两次,后来养成了“先 cat -A,再 ls -l”的固定套路。

5.3 模拟正常结束但浓度场全零

现象:粒子数设了 10 万,跑了几小时,日志显示正常释放、正常结束,但打开 grid_conc 全是 0 或 NaN。

原因:OUTGRID 文件定义的网格范围没有覆盖释放源,或者 COMMAND 里 LOUTSTEP 设得太大,以至于每个输出间隔里粒子恰好还没进入网格。还有一种情况是释放质量设了 0,RELEASES 里那一行的质量参数写成了 0.0,粒子虽然没有密度但浓度算出来自然是零。

解决:先检查 OUTGRID 的经纬度范围,确认释放点落在网格内;再确认 RELEASES 里的质量参数大于 0;最后把 LOUTSTEP 缩短到 600 秒做一次小规模测试。做污染溯源时,网格范围宁大勿小,哪怕分辨率低一点,先把浓度场跑出来再局部加密。

5.4 反向追踪结果在原地打转

现象:设置 LDIRD = -1 做溯源,跑出来的粒子轨迹不往外走,就围着释放点附近转圈。

原因:反向模式对气象数据的起始时间要求与正向不同。反向追踪时粒子从观测时刻出发往回走,如果气象数据没有覆盖观测时刻之前的时间窗,或者 RELEASES 里的释放时间写得比气象数据还早,粒子在初始时刻就找不到有效风场,只能原地乱飘。

解决:把 RELEASES 的释放时间设为观测时刻之前至少一个 MAXAGE 小时,同时确认气象数据的起始时间比释放时间还早。比如观测是 6 月 1 日 02 时,MAXAGE 设 72 小时,气象数据从 5 月 30 日 00 时就要有,不能只覆盖 6 月 1 日当天。另外反向模式下粒子数建议加大到至少 5 万,不然统计噪声会被近似成原地不动。

5.5 GRIB 数据读取报错“key not found”

现象:程序运行到读取 GRIB 文件时报错,提示某个 key 找不到。

原因:气象数据的格式或变量命名与 flexpart 预期不一致。flexpart 对不同来源的气象数据有严格约定,它要求特定的字段名、层数、网格类型。下载数据时选了错误的参数组合,就会这样。

解决:先看报错里提到的 key 是什么,对比数据里的变量名和包内文档中列出的变量清单。最常见的是用了单层场而非多层场,或网格类型是 regular 而非 reduced。另外检查 eccodes 版本,新版库对 GRIB2 支持更完整,旧版容易出现 key 缺失。我一般会先跑一个只用 3 小时数据的极短模拟,确认数据读取正常,再跑正式案例。

6. 进阶技巧:批量跑情景模拟与 partposit 可视化

配置和排错都稳了之后,日常使用中我最常用的两个进阶操作:批量改释放点跑多情景,以及把 partposit 轨迹画出来。

批量做情景时不需要手动一个一个改 RELEASES,用 bash 循环加 sed 替换就可以。RELEASES 文件里的经纬度可以用占位符代替,比如把经度写成变量 LAT,然后循环替换:

for i in 1 2 3 4 5; do mkdir -p run_case_$i sed "s/LAT/$i/g" template_RELEASES > run_case_$i/RELEASES cd run_case_$i && cp ../COMMAND ../pathnames ../OUTGRID ../available . ../src/flexpart > run.log 2>&1 cd .. done

每次循环生成一个独立目录,各自放一套配置,跑完不互相干扰。这里 sed 命令把模板里的 LAT 替换成实际数值,适合快速生成多释放点实验。需要注意 COMMAND 里输出的文件名如果指定了绝对路径,每次循环要一并改掉,否则结果会覆盖。

partposit 转成文本后,画轨迹最直接。readpartposit 转换出的列一般依次是时间、经度、纬度、高度、序号等。画三维轨迹时用 matplotlib:

import matplotlib.pyplot as plt import pandas as pd df = pd.read_csv("partposit.txt", delim_whitespace=True, header=None, names=["time", "lon", "lat", "z", "num"]) sample = df.sample(2000, random_state=42) fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, projection="3d") ax.plot(sample["lon"], sample["lat"], sample["z"], lw=0.3, color="steelblue") ax.set_xlabel("Longitude") ax.set_ylabel("Latitude") ax.set_zlabel("Height (m)") plt.savefig("trajectory.png", dpi=150)

粒子数多的时候画 10 万条线会很密集,所以先随机抽 2000 个粒子再画,轨迹趋势不受影响。如果 partposit 列数和你手上版本不同,先head -5看实际列数再改 names 列表。

关于 partposit 还有一个容易被忽略的点:不同版本的转换工具输出的列顺序不完全一样。我用过一次某个历史版本,时间列和高度列是反的,画出来的轨迹高度变化看起来非常离谱。后来我固定了“先 head 看表头,再画图”的流程,从那以后每次跑完模拟都强制走一遍:先 cat -A 检查路径文件,再 head 检查转换结果的列数,最后才进画图和浓度分析。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进