简介:WOFOST源代码压缩包,面向农业科研人员、作物模型开发者及农业院校师生,可支撑作物生产潜力模拟、农业管理策略评估与气候变化影响预测等研究场景。代码源自荷兰瓦赫宁根大学,内部含316个文件,核心类型包括wof(模型配置与输入参数)、for(Fortran程序源码)、ltm/cab(辅助配置与数据文件)等,另有少量dat、inc及bat/shell脚本,整体体积仅1.51MB,结构清晰便于阅读与移植。目前已有990人浏览学习,在农业模型圈内受到一定关注。通过源码可深入理解气候、土壤水文、养分、作物生理、收获与管理六大模块的算法逻辑与数据结构,既能调试优化运行效率,也能扩展新作物或新环境参数,还可结合遥感/GIS数据做敏感性分析与精度提升,是学习经典作物模型机制、开展本地化二次开发的一份宝贵基础材料。
1. WOFOST 源代码:一套把作物生长过程变成可计算模块的经典实现
WOFOST(World FOod STudies)是瓦赫宁根大学开发的作物生长模拟模型,源代码从 1980 年代延续至今,Fortran 到 Python 的版本都有。很多做农业遥感估产、气候变化影响评估的工程师第一次接触它,是想搞懂 CERES 之外另一套机理模型是怎么组织的。和统计模型不同,WOFOST 不追求拟合产量,而是从光能利用、叶片生长、生物量分配这些生理过程推导出逐日生长量——这意味着源代码里几乎每一个函数都对应一个农学概念。
读这份代码的价值在于:它把“作物生长”这件看似依赖经验的事拆成了可替换的模块,你既可以直接跑通做区域产量模拟,也可以替换其中光截获、蒸散、物候等子模块去适配自己的研究场景。本文从代码阅读和运行的角度,把 WOFOST 的工程实现路径梳理一遍:源码结构、关键算法的代码落点、参数文件怎么改、输出怎么验证。
2. WOFOST 源码形态:版本选择、仓库结构与数据流
2.1 Fortran 原版与 Python 重写的取舍
接触 WOFOST 时第一个选择是版本。经典 Fortran 版本(如 WOFOST 7.x)是模型科学的源头,所有算法从这里扩散到 DSSAT、APSIM 等平台。Fortran 版的优点是计算效率高、与历史文献中的参数表完全对应,比如 van Diepen 等 1989 年发表的那套参数说明直接以 Fortran 代码逻辑为参照。缺点是它保留了早期结构化编程的风格,全局变量较多,数据传递路径读起来比较费劲。
Python 版的 pcse 库则是瓦赫宁根研究团队官方维护的重新实现。它的核心益处是模块边界清晰:把 Fortran 版里的 COMMON BLOCK 拆成单独的类,比如Assimilation、Evapotranspiration、Partitioning各自独立,遇到问题可以直接断点到某个类里排查。对于刚上手的人,我一般建议从 pcse 入手跑通整体流程,同时把 Fortran 版的教科书和源码放在手边对照——因为许多参数的原始描述、默认值注释还保留在 Fortran 代码里。
2.2 代码仓库里必须认识的关键文件与目录
以 pcse 库为例,源代码的核心结构大致如下:
pcse/ ├── core.py # 模型运行框架,定义了 run 循环 ├── models/ │ ├── Wofost80.py # WOFOST 8.0 版本的具体组装 │ ├── Wofost71.py # 兼容 7.1 的老版本参数结构 ├── inputs.py # 天气数据、作物参数、土壤参数接入层 ├── outputs.py # 输出变量定义与格式控制 ├── util.py # 通用工具,如湿度计算、日期转换 ├── soil.py # 土壤水分平衡模块 ├── assimilation.py # CO2 同化模块 ├── evapotranspiration.py# 蒸散模块 ├── phenology.py # 物候发育模块 ├── partitioning.py # 干物质分配模块 ├── leaf_dynamics.py # 叶片生长与衰老模块 ├── stem_dynamics.py # 茎生长模块 ├── storage_organ.py # 储存器官(产量)形成模块 ├── root_dynamics.py # 根系生长模块 ├── waterbalance.py # 水分平衡模块 ├── warnings.py # 警告处理 ├── data/ │ ├── cropdata/ # 作物参数文件(cab.par, wwhe.par 等) │ ├── soildata/ # 土壤参数文件(ec1.par, ec3.par 等) │ └── weatherdata/ # 示例气象数据以上是 Python 版的结构参考,每个模块名称在 Fortran 版中也都能找到对应的子程序。
阅读时有几条主线:一是物候链,从phenology.py出发追踪发育阶段怎么算;二是碳链,从assimilation.py出发追踪 CO2 同化产物怎么变成生物量;三是水链,从waterbalance.py出发追踪水分胁迫使气孔关闭的反馈路径。这三条线在models/Wofost80.py的__call__方法中汇合,这就构成了模型运行的主循环。
2.3 主循环的驱动方式与模块间的数据契约
WOFOST 源代码的骨架是“逐日推进”的循环——每天的计算依赖前一天的状态变量,所以本质上是一个离散时间动态系统。在 pcse 中,最外层由时间步进器驱动,从出苗/播种期开始,逐日调用模型,直到达到成熟期或最大物候期。
# 简单示例:手动跑一个生长季 from pcse.base import WeatherDataContainer from pcse.models import Wofost80 # 生成一个简化的天气数据容器 weather = WeatherDataContainer( latitude=31.2, longitude=121.5, elevation=4, day=2000 # 这里仅是示意,实际需逐日提供 )实际使用中不会这样逐日构造天气,而是从pcse.inputs.CABOWeatherDataProvider读取包含逐日数据的文本文件。关键点在于模型的每个模块都接收并返回约定好的变量名,比如:
DVS(发育阶段)TSUM(积温)LAI(叶面积指数)TAGP(地上总生物量)WSO(储存器官重量)
模块之间的耦合由变量名的约定来保证,这就是为什么读源码时看到某个类的方法是带self.kiosk这种字典——它是变量共享总线。理解这一点后再去追具体的算法实现,思路会顺很多。
3. 核心算法在源代码中的实现与参数作用位置
3.1 物候发育模块:积温驱动与源代码中的分阶段处理
物候模型控制作物什么时期进入什么发育阶段。WOFOST 用DVS表示发育状态,出苗为 0,开花通常在 1,成熟为 2。DVS 的推进靠积温TSUM的累计——但它不是简单的逐日温度求和,而是区分营养生长和生殖生长两个阶段,每个阶段有各自的积温上限。
在phenology.py中,核心代码大致逻辑如下:
class DVS_Phenology: """基于积温的发育阶段推进""" def __init__(self, params): self.TSUM1 = params["TSUM1"] # 出苗到开花的有效积温 self.TSUM2 = params["TSUM2"] # 开花到成熟的积温 self.DVS = 0.0 self.TSUM = 0.0 def __call__(self, day, delta_t, weather): # 日平均温度 temp = (weather.TMIN + weather.TMAX) / 2.0 # 有效积温:扣除基点温度 if temp > self.TBASE: dtsum = temp - self.TBASE else: dtsum = 0.0 # 依据当前阶段决定推进增量 if self.DVS < 1.0: # 营养生长期,积温增量直接累加 self.TSUM += dtsum self.DVS = self.TSUM / self.TSUM1 else: # 生殖生长期,只累加开花后的积温 self.TSUM += dtsum self.DVS = 1.0 + (self.TSUM - self.TSUM1) / self.TSUM2 # 发育阶段必须限制在 0~2 之间 self.DVS = min(self.DVS, 2.0) return self.DVS这段代码要注意:TSUM1与TSUM2的实际取值在作物参数文件里。比如春小麦的TSUM1约在 900~1200 ℃·d 区间,冬小麦则要根据春化处理来确定。很多新手调参时只改产量相关的参数,实际上生育期长短由这两个值决定——改错了,后续光合作用、分配全部错位。
3.2 光能利用与 CO2 同化:从光截获到总同化的代码链路
这是 WOFOST 机理最强的部分。模型没有直接用“光能利用率”这种经验系数,而是先算冠层截获的光合有效辐射,再通过光合作用响应曲线求瞬时同化速率,最后对一天的光周期积分。
assimilation.py中的实现核心是高斯积分——因为同化速率在一天内随太阳高度角变化,WOFOST 用三点高斯积分逼近日总同化量。
# WOFOST 日同化量的核心逻辑(简化自 assimilation.py) import math # 大气顶层辐射转化为光合有效辐射的系数 F_PAR = 0.5 def total_assimilation(daytime_temp, radiation, lai, params): """ 计算日总同化量(kg CO2 / ha / day) """ AMAX = params["AMAX"] # 单叶最大光合速率 EFF = params["EFF"] # 光能初始利用效率 KDIF = params["KDIF"] # 散射光消光系数 # 将总辐射换算为光合有效辐射 PAR = radiation * F_PAR # 依据叶面积指数计算截获比例 if lai > 0: f_int = 1.0 - math.exp(-KDIF * lai) else: f_int = 0.0 # 日同化总量 = 入射PAR × 截获比例 × 最大同化速率的函数(密度依赖) assimilation = PAR * f_int * AMAX return assimilation实际源码中会区分阴叶和阳叶、考虑湿度对气孔导度的影响,所以远比上面复杂。但读者抓住这条主线就够用来追踪问题:如果模拟产量偏低,先查 LAI——如果 LAI 上不去,光截获就低,日同化总量自然低,产量不可能高。
EFF表示光响应曲线的初始斜率,通常在 0.45~0.55 kg CO2 / (MJ / m²) 左右;AMAX在 30~70 kg CO2 / ha / h 之间,C3 和 C4 作物差异很大;KDIF约 0.6 左右。调参时注意:AMAX 的调整直接影响产量上限,但绝不能盲目调高——它需要与叶片氮含量、温度响应曲线配套调整。
3.3 干物质分配:DVS 驱动的分配系数表
WOFOST 将每天形成的同化产物按阶段分配到根、茎、叶、储存器官。分配系数是 DVS 的分段线性表,具体数值存在作物参数文件的FR表里。
partitioning.py中有一个关键表:
DVS_FR = [ (0.00, 0.50, 0.35, 0.15, 0.00), (0.50, 0.35, 0.35, 0.30, 0.00), (1.00, 0.15, 0.30, 0.40, 0.15), (2.00, 0.00, 0.10, 0.20, 0.70), ]每行格式是:(DVS, 根分配系数, 茎分配系数, 叶分配系数, 储存器官分配系数)。
逐日计算时先找到当前 DVS 所在区间,用线性插值得到当天的分配比例。这种分段表的特点是“刚性”——某个阶段的分配比例固定不变,它假设环境不改变分配策略。实际上高温或干旱会使更多碳流向根部,如果要模拟这样的胁迫效应,就需要修改partitioning.py的逻辑或引入干旱系数。
我一般建议初学者把主要精力放在这几个模块上:phenology→assimilation→partitioning,先把这三段的因果关系练熟,再碰蒸散和水分平衡。
4. 跑通一个最小 WOFOST 模拟并定位输出变量
4.1 环境准备与最小运行示例
实际操作时,先用 pcse 库跑一个最小案例。需要注意的是 WOFOST 对天气数据格式有严格要求:缺测值用 -999 占位,日期必须是连续日序列。可以用示例数据快速验证安装成功:
pip install pcse # 查看自带的示例数据位置 python -c "import pcse; print(pcse.__file__)"然后写一个最小脚本跑完整个过程:
import sys from pathlib import Path import pandas as pd from pcse.inputs import CABOWeatherDataProvider from pcse.fileinput import PCSEFileReader from pcse.models import Wofost80 from pcse.base import ParameterProvider # 配置数据路径,需要事先下载 WOFOST 的 crop/soil/weather 示例数据 crop_data = PCSEFileReader("cropdata/wwhe.par") soil_data = PCSEFileReader("soildata/ec3.par") weather_data = CABOWeatherDataProvider("weatherdata/示例气象站") # 组装参数提供器 parameters = ParameterProvider(cropdata=crop_data, soildata=soil_data) # 建立模型并运行 wofost = Wofost80(parameters, weather_data, soil_data) wofost.run_till_terminate() # 获取输出结果 output = wofost.get_output() df = pd.DataFrame(output) print(df.tail())这段代码有几个容易踩的坑:
PCSEFileReader要求参数文件编码为 ASCII,含中文注释的文件会报编码错;- 土壤参数文件里的初始含水量字段必须和所选土壤类型匹配,不匹配时模型的蒸发过程会异常;
run_till_terminate()在没有设置最大天数时会一直跑到成熟条件触发。
4.2 输出变量的单位换算与核心检查项
模型输出的原始变量单位与国际制单位不同,这是初读源代码最容易误解的地方:
| 变量名 | 含义 | 输出单位 | 换算成常用单位 |
|---|---|---|---|
LAI | 叶面积指数 | m²/m² | 无需换算 |
TAGP | 地上总生物量 | kg/ha | 直接读数 × 0.001 = t/ha |
WSO | 储存器官干重 | kg/ha | 直接读数 × 0.001 = t/ha |
TRA | 作物实际蒸腾 | cm/day | × 10 = mm/day |
SM | 根区土壤含水量 | cm³/cm³ | 无需换算 |
验证模型合理性时按这个顺序来:先看物候——DVS是否在预期天数达到 1 和 2;再看LAI峰值——一般作物在 4~6 之间,过大或过小都说明光截获参数或分配系数有问题;最后看WSO与TAGP的比例,收获指数是否处于合理范围。
4.3 用 Python 批量跑多个站点或年份
实际业务中不会只跑单站。借助 Python 脚本可以批量模拟:
import glob import pandas as pd from pcse.models import Wofost80 from pcse.base import ParameterProvider from pcse.fileinput import PCSEFileReader, CABOWeatherDataProvider results = [] weather_files = glob.glob("weather/*.csv") crop_params = PCSEFileReader("cropdata/wwhe.par") soil_params = PCSEFileReader("soildata/ec3.par") for weather_file in weather_files: # 从文件名提取站点名称 station = Path(weather_file).stem weather_data = CABOWeatherDataProvider(weather_file) parameters = ParameterProvider(cropdata=crop_params, soildata=soil_params) wofost = Wofost80(parameters, weather_data, soil_data) wofost.run_till_terminate() output = pd.DataFrame(wofost.get_output()) # 提取出苗至成熟的终点时刻数据 final = output.iloc[-1] results.append({ "station": station, "maturity_date": final["day"], "WSO": final["WSO"], "TAGP": final["TAGP"], "LAI_max": output["LAI"].max(), }) summary = pd.DataFrame(results)这段代码比单站版本多了两层:一是遍历所有天气文件,二是从完整输出里提取成熟期终值。注意运行多个站点时,需要为每个站点重新创建Wofost80实例——因为模型的内部状态是有记忆的,复用同一个实例会让后一个站点的初始状态承接前一个站点的结束状态。
4.4 参数文件的直接修改与敏感性初判
WOFOST 的参数文件是文本格式,直接编辑即可。以作物参数为例,关键字段如下:
AMAX = 45.0 # 单叶最大光合速率 [kg CO2/ha/hr] EFF = 0.50 # 光能初始利用效率 [kg CO2/(MJ/m2)] TSUM1 = 1000.0 # 出苗到开花积温 [degC·d] TSUM2 = 800.0 # 开花到成熟积温 [degC·d] TBASE = 0.0 # 发育基点温度 [degC] KDIF = 0.6 # 散射辐射消光系数 RGRLAI = 0.008 # 早期叶面积指数相对增长率 [1/day]做区位迁移研究时,优先调TSUM1/TSUM2匹配当地观测的生育期;做水分胁迫研究时,优先检查soil.par的土壤水分特征曲线参数,而不是调作物的光合参数。
5. 从源代码延伸:模型替换、加速运算与气象数据接口
5.1 基于源代码替换光合模块的一个实践技巧
WOFOST 的光合模块假定每日同化速率只受温度和辐射影响,在极端高温(>38℃)场景下模拟值常常偏高。常见做法是修改assimilation.py,把高温胁迫引入 AMAX 的日衰减:
# 自定义的日同化模块(示意) def calc_assimilation_with_heat_stress(TMAX, radiation, lai, params): """ 把极端高温对光合的抑制加入原模型 """ amax = params["AMAX"] # 高温抑制曲线:>35℃ 后线性衰减 if TMAX > 35.0: reduction = max(0.0, 1.0 - (TMAX - 35.0) * 0.05) amax *= reduction # 其余逻辑复用原始代码 ...修改源代码后需要做的验证是:跑一遍对照组的旧版本,比较产量和 LAI 的差值,确认只有高温日的同化量发生变化,而不是储入了系统性偏差。
5.2 气象数据接口:把 NetCDF 转成 WOFOST 可读格式
实际操作中常遇到气象数据是 NetCDF 格式,而 WOFOST 需要逐日文本。转换时注意单位:
import xarray as xr import pandas as pd ds = xr.open_dataset("era5_daily.nc") lat, lon = 31.2, 121.5 # 选择最近的格点 ds_point = ds.sel(latitude=lat, longitude=lon, method="nearest") # 构建 WOFOST 所需的字段 df = pd.DataFrame({ "DAY": ds_point.time.values, "IRRAD": ds_point.ssrd.values / 1e6, # J/m2 -> MJ/m2 "TMIN": ds_point.t2m.values - 273.15, # K -> C "TMAX": ds_point.t2m.values - 273.15, "VAP": ds_point.d2m.values, "WIND": ds_point.wind_speed.values, # 需要单独变量或从 u/v 合成 "RAIN": ds_point.tp.values * 1000, # m -> mm }) df.to_csv("weather_station.csv", index=False)WOFOST 的天气格式中IRRAD是全天天顶辐射(MJ/m²/day),注意别和光合有效辐射混淆。VAP是水汽压,单位 kPa——ERA5 的露点温度需要先换算成水汽压再填充。
5.3 加速大规模模拟的两个实用策略
面积较大的应用需要模拟成千上万个格点。WOFOST 每个格点就是一次完整的时间循环,串行很慢。常见做法:
- 多进程并发:
multiprocessing.Pool按格点并行,网格数据适合 CPU 密集场景。 - 相关性裁剪:大面积运行时很多格点气候相似,可以先把气象数据聚类,每类只跑一次,再用空间插值恢复全区域。这种方法做省级估产时加速比可以到 20 倍以上。
from multiprocessing import Pool def run_one_grid(grid_info): lat, lon = grid_info weather = load_weather_for_point(lat, lon) wofost = Wofost80(parameters, weather, soil) wofost.run_till_terminate() # 取出终点产量 output = pd.DataFrame(wofost.get_output()) return {"lat": lat, "lon": lon, "yield": output["WSO"].iloc[-1]} if __name__ == "__main__": grid_cells = [(lat, lon) for lat in range(28, 35) for lon in range(118, 123)] with Pool(processes=8) as pool: result = pool.map(run_one_grid, grid_cells)注意 Windows 系统上需要把Pool调用放在if __name__ == "__main__":下,否则进程递归执行时会报错。
5.4 检验你的修改是否偏离原始模型的基准测试策略
修改源码后,判断是否偏离基准,最好用同一套天气、参数、土壤数据跑修改前后两个版本,输出逐日的 DVS、LAI、TAGP、WSO 四组变量,计算均方根误差:
from sklearn.metrics import mean_squared_error import numpy as np rmse = np.sqrt(mean_squared_error(base_df["WSO"], modified_df["WSO"])) print(f"WSO RMSE = {rmse:.2f} kg/ha")WSO 的 RMSE 在营养生长阶段应当接近 0(因为此时 WSO 通常为 0),到灌浆期后才无意义——因此更合理的做法是把对比区间限定在开花以后。此外检查 LAI 的峰值,如果 LAI 在所有时段都系统性偏低,说明光合参数修改引入了与辐射无关的偏移;如果仅高温日偏离,则说明胁迫逻辑起效了。
代码修改的边界也要明确:不要为了拟合一个站点的产量而把AMAX调到 100——这会让模型对辐射的响应完全失真。每一步代码修改都要有农学依据,否则模拟结果只是数字游戏。
本文还有配套的精品资源,点击获取