上篇聊完对流方案怎么选,师弟把文献筛完了,候选池里列了几套方案。然后他跑来问我第二个问题:师兄,我是在超算上手动创建四个文件夹,一个一个改 namelist 再一个一个提交吗?
我说你可以先跑完一个,剩下的看看我发你的脚本。
上篇说过,批量试验的核心原则是「控制变量」——所有组除了 cu_physics 之外,其他参数必须完全一致。你手动改四遍 namelist,稍微手滑多删了一个逗号,或者复制漏了一行 cudt,四组结果之间的差异就不再单纯是方案差异,而是混合了你操作失误的噪音。这种事后评估的时候根本发现不了,你只会觉得某个方案效果差,但其实是你 namelist 写错了。
所以这一篇讲讲怎么用脚本批量生成实验目录,一键改 namelist,让你的对照组真正「干净」。
批量试验的机械性操作无非三件事:克隆文件、修改参数、提交作业。动手前先把这个流程想清楚:

image
这个流程说起来简单,但手动作业的时候最容易出错的地方恰恰在 B→D 这两步。脚本的价值就是把这两步变成一行命令,消除人为差异。
下面这个脚本我写了比较详细的注释,你可以直接复制到超算上改几个变量就能用。我不贴完整的一坨代码,分块来讲。
这是你唯一需要改的地方。路径换成你自己的,实验方案按你的候选池填。
# 基础 namelist 路径(以此为模板,sed 替换参数)
BASE_NAMELIST="/home/yourname/wrf_project/ref_run/namelist.input"
# 参考目录(已包含 wrf.exe、met_em、wrfbdy、wrfinput 等所有必需文件)
SOURCE_DIR="/home/yourname/wrf_project/ref_run"
# 所有实验放在一个根目录下
EXP_BASE="/home/yourname/wrf_project/experiments"
# 文件复制方式:link 省空间但要求不删源文件,copy 稳妥但占磁盘
COPY_MODE="link"
这里 COPY_MODE 值得多说两句。WRF 的 met_em 文件和 wrfinput 动辄几十 GB,如果你的候选方案有五六组,copy 模式会直接把磁盘打满。link 模式创建软链接,只占 inode 不占数据块,但有个隐患——源目录被误删了所有实验目录都会断链。我的习惯是:参考目录放在一个专门的路径下,只读不写,然后所有实验目录 link 过去。这样既省空间,又不担心误操作。
这一块是核心。每一行定义一个实验组,格式是 实验名|mp_physics|cu_physics|bl_pbl_physics|sf_sfclay_physics|备注。
EXPERIMENTS=(
"OLD|9, 8, 8|1, 0, 0|2, 2, 2|2, 2, 2|"
"EXP_A|8, 8, 8|1, 0, 0|2, 2, 2|2, 2, 2|"
"EXP_B|9, 8, 8|6, 0, 0|2, 2, 2|2, 2, 2|"
"EXP_C|8, 8, 8|14, 0, 0|2, 2, 2|2, 2, 2|"
"EXP_D|8, 8, 8|1, 0, 0|2, 2, 2|2, 2, 2|"
)
这里有几个设计细节你可能注意到:
为什么 cu_physics 的 d02 和 d03 都是 0? 因为 d02 设的 9 km,d03 设的 3 km,按上篇讲的灰区原则,3 km 内层直接关对流参数化(cu_physics = 0)。如果你的嵌套方案不同,自己改数字就好。
为什么每组实验名前面都保留了一个 OLD 作为基线? 这个习惯很重要。批量对比里必须有一组参考基线,一般是你们课题组之前一直在用的默认配置,或者文献里最常见的组合。没有基线,你跑完四组不同的结果,说不出谁好——你只能说 A 最优,但不知道 A 比默认配置提升了多少。
备注列写清楚改动逻辑。两个月后你回头看你跑过的实验,大概率已经忘了 EXP_C 改了哪个参数、为什么这么改。备注就是给你未来的自己看的。
clone_item() {
local src="$1"
local dst="$2"
local mode="$3"
local basename=$(basename "$src")
# 跳过 wrfout 和日志文件——这些在参考目录里可能是残留物
case"$basename"in
wrfout*|rsl.out*|rsl.error*|slurm-*.out|*.log|namelist.input|run.log)
return 0
;;
esac
if [[ "$mode" == "copy" ]]; then
[[ -d "$src" ]] && cp -rL "$src""$dst" || cp -L "$src""$dst"
else
ln -sf "$src""$dst"
fi
}
这里的坑有两个。
第一个是 wrfout* 这类输出文件。如果你的参考目录之前跑过一轮,里面可能残留了上一次的 wrfout,几十上百 GB。如果不跳过它们,link 或 copy 过去之后每个实验目录都会多出一堆废数据,然后你在超算上可能收到管理员的「磁盘超额」邮件。
第二个是 namelist.input。参考目录里会有一个模板 namelist,但我们要写入每组的定制版本,所以克隆时跳过它,克隆完再单独写。
sed \
-e "s|^[[:space:]]*mp_physics[[:space:]]*=.*,[[:space:]]*$| mp_physics = ${MP_PHYSICS},|" \
-e "s|^[[:space:]]*cu_physics[[:space:]]*=.*,[[:space:]]*$| cu_physics = ${CU_PHYSICS},|" \
-e "s|^[[:space:]]*bl_pbl_physics[[:space:]]*=.*,[[:space:]]*$| bl_pbl_physics = ${PBL_PHYSICS},|" \
-e "s|^[[:space:]]*sf_sfclay_physics[[:space:]]*=.*,[[:space:]]*$| sf_sfclay_physics = ${SFC_PHYSICS},|" \
"$BASE_NAMELIST" > "$OUT_NAMELIST"
很多人看到这一串 sed 就头大,我说一下为什么用 sed 而不是重新生成整个 namelist。
namelist.input 不是只有物理参数这一节。&time_control、&domains、&dynamics 里可能有几十行,全是跟你的个例、区域、起报时间绑定的,这些在批量对比里必须一模一样。如果重新生成整个文件,你等于要维护两份 namelist(一份完整模板加一份参数对照表),改一次起报时间就要同步两处——迟早会忘。
sed 的方案是「基于一个已验证的完整 namelist,只替换需要变化的四行」,其余原封不动保留。这是控制变量的最小侵入方式。
但这并不意味着 sed 的正则表达式可以闭着眼睛写。 上面那四个正则里 ^[[:space:]]* 匹配行首可能的空格,=.*, 匹配等号后面任意内容直到行末逗号。如果你的 namelist 里参数后面没有逗号(比如 WRF 4.5 某些配置下不强制要求逗号结尾),这条正则就匹配不上——sed 不会报错,只会静默跳过,最后你跑的四组实验用的还是同一个 cu_physics。
所以脚本末尾我加了一个校验步骤:
grep -E "mp_physics|cu_physics|bl_pbl_physics|sf_sfclay_physics" "$OUT_NAMELIST" | sed 's/^/ /'
它在每次写入后打印替换结果。不要嫌烦,每次跑之前扫一眼输出,确认四组数字确实不一样。
下面是完整脚本。保存为 gen_experiments.sh,在超算上 bash gen_experiments.sh 即可。
#!/bin/bash
# =============================================================================
# WRF namelist 物理方案批量修改脚本(含自动创建实验目录)
# 用途: 基于同一个参考目录,自动克隆出多个实验目录并修改 namelist
# 前提: 参考目录内含完整的 wrf.exe、met_em、表格文件、wrfbdy、wrfinput 等
# =============================================================================
set -euo pipefail
# ============================================================================
# 0. 用户配置区(按需修改)
# ============================================================================
# --- 基础 namelist 路径 ---
# 文件中必须已存在 mp_physics / cu_physics / bl_pbl_physics / sf_sfclay_physics 行
BASE_NAMELIST="/home/yourname/wrf_project/ref_run/namelist.input"
# --- 参考目录(已配置完整的 WRF 运行环境)---
SOURCE_DIR="/home/yourname/wrf_project/ref_run"
# --- 实验根目录 ---
EXP_BASE="/home/yourname/wrf_project/experiments"
# --- 文件复制模式: "link" 创建软链接(省空间), "copy" 硬拷贝 ---
COPY_MODE="link"
# --- 实验方案定义: 实验名|mp|cu|pbl|sfc|注释 ---
# 每组三个域 (d01, d02, d03),值之间用逗号分隔
EXPERIMENTS=(
"OLD|9, 8, 8|1, 0, 0|2, 2, 2|2, 2, 2|"
"EXP_A|8, 8, 8|1, 0, 0|2, 2, 2|2, 2, 2|"
"EXP_B|9, 8, 8|6, 0, 0|2, 2, 2|2, 2, 2|"
"EXP_C|8, 8, 8|14, 0, 0|2, 2, 2|2, 2, 2|"
"EXP_D|8, 8, 8|1, 0, 0|2, 2, 2|2, 2, 2|"
)
# ============================================================================
# 1. 辅助函数
# ============================================================================
log_info() { echo -e "\033[32m[INFO]\033[0m $*"; }
log_warn() { echo -e "\033[33m[WARN]\033[0m $*"; }
log_error() { echo -e "\033[31m[ERROR]\033[0m $*"; }
# 复制或链接文件/目录
clone_item() {
local src="$1"
local dst="$2"
local mode="$3"
local basename=$(basename "$src")
# 跳过输出类文件和日志
case"$basename"in
wrfout*|rsl.out*|rsl.error*|slurm-*.out|slurm-*.err|*.log|namelist.input|run.log)
return 0
;;
esac
if [[ "$mode" == "copy" ]]; then
if [[ -d "$src" ]]; then
cp -rL "$src""$dst" 2>/dev/null || true
else
cp -L "$src""$dst" 2>/dev/null || true
fi
else
ln -sf "$src""$dst" 2>/dev/null || true
fi
}
# ============================================================================
# 2. 主程序
# ============================================================================
echo"============================================"
echo"WRF namelist 物理方案批量修改"
echo"参考目录: ${SOURCE_DIR}"
echo"实验根目录: ${EXP_BASE}"
echo"复制模式: ${COPY_MODE}"
echo"============================================"
# 基础校验
if [[ ! -f "$BASE_NAMELIST" ]]; then
log_error "基础 namelist 不存在: $BASE_NAMELIST"
exit 1
fi
if [[ ! -d "$SOURCE_DIR" ]]; then
log_error "参考目录不存在: $SOURCE_DIR"
exit 1
fi
mkdir -p "$EXP_BASE"
for exp_def in"${EXPERIMENTS[@]}"; do
IFS='|'read -r EXP_NAME MP_PHYSICS CU_PHYSICS PBL_PHYSICS SFC_PHYSICS DESC <<< "$exp_def"
EXP_DIR="${EXP_BASE}/${EXP_NAME}"
OUT_NAMELIST="${EXP_DIR}/namelist.input"
echo""
echo"--- 实验: ${EXP_NAME} ---"
echo" 描述: ${DESC}"
# --- 2.1 创建实验目录并克隆文件 ---
if [[ ! -d "$EXP_DIR" ]]; then
log_info "创建实验目录: ${EXP_DIR}"
mkdir -p "$EXP_DIR"
log_info "从参考目录克隆文件 (mode=${COPY_MODE})..."
for item in"$SOURCE_DIR"/*; do
[[ -e "$item" ]] || continue
clone_item "$item""$EXP_DIR/""$COPY_MODE"
done
# 删除参考目录带过来的 namelist(后面会写入定制版)
rm -f "$EXP_DIR/namelist.input"
log_info "目录初始化完成"
else
log_warn "目录已存在,跳过文件克隆,仅更新 namelist.input"
fi
# --- 2.2 写入 namelist ---
sed \
-e "s|^[[:space:]]*mp_physics[[:space:]]*=.*,[[:space:]]*$| mp_physics = ${MP_PHYSICS},|" \
-e "s|^[[:space:]]*cu_physics[[:space:]]*=.*,[[:space:]]*$| cu_physics = ${CU_PHYSICS},|" \
-e "s|^[[:space:]]*bl_pbl_physics[[:space:]]*=.*,[[:space:]]*$| bl_pbl_physics = ${PBL_PHYSICS},|" \
-e "s|^[[:space:]]*sf_sfclay_physics[[:space:]]*=.*,[[:space:]]*$| sf_sfclay_physics = ${SFC_PHYSICS},|" \
"$BASE_NAMELIST" > "$OUT_NAMELIST"
log_info "已写入 namelist: ${OUT_NAMELIST}"
# 校验替换结果
grep -E "mp_physics|cu_physics|bl_pbl_physics|sf_sfclay_physics""$OUT_NAMELIST" | sed 's/^/ /'
done
echo""
echo"============================================"
echo"全部完成。请进入各实验目录手动提交 Slurm 作业。"
echo""
echo"示例:"
echo" cd ${EXP_BASE}/EXP_A && sbatch run_wrf.slurm"
echo"============================================"
脚本执行完,你的 experiments/ 目录下会有这些文件夹:
experiments/
├── OLD/
├── EXP_A/
├── EXP_B/
├── EXP_C/
└── EXP_D/
每个文件夹里都有完整的 WRF 运行环境,且 namelist 中的四行关键参数已经替换为对应方案的值。接下来逐一手动 sbatch 提交,或者自己再加一个循环自动提交。
# 手动逐个提交(推荐,可以在提交前再检查一下 namelist)
for exp in OLD NEW_BAD EXP_A EXP_B EXP_C EXP_D; do
echo "提交 ${exp}..."
cd "${EXP_BASE}/${exp}" && sbatch run_wrf.slurm
done
1. namelist 里参数结尾有没有逗号,sed 正则的行为完全不同。 上面脚本假设所有参数行以逗号结尾(WRF 标准写法)。如果你的 namelist 里某一行没有逗号,比如 mp_physics = 6, 8, 8 写成了 mp_physics = 6 8 8(没逗号但 WRF 其实能读),那正则 =.*, 匹配不到,sed 跳过不报错。你跑半天发现所有组 mp_physics 都一样——检查方法就是看脚本末尾的 grep 校验输出是不是四组不同的数字。
2. link 模式下修改参考目录文件会影响所有实验。 假设你用 COPY_MODE="link",然后某天手滑在 ref_run/ 下改了一个 wrf.exe 重新编译了一版。所有实验目录因为软链接指向同一个文件,会全部用上新版 executable。
师弟按我给的脚本跑了一轮。两天之后他发消息说,师兄,不好,超算账号余额蒸发了
完啦,good luck,快呼叫导师。