Snakemake基础:规则定义、输入输出、目标规则
好,我们正式开始搭建流水线的核心零件——规则(rule)。
我个人觉得,Snakemake 的灵魂就在规则里。它就像一个精密的加工指令:
- 输入什么原料?
- 输出什么产品?
- 用什么方法加工?
这三者构成了 Snakemake 的最小工作单元。理解透彻了,后面再复杂的流程也不过是这些单元的排列组合。
规则的基本骨架
先看一个最简单的例子。假设我们要对原始数据进行质量过滤:
rule fastq_filter:
input:
"data/sample1.fastq"
output:
"results/sample1_clean.fastq"
shell:
"fastp -i {input} -o {output} -q 20"
这条规则做了三件事:
- 定义名字:`fastq_filter`,方便管理和排错。
- 声明输入输出:告诉 Snakemake 依赖关系。
- 执行命令:用 `shell` 指令包裹具体操作。
我在项目中遇到过不少新手把输入路径写错,结果跑了半天才发现输出文件是空的。嗯,路径一定要仔细。
规则的本质:Snakemake 通过 output 文件来推导整个依赖关系图。它会反向查找——想要 output,就得先完成 input 对应的规则。
输入input的灵活用法
输入不一定是单个文件。实际项目中,我们经常需要多个输入:
rule align_reads:
input:
ref="genome/hg38.fa",
reads=["data/sample1_R1.fastq", "data/sample1_R2.fastq"]
output:
"results/sample1.bam"
shell:
"bwa mem {input.ref} {input.reads} > {output}"
为什么这里要用命名输入?说白了,就是在命令行里调用更方便。你可以用 {input.ref} 指名道姓地引用,避免出错。
小提示:当输入文件超过 3 个时,我建议全部用命名方式。这样规则的可读性会直线上升。之后别人看你的 Snakefile 也会轻松很多。
输出output与通配符
输出是规则的“身份证”。Snakemake 用输出来决定这条规则要不要跑。如果输出文件已经存在且比输入新,它就跳过。
但这里有个更强大的机制——通配符(wildcards)。
rule convert_to_bed:
input:
"results/{sample}.bam"
output:
"results/{sample}.bed"
shell:
"bedtools bamtobed -i {input} > {output}"
你看,{sample} 就是一个通配符。它会自动从一个全局的输入文件名里继承具体值。比如目标文件是 results/SRR123.bed,Snakemake 就会自动推导:
- 输入为
results/SRR123.bam wildcards.sample = "SRR123"
为什么要这样?你想想看,没有通配符,你就得为每个样本写一条规则,那得多恐怖。有了它,一条规则管几千个样本。
警告:通配符的名字必须在 input 和 output 中保持一致。如果你在输出里写成 {sample}.bed,输入里却写成 {sample_name}.bam,Snakemake 会报错。我曾经在这个坑里卡了半小时。
目标规则:指定执行入口
Snakemake 怎么知道该从哪条规则开始?答案是目标规则(target rule)。
默认情况下,它会执行 Snakefile 中的第一条规则。所以,我们通常把第一条规则定义成一个“总指挥”:
rule all:
input:
"results/SRR001.bed",
"results/SRR002.bed",
"results/SRR003.bed"
这条 rule all 只定义了 input,没有 output 和 shell。它的作用就是告诉 Snakemake:“嘿,我要最终得到这些文件,你帮我把前面所有依赖的规则都跑完。”
在实际项目中,我会把 rule all 的 input 写成需要批量生成的文件列表,配合通配符或函数动态生成。
规则使用的完整表格对比
| 组件 | 是否必须 | 作用 | 注意事项 |
|---|---|---|---|
input | 否 | 声明依赖文件 | 支持列表、字典和函数 |
output | 否(有例外) | 声明生成文件 | 通配符必须匹配 input |
shell | 可选 | 执行命令行指令 | 与 run 互斥 |
run | 可选 | 执行 Python 代码 | 用于复杂逻辑处理 |
params | 否 | 传递额外参数 | 在 shell 中用 {params.xxx} |
log | 否 | 声明日志文件 | 避免标准输出被截断 |
用run替换shell:什么时候用?
如果加工逻辑很简单,用 shell 就够。但如果涉及到条件判断、循环或者复杂的数据处理,我会选择 run 块:
rule quality_report:
input:
"results/{sample}.bam"
output:
"results/{sample}_report.txt"
run:
import subprocess
result = subprocess.run(
["samtools", "flagstat", input[0]],
capture_output=True, text=True
)
with open(output[0], "w") as f:
f.write(result.stdout)
run 块里你可以写任意 Python 代码。说白了,就是把 Snakemake 当作一个“工作流调度器 + Python 脚本引擎”来用。
避坑指南:常见错误
这些年我见过很多人在规则定义上翻车,这里列几个典型的:
- 输入文件和输出文件同目录:Snakemake 会认为输出没生成,陷入死循环。
- 通配符未在
input中出现:输出里有{sample},但输入是固定路径,Snakemake 不知道 sample 的值从哪来。 - 遗漏
rule all:默认执行第一条规则,如果那条规则不是总目标,你就只能得到中间产物。
调试技巧:当你对一条规则的执行有疑惑时,可以加 --dry-run -p 参数。Snakemake 会打印出它计划执行的每条命令,但并不会真的运行。这是我最常用的调试手段。
小结一下
规则定义的三个基石:input、output、shell/run。理解通配符和目标规则,你就掌握了 Snakemake 的调度核心。之后的章节里,我们会在这个基础上叠加参数、日志、资源控制和条件分支,但核心引擎从未改变。
你只要记住一句话:给我输出,剩下的逻辑 Snakemake 帮你推导。