第4章:进程与通道初识,工作流定义
好,我们正式开始接触 Nextflow 的核心了。进程和通道,说白了就是整个流程的"骨架"和"血管"。进程负责干活,通道负责传数据。我当年刚学的时候,总把它俩分开理解,后来发现——把它们看作一对亲密战友,才能写出真正顺滑的流程。
4.1 进程(process)——干活的基本单元
在 Nextflow 里,process 就是最小的执行单元。每个进程独立运行,有自己的输入输出,你可以把它想象成一个带壳的脚本。进程内部怎么写?看一个最简单的例子,我通常用它来测试环境是否通。
process sayHello {
debug true
output:
stdout
script:
"""
echo "Hello from Nextflow!"
"""
}
这个进程只输出一行文本到标准输出。注意几个关键点:script: 后面跟的是要执行的 shell 命令,用三引号括起来。如果只有一行命令,也可以写在一行,但我个人习惯写成多行,后面加东西方便。
进程的结构其实挺固定的,一般长这样:
process 进程名 {
[ directives ]
[ inputs ]
[ outputs ]
[ when clause ]
[ exec | script 块 ]
}
Directives 是进程参数,比如指定 CPU 核数、内存、容器等。Inputs 定义输入数据从哪来,Outputs 定义输出数据往哪走。when 控制条件执行。最后 script 或 exec 块写实际要跑的命令。
嗯,这里要注意:exec 块是 Nextflow 新引入的,可以直接写 Groovy 代码,替代 shell 脚本。但对于 GROMACS 这种纯命令行工具,我们还是用传统的 script 更实在。
4.2 通道(channel)——数据的管道
进程之间怎么传数据?靠通道。通道就像一根管子,一头连接进程的输出,另一头连接另一个进程的输入。通道有两种:队列通道和值通道。
队列通道(Queue Channel):按顺序发送数据,一次一个。适合处理文件列表。
值通道(Value Channel):只存一个值,可被多个进程共享。适合配置参数。
我在项目中遇到过有人把参数放在队列通道里,导致进程被重复启动——其实应该用值通道。看个例子,创建通道的方式:
// 队列通道,包含多个元素
ch_files = channel.fromPath("data/*.pdb")
// 值通道,只存一个字符串
ch_param = channel.value("protein.pdb")
创建好通道后,就可以在进程里引用了。比如一个读 PDB 文件的进程:
process readPDB {
input:
path pdb_file from ch_files
output:
path "${pdb_file}.processed" into ch_processed
script:
"""
gmx pdb2gmx -f ${pdb_file} -o ${pdb_file}.processed -ff gromos54a7
"""
}
这里 from 后面的 ch_files 就是之前创建的通道。输出用 into 把结果文件放进另一个通道 ch_processed。通道的名字你可以随便起,但我习惯加 ch_ 前缀,一眼就能认出来。
4.3 工作流定义——把进程串起来
有了进程和通道,下一步就是定义它们之间的连接关系。Nextflow 里用 workflow 块来声明。一个最简单的 workflow 长这样:
workflow {
ch_pdb = channel.fromPath("input/*.pdb")
readPDB(ch_pdb)
processNextStep(readPDB.out)
}
进程的名字可以直接当函数用。输入参数就是通道,输出可以通过 .out 拿到。注意进程可以没有 output:,但多数场景下你一定需要把数据往下传。
我曾经掉过坑:进程名不小心用了大驼峰(比如 ReadPDB),结果 workflow 调用时却写了小驼峰(readPDB),差点没发现。Nextflow 大小写敏感,命名时统一风格很重要。我自己习惯用首字母小写的驼峰,比如 process pdb2gmxProcess,然后调用写 pdb2gmxProcess(ch_files)。
4.4 进程与通道的协作模型
为了让大家更直观地理解,我画了一张图。你可以看到数据从文件系统出发,经过通道进入第一个进程,然后继续流向后续进程。
图中蓝框是通道,红框和绿框是进程。箭头方向就是数据流动方向。实际写代码时,你不需要手动创建这么多中间通道——Nextflow 会自动帮你管。但理解这张图,对调试会很有帮助。
4.5 常用通道操作符
通道本身还支持很多操作符,比如 filter、map、collect。我们做 GROMACS 时,最常用的可能就是 flatMap 和 mix。举个小例子:
ch_all = ch_pdb.mix(ch_gro).collect()
// ch_all 现在包含所有 pdb 和 gro 文件
我个人习惯在数据处理步骤用 map 给文件名换个后缀:
ch_gro = ch_pdb.map { file -> file.baseName + ".gro" }
注意 map 操作返回的是新通道,不会改原来的通道。通道操作符配合进程,能干很多漂亮的事,后面章节我们会专门深入。
避坑指南:我之前在 GROMACS 流程里用 collect 把多个文件收成一个列表传给下一个进程,结果那个进程只接收 path 类型,列表不行。最终我改成 flatten 才解决问题。所以记住:大多数进程输入期望的是单个文件而非列表,除非你显式定义 val 类型。
4.6 实战:一个最小 GROMACS 流程骨架
最后我们拼一个小例子,把上面东西串起来。假设我们想跑最简单的 pdb2gmx + energy minimization 两步:
process pdb2gmx {
input:
path pdb_file
output:
path "processed.gro" into ch_gro
path "topol.top"
script:
"""
gmx pdb2gmx -f ${pdb_file} -o processed.gro -water spc
"""
}
process em {
input:
path gro_file from ch_gro
path top_file from ch_gro.map { gro -> "topol.top" } // 注意这里共享了top文件
output:
path "em.tpr"
script:
"""
gmx grompp -f em.mdp -c ${gro_file} -p ${top_file} -o em.tpr
gmx mdrun -deffnm em
"""
}
workflow {
ch_pdb = channel.fromPath("*.pdb")
pdb2gmx(ch_pdb)
em(pdb2gmx.out[0], pdb2gmx.out[1])
}
这里 pdb2gmx.out[0] 代表第一个输出(processed.gro),pdb2gmx.out[1] 代表第二个输出(topol.top)。如果你只有单个输出,直接写 .out 就够了。
注意:上面例子我故意简化了,实际工作中 em.mdp 需要单独准备,Topol 文件路径也要处理。但没关系,目前只演示进程和通道的基础用法。先跑通这个骨架,后面再慢慢加细节。