Snakefile 基础结构与规则定义

好,咱们进入正题。一个 Snakemake 工作流到底长什么样?说白了,它的核心就是 Snakefile。你写流程,就是写这个文件。

我刚开始用 Snakemake 的时候,总觉得它跟 Makefile 差不多。但实际上,Snakemake 的语法更贴近 Python,更灵活。今天我们就把它扒开,看看里面到底是啥。

Snakefile 的骨架

一个最简单的 Snakefile,其实就是一堆 规则(rule) 的集合。每个规则定义一件事:输入什么、输出什么、怎么干。

来,直接看代码:

rule hello_world:
    output:
        "hello.txt"
    shell:
        "echo 'Hello, Snakemake!' > hello.txt"

看到没?这个规则叫 hello_world,输出一个 hello.txt,执行的 Shell 命令就是把一句话写进去。运行的时候,Snakemake 会自动检查 hello.txt 是否存在,如果不存在,它就执行 shell 里的命令。

核心点:Snakemake 是“目标驱动”的。你告诉它要产出什么,它自己倒推需要哪些输入、运行哪些规则。

规则的三要素:input、output、shell/run

每个规则,至少要有 output。至于 input 和 shell 或 run,是常用但不是必须的。比如你可以只定义一个 output,然后用手工方式满足它。

不过,实际项目中,这三者通常一起出现。我整理了一个常用字段表:

字段 作用 是否必需
input 指定依赖的文件或规则 否
output 指定本规则产出的文件 是
shell 执行 Shell 命令 选一
run 执行 Python 代码 选一
params 传递额外参数,如阈值、路径 否
log 指定日志文件路径 否
threads 设定本规则使用的线程数 否
resources 指定内存、运行时间等资源 否

小技巧:如果你的命令特别长,或者需要条件判断,我建议用 run 写 Python 代码来调用 Shell,可读性会好很多。

通配符:让规则活起来

硬编码文件名?那只能处理一个样本。真实项目里,你有上百个样本,难道写一百条规则?

答案是用 通配符。比如:

rule align:
    input:
        "data/{sample}.fastq"
    output:
        "results/{sample}.bam"
    shell:
        "bwa mem ref.fa {input} | samtools sort > {output}"

这里 {sample} 就是通配符。当你让 Snakemake 生成 results/A.bam 时,它会自动把 {sample} 替换成 A,然后去找 data/A.fastq。

我一同事刚用的时候,老是在 input 里写错通配符名字。记住:input 和 output 里同名通配符必须一致,不然会报错说找不到文件。

避坑:我曾经在一个项目里,把 output 写成了 results/{sample}.sorted.bam,但 input 里用的 {sample_id}。Snakemake 匹配不上,卡了我半天。通配符名称一定要统一。

规则依赖:隐式 vs 显式

工作流最迷人的地方,就是规则之间可以联动。你想想看,一个流程里,先对原始数据做质控,再比对,再变异检测,环环相扣。

Snakemake 有两种方式表达依赖:

  • 隐式依赖:规则 B 的 input 文件,正好是规则 A 的 output 文件。Snakemake 会自动识别。
  • 显式依赖:规则 B 用 rule A 名字来引用,明确说“我需要先执行 A”。

看个显式依赖的例子:

rule trim:
    input:
        "data/{sample}.fastq"
    output:
        "results/{sample}.trimmed.fastq"
    shell:
        "cutadapt -o {output} {input}"

rule align:
    input:
        rules.trim.output
    output:
        "results/{sample}.bam"
    shell:
        "bwa mem ref.fa {input} | samtools sort > {output}"

这里 rules.trim.output 就是显式引用。好处是什么?如果你改了 trim 规则里的输出文件名,align 能自动感知,不用手动改路径。

我个人习惯用隐式依赖,因为更直观。但复杂流程里,显式依赖可以让依赖关系更清楚,方便排查问题。

多个输入输出怎么办?

有时候一个规则要读好几个文件,或者产出好几个文件。Snakemake 用列表或字典搞定。

rule merge:
    input:
        "results/A.bam",
        "results/B.bam"
    output:
        merged="results/merged.bam",
        index="results/merged.bam.bai"
    shell:
        "samtools merge {output.merged} {input} && samtools index {output.merged}"

注意这里 output 用了命名方式。你可以用 {output.merged} 来引用,比 {output[0]} 更清晰。运行时,Snakemake 会把 {input} 自动展开成空格分隔的文件列表。

章末框架图:Snakefile 知识体系

这一章的信息量不小,我画了个框架图,帮你把整章知识点串起来。

Snakefile 基础结构与规则定义 Snakefile 骨架 规则定义 (rule) 三要素 input output shell run 通配符 {sample} 自动匹配扩展 隐式依赖 显式依赖 规则依赖:通过文件或规则名字串联工作流 多个输入/输出:命名输出可提高可读性

一点总结

Snakefile 的规则定义,其实就是这三件事:输入、输出、执行逻辑。配合通配符,一个规则能覆盖几十上百个样本。依赖关系让整个流程自动串联,你不用操心中间步骤的顺序。

我个人认为,刚开始写 Snakefile 时,不用贪多。先从一个规则开始,跑通一个样本。然后慢慢加通配符、加依赖、加参数。等一个流程稳定了,再去考虑模块化。

要点回顾:
- rule 是基本单元,必须有 output。
- 通配符 {...} 让规则可以批量处理不同样本。
- 依赖可以是隐式的(通过文件匹配)或显式的(通过 rules.xxx 引用)。
- 多个输入/输出用列表或命名方式,命名方式更清晰。

底子打好了,下一块就是怎么让这些规则更灵活?怎么传递参数、控制线程资源?这些事儿,我们后面会一步步展开。