第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 进程与通道的协作模型

为了让大家更直观地理解,我画了一张图。你可以看到数据从文件系统出发,经过通道进入第一个进程,然后继续流向后续进程。

进程与通道协作关系 输入通道 进程1: pdb2gmx 中间通道 进程2: energy 输出通道 通道缓存数据,进程消费后自动移除已处理元素 值通道可被多个进程共享,不会被消费

图中蓝框是通道,红框和绿框是进程。箭头方向就是数据流动方向。实际写代码时,你不需要手动创建这么多中间通道——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 文件路径也要处理。但没关系,目前只演示进程和通道的基础用法。先跑通这个骨架,后面再慢慢加细节。