规则与执行:input、output、run、shell、log指令,默认规则
写 Snakemake 工作流,说白了就是在定义规则。规则是工作流的最小执行单元。你告诉 Snakemake:「我要用什么输入,得到什么输出,怎么运行。」它就照着做。
这一章我们拆开来看规则里的几个核心字段。弄明白它们,你的流水线才算有了骨架。
一条规则的完整结构
先看一个最简单的例子:
rule align:
input:
"data/sample.fastq"
output:
"results/aligned.bam"
log:
"logs/align.log"
shell:
"bwa mem ref.fa {input} > {output} 2> {log}"
嗯,这几行其实已经包含了规则的所有关键部分。我一个个讲。
input —— 你拿什么干活
input 定义的是规则的「原材料」。可以是一个文件,也可以是多个。
rule merge:
input:
"chunk1.bam",
"chunk2.bam",
"chunk3.bam"
output:
"merged.bam"
shell:
"samtools merge {output} {input}"
注意看,{input} 在 shell 里会被展开成完整列表。如果顺序重要,你最好在 input 里写清楚。我在项目中遇到过几次因为输入顺序不对导致后续分析出错的坑,后来养成了按字母或者按预期顺序排列的习惯。
expand() 函数生成列表。input: expand("data/{sample}.fastq", sample=SAMPLES) 会一次展开所有样本。
output —— 你产出什么
output 是规则的「交付物」。Snakemake 用 output 来检查工作有没有完成。如果某个规则的上游输出文件不存在,它就不会往下走。
rule call_variants:
output:
"results/variants.vcf.gz"
shell:
"bcftools call -v -Oz {input} > {output}"
temp() 标记临时文件。output: temp("tmp.bam") 表示这个文件用完后自动删除。我刚开始时经常忘记清理中间文件,结果一个流水线跑完,磁盘被塞爆了……
run —— 用 Python 写执行逻辑
有些任务不适合用 shell 完成,比如:需要读取 Python 变量、调用 API、或者做复杂的条件判断。这时候用 run 块。
rule stats:
input:
"data/input.bed"
output:
"results/stats.txt"
run:
with open(input[0]) as f:
lines = [l.strip() for l in f]
with open(output[0], "w") as out:
out.write(f"Total lines: {len(lines)}\n")
在 run 块里,input、output、log 都是 Python 列表或字符串,可以直接操作。我个人喜欢在需要数据清洗或者格式转换时用 run,写起来比 shell 命令清爽多了。
shell —— 命令行选手的首选
shell 块就是直接扔给 bash 执行的字符串。大部分生信工具都是用命令行调用的,所以 shell 是使用频率最高的字段。
rule index:
input:
"assembly.fasta"
output:
"assembly.fasta.fai"
shell:
"samtools faidx {input}"
你可以在 shell 里用 {input}、{output}、{log}、{params} 这些占位符。Snakemake 会自动替换成真实路径。
if 之类的复杂逻辑,那是 run 该干的活。
log —— 记录运行痕迹
log 字段常常被新手忽略。但等你跑一个需要几小时的任务时,你会感谢它的存在。
rule assemble:
input:
"reads.fastq"
output:
"assembly.fasta"
log:
"logs/assemble.log"
shell:
"spades.py -o /tmp/spades_out -1 {input} 2> {log}"
你看,我把 stderr 重定向到了 {log} 指定的文件。之后如果任务失败,我只需要看日志文件,不用再翻控制台输出。
默认规则 —— 来自 deep 深处的全局回退
有时候你会发现,有些参数在很多规则里反复出现。比如每个规则都要用同一个线程数,或者同一个内存限制。这时候可以用 default 搞定。
default:
threads: 8
resources:
mem_mb = 16000
写了这个之后,所有没有显式指定 threads 的规则都会默认用 8 个线程。我曾经在项目里把默认内存设得太小,结果一个大样本分析直接 OOM 了……后来改成 32G 才解决问题。所以默认值一定要依据你的数据规模来定。
把这些串起来:一张流程图
下面这张图展示了规则的核心逻辑。从 input 读进来,经过 shell 或 run 处理,产出 output,同时 log 记录过程。默认规则为整个工作流提供全局默认值。
几个容易踩的坑
- 同名输出冲突:两个规则不能输出同一个文件。Snakemake 会报错。我在项目中遇到过因为粗心复制粘贴,结果两个规则输出同名文件导致流水线崩溃。
- input 文件路径写错:建议用绝对路径或者相对工作目录的写法。我曾经因为相对路径计算错误,浪费了半天调试。
- log 路径的目录不存在:log 指定的目录需要手动创建,或者用
shell: "mkdir -p logs && ..."提前创建。否则 Snakemake 不会自动创建 log 目录。
实战建议
| 字段 | 适合场景 | 我个人常用的习惯 |
|---|---|---|
| input | 声明依赖文件 | 用 expand() 生成多文件列表 |
| output | 声明最终产物 | 对临时文件加 temp() |
| run | 需要 Python 逻辑 | 尽量把逻辑封装成函数,run 里只调用 |
| shell | 直接调命令行 | 长命令用反斜杠换行保持可读性 |
| log | 记录运行日志 | 始终把 stderr 重定向到 log |
| default | 全局默认值 | 设合理的线程数和内存,避免整体资源浪费 |
这一套组合拳打下来,你的规则既清晰又健壮。接下来要做的就是理解 Snakemake 怎么根据文件依赖自动确定执行顺序了。