Skip to content

Subworkflow configuration

For subworkflows, configuration acts on two fronts :

  1. Allowing for the subworkflow itself to change behavior, such as selecting between two different algorithms or choosing to run a specific pre-processing part.
  2. Configuring the behavior of its included components to fit its use-case, and allows end-users to modify it.

Subworkflows parameters are defined using the params configuration scope. This scope is available in all .nf files, but params defined there can be overriden by .config files at execution, or by other subworkflows that would include yours. For the preproc_anat subworkflow, there are no subworkflow parameters defined for now, but we could, for example, add one to skip the denoising step :

main.nf
...
params.preproc_anat_denoise = true
subworkflow preproc_anat {
take:
ch_anatomical // Structure : [ [id: string] , path(anat_image) ]
...
main:
...
if (params.preproc_anat_denoise) {
ch_denoising_nlmeans = ch_anatomical
.join(ch_brain_mask, remainder: true)
.map{ meta, image, mask -> [meta, image, [], mask ?: []] }
DENOISING_NLMEANS(ch_denoising_nlmeans)
ch_versions = ch_versions.mix(DENOISING_NLMEANS.out.versions)
ch_anatomical = DENOISING_NLMEANS.out.image
}
ch_betcrop_synthbet = ch_anatomical
.join(ch_brain_mask, remainder: true)
...
ch_preproc_n4 = ch_anatomical
.join(ch_n4_reference, remainder: true)
...

Changing the behavior of included components is not as straightforward as it seems.

Modules are configured using the task.ext scope, which is not assignable in the params or inside any .nf file. Instead, those configurations have to be defined inside .config files.

Create a new nextflow.config file at the root of your subworkflow directory. While this file is not included at runtime, you will document its existence and still use it profusely in tests :

nextflow.config
params {
preproc_anat_denoise = true
preproc_anat_n4 = true
}

As a first, you’ll want to make the number_of_coils parameter of DENOISING_NLMEANS configurable by users. This requires a new configuration parameter in params associated to task.ext.number_of_coils for the module. To do so, in the configuration file created, define a new entry for the process scope that targets this one and only module, using a process selector :

nextflow.config
params {
preproc_anat_denoise = true
preproc_anat_n4 = true
preproc_anat_nlmeans_number_of_coils = 1
}
process {
withName: "DENOISING_NLMEANS" {
task.ext.number_of_coils = params.preproc_anat_nlmeans_number_of_coils ?: 1
}
}

Now that the subworkflow is configurable, it’s time to document everything. Refer below for a full configuration of all included components :

main.nf
// MODULES
include { DENOISING_NLMEANS } from '../../../modules/nf-neuro/denoising/nlmeans/main'
include { BETCROP_SYNTHBET } from '../../../modules/nf-neuro/betcrop/synthbet/main'
include { BETCROP_ANTSBET } from '../../../modules/nf-neuro/betcrop/antsbet/main'
include { PREPROC_N4 } from '../../../modules/nf-neuro/preproc/n4/main'
// SUBWORKFLOWS
include { ANATOMICAL_SEGMENTATION } from '../anatomical_segmentation/main'
params.preproc_anat_denoise = true
params.preproc_anat_bet_before_n4 = true
params.preproc_anat_n4 = true
workflow PREPROC_ANAT {
take:
ch_anatomical // Structure : [ [id: string] , path(anat_image) ]
ch_template // Structure : [ path(anat_ref), path(brain_proba) ]
ch_brain_mask // Structure : [ [id: string] , path(brain_mask) ], optional
ch_synthbet_weights // Structure : [ [id: string] , path(weights) ], optional
ch_n4_reference // Structure : [ [id: string] , path(reference) ], optional
ch_freesurferseg // Structure : [ [id: string] , path(aparc+aseg) , path(wmparc) ], optional
ch_lesion // Structure : [ [id: string] , path(lesion) ], optional
ch_fs_license // Structure : [ path(license) ], optional
main:
ch_versions = Channel.empty()
if (params.preproc_anat_denoise) {
ch_denoising_nlmeans = ch_anatomical
.join(ch_brain_mask, remainder: true)
.map{ meta, image, mask -> [meta, image, [], mask ?: []] }
DENOISING_NLMEANS(ch_denoising_nlmeans)
ch_versions = ch_versions.mix(DENOISING_NLMEANS.out.versions)
ch_anatomical = DENOISING_NLMEANS.out.image
}
ch_brain_pre_mask = Channel.empty()
if (params.preproc_anat_bet_before_n4) {
ch_betcrop_synthbet = ch_anatomical
.join(ch_brain_mask, remainder: true)
.filter{ meta, image, mask -> !mask }
.join(ch_synthbet_weights, remainder: true)
.map{ meta, image, mask, weights -> [meta, image, weights ?: []] }
BETCROP_SYNTHBET( ch_betcrop_synthbet )
ch_versions = ch_versions.mix(BETCROP_SYNTHBET.out.versions)
ch_brain_pre_mask = ch_brain_mask.mix(BETCROP_SYNTHBET.out.brain_mask)
}
if (params.preproc_anat_n4) {
ch_preproc_n4 = ch_anatomical
.join(ch_n4_reference, remainder: true)
.join(ch_brain_pre_mask, remainder: true)
.map{ meta, image, reference, mask -> [meta, image, reference ?: [], mask ?: []] }
PREPROC_N4( ch_preproc_n4 )
ch_versions = ch_versions.mix(PREPROC_N4.out.versions)
ch_anatomical = PREPROC_N4.out.image
}
ch_betcrop_antsbet = ch_anatomical
.join(ch_brain_mask, remainder: true)
.filter{ meta, image, mask -> !mask }
.map{ meta, image, mask -> [meta, image] }
.combine(ch_template)
BETCROP_ANTSBET( ch_betcrop_antsbet )
ch_versions = ch_versions.mix(BETCROP_ANTSBET.out.versions)
ANATOMICAL_SEGMENTATION(
ch_anatomical,
ch_freesurferseg,
ch_lesion,
ch_license
)
emit:
ch_anatomical = ch_anatomical // channel: [ [id: string] , path(image) ]
ch_brain_mask = BETCROP_ANTSBET.out.mask // channel: [ [id: string] , path(brain_mask) ]
wm_mask = ANATOMICAL_SEGMENTATION.out.wm_mask // channel: [ [id: string] , path(wm_mask) ]
gm_mask = ANATOMICAL_SEGMENTATION.out.gm_mask // channel: [ [id: string] , path(gm_mask) ]
csf_mask = ANATOMICAL_SEGMENTATION.out.csf_mask // channel: [ [id: string] , path(csf_mask) ]
wm_map = ANATOMICAL_SEGMENTATION.out.wm_map // channel: [ [id: string] , path(wm_map) ]
gm_map = ANATOMICAL_SEGMENTATION.out.gm_map // channel: [ [id: string] , path(gm_map) ]
csf_map = ANATOMICAL_SEGMENTATION.out.csf_map // channel: [ [id: string] , path(csf_map) ]
versions = ch_versions // channel: [ path(versions.yml) ]
}
nextflow.config
params {
preproc_anat_denoise = true
preproc_anat_bet_before_n4 = true
preproc_anat_n4 = true
// Configure DENOISING_NLMEANS
preproc_anat_nlmeans_number_of_coils = 1
preproc_anat_nlmeans_sigma = 0.5
preproc_anat_nlmeans_sigma_from_all_voxels = false
preproc_anat_nlmeans_gaussian = false
preproc_anat_nlmeans_method = "basic_sigma"
// Configure BETCROP_SYNTHBET
preproc_anat_betcrop_synthbet_border = null
preproc_anat_betcrop_synthbet_nocsf = false
// Configure PREPROC_N4
preproc_anat_n4_knots_per_voxel = 1
preproc_anat_n4_shrink_factor = 1
// Configure ANATOMICAL_SEGMENTATION
run_synthbet = false // Reusing the same name, we could have changed if wanted
}
process {
withName: "DENOISING_NLMEANS" {
task.ext.number_of_coils = params.preproc_anat_nlmeans_number_of_coils ?: 1
task.ext.sigma = params.preproc_anat_nlmeans_sigma ?: 0.5
task.ext.sigma_from_all_voxels = params.preproc_anat_nlmeans_sigma_from_all_voxels ?: false
task.ext.gaussian = params.preproc_anat_nlmeans_gaussian ?: false
task.ext.method = params.preproc_anat_nlmeans_method ?: "basic_sigma"
}
withName: "BETCROP_SYNTHBET" {
task.ext.border = params.preproc_anat_betcrop_synthbet_border ?: null
task.ext.nocsf = params.preproc_anat_betcrop_synthbet_nocsf ?: false
}
withName: "PREPROC_N4" {
task.ext.bspline_knot_per_voxel = params.preproc_anat_n4_knots_per_voxel ?: 1
task.ext.shrink_factor = params.preproc_anat_n4_shrink_factor ?: 1
}
}