Nextflow 是一种强大而灵活的语言,可以通过多种方式进行编码。这可能导致一些不好的编码实践。例如,这可能导致工作流只能在某些配置或执行平台下工作。下面是一些有用的编码技巧,它们可以使维护和移植工作流更加容易。
使用空格提高可读性
Nextflow 通常对代码中的空格不敏感。这允许使用缩进、垂直间距、新行和空格来提高代码的可读性。例如:
#! /usr/bin/env nextflow
// Tip: Allow spaces around assignments ( = )
nextflow.enable.dsl = 2
// Tip: Separate blocks of code into groups with common purpose
// e.g., parameter blocks, include statements, workflow blocks, process blocks
// Tip: Align assignment operators vertically in a block
params.reads = ''
params.gene_list = ''
params.gene_db = 'ftp://path/to/database'
// Tip: Align braces or instruction parts vertically
include { BAR } from 'modules/bar'
include { TAN as BAZ } from 'modules/tan'
workflow {
// Tip: Indent process calls
// Tip: Use spaces around process/function parameters
FOO ( Channel.fromPath( params.reads, checkIfExists: true ) )
BAR ( FOO.out )
// Tip: Use vertical spacing and indentation for many parameters.
BAZ (
Channel.fromPath( params.gene_list, checkIfExists: true ),
FOO.out,
BAR.out,
file( params.gene_db, checkIfExists: true )
)
}
// Tip: Uppercase process names help readability
process FOO {
// Tip: Separate process parts into distinct blocks
input:
path fastq
output:
path "*.fasta"
script:
prefix = fastq.baseName
"""
tofasta $fastq > $prefix.fasta
"""
}
使用注释
注释能提高代码的可读性和可维护性。可以在以下地方添加注释:
- 注释通道中的期望的输入数据结构。
- 描述高阶的函数。
- 描述是否存在(非)预期的代码。
- 必须和可选的进程的输入。
workflow ALIGN_SEQ {
take:
reads // queue channel; [ sample_id, [ file(read1), file(read2) ] ]
reference // file( "path/to/reference" )
main:
// Quality Check input reads
READ_QC ( reads )
// Align reads to reference
Channel.empty()
.set { aligned_reads_ch }
if( params.aligner == 'hisat2' ){
ALIGN_HISAT2 ( READ_QC.out.reads, reference )
aligned_reads_ch.mix( ALIGN_HISAT2.out.bam )
.set { aligned_reads_ch }
} else if ( params.aligner == 'star' ) {
ALIGN_STAR ( READ_QC.out.reads, reference )
aligned_reads_ch.mix( ALIGN_STAR.out.bam )
.set { aligned_reads_ch }
}
aligned_reads_ch.view()
emit:
bam = aligned_reads_ch // queue channel: [ sample_id, file(bam_file) ]
}
process COUNT_KMERS {
input:
// Mandatory
tuple val(sample), path(reads) // [ 'sample_id', [ read1, read2 ] ]: Reads in which to count kmers
// Optional
path kmer_table // 'path/to/kmer_table': Table of k-mers to count
...
}
收集工具版本
软件打包是一个难题,对于一个包来说,报告它所拥有的所有工具的版本是很困难的。在只使用少量工具的情况下,报告包中包含的所有内容的版本也可能有些过分。这意味着我们要有效地报告我们用来提高可重现性的工具的版本。
process HISAT2_ALIGN {
...
script:
def HISAT2_VERSION = '2.2.0' // Version not available using command-line
"""
hisat2 ... | samtools ...
cat <<-END_VERSIONS > versions.yml
"${task.process}":
hisat2: $HISAT2_VERSION
samtools: \$( samtools --version 2>&1 | sed 's/^.*samtools //; s/Using.*\$//' )
END_VERSIONS
"""
}
命名输出channel
process和workflow的输出channel可以使用关键字emit
来命名,例如:
...
emit:
alignment = HISAT2_ALIGN.out.bam
}
process HISAT2_ALIGN {
...
output:
tuple val(sample), path("*.bam"), emit: bam
tuple val(sample), path("*.log"), emit: summary
path "versions.yml" , emit: versions
...
}
在workflow中使用parmas.xxx
,而不是process块
可以从工作流中的任何地方访问 params
变量。它们可以提供各种各样的属性和决策选项。例如:
process ALIGN {
input:
tuple val(sample), path(reads)
path index
...
script:
if ( params.aligner == 'hisat2' ) {
"""
hisat2 ... | samtools ...
...
"""
} else if ( params.aligner == 'star' ){
"""
star ...
...
"""
}
}
更好的做法是将其作为输入参数。
process ALIGN {
input:
tuple val(sample), path(reads)
path index
val aligner // params.aligner is provided as a third parameter
...
script:
if ( aligner == 'hisat2' ) {
"""
hisat2 ... | samtools ...
...
"""
} else if ( aligner == 'star' ){
"""
star ...
...
"""
}
}
这使得人们可以从workflow
块中查看所有参数的使用情况,从而使其更容易维护。还有一种危险是,在管道执行期间可能修改params
变量,从而可能导致更复杂的工作流中出现不可重复的结果。
所有的文件/目录都应该是process的输入
你执行工作流的平台,文件可能很容易通过网络访问,或下载。然而,并非所有执行平台都支持这一点。下面的示例可以在你的系统上很好地工作,但在另一个系统上失败(例如计算没有互联网连接)。
process READ_CHECK {
input:
tuple val(sample), path(reads)
...
script:
"""
wget ftp://path/to/database
check_reads $reads /local/copy/database > $sample.report
...
"""
}
Nextflow 的一个优势是文件暂存,即在process任务中准备使用的文件。暂存文件通过提供作为process输入有几个好处。
- 文件仅在必要时才会更新;
- 一个文件/目录可以被共享,而不用多次下载;
- Nextflow 支持从任何有效的 URL 中检索文件,这意味着代码行可能更少。
process READ_CHECK {
input:
tuple val(sample), path(reads)
path database
...
script:
"""
check_reads $reads $database > $sample.report
...
"""
}
如果文件可能是一个可选的输入,这取决于其他参数。在不应该提供任何文件的情况下,可以传递一个空列表[]
来代替。
workflow {
COUNT_KMERS ( reads, [] )
}
process COUNT_KMERS {
input:
// Mandatory
tuple val(sample), path(reads) // [ 'sample_id', [ read1, read2 ] ]: Reads in which to count kmers
// Optional
path kmer_table // 'path/to/kmer_table': Table of k-mers to count
...
}
避免大量短时间运行的进程
如果工作流试图执行许多短时间运行的process,那么许多执行平台都是低效的。与将短流程打包成更大的流程任务相比,为每个小实例调度和请求资源可能需要更多的时间。Nextflow 提供了方便的channel操作符,例如 buffer
、collate
、collect
和 collectFile
,这有助于将输入组合成批处理,这些批处理可以使用给定的请求资源运行更长时间。短任务本身也可以在process脚本中使用命令行工具 xargs
或parallel
进行并行化。
workflow REFINE_DATA {
take:
datapoints
main:
BATCH_TASK ( datapoints.collate(100) )
}
process BATCH_TASK {
input:
val data
script:
"""
printf "%s\\n" $data | \
xargs -P $task.cpus -I {} \
short_task {}
# Alternative:
# parallel --jobs $task.cpus "short_task {1}" :::: $data
"""
}
包含测试profile
test
profile指定一个短的运行的测试数据集,用于检查整个管道的功能。它还可以向工作流的用户演示期望的输入和输出类型。另一个好处是可以对工作流进行自动化测试,确保在添加新功能时工作流能够继续工作。例如:
profiles {
test {
params {
reads = 'https://github.com/my_repo/test/test_reads.fastq.gz'
reference = 'https://github.com/my_repo/test/test_reference.fasta.gz'
}
}
}
使用现有的容器编写模块
强烈建议使用容器进行软件打包,因为它们以相同的方式运行,而与运行它的操作系统无关。使用现有容器编写模块可以减少工作流的维护,并减少包冲突。一个有用资源是包管理器 conda 的 bioconda 通道,它为许多生物信息学工具提供包装。
process FASTQC {
container "${ workflow.containerEngine == 'singularity' ?
'https://depot.galaxyproject.org/singularity/fastqc:0.11.9--0' :
'quay.io/biocontainers/fastqc:0.11.9--0' }"
...
}
尽可能使用文件压缩和临时磁盘空间
磁盘空间通常是大多数计算机系统上有用的资源。如果可能,请使用压缩文件。有几个有用的 shell 命令和操作可以很好地处理压缩数据,例如 gunzip -c
和 zgrep
。
process COUNT_FASTA {
input:
path fasta // reference.fasta.gz
script:
"""
zgrep -c '^>' $fasta
"""
}
另一个好的实践是使用本地临时磁盘空间(scratch)。通常,work
目录位于网络上的共享磁盘空间上,这会减慢对磁盘进行大量读写的进程。使用暂存空间不仅可以加快磁盘 I/O,而且还可以节省 workDir 中的空间,因为只有与进程输出指令匹配的文件才会被复制回来进行缓存。进程指令 process.scratch
可以提供布尔值或用于暂存空间的路径。
process {
scratch = '/tmp'
}
使用一致的命名约定
使用一致的命名约定极大地提高了可读性。例如,对process名称使用大写有助于将其与其他工作流组件(如通道或操作符)区分开来。
总结
- Nextflow对空格不敏感。可以使用空格来提高可读性。
- 使用注释和空格来对代码块进行分组。
- 在脚本中报告工具版本。
- 使用
emit:
关键字命名输出channel。 - 避免在process中使用
params.xxx
。将其作为输入参数传入。 - 输入文件应该使用输入通道传入。
- 将短的运行的命令组织到一个更大的进程中。
- 包含一个在小型测试数据集上运行工作流的测试配置文件。
- 重用现有的容器/软件包来编写process。
- 尽可能使用压缩文件和临时磁盘空间。
- 使用一致的命名约定。