Snakefile 基本结构:规则、输入、输出、shell 命令
Snakefile 是 Snakemake 的核心配置文件。说白了,它就是告诉 Snakemake 你要做什么、用到哪些数据、产出哪些东西。很多初学者一上来就被 Python 语法和 rule 嵌套吓住,其实根本不用慌。
我个人习惯把 Snakefile 拆成三部分来看:规则(rule)、输入输出(input/output)、以及真正干活的 shell 命令。今天咱们就把这三块讲透。
规则(rule)—— 管线的“零件”
每条 rule 就是流水线上的一道工序。比如在 GATK 管线上,你会有 bwa_alignment、mark_duplicates、base_recalibrator 这样的规则。它们各自只干一件事,组合起来就是完整的变异检测流程。
我曾经见过有人把整个基因组比对、去重、重校正塞进同一条 rule 里……说真的,那调试起来简直要命。所以我的建议是:一条 rule 只做一个原子操作。
rule bwa_alignment:
input:
"data/sample_1.fastq.gz"
output:
"results/sample_1.sorted.bam"
shell:
"""
bwa mem -t 4 ref/hg38.fa {input} | samtools sort -o {output}
"""
注意看,这里的 input 和 output 都是字符串路径,shell 里用花括号 {input}、{output} 引用它们。这是最基础的写法。你想想看,如果把 input 写成 ["data/sample_1.fastq.gz"] 列表也行,但那样在 shell 中引用时需要多一层 {input[0]},我个人觉得非必要不折腾。
输入(input)—— 上游的“半成品”
输入可以是本地文件,也可以是其他规则的输出。Snakemake 会自动根据文件名匹配规则依赖。举个例子:假如 mark_duplicates 这个规则的输入是 "results/sample_1.sorted.bam",而恰好 bwa_alignment 规则的输出也是这个文件,那 Snakemake 就会自动先跑 bwa_alignment,再跑 mark_duplicates。
rule mark_duplicates:
input:
"results/sample_1.sorted.bam"
output:
"results/sample_1.dedup.bam"
shell:
"""
gatk MarkDuplicates \
-I {input} \
-O {output} \
-M results/sample_1.metrics.txt
"""
这里有个小坑:文件路径必须完全一致,包括目录和扩展名。我之前在项目里因为路径里多了一个 ./,结果 Snakemake 把它当成两个不同的文件,死活不匹配依赖关系。排查了半小时……嗯,从那以后我统一用绝对路径或固定相对路径。
输出(output)—— 你想保留的结果
输出文件是每条 rule 的“产品”。Snakemake 会把输出文件作为下一级规则的输入候选。但要注意:输出文件不需要显式声明所有中间产物。比如你只需要最终的 VCF,那么中间 BAM 文件只要被某个规则作为输入引用,Snakemake 就会自动保留。如果你不引用它,Snakemake 可能会在管线结束后删除它(这由 --delete-all-output 或资源限制决定)。
我习惯把所有重要输出放在 results/ 下,按样本名组织。这样既清晰,又便于并行。对于临时文件,比如 sample_1.sam(BWA 的原始输出),如果后续不需要,我会在 shell 命令里顺手删除,省得占空间。
{sample} 让规则适配多个样本。比如 output: "results/{sample}.sorted.bam",这样一条规则就能处理所有样本,无需重复写。
shell 命令—— 真正干活的代码
shell 块里写的就是你平时在终端敲的命令。Snakemake 会用 Python 的 subprocess 执行它。花括号里可以引用 input、output、params(自定义参数)、wildcards(通配符值)等。
我遇到过一个常见问题:shell 命令太长,比如 GATK 的命令行动辄几十个参数。这时候 不要硬拼成一行,用三对引号包起来,每条参数写一行,反斜杠换行,可读性会好很多。
rule base_recalibrator:
input:
bam = "results/{sample}.dedup.bam",
known_sites = "known_sites/hg38.dbsnp.vcf"
output:
"results/{sample}.recal.table"
params:
ref = "ref/hg38.fa"
shell:
"""
gatk BaseRecalibrator \
-R {params.ref} \
-I {input.bam} \
--known-sites {input.known_sites} \
-O {output}
"""
你看,我用 input.bam 和 input.known_sites 分别引用两个输入文件。这样语义清晰,以后改输入路径只动 rule 头部,不用改 shell 正文。
{input} 如果对应的是列表,会被自动展开成空格分隔的字符串。但如果你需要更复杂的处理,比如遍历列表,那就得用 Python 助手的 expand 函数或者在 shell 里写循环。
一个完整的迷你 GATK 管线示例
下面我把上面几条规则串起来,展示一个从 FASTQ 到重校正 BAM 的 Snakefile 片段。你可以看到规则之间是如何通过文件名自动链接的。
rule all:
input:
"results/sample_1.recal.bam"
rule bwa_alignment:
input:
"data/sample_1.fastq.gz"
output:
"results/sample_1.sorted.bam"
shell:
"""
bwa mem -t 4 ref/hg38.fa {input} | samtools sort -o {output}
"""
rule mark_duplicates:
input:
"results/sample_1.sorted.bam"
output:
"results/sample_1.dedup.bam"
shell:
"""
gatk MarkDuplicates -I {input} -O {output} -M results/sample_1.metrics.txt
"""
rule base_recalibrator:
input:
bam = "results/sample_1.dedup.bam",
known_sites = "known_sites/hg38.dbsnp.vcf"
output:
table = "results/sample_1.recal.table"
shell:
"""
gatk BaseRecalibrator -R ref/hg38.fa -I {input.bam} --known-sites {input.known_sites} -O {output.table}
"""
rule apply_bqsr:
input:
bam = "results/sample_1.dedup.bam",
table = "results/sample_1.recal.table"
output:
"results/sample_1.recal.bam"
shell:
"""
gatk ApplyBQSR -R ref/hg38.fa -I {input.bam} --bqsr-recal-file {input.table} -O {output}
"""
这个文件简单到只有 50 行不到,但它已经是一个可运行的变异检测前端流程了。你只需要在终端执行 snakemake --cores 4,Snakemake 就会自动判断先跑哪个规则、并行执行哪些步骤。
规则组合:input functions 与动态输入
上面展现的静态输入适合小规模项目。但实际 GATK 管线经常会遇到动态输入的情况,比如你要同时用多个已知位点文件。这时候可以用 input functions——它是一个返回路径或路径列表的 Python 函数。
我举个例子:在 ApplyBQSR 步骤中,可能需要把 dbsnp 和 gnomad 两个 VCF 都作为 --known-sites 传进去。如果用静态写法,你得写两遍 --known-sites 参数。聪明的做法是定义一个函数来动态拼接。
def known_sites_input(wildcards):
# wildcards 包含当前样本名等通配符
return [
"known_sites/hg38.dbsnp.vcf",
"known_sites/hg38.gnomad.vcf"
]
rule apply_bqsr:
input:
bam = "results/{sample}.dedup.bam",
table = "results/{sample}.recal.table",
known_sites = known_sites_input
output:
"results/{sample}.recal.bam"
shell:
"""
gatk ApplyBQSR \
-R ref/hg38.fa \
-I {input.bam} \
--bqsr-recal-file {input.table} \
{input.known_sites} \
-O {output}
"""
这样当 Snakemake 执行 apply_bqsr 时,会自动调用 known_sites_input 函数,传入当前的 wildcards 对象。函数返回的列表会被展开成两个文件路径,然后依次被放入 shell 命令中。
一种常见的误解:shell 与 run 的区别
有些教程会告诉你还可以用 run: 或 script: 来执行 Python 代码或外部的 R/Python 脚本。我个人在 GATK 管线上几乎只用 shell:,因为 GATK 命令本身就是成熟的命令行工具,用 shell 最直接、最易调试。
如果你非得用 run: 来写 Python 逻辑,比如需要根据输入文件名动态生成参数,那也可以。但记得加上 shell 模式的回退——万一 Python 环境里没装 GATK 的 Python 绑定呢?对吧。
嗯,关于 Snakefile 的基本结构,今天就聊这么多。你只要记住:一条规则一个任务,输入输出清晰,shell 命令写完整,搭建可复用的 GATK 管线就不难。