diff --git a/.gitignore b/.gitignore index 69744f0..4223c1c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ tests/.runs/ work/ -.nf-test/ \ No newline at end of file +.nf-test/ +result +.DS_Store \ No newline at end of file diff --git a/assets/multiqc_config.yml b/assets/multiqc_config.yml new file mode 100644 index 0000000..0ecdba5 --- /dev/null +++ b/assets/multiqc_config.yml @@ -0,0 +1,21 @@ +report_comment: > + This report has been generated by the nf-neuro tutorial! + +report_section_order: + dti_qc: + order: -1001 + +custom_data: + dti_qc: + file_format: "png" + section_name: "DTI QC" + description: | + This section contains QC images for diffusion tensor imaging (DTI) metric + maps. Add specifications regarding how to evaluate those images, for example: + To assess the quality of the DTI metrics, ensure that FA highlights major white + matter tracts with expected high values (e.g., corpus callosum, corticospinal tract, etc.)... + plot_type: "image" + +sp: + dti_qc: + fn: "*dti_mqc.png" \ No newline at end of file diff --git a/main.nf b/main.nf index 754faf1..e3aa586 100644 --- a/main.nf +++ b/main.nf @@ -1,30 +1,91 @@ #!/usr/bin/env nextflow -//include { RECONST_DTIMETRICS } from 'modules/nf-neuro/reconst/dtimetrics/main' +include { PREPROC_T1 } from './subworkflows/nf-neuro/preproc_t1/main' +include { STATS_METRICSINROI } from './modules/local/stats/metricsinrois/main' +include { PREPROC_DIFF } from './subworkflows/local/preproc_diff/main' +include { MULTIQC } from "./modules/nf-core/multiqc/main" + workflow get_data { main: if ( !params.input ) { - log.info "You must provide an input directory containing all files using:" + log.info "You must provide an input directory containing all images using:" log.info "" - log.info " --input=/path/to/[input] Input directory containing the file needed" + log.info " --input=/path/to/[input] Input directory containing your subjects" log.info " |" - log.info " └-- Input" - log.info " └-- participants.*" + log.info " ├-- S1" + log.info " | ├-- *dwi.nii.gz" + log.info " | ├-- *dwi.bval" + log.info " | ├-- *dwi.bvec" + log.info " | └-- *t1.nii.gz" + log.info " └-- S2" + log.info " ├-- *dwi.nii.gz" + log.info " ├-- *bval" + log.info " ├-- *bvec" + log.info " └-- *t1.nii.gz" log.info "" error "Please resubmit your command with the previous file structure." } input = file(params.input) - // ** Loading all files. ** // - participants_channel = Channel.fromFilePairs("$input/participants.*", flat: true) - { "participants_files" } - + // ** Loading DWI files. ** // + dwi_channel = Channel.fromFilePairs("$input/**/**/dwi/*dwi.{nii.gz,bval,bvec}", size: 3, flat: true) + { it.parent.parent.parent.name + "_" + it.parent.parent.name} // Set the subject filename as subjectID + '_' + session. + .map{ sid, bvals, bvecs, dwi -> [ [id: sid], dwi, bvals, bvecs ] } // Reordering the inputs. + // ** Loading T1 file. ** // + t1_channel = Channel.fromFilePairs("$input/**/**/anat/*T1w.nii.gz", size: 1, flat: true) + { it.parent.parent.parent.name + "_" + it.parent.parent.name } // Set the subject filename as subjectID + '_' + session. + .map{ sid, t1 -> [ [id: sid], t1 ] } emit: - participants = participants_channel + dwi = dwi_channel + anat = t1_channel } workflow { - // ** Now call your input workflow to fetch your files ** // - data = get_data() - data.participants.view() + ch_multiqc_files = Channel.empty() + + inputs = get_data() + + //Processing DWI + PREPROC_DIFF( inputs.dwi ) + ch_multiqc_files = ch_multiqc_files.mix(PREPROC_DIFF.out.mqc) + + // Preprocessing T1 images + //inputs.anat.view() + + PREPROC_T1( + inputs.anat, + Channel.empty(), + Channel.empty(), + Channel.empty(), + Channel.empty(), + Channel.empty(), + Channel.empty() + ) + + // Extract FA value + input_extract_metric = PREPROC_T1.out.image_bet + .join(PREPROC_DIFF.out.fa) + .map{ it } + + STATS_METRICSINROI( input_extract_metric ) + + ch_multiqc_files = ch_multiqc_files + .groupTuple() + .map { meta, files_list -> + def files = files_list.flatten().findAll { it != null } + return tuple(meta, files) + } + + ch_multiqc_config = Channel.fromPath("$projectDir/assets/multiqc_config.yml", checkIfExists: true) + + // MultiQC + MULTIQC( + ch_multiqc_files, + [], + ch_multiqc_config.toList(), + [], + [], + [], + [] + ) } \ No newline at end of file diff --git a/modules.json b/modules.json index 7dd2540..9886e83 100644 --- a/modules.json +++ b/modules.json @@ -2,12 +2,58 @@ "name": "", "homePage": "", "repos": { + "https://github.com/nf-core/modules.git": { + "modules": { + "nf-core": { + "multiqc": { + "branch": "master", + "git_sha": "81880787133db07d9b4c1febd152c090eb8325dc", + "installed_by": ["modules"] + } + } + } + }, "https://github.com/scilus/nf-neuro.git": { "modules": { "nf-neuro": { + "betcrop/antsbet": { + "branch": "main", + "git_sha": "b8949dd284432bbe1399dbc3f54cdf9191855f8e", + "installed_by": ["preproc_t1"] + }, + "betcrop/synthbet": { + "branch": "main", + "git_sha": "dca20370d97a69c6a91c80843f417206212568e6", + "installed_by": ["preproc_t1"] + }, + "denoising/mppca": { + "branch": "main", + "git_sha": "2e222d18c89e5547a6bf5c0c74673baeb63bcd52", + "installed_by": ["modules"] + }, + "denoising/nlmeans": { + "branch": "main", + "git_sha": "261a7e0606645eeaf863e401cb9fc99c130b3a19", + "installed_by": ["preproc_t1"] + }, + "image/cropvolume": { + "branch": "main", + "git_sha": "3e2e971f5bdaafcd5f72cb9c69f9d0b2a6f20de3", + "installed_by": ["preproc_t1"] + }, + "image/resample": { + "branch": "main", + "git_sha": "36e010a236a0bd86334ab99b0cac4f7c4ff51532", + "installed_by": ["preproc_t1"] + }, + "preproc/n4": { + "branch": "main", + "git_sha": "18273a2cef9ffdaf7088e305b9c4ebf4dd439079", + "installed_by": ["preproc_t1"] + }, "reconst/dtimetrics": { "branch": "main", - "git_sha": "452075a707a9769b0509fc33a1051e8ba80799bf", + "git_sha": "47e91ecc151180b81de0d4945c892bd616a6ad03", "installed_by": ["modules"] } } @@ -18,6 +64,11 @@ "branch": "main", "git_sha": "a79cb5c9645269db389c563f674b17c5e900a50b", "installed_by": ["subworkflows"] + }, + "preproc_t1": { + "branch": "main", + "git_sha": "84317cd68567fa8651c120bd19028177f90d41ae", + "installed_by": ["subworkflows"] } } } diff --git a/modules/local/stats/metricsinrois/main.nf b/modules/local/stats/metricsinrois/main.nf new file mode 100644 index 0000000..62c4a8b --- /dev/null +++ b/modules/local/stats/metricsinrois/main.nf @@ -0,0 +1,46 @@ +process STATS_METRICSINROI { + tag "$meta.id" + label 'process_single' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://scil.usherbrooke.ca/containers/scilus_latest.sif': + 'scilus/scilus:latest' }" + + input: + tuple val(meta), path(t1), path(metrics) + + output: + tuple val(meta), path("*.json") , emit: stats + tuple val(meta), path("*mask_wm.nii.gz") , emit: wm_mask + tuple val(meta), path("*mask_gm.nii.gz") , emit: gm_mask + tuple val(meta), path("*mask_csf.nii.gz") , emit: csf_mask + tuple val(meta), path("*map_wm.nii.gz") , emit: wm_map + tuple val(meta), path("*map_gm.nii.gz") , emit: gm_map + tuple val(meta), path("*map_csf.nii.gz") , emit: csf_map + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + def suffix = task.ext.first_suffix ? "${task.ext.first_suffix}_stats" : "stats" + def bin = task.ext.bin ? "--bin " : "" + def normalize_weights = task.ext.normalize_weights ? "--normalize_weights " : "" + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + fast -t 1 -n 3\ + -H 0.1 -I 4 -l 20.0 -g -o t1.nii.gz $t1 + scil_volume_math.py convert t1_seg_2.nii.gz ${prefix}__mask_wm.nii.gz --data_type uint8 + scil_volume_math.py convert t1_seg_1.nii.gz ${prefix}__mask_gm.nii.gz --data_type uint8 + scil_volume_math.py convert t1_seg_0.nii.gz ${prefix}__mask_csf.nii.gz --data_type uint8 + mv t1_pve_2.nii.gz ${prefix}__map_wm.nii.gz + mv t1_pve_1.nii.gz ${prefix}__map_gm.nii.gz + mv t1_pve_0.nii.gz ${prefix}__map_csf.nii.gz + + scil_volume_stats_in_ROI.py ${prefix}__mask*.nii.gz \ + --metrics $metrics \ + --sort_keys \ + $bin $normalize_weights > ${prefix}__${suffix}.json + + """ +} \ No newline at end of file diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml new file mode 100644 index 0000000..c3b3413 --- /dev/null +++ b/modules/nf-core/multiqc/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::multiqc=1.27 diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf new file mode 100644 index 0000000..fa35959 --- /dev/null +++ b/modules/nf-core/multiqc/main.nf @@ -0,0 +1,63 @@ +process MULTIQC { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ 'multiqc/multiqc:v1.27.1' }" + + input: + tuple val(meta), path(qc_images) // Added input with subject meta field. + path multiqc_files + path(multiqc_config) + path(extra_multiqc_config) + path(multiqc_logo) + path(replace_names) + path(sample_names) + + output: + path "*multiqc_report.html", emit: report + path "*_data" , emit: data + path "*_plots" , optional:true, emit: plots + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = "--filename ${meta.id}_multiqc_report" + def config = multiqc_config ? "--config $multiqc_config" : '' + def extra_config = extra_multiqc_config ? "--config $extra_multiqc_config" : '' + def logo = multiqc_logo ? "--cl-config 'custom_logo: \"${multiqc_logo}\"'" : '' + def replace = replace_names ? "--replace-names ${replace_names}" : '' + def samples = sample_names ? "--sample-names ${sample_names}" : '' + """ + multiqc \\ + --force \\ + $args \\ + $config \\ + $prefix \\ + $extra_config \\ + $logo \\ + $replace \\ + $samples \\ + . + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + multiqc: \$( multiqc --version | sed -e "s/multiqc, version //g" ) + END_VERSIONS + """ + + stub: + """ + mkdir multiqc_data + mkdir multiqc_plots + touch multiqc_report.html + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + multiqc: \$( multiqc --version | sed -e "s/multiqc, version //g" ) + END_VERSIONS + """ +} diff --git a/modules/nf-core/multiqc/meta.yml b/modules/nf-core/multiqc/meta.yml new file mode 100644 index 0000000..b16c187 --- /dev/null +++ b/modules/nf-core/multiqc/meta.yml @@ -0,0 +1,78 @@ +name: multiqc +description: Aggregate results from bioinformatics analyses across many samples into + a single report +keywords: + - QC + - bioinformatics tools + - Beautiful stand-alone HTML report +tools: + - multiqc: + description: | + MultiQC searches a given directory for analysis logs and compiles a HTML report. + It's a general use tool, perfect for summarising the output from numerous bioinformatics tools. + homepage: https://multiqc.info/ + documentation: https://multiqc.info/docs/ + licence: ["GPL-3.0-or-later"] + identifier: biotools:multiqc +input: + - - multiqc_files: + type: file + description: | + List of reports / files recognised by MultiQC, for example the html and zip output of FastQC + - - multiqc_config: + type: file + description: Optional config yml for MultiQC + pattern: "*.{yml,yaml}" + - - extra_multiqc_config: + type: file + description: Second optional config yml for MultiQC. Will override common sections + in multiqc_config. + pattern: "*.{yml,yaml}" + - - multiqc_logo: + type: file + description: Optional logo file for MultiQC + pattern: "*.{png}" + - - replace_names: + type: file + description: | + Optional two-column sample renaming file. First column a set of + patterns, second column a set of corresponding replacements. Passed via + MultiQC's `--replace-names` option. + pattern: "*.{tsv}" + - - sample_names: + type: file + description: | + Optional TSV file with headers, passed to the MultiQC --sample_names + argument. + pattern: "*.{tsv}" +output: + - report: + - "*multiqc_report.html": + type: file + description: MultiQC report file + pattern: "multiqc_report.html" + - data: + - "*_data": + type: directory + description: MultiQC data dir + pattern: "multiqc_data" + - plots: + - "*_plots": + type: file + description: Plots created by MultiQC + pattern: "*_data" + - versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" +authors: + - "@abhi18av" + - "@bunop" + - "@drpatelh" + - "@jfy133" +maintainers: + - "@abhi18av" + - "@bunop" + - "@drpatelh" + - "@jfy133" diff --git a/modules/nf-core/multiqc/tests/main.nf.test b/modules/nf-core/multiqc/tests/main.nf.test new file mode 100644 index 0000000..33316a7 --- /dev/null +++ b/modules/nf-core/multiqc/tests/main.nf.test @@ -0,0 +1,92 @@ +nextflow_process { + + name "Test Process MULTIQC" + script "../main.nf" + process "MULTIQC" + + tag "modules" + tag "modules_nfcore" + tag "multiqc" + + config "./nextflow.config" + + test("sarscov2 single-end [fastqc]") { + + when { + process { + """ + input[0] = Channel.of(file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true)) + input[1] = [] + input[2] = [] + input[3] = [] + input[4] = [] + input[5] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, + { assert process.out.data[0] ==~ ".*/multiqc_data" }, + { assert snapshot(process.out.versions).match("multiqc_versions_single") } + ) + } + + } + + test("sarscov2 single-end [fastqc] [config]") { + + when { + process { + """ + input[0] = Channel.of(file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true)) + input[1] = Channel.of(file("https://github.com/nf-core/tools/raw/dev/nf_core/pipeline-template/assets/multiqc_config.yml", checkIfExists: true)) + input[2] = [] + input[3] = [] + input[4] = [] + input[5] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert process.out.report[0] ==~ ".*/multiqc_report.html" }, + { assert process.out.data[0] ==~ ".*/multiqc_data" }, + { assert snapshot(process.out.versions).match("multiqc_versions_config") } + ) + } + } + + test("sarscov2 single-end [fastqc] - stub") { + + options "-stub" + + when { + process { + """ + input[0] = Channel.of(file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/fastqc/test_fastqc.zip', checkIfExists: true)) + input[1] = [] + input[2] = [] + input[3] = [] + input[4] = [] + input[5] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.report.collect { file(it).getName() } + + process.out.data.collect { file(it).getName() } + + process.out.plots.collect { file(it).getName() } + + process.out.versions ).match("multiqc_stub") } + ) + } + + } +} diff --git a/modules/nf-core/multiqc/tests/main.nf.test.snap b/modules/nf-core/multiqc/tests/main.nf.test.snap new file mode 100644 index 0000000..7b7c132 --- /dev/null +++ b/modules/nf-core/multiqc/tests/main.nf.test.snap @@ -0,0 +1,41 @@ +{ + "multiqc_versions_single": { + "content": [ + [ + "versions.yml:md5,8f3b8c1cec5388cf2708be948c9fa42f" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.4" + }, + "timestamp": "2025-01-27T09:29:57.631982377" + }, + "multiqc_stub": { + "content": [ + [ + "multiqc_report.html", + "multiqc_data", + "multiqc_plots", + "versions.yml:md5,8f3b8c1cec5388cf2708be948c9fa42f" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.4" + }, + "timestamp": "2025-01-27T09:30:34.743726958" + }, + "multiqc_versions_config": { + "content": [ + [ + "versions.yml:md5,8f3b8c1cec5388cf2708be948c9fa42f" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.4" + }, + "timestamp": "2025-01-27T09:30:21.44383553" + } +} \ No newline at end of file diff --git a/modules/nf-core/multiqc/tests/nextflow.config b/modules/nf-core/multiqc/tests/nextflow.config new file mode 100644 index 0000000..c537a6a --- /dev/null +++ b/modules/nf-core/multiqc/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + withName: 'MULTIQC' { + ext.prefix = null + } +} diff --git a/modules/nf-core/multiqc/tests/tags.yml b/modules/nf-core/multiqc/tests/tags.yml new file mode 100644 index 0000000..bea6c0d --- /dev/null +++ b/modules/nf-core/multiqc/tests/tags.yml @@ -0,0 +1,2 @@ +multiqc: + - modules/nf-core/multiqc/** diff --git a/modules/nf-neuro/betcrop/antsbet/environment.yml b/modules/nf-neuro/betcrop/antsbet/environment.yml new file mode 100644 index 0000000..893edb3 --- /dev/null +++ b/modules/nf-neuro/betcrop/antsbet/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: betcrop_antsbet diff --git a/modules/nf-neuro/betcrop/antsbet/main.nf b/modules/nf-neuro/betcrop/antsbet/main.nf new file mode 100644 index 0000000..dc2867e --- /dev/null +++ b/modules/nf-neuro/betcrop/antsbet/main.nf @@ -0,0 +1,72 @@ + +process BETCROP_ANTSBET { + tag "$meta.id" + label 'process_high' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" + + input: + tuple val(meta), path(t1), path(template), path(tissues_probabilities), path(mask), path(initial_affine) + + output: + tuple val(meta), path("*t1_bet.nii.gz") , emit: t1 + tuple val(meta), path("*t1_bet_mask.nii.gz"), emit: mask + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + def args = [] + if (mask) args += ["-f $mask"] + if (initial_affine) args += ["-r $initial_affine"] + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + export ANTS_RANDOM_SEED=1234 + + antsBrainExtraction.sh -d 3 -a $t1 -o bet/ -u 0 \ + -e $template -m $tissues_probabilities ${args.join(' ')} + scil_volume_math.py convert bet/BrainExtractionMask.nii.gz \ + ${prefix}__t1_bet_mask.nii.gz --data_type uint8 + scil_volume_math.py multiplication $t1 ${prefix}__t1_bet_mask.nii.gz \ + ${prefix}__t1_bet.nii.gz --data_type float32 + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\+\\).*/\\1/') + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + + touch ${prefix}__t1_bet.nii.gz + touch ${prefix}__t1_bet_mask.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\+\\).*/\\1/') + END_VERSIONS + + function handle_code () { + local code=\$? + ignore=( 1 ) + exit \$([[ " \${ignore[@]} " =~ " \$code " ]] && echo 0 || echo \$code) + } + trap 'handle_code' ERR + + antsBrainExtraction.sh + scil_volume_math.py -h + + """ +} diff --git a/modules/nf-neuro/betcrop/antsbet/meta.yml b/modules/nf-neuro/betcrop/antsbet/meta.yml new file mode 100644 index 0000000..e85bfcb --- /dev/null +++ b/modules/nf-neuro/betcrop/antsbet/meta.yml @@ -0,0 +1,78 @@ +--- +name: "betcrop_antsbet" +description: Perform Brain extraction using antsBrainExtraction.sh on T1 image. +keywords: + - T1 + - BET + - ants + - scilpy +tools: + - "scilpy": + description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." + homepage: "https://github.com/scilus/scilpy.git" + - "ants": + description: "Advanced Normalization Tools." + homepage: "https://github.com/ANTsX/ANTs" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - t1: + type: file + description: Nifti t1 volume to perform BET. + pattern: "*.{nii,nii.gz}" + + - template: + type: file + description: Nifti Anatomical template to perform BET. + pattern: "*.{nii,nii.gz}" + + - tissues_probabilities: + type: file + description: | + Brain probability mask (in template space), with intensity + range 1 (definitely brain) to 0 (definitely background). + pattern: "*.{nii,nii.gz}" + + - mask: + type: file + description: | + Brain mask (in template space) used to restrict metric + computation when performing registration. + pattern: "*.{nii,nii.gz}" + + - initial_affine: + type: file + description: | + Affine transform from T1w space to DWI space, used as + initialization for registration algorithms + pattern: "*.{mat/txt}" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - t1: + type: file + description: Nifti T1 volume brain-extracted. + pattern: "*t1_bet.{nii,nii.gz}" + + - mask: + type: file + description: T1 mask brain-extracted and cropped. + pattern: "*t1_bet_mask.{nii,nii.gz}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@ThoumyreStanislas" diff --git a/modules/nf-neuro/betcrop/antsbet/tests/main.nf.test b/modules/nf-neuro/betcrop/antsbet/tests/main.nf.test new file mode 100644 index 0000000..c42aa6b --- /dev/null +++ b/modules/nf-neuro/betcrop/antsbet/tests/main.nf.test @@ -0,0 +1,141 @@ +nextflow_process { + + name "Test Process BETCROP_ANTSBET" + script "../main.nf" + process "BETCROP_ANTSBET" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "betcrop" + tag "betcrop/antsbet" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + test("betcrop - antsbet") { + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "T1w.zip", "transform.zip", "antsbet.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1: it.simpleName == "T1w" + transform: it.simpleName == "transform" + template: it.simpleName == "antsbet" + } + ch_t1 = ch_split_test_data.t1.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz") + ] + } + ch_template = ch_split_test_data.template.map{ + test_data_directory -> [ + [ id: 'test' ], + file("\${test_data_directory}/t1_template.nii.gz"), + file("\${test_data_directory}/t1_brain_probability_map.nii.gz") + ] + } + ch_mask = ch_split_test_data.transform.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/t1_to_bet_template_mask.nii.gz") + ] + } + ch_transform = ch_split_test_data.transform.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/t1_to_bet_template.mat") + ] + } + input[0] = ch_t1 + .join(ch_template) + .join(ch_mask) + .join(ch_transform) + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + niftiMD5SUM(process.out.t1.get(0).get(1), 1), + niftiMD5SUM(process.out.mask.get(0).get(1)), + process.out.versions + ).match() } + ) + } + } + test("betcrop - antsbet - stub-run") { + options "-stub-run" + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "T1w.zip", "transform.zip", "antsbet.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1: it.simpleName == "T1w" + transform: it.simpleName == "transform" + template: it.simpleName == "antsbet" + } + ch_t1 = ch_split_test_data.t1.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz") + ] + } + ch_template = ch_split_test_data.template.map{ + test_data_directory -> [ + [ id: 'test' ], + file("\${test_data_directory}/t1_template.nii.gz"), + file("\${test_data_directory}/t1_brain_probability_map.nii.gz") + ] + } + ch_mask = ch_split_test_data.transform.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/t1_to_bet_template_mask.nii.gz") + ] + } + ch_transform = ch_split_test_data.transform.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/t1_to_bet_template.mat") + ] + } + input[0] = ch_t1 + .join(ch_template) + .join(ch_mask) + .join(ch_transform) + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + } +} diff --git a/modules/nf-neuro/betcrop/antsbet/tests/main.nf.test.snap b/modules/nf-neuro/betcrop/antsbet/tests/main.nf.test.snap new file mode 100644 index 0000000..8777011 --- /dev/null +++ b/modules/nf-neuro/betcrop/antsbet/tests/main.nf.test.snap @@ -0,0 +1,28 @@ +{ + "betcrop - antsbet": { + "content": [ + "test__t1_bet.nii.gz:md5:header,e7cfbd06624321d70cbd667a77315ba3,data,381da559c1f38a2526f58b741eb0f7fc", + "test__t1_bet_mask.nii.gz:md5:header,f7389fe98c9a7e3a87c90b7ca05ea14a,data,1572808125554e50ff73fbe0e28037a9", + [ + "versions.yml:md5,bb378e913d4002d0b644d3277892ae20" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-15T16:55:54.33351491" + }, + "betcrop - antsbet - stub-run": { + "content": [ + [ + "versions.yml:md5,bb378e913d4002d0b644d3277892ae20" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-14T20:42:47.191136661" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/betcrop/antsbet/tests/nextflow.config b/modules/nf-neuro/betcrop/antsbet/tests/nextflow.config new file mode 100644 index 0000000..d8b1ce8 --- /dev/null +++ b/modules/nf-neuro/betcrop/antsbet/tests/nextflow.config @@ -0,0 +1,6 @@ +process { + withName: "BETCROP_ANTSBET" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + } + cpus = 1 +} diff --git a/modules/nf-neuro/betcrop/antsbet/tests/tags.yml b/modules/nf-neuro/betcrop/antsbet/tests/tags.yml new file mode 100644 index 0000000..e4387ae --- /dev/null +++ b/modules/nf-neuro/betcrop/antsbet/tests/tags.yml @@ -0,0 +1,2 @@ +betcrop/antsbet: + - "modules/nf-neuro/betcrop/antsbet/**" diff --git a/modules/nf-neuro/betcrop/synthbet/environment.yml b/modules/nf-neuro/betcrop/synthbet/environment.yml new file mode 100644 index 0000000..4e0d7eb --- /dev/null +++ b/modules/nf-neuro/betcrop/synthbet/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: betcrop_synthbet diff --git a/modules/nf-neuro/betcrop/synthbet/main.nf b/modules/nf-neuro/betcrop/synthbet/main.nf new file mode 100644 index 0000000..cbbedde --- /dev/null +++ b/modules/nf-neuro/betcrop/synthbet/main.nf @@ -0,0 +1,60 @@ +process BETCROP_SYNTHBET { + tag "$meta.id" + label 'process_single' + + container "freesurfer/freesurfer:7.4.1" + + input: + tuple val(meta), path(image), path(weights) /* optional, input = [] */ + + output: + tuple val(meta), path("*__bet_image.nii.gz"), emit: bet_image + tuple val(meta), path("*__brain_mask.nii.gz"), emit: brain_mask + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + + def gpu = task.ext.gpu ? "--gpu" : "" + def border = task.ext.border ? "-b " + task.ext.border : "" + def nocsf = task.ext.nocsf ? "--no-csf" : "" + def model = "$weights" ? "--model $weights" : "" + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + mri_synthstrip -i $image --out ${prefix}__bet_image.nii.gz --mask ${prefix}__brain_mask.nii.gz $gpu $border $nocsf $model + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + Freesurfer: \$(mri_convert -version | grep "freesurfer" | sed -E 's/.* ([0-9]+\\.[0-9]+\\.[0-9]+).*/\\1/') + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + touch ${prefix}__bet_image.nii.gz + touch ${prefix}__brain_mask.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + Freesurfer: \$(mri_convert -version | grep "freesurfer" | sed -E 's/.* ([0-9]+\\.[0-9]+\\.[0-9]+).*/\\1/') + END_VERSIONS + + function handle_code () { + local code=\$? + ignore=( 1 ) + exit \$([[ " \${ignore[@]} " =~ " \$code " ]] && echo 0 || echo \$code) + } + trap 'handle_code' ERR + + mri_synthstrip -h + """ +} diff --git a/modules/nf-neuro/betcrop/synthbet/meta.yml b/modules/nf-neuro/betcrop/synthbet/meta.yml new file mode 100644 index 0000000..97c445d --- /dev/null +++ b/modules/nf-neuro/betcrop/synthbet/meta.yml @@ -0,0 +1,53 @@ +--- +name: "betcrop_synthbet" +description: Perform brain extraction using synthstrip on image +keywords: + - anatomical image + - BET + - freesurfer +tools: + - "Freesurfer": + description: "Software package for the analysis and visualization of structural and functional neuroimaging data." + homepage: "https://surfer.nmr.mgh.harvard.edu/fswiki" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: Nifti image volume to perform BET. + pattern: "*.{nii,nii.gz}" + + - weights: + type: file + description: Alternative model weights + pattern: "*.pt" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - bet_image: + type: file + description: Nifti brain-extracted volume. + pattern: "*__bet_image.{nii,nii.gz}" + + - brain_mask: + type: file + description: Brain-extracted image mask . + pattern: "*__brain_mask.{nii,nii.gz}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@anroy1" diff --git a/modules/nf-neuro/betcrop/synthbet/tests/main.nf.test b/modules/nf-neuro/betcrop/synthbet/tests/main.nf.test new file mode 100644 index 0000000..7e71d66 --- /dev/null +++ b/modules/nf-neuro/betcrop/synthbet/tests/main.nf.test @@ -0,0 +1,82 @@ +nextflow_process { + + name "Test Process BETCROP_SYNTHBET" + script "../main.nf" + process "BETCROP_SYNTHBET" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "betcrop" + tag "betcrop/synthbet" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + test("betcrop - synthbet") { + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "freesurfer.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/anat_image.nii.gz"), + [] + ] + } + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("betcrop - synthbet - stub-run") { + options "-stub-run" + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "freesurfer.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/anat_image.nii.gz"), + [] + ] + } + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + } +} diff --git a/modules/nf-neuro/betcrop/synthbet/tests/main.nf.test.snap b/modules/nf-neuro/betcrop/synthbet/tests/main.nf.test.snap new file mode 100644 index 0000000..9458823 --- /dev/null +++ b/modules/nf-neuro/betcrop/synthbet/tests/main.nf.test.snap @@ -0,0 +1,67 @@ +{ + "betcrop - synthbet - stub-run": { + "content": [ + [ + "versions.yml:md5,c639461870ca534b5105f61d672f740f" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.1" + }, + "timestamp": "2024-12-12T10:54:37.644838" + }, + "betcrop - synthbet": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__bet_image.nii.gz:md5,6f5eb365a11dfc52b8c80cd7bcee7b9d" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test__brain_mask.nii.gz:md5,52871114b59d09dca864cb0ea4f12832" + ] + ], + "2": [ + "versions.yml:md5,c639461870ca534b5105f61d672f740f" + ], + "bet_image": [ + [ + { + "id": "test", + "single_end": false + }, + "test__bet_image.nii.gz:md5,6f5eb365a11dfc52b8c80cd7bcee7b9d" + ] + ], + "brain_mask": [ + [ + { + "id": "test", + "single_end": false + }, + "test__brain_mask.nii.gz:md5,52871114b59d09dca864cb0ea4f12832" + ] + ], + "versions": [ + "versions.yml:md5,c639461870ca534b5105f61d672f740f" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-11-25T18:19:01.459548926" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/betcrop/synthbet/tests/nextflow.config b/modules/nf-neuro/betcrop/synthbet/tests/nextflow.config new file mode 100644 index 0000000..a88a57d --- /dev/null +++ b/modules/nf-neuro/betcrop/synthbet/tests/nextflow.config @@ -0,0 +1,7 @@ +process { + withName: "BETCROP_SYNTHBET" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + memory = 10.GB + ext.nocsf = true + } +} diff --git a/modules/nf-neuro/betcrop/synthbet/tests/tags.yml b/modules/nf-neuro/betcrop/synthbet/tests/tags.yml new file mode 100644 index 0000000..34f7987 --- /dev/null +++ b/modules/nf-neuro/betcrop/synthbet/tests/tags.yml @@ -0,0 +1,2 @@ +betcrop/synthbet: + - "modules/nf-neuro/betcrop/synthbet/**" diff --git a/modules/nf-neuro/denoising/mppca/environment.yml b/modules/nf-neuro/denoising/mppca/environment.yml new file mode 100644 index 0000000..8d43cac --- /dev/null +++ b/modules/nf-neuro/denoising/mppca/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: denoising_mppca diff --git a/modules/nf-neuro/denoising/mppca/main.nf b/modules/nf-neuro/denoising/mppca/main.nf new file mode 100755 index 0000000..4eb498d --- /dev/null +++ b/modules/nf-neuro/denoising/mppca/main.nf @@ -0,0 +1,55 @@ + +process DENOISING_MPPCA { + tag "$meta.id" + label 'process_medium' + + container "mrtrix3/mrtrix3:latest" + + input: + tuple val(meta), path(dwi), path(mask) + + output: + tuple val(meta), path("*_dwi_denoised.nii.gz") , emit: image + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + def extent = task.ext.extent ? "-extent " + task.ext.extent : "" + def args = ["-nthreads ${task.cpus - 1}"] + if (mask) args += ["-mask $mask"] + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + export MRTRIX_RNG_SEED=112524 + + dwidenoise $dwi ${prefix}_dwi_denoised.nii.gz $extent ${args.join(" ")} + mrcalc ${prefix}_dwi_denoised.nii.gz 0 -gt ${prefix}_dwi_denoised.nii.gz 0 \ + -if ${prefix}_dwi_denoised.nii.gz -force + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + mrtrix: \$(mrcalc -version 2>&1 | sed -n 's/== mrcalc \\([0-9.]\\+\\).*/\\1/p') + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + dwidenoise -h + mrcalc -h + + touch ${prefix}_dwi_denoised.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + mrtrix: \$(mrcalc -version 2>&1 | sed -n 's/== mrcalc \\([0-9.]\\+\\).*/\\1/p') + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/denoising/mppca/meta.yml b/modules/nf-neuro/denoising/mppca/meta.yml new file mode 100755 index 0000000..5ce90fe --- /dev/null +++ b/modules/nf-neuro/denoising/mppca/meta.yml @@ -0,0 +1,51 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json +name: "denoising_mppca" +description: denoise a dataset with the Marchenko-Pastur principal component analysis +keywords: + - nifti + - denoising + - mppca + - mrtrix + - fsl +tools: + - "MRtrix3": + description: "Toolbox for image processing, analysis and visualisation of dMRI." + homepage: "https://mrtrix.readthedocs.io/en/latest/" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - dwi: + type: file + description: Nifti dwi file to denoise + pattern: "*.{nii,nii.gz}" + + - mask: + type: file + description: Nifti mask file for the dwi, optional + pattern: "*.{nii,nii.gz}" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: Denoised Nifti image file + pattern: "*_dwi_denoised.{nii,nii.gz}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@scilus" diff --git a/modules/nf-neuro/denoising/mppca/tests/main.nf.test b/modules/nf-neuro/denoising/mppca/tests/main.nf.test new file mode 100644 index 0000000..42d732d --- /dev/null +++ b/modules/nf-neuro/denoising/mppca/tests/main.nf.test @@ -0,0 +1,65 @@ +nextflow_process { + + name "Test Process DENOISING_MPPCA" + script "../main.nf" + process "DENOISING_MPPCA" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "denoising" + tag "denoising/mppca" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + test("denoising - mppca") { + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "raw_DWIss300-dir8.zip", "raw_segmentation.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + dwi: it.simpleName == "raw_DWIss300-dir8" + segmentation: it.simpleName == "raw_segmentation" + } + ch_dwi = ch_split_test_data.dwi.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/dwi.nii.gz") + ] + } + ch_mask = ch_split_test_data.segmentation.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/brainmask/slices/axial.nii.gz") + ] + } + input[0] = ch_dwi + .join(ch_mask) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-neuro/denoising/mppca/tests/main.nf.test.snap b/modules/nf-neuro/denoising/mppca/tests/main.nf.test.snap new file mode 100644 index 0000000..21195dd --- /dev/null +++ b/modules/nf-neuro/denoising/mppca/tests/main.nf.test.snap @@ -0,0 +1,35 @@ +{ + "denoising - mppca": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test_dwi_denoised.nii.gz:md5,697aa941c0cb71fe1865662da5b727fa" + ] + ], + "1": [ + "versions.yml:md5,adbce7b09c63d541cdc2782235363275" + ], + "image": [ + [ + { + "id": "test" + }, + "test_dwi_denoised.nii.gz:md5,697aa941c0cb71fe1865662da5b727fa" + ] + ], + "versions": [ + "versions.yml:md5,adbce7b09c63d541cdc2782235363275" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.2" + }, + "timestamp": "2024-12-12T18:27:14.057961679" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/denoising/mppca/tests/nextflow.config b/modules/nf-neuro/denoising/mppca/tests/nextflow.config new file mode 100644 index 0000000..7a19d53 --- /dev/null +++ b/modules/nf-neuro/denoising/mppca/tests/nextflow.config @@ -0,0 +1,7 @@ +process { + withNAME: "DENOISING_MPPCA" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.extent = 3 + } + cpus = 1 +} diff --git a/modules/nf-neuro/denoising/mppca/tests/tags.yml b/modules/nf-neuro/denoising/mppca/tests/tags.yml new file mode 100644 index 0000000..06d15a2 --- /dev/null +++ b/modules/nf-neuro/denoising/mppca/tests/tags.yml @@ -0,0 +1,2 @@ +denoising/mppca: + - "modules/nf-neuro/denoising/mppca/**" diff --git a/modules/nf-neuro/denoising/nlmeans/environment.yml b/modules/nf-neuro/denoising/nlmeans/environment.yml new file mode 100644 index 0000000..7818335 --- /dev/null +++ b/modules/nf-neuro/denoising/nlmeans/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: denoising_nlmeans diff --git a/modules/nf-neuro/denoising/nlmeans/main.nf b/modules/nf-neuro/denoising/nlmeans/main.nf new file mode 100755 index 0000000..964b4b4 --- /dev/null +++ b/modules/nf-neuro/denoising/nlmeans/main.nf @@ -0,0 +1,52 @@ + +process DENOISING_NLMEANS { + tag "$meta.id" + label 'process_medium' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" + + input: + tuple val(meta), path(image), path(mask) + + output: + tuple val(meta), path("*_denoised.nii.gz") , emit: image + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + def ncoils = task.ext.number_of_coils ?: 1 + def args = ["--processes $task.cpus"] + if (mask) args += ["--mask $mask"] + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + scil_denoising_nlmeans.py $image ${prefix}__denoised.nii.gz $ncoils ${args.join(" ")} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + scil_denoising_nlmeans.py -h + + touch ${prefix}_denoised.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/denoising/nlmeans/meta.yml b/modules/nf-neuro/denoising/nlmeans/meta.yml new file mode 100755 index 0000000..23d7d1b --- /dev/null +++ b/modules/nf-neuro/denoising/nlmeans/meta.yml @@ -0,0 +1,50 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json +name: "denoising_nlmeans" +description: denoise a dataset with the Non Local Means algorithm +keywords: + - nifti + - denoising + - nlmeans + - scilpy +tools: + - "scilpy": + description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." + homepage: "https://github.com/scilus/scilpy.git" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: Nifti image file to denoise + pattern: "*.{nii,nii.gz}" + + - mask: + type: file + description: Nifti image file used to mask the input image + pattern: "*.{nii,nii.gz}" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: Denoised Nifti image file + pattern: "*_denoised.{nii,nii.gz}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@scilus" diff --git a/modules/nf-neuro/denoising/nlmeans/tests/main.nf.test b/modules/nf-neuro/denoising/nlmeans/tests/main.nf.test new file mode 100644 index 0000000..80baec2 --- /dev/null +++ b/modules/nf-neuro/denoising/nlmeans/tests/main.nf.test @@ -0,0 +1,65 @@ +nextflow_process { + + name "Test Process DENOISING_NLMEANS" + script "../main.nf" + process "DENOISING_NLMEANS" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "denoising" + tag "denoising/nlmeans" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "raw_b0.zip", "raw_segmentation.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("denoising - nlmeans") { + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + b0: it.simpleName == "raw_b0" + segmentation: it.simpleName == "raw_segmentation" + } + ch_b0 = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + ch_mask = ch_split_test_data.segmentation.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/brainmask/slices/axial.nii.gz") + ] + } + input[0] = ch_b0 + .join(ch_mask) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-neuro/denoising/nlmeans/tests/main.nf.test.snap b/modules/nf-neuro/denoising/nlmeans/tests/main.nf.test.snap new file mode 100644 index 0000000..545bbee --- /dev/null +++ b/modules/nf-neuro/denoising/nlmeans/tests/main.nf.test.snap @@ -0,0 +1,35 @@ +{ + "denoising - nlmeans": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__denoised.nii.gz:md5,4f679ca8c49a82ad566a814a819c6117" + ] + ], + "1": [ + "versions.yml:md5,9336a0abe0f0c988b89f895943b0a153" + ], + "image": [ + [ + { + "id": "test" + }, + "test__denoised.nii.gz:md5,4f679ca8c49a82ad566a814a819c6117" + ] + ], + "versions": [ + "versions.yml:md5,9336a0abe0f0c988b89f895943b0a153" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-11-05T18:28:29.498709866" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/denoising/nlmeans/tests/nextflow.config b/modules/nf-neuro/denoising/nlmeans/tests/nextflow.config new file mode 100755 index 0000000..0293c16 --- /dev/null +++ b/modules/nf-neuro/denoising/nlmeans/tests/nextflow.config @@ -0,0 +1,3 @@ +process { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } +} diff --git a/modules/nf-neuro/denoising/nlmeans/tests/tags.yml b/modules/nf-neuro/denoising/nlmeans/tests/tags.yml new file mode 100644 index 0000000..7472a5b --- /dev/null +++ b/modules/nf-neuro/denoising/nlmeans/tests/tags.yml @@ -0,0 +1,2 @@ +denoising/nlmeans: + - "modules/nf-neuro/denoising/nlmeans/**" diff --git a/modules/nf-neuro/image/cropvolume/environment.yml b/modules/nf-neuro/image/cropvolume/environment.yml new file mode 100644 index 0000000..670802a --- /dev/null +++ b/modules/nf-neuro/image/cropvolume/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: image_cropvolume diff --git a/modules/nf-neuro/image/cropvolume/main.nf b/modules/nf-neuro/image/cropvolume/main.nf new file mode 100755 index 0000000..08c5d38 --- /dev/null +++ b/modules/nf-neuro/image/cropvolume/main.nf @@ -0,0 +1,56 @@ + +process IMAGE_CROPVOLUME { + tag "$meta.id" + label 'process_single' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" + + input: + tuple val(meta), path(image), path(bounding_box) + + output: + tuple val(meta), path("*_cropped.nii.gz"), emit: image + tuple val(meta), path("*.pkl") , emit: bounding_box, optional: true + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + + def input_bbox = bounding_box ? "--input_bbox $bounding_box" : "" + def suffix = task.ext.first_suffix ? "${task.ext.first_suffix}_cropped" : "cropped" + def output_bbox = task.ext.output_bbox ? "--output_bbox ${prefix}_${suffix}_bbox.pkl" : "" + + """ + scil_volume_crop.py $image ${prefix}_${suffix}.nii.gz $input_bbox $output_bbox + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + def suffix = task.ext.first_suffix ? "${task.ext.first_suffix}_cropped" : "cropped" + + """ + scil_volume_crop.py -h + + touch ${prefix}_${suffix}.nii.gz + + if $task.ext.output_bbox; + then + touch ${prefix}_${suffix}_bbox.pkl + fi + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/image/cropvolume/meta.yml b/modules/nf-neuro/image/cropvolume/meta.yml new file mode 100755 index 0000000..df13e95 --- /dev/null +++ b/modules/nf-neuro/image/cropvolume/meta.yml @@ -0,0 +1,54 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json +name: "image_cropvolume" +description: Crop empty planes around the data in a volume +keywords: + - nifti + - crop + - scilpy +tools: + - "scilpy": + description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." + homepage: "https://github.com/scilus/scilpy.git" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: Nifti image file to crop + pattern: "*.{nii,nii.gz}" + + - bounding_box: + type: file + description: Input bounding box to use to crop the image + pattern: "*.{pkl}" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: Cropped Nifti image file + pattern: "*_cropped.{nii,nii.gz}" + + - bounding_box: + type: file + description: Bouding box defining the limits of the crop + pattern: "*.pkl" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@AlexVCaron" diff --git a/modules/nf-neuro/image/cropvolume/tests/main.nf.test b/modules/nf-neuro/image/cropvolume/tests/main.nf.test new file mode 100644 index 0000000..af2c948 --- /dev/null +++ b/modules/nf-neuro/image/cropvolume/tests/main.nf.test @@ -0,0 +1,80 @@ +nextflow_process { + + name "Test Process IMAGE_CROPVOLUME" + script "../main.nf" + process "IMAGE_CROPVOLUME" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "image" + tag "image/cropvolume" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "heavy.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("image - cropvolume") { + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/anat/anat_image.nii.gz"), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("image - cropvolume - outputbbox") { + + config "./nextflow_bbox.config" + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/anat/anat_image.nii.gz"), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-neuro/image/cropvolume/tests/main.nf.test.snap b/modules/nf-neuro/image/cropvolume/tests/main.nf.test.snap new file mode 100644 index 0000000..b7c4766 --- /dev/null +++ b/modules/nf-neuro/image/cropvolume/tests/main.nf.test.snap @@ -0,0 +1,96 @@ +{ + "image - cropvolume": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_cropped.nii.gz:md5,93cc10f69b0829409c69bf743e9c95ef" + ] + ], + "1": [ + + ], + "2": [ + "versions.yml:md5,c9f9039fc9103e716e831016a61807f9" + ], + "bounding_box": [ + + ], + "image": [ + [ + { + "id": "test", + "single_end": false + }, + "test_cropped.nii.gz:md5,93cc10f69b0829409c69bf743e9c95ef" + ] + ], + "versions": [ + "versions.yml:md5,c9f9039fc9103e716e831016a61807f9" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-12-12T18:59:41.317523464" + }, + "image - cropvolume - outputbbox": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_cropped.nii.gz:md5,93cc10f69b0829409c69bf743e9c95ef" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test_cropped_bbox.pkl:md5,d6a03b61669b07683e8c4b21a613f16c" + ] + ], + "2": [ + "versions.yml:md5,c9f9039fc9103e716e831016a61807f9" + ], + "bounding_box": [ + [ + { + "id": "test", + "single_end": false + }, + "test_cropped_bbox.pkl:md5,d6a03b61669b07683e8c4b21a613f16c" + ] + ], + "image": [ + [ + { + "id": "test", + "single_end": false + }, + "test_cropped.nii.gz:md5,93cc10f69b0829409c69bf743e9c95ef" + ] + ], + "versions": [ + "versions.yml:md5,c9f9039fc9103e716e831016a61807f9" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-12-12T18:59:50.080977383" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/image/cropvolume/tests/nextflow.config b/modules/nf-neuro/image/cropvolume/tests/nextflow.config new file mode 100644 index 0000000..3a9e735 --- /dev/null +++ b/modules/nf-neuro/image/cropvolume/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + withName: "IMAGE_CROPVOLUME" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + } +} diff --git a/modules/nf-neuro/image/cropvolume/tests/nextflow_bbox.config b/modules/nf-neuro/image/cropvolume/tests/nextflow_bbox.config new file mode 100644 index 0000000..6055585 --- /dev/null +++ b/modules/nf-neuro/image/cropvolume/tests/nextflow_bbox.config @@ -0,0 +1,6 @@ +process { + withName: "IMAGE_CROPVOLUME" { + ext.output_bbox = true + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + } +} diff --git a/modules/nf-neuro/image/cropvolume/tests/tags.yml b/modules/nf-neuro/image/cropvolume/tests/tags.yml new file mode 100644 index 0000000..eeae1b2 --- /dev/null +++ b/modules/nf-neuro/image/cropvolume/tests/tags.yml @@ -0,0 +1,2 @@ +image/cropvolume: + - "modules/nf-neuro/image/cropvolume/**" diff --git a/modules/nf-neuro/image/resample/environment.yml b/modules/nf-neuro/image/resample/environment.yml new file mode 100644 index 0000000..5d952d2 --- /dev/null +++ b/modules/nf-neuro/image/resample/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: image_resample diff --git a/modules/nf-neuro/image/resample/main.nf b/modules/nf-neuro/image/resample/main.nf new file mode 100755 index 0000000..75fb5ba --- /dev/null +++ b/modules/nf-neuro/image/resample/main.nf @@ -0,0 +1,60 @@ +process IMAGE_RESAMPLE { + tag "$meta.id" + label 'process_single' + label 'process_high_memory' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" + + input: + tuple val(meta), path(image), path(ref) /* optional, input = [] */ + + output: + tuple val(meta), path("*_resampled.nii.gz") , emit: image + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + def suffix = task.ext.first_suffix ? "${task.ext.first_suffix}_resampled" : "resampled" + def reference = "$ref" ? "--ref $ref" : "" + def voxel_size = task.ext.voxel_size ? "--voxel_size " + task.ext.voxel_size : "" + def volume_size = task.ext.volume_size ? "--volume_size " + task.ext.volume_size : "" + def iso_min = task.ext.iso_min ? "--iso_min" : "" + def interp = task.ext.interp ? "--interp " + task.ext.interp : "" + def f = task.ext.f ? "-f" : "" + def enforce_dimensions = task.ext.enforce_dimensions ? "--enforce_dimensions" : "" + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + scil_volume_resample.py $image ${prefix}_${suffix}.nii.gz \ + $voxel_size $volume_size $reference $iso_min \ + $f $enforce_dimensions $interp + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def suffix = task.ext.first_suffix ? "${task.ext.first_suffix}_resampled" : "resampled" + """ + scil_volume_resample.py -h + + touch ${prefix}_${suffix}.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/image/resample/meta.yml b/modules/nf-neuro/image/resample/meta.yml new file mode 100755 index 0000000..83aabf0 --- /dev/null +++ b/modules/nf-neuro/image/resample/meta.yml @@ -0,0 +1,49 @@ +--- +name: "image_resample" +description: Script to resample a dataset to match the resolution of another reference dataset or to the resolution specified in argument. Needs either one of voxel_size, volume_size, iso_min or reference as parameter/input. Interpolation defaults as linear. +keywords: + - resample + - nifti + - volume + - scilpy +tools: + - "scilpy": + description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." + homepage: "https://github.com/scilus/scilpy.git" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: Nifti image file to resample to + pattern: "*.{nii,nii.gz}" + + - ref: + type: file + description: Nifti reference image file + pattern: "*.{nii,nii.gz}" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: Nifti image file resampled + pattern: "*_resampled.{nii,nii.gz}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@scilus" diff --git a/modules/nf-neuro/image/resample/tests/main.nf.test b/modules/nf-neuro/image/resample/tests/main.nf.test new file mode 100644 index 0000000..36ba92f --- /dev/null +++ b/modules/nf-neuro/image/resample/tests/main.nf.test @@ -0,0 +1,191 @@ +nextflow_process { + + name "Test Process IMAGE_RESAMPLE" + script "../main.nf" + process "IMAGE_RESAMPLE" + + tag "modules" + tag "modules_nfcore" + tag "image" + tag "image/resample" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "others.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("image - resample - voxsize") { + + config "./nextflow_voxsize.config" + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/fa.nii.gz"), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("image - resample - volsize") { + + config "./nextflow_volsize.config" + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/fa.nii.gz"), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("image - resample - isomin") { + + config "./nextflow_isomin.config" + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/fa.nii.gz"), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("image - resample - ref") { + + config "./nextflow_ref.config" + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/fa.nii.gz"), + file("\${test_data_directory}/fa_resample.nii.gz") + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("image - resample - nn") { + + config "./nextflow_nn.config" + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/fa.nii.gz"), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("image - resample - stub-run") { + + options '-stub-run' + + config "./nextflow_voxsize.config" + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/fa.nii.gz"), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + + } + +} diff --git a/modules/nf-neuro/image/resample/tests/main.nf.test.snap b/modules/nf-neuro/image/resample/tests/main.nf.test.snap new file mode 100644 index 0000000..24e366e --- /dev/null +++ b/modules/nf-neuro/image/resample/tests/main.nf.test.snap @@ -0,0 +1,189 @@ +{ + "image - resample - stub-run": { + "content": [ + [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-11-28T18:48:51.114688771" + }, + "image - resample - isomin": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,f3164907d16ceabf606a03ea0efd3a2f" + ] + ], + "1": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ], + "image": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,f3164907d16ceabf606a03ea0efd3a2f" + ] + ], + "versions": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T15:30:19.061192" + }, + "image - resample - nn": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,8d3e1c9552673b1ce60bc4d090198564" + ] + ], + "1": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ], + "image": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,8d3e1c9552673b1ce60bc4d090198564" + ] + ], + "versions": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T15:30:42.725713" + }, + "image - resample - volsize": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,7c83b666f9fb2d68643a20d1c7ac21d8" + ] + ], + "1": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ], + "image": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,7c83b666f9fb2d68643a20d1c7ac21d8" + ] + ], + "versions": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T15:30:07.1626" + }, + "image - resample - voxsize": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_voxsize_resampled.nii.gz:md5,6ffdabab138186db2dab6f98ea6e9ffb" + ] + ], + "1": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ], + "image": [ + [ + { + "id": "test", + "single_end": false + }, + "test_voxsize_resampled.nii.gz:md5,6ffdabab138186db2dab6f98ea6e9ffb" + ] + ], + "versions": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-11-28T18:31:36.84484801" + }, + "image - resample - ref": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,0a45d21ff294fda2e41c620cf83d0406" + ] + ], + "1": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ], + "image": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,0a45d21ff294fda2e41c620cf83d0406" + ] + ], + "versions": [ + "versions.yml:md5,06181e25531a3ebdefdfd2e641a3d645" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T15:30:31.018615" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/image/resample/tests/nextflow_isomin.config b/modules/nf-neuro/image/resample/tests/nextflow_isomin.config new file mode 100644 index 0000000..3ffb7b6 --- /dev/null +++ b/modules/nf-neuro/image/resample/tests/nextflow_isomin.config @@ -0,0 +1,7 @@ +process { + withName: "IMAGE_RESAMPLE" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.iso_min = true + ext.interp = "lin" + } +} diff --git a/modules/nf-neuro/image/resample/tests/nextflow_nn.config b/modules/nf-neuro/image/resample/tests/nextflow_nn.config new file mode 100644 index 0000000..1930e62 --- /dev/null +++ b/modules/nf-neuro/image/resample/tests/nextflow_nn.config @@ -0,0 +1,7 @@ +process { + withName: "IMAGE_RESAMPLE" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.voxel_size = 1 + ext.interp = "nn" + } +} diff --git a/modules/nf-neuro/image/resample/tests/nextflow_ref.config b/modules/nf-neuro/image/resample/tests/nextflow_ref.config new file mode 100644 index 0000000..f0e1beb --- /dev/null +++ b/modules/nf-neuro/image/resample/tests/nextflow_ref.config @@ -0,0 +1,6 @@ +process { + withName: "IMAGE_RESAMPLE" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.interp = "lin" + } +} diff --git a/modules/nf-neuro/image/resample/tests/nextflow_volsize.config b/modules/nf-neuro/image/resample/tests/nextflow_volsize.config new file mode 100644 index 0000000..cb892de --- /dev/null +++ b/modules/nf-neuro/image/resample/tests/nextflow_volsize.config @@ -0,0 +1,7 @@ +process { + withName: "IMAGE_RESAMPLE" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.volume_size = 256 + ext.interp = "lin" + } +} diff --git a/modules/nf-neuro/image/resample/tests/nextflow_voxsize.config b/modules/nf-neuro/image/resample/tests/nextflow_voxsize.config new file mode 100644 index 0000000..51643e5 --- /dev/null +++ b/modules/nf-neuro/image/resample/tests/nextflow_voxsize.config @@ -0,0 +1,7 @@ +process { + withName: "IMAGE_RESAMPLE" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.voxel_size = 1 + ext.first_suffix = "voxsize" + } +} diff --git a/modules/nf-neuro/image/resample/tests/tags.yml b/modules/nf-neuro/image/resample/tests/tags.yml new file mode 100644 index 0000000..63487a8 --- /dev/null +++ b/modules/nf-neuro/image/resample/tests/tags.yml @@ -0,0 +1,2 @@ +image/resample: + - "modules/nf-neuro/image/resample/**" diff --git a/modules/nf-neuro/preproc/n4/environment.yml b/modules/nf-neuro/preproc/n4/environment.yml new file mode 100644 index 0000000..a77b1b6 --- /dev/null +++ b/modules/nf-neuro/preproc/n4/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: preproc_n4 diff --git a/modules/nf-neuro/preproc/n4/main.nf b/modules/nf-neuro/preproc/n4/main.nf new file mode 100644 index 0000000..446ca5c --- /dev/null +++ b/modules/nf-neuro/preproc/n4/main.nf @@ -0,0 +1,73 @@ +process PREPROC_N4 { + tag "$meta.id" + label 'process_medium' + label "process_high_memory" + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" + + input: + tuple val(meta), path(image), path(ref), path(ref_mask) + + output: + tuple val(meta), path("*__image_n4.nii.gz") , emit: image + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def bspline_knot_per_voxel = task.ext.bspline_knot_per_voxel ? "$task.ext.bspline_knot_per_voxel" : "1" + def shrink_factor = task.ext.shrink_factor ? "$task.ext.shrink_factor" : "1" + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + export ANTS_RANDOM_SEED=1234 + + if [[ -f "$ref" ]] + then + spacing=\$(mrinfo -spacing $ref | tr " " "\\n" | sort -n | tail -1) + knot_spacing=\$(echo "\$spacing/$bspline_knot_per_voxel" | bc -l) + + N4BiasFieldCorrection -i $ref\ + -o [${prefix}__ref_n4.nii.gz, bias_field_ref.nii.gz]\ + -c [300x150x75x50, 1e-6] -v 1\ + -b [\${knot_spacing}, 3] \ + -s $shrink_factor + + scil_dwi_apply_bias_field.py $image bias_field_ref.nii.gz\ + ${prefix}__image_n4.nii.gz --mask $ref_mask -f + + else + N4BiasFieldCorrection -i $image\ + -o [${prefix}__image_n4.nii.gz, bias_field_t1.nii.gz]\ + -c [300x150x75x50, 1e-6] -v 1 + fi + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + N4BiasFieldCorrection: \$(N4BiasFieldCorrection --version 2>&1 | sed -n 's/ANTs Version: v\\([0-9.]\\+\\)/\\1/p') + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + """ + N4BiasFieldCorrection -h + scil_dwi_apply_bias_field.py -h + + touch ${prefix}__image_n4.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + N4BiasFieldCorrection: \$(N4BiasFieldCorrection --version 2>&1 | sed -n 's/ANTs Version: v\\([0-9.]\\+\\)/\\1/p') + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/preproc/n4/meta.yml b/modules/nf-neuro/preproc/n4/meta.yml new file mode 100644 index 0000000..a473851 --- /dev/null +++ b/modules/nf-neuro/preproc/n4/meta.yml @@ -0,0 +1,54 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json +name: "preproc_n4" +description: Bias field correction using N4 +keywords: + - correction + - N4 + - bias field +tools: + - "scilpy": + description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." + homepage: "https://github.com/scilus/scilpy.git" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: Nifti image file to correct + pattern: "*.{nii,nii.gz}" + + - ref: + type: file + description: Nifti image file for the reference + pattern: "*.{nii,nii.gz}" + + - ref_mask: + type: file + description: Nifti image file mask for the reference + pattern: "*.{nii,nii.gz}" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - image: + type: file + description: N4 corrected image + pattern: "*.{nii,nii.gz}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@arnaudbore" diff --git a/modules/nf-neuro/preproc/n4/tests/main.nf.test b/modules/nf-neuro/preproc/n4/tests/main.nf.test new file mode 100644 index 0000000..9cc2037 --- /dev/null +++ b/modules/nf-neuro/preproc/n4/tests/main.nf.test @@ -0,0 +1,69 @@ +nextflow_process { + + name "Test Process PREPROC_N4" + script "../main.nf" + process "PREPROC_N4" + + tag "modules" + tag "modules_nfcore" + tag "preproc" + tag "preproc/n4" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "heavy.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("preproc - n4 - dwi") { + config "./nextflow_dwi.config" + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory + .map{ test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/dwi_with_b0/dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi_with_b0/b0.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi_with_b0/mask.nii.gz", checkIfExists: true)]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("preproc - n4 - anat") { + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory + .map{ test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/anat/anat_image.nii.gz", checkIfExists: true), + [], + []]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-neuro/preproc/n4/tests/main.nf.test.snap b/modules/nf-neuro/preproc/n4/tests/main.nf.test.snap new file mode 100644 index 0000000..9980943 --- /dev/null +++ b/modules/nf-neuro/preproc/n4/tests/main.nf.test.snap @@ -0,0 +1,72 @@ +{ + "preproc - n4 - dwi": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__image_n4.nii.gz:md5,6383b9ffb68d04bf089ba7a040c4a77f" + ] + ], + "1": [ + "versions.yml:md5,08cced153fb00b483f3a59216e05aaff" + ], + "image": [ + [ + { + "id": "test", + "single_end": false + }, + "test__image_n4.nii.gz:md5,6383b9ffb68d04bf089ba7a040c4a77f" + ] + ], + "versions": [ + "versions.yml:md5,08cced153fb00b483f3a59216e05aaff" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-10T16:06:45.886061" + }, + "preproc - n4 - anat": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__image_n4.nii.gz:md5,a0329a81f3cc26dfa04e3fb8c6e6dc66" + ] + ], + "1": [ + "versions.yml:md5,08cced153fb00b483f3a59216e05aaff" + ], + "image": [ + [ + { + "id": "test", + "single_end": false + }, + "test__image_n4.nii.gz:md5,a0329a81f3cc26dfa04e3fb8c6e6dc66" + ] + ], + "versions": [ + "versions.yml:md5,08cced153fb00b483f3a59216e05aaff" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-10T16:09:11.896134" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/preproc/n4/tests/nextflow_dwi.config b/modules/nf-neuro/preproc/n4/tests/nextflow_dwi.config new file mode 100644 index 0000000..5c9246e --- /dev/null +++ b/modules/nf-neuro/preproc/n4/tests/nextflow_dwi.config @@ -0,0 +1,6 @@ +process { + withName: "PREPROC_N4" { + ext.bspline_knot_per_voxel = 0.25 + ext.shrink_factor = 4 + } +} diff --git a/modules/nf-neuro/preproc/n4/tests/tags.yml b/modules/nf-neuro/preproc/n4/tests/tags.yml new file mode 100644 index 0000000..c65df28 --- /dev/null +++ b/modules/nf-neuro/preproc/n4/tests/tags.yml @@ -0,0 +1,2 @@ +preproc/n4: + - "modules/nf-neuro/preproc/n4/**" diff --git a/modules/nf-neuro/reconst/dtimetrics/main.nf b/modules/nf-neuro/reconst/dtimetrics/main.nf index c992930..ab9609b 100644 --- a/modules/nf-neuro/reconst/dtimetrics/main.nf +++ b/modules/nf-neuro/reconst/dtimetrics/main.nf @@ -82,9 +82,18 @@ process RECONST_DTIMETRICS { scil_dti_metrics.py dwi_dti_shells.nii.gz bval_dti_shells bvec_dti_shells \ --not_all $args $b0_threshold -f + ransac_metrics=\$(echo "$args" | awk '{for(i=1; i versions.yml @@ -144,6 +156,7 @@ process RECONST_DTIMETRICS { """ scil_dwi_extract_shell.py -h scil_dti_metrics.py -h + scil_volume_remove_outliers_ransac.py -h touch ${prefix}__ad.nii.gz touch ${prefix}__evecs.nii.gz diff --git a/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test b/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test index 1fa8bf8..ff88cee 100644 --- a/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test +++ b/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test @@ -25,6 +25,105 @@ nextflow_process { } } + test("reconst - dtimetrics") { + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + dwi: it.simpleName == "DWIss1000-dir32" + segmentation: it.simpleName == "segmentation" + } + ch_dwi = ch_split_test_data.dwi.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/dwi.nii.gz"), + file("\${test_data_directory}/dwi.bval"), + file("\${test_data_directory}/dwi.bvec") + ] + } + ch_mask = ch_split_test_data.segmentation.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/brainmask/brainmask.nii.gz") + ] + } + input[0] = ch_dwi + .join(ch_mask) + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + niftiMD5SUM(process.out.ad.get(0).get(1), 6), + file(process.out.mqc.get(0).get(1)).name, + file(process.out.evecs.get(0).get(1)).name, + file(process.out.evecs_v1.get(0).get(1)).name, + file(process.out.evecs_v2.get(0).get(1)).name, + file(process.out.evecs_v3.get(0).get(1)).name, + niftiMD5SUM(process.out.evals.get(0).get(1), 6), + niftiMD5SUM(process.out.evals_e1.get(0).get(1), 6), + niftiMD5SUM(process.out.evals_e2.get(0).get(1), 6), + niftiMD5SUM(process.out.evals_e3.get(0).get(1), 6), + niftiMD5SUM(process.out.fa.get(0).get(1), 6), + niftiMD5SUM(process.out.ga.get(0).get(1), 6), + niftiMD5SUM(process.out.rgb.get(0).get(1), 6), + niftiMD5SUM(process.out.md.get(0).get(1), 6), + file(process.out.mode.get(0).get(1)).name, + niftiMD5SUM(process.out.norm.get(0).get(1), 6), + niftiMD5SUM(process.out.rd.get(0).get(1), 6), + file(process.out.tensor.get(0).get(1)).name, + niftiMD5SUM(process.out.nonphysical.get(0).get(1), 6), + niftiMD5SUM(process.out.pulsation_std_dwi.get(0).get(1), 6), + niftiMD5SUM(process.out.pulsation_std_b0.get(0).get(1), 6), + niftiMD5SUM(process.out.residual.get(0).get(1), 2), + process.out.residual_iqr_residuals, + process.out.residual_mean_residuals, + process.out.residual_q1_residuals, + process.out.residual_q3_residuals, + process.out.residual_residuals_stats, + process.out.residual_std_residuals, + process.out.versions + ).match() } + ) + } + } + + test("reconst - dtimetrics_with_b0mask") { + config "./nextflow_light.config" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + dwi: it.simpleName == "DWIss1000-dir32" + segmentation: it.simpleName == "segmentation" + } + input[0] = ch_split_test_data.dwi.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/dwi.nii.gz"), + file("\${test_data_directory}/dwi.bval"), + file("\${test_data_directory}/dwi.bvec"), + [] + ] + } + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + niftiMD5SUM(process.out.fa.get(0).get(1), 6), + process.out.versions + ).match() } + ) + } + } + test("reconst - stub-run dtimetrics") { options "-stub-run" when { diff --git a/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test.snap b/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test.snap index 02d355c..6064273 100644 --- a/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test.snap +++ b/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test.snap @@ -10,5 +10,100 @@ "nextflow": "24.10.4" }, "timestamp": "2025-03-06T15:57:03.763217558" + }, + "reconst - dtimetrics_with_b0mask": { + "content": [ + "test__fa.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,cd80bf54bc8529c97cb042d49c50d4fc", + [ + "versions.yml:md5,76869f1b7075822c64ec0ff67408144a" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.4" + }, + "timestamp": "2025-03-06T15:56:54.731216819" + }, + "reconst - dtimetrics": { + "content": [ + "test__ad.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,a4f053c9d65bed0062f7778818c555b4", + "test__dti_mqc.png", + "test__evecs.nii.gz", + "test__evecs_v1.nii.gz", + "test__evecs_v2.nii.gz", + "test__evecs_v3.nii.gz", + "test__evals.nii.gz:md5:header,6a667c1adb8ac1636526963bb9091ea6,data,194ed3036c73a0120615e95bc343607e", + "test__evals_e1.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,a4f053c9d65bed0062f7778818c555b4", + "test__evals_e2.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,ffa92eecb19b6ffbfb27d1696e521d44", + "test__evals_e3.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,7a79a34de17b0653d10242455f3b8a3a", + "test__fa.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,cd80bf54bc8529c97cb042d49c50d4fc", + "test__ga.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,a76a594121a0ef669616621eb15aec57", + "test__rgb.nii.gz:md5:header,952bd2b13b64d7f1b128af7d85191a18,data,a2063164b39833b2a1596683b63494bd", + "test__md.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,4e1b42f49af7ed531d0d85fcae5effa8", + "test__mode.nii.gz", + "test__norm.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,e4f9a5a1ea4021f01879c2ba269b33dd", + "test__rd.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,48d8030185bea2f3b11a0a648bbb6abd", + "test__tensor.nii.gz", + "test__nonphysical.nii.gz:md5:header,0968ff06e75bcbd0d7b2dcbcc5bf6ff7,data,4e865d417aae67c66e2a7c681aacfcae", + "test__pulsation_std_dwi.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,62fe8dc99a6e3733c28b2c05281b6781", + "test__pulsation_std_b0.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,e5bed827c14a49c4ff1aa97844ade64f", + "test__residual.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,a01c5897af1087af9a9153e20b491e91", + [ + [ + { + "id": "test" + }, + "test__residual_iqr_residuals.npy:md5,58dfe3925469668723c16dc37f5c787c" + ] + ], + [ + [ + { + "id": "test" + }, + "test__residual_mean_residuals.npy:md5,5429f2a46c9b14efa47278c7cdb32b3c" + ] + ], + [ + [ + { + "id": "test" + }, + "test__residual_q1_residuals.npy:md5,2790db91a0e7727358dd2aaccb887983" + ] + ], + [ + [ + { + "id": "test" + }, + "test__residual_q3_residuals.npy:md5,15c3d97be583a3e39bad3731aaea082d" + ] + ], + [ + [ + { + "id": "test" + }, + "test__residual_residuals_stats.png:md5,bb819c98e3c98a8cd74cb1e79ae044d5" + ] + ], + [ + [ + { + "id": "test" + }, + "test__residual_std_residuals.npy:md5,0d41ba9ad5d2ac4543f957a1d84c187f" + ] + ], + [ + "versions.yml:md5,76869f1b7075822c64ec0ff67408144a" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.4" + }, + "timestamp": "2025-03-06T15:56:33.450256049" } -} \ No newline at end of file +} diff --git a/modules/nf-neuro/reconst/dtimetrics/tests/nextflow_light.config b/modules/nf-neuro/reconst/dtimetrics/tests/nextflow_light.config new file mode 100644 index 0000000..bd0ceaf --- /dev/null +++ b/modules/nf-neuro/reconst/dtimetrics/tests/nextflow_light.config @@ -0,0 +1,22 @@ +process { + withName: "RECONST_DTIMETRICS" { + ext.ad = false + ext.evecs = false + ext.evals = false + ext.fa = true + ext.ga = false + ext.rgb = false + ext.md = false + ext.mode = false + ext.norm = false + ext.rd = false + ext.tensor = false + ext.nonphysical = false + ext.pulsation = false + ext.residual = false + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.max_dti_shell_value = 1200 + ext.run_qc = true + } +} diff --git a/nextflow.config b/nextflow.config index aa572be..30ce3f8 100644 --- a/nextflow.config +++ b/nextflow.config @@ -15,4 +15,62 @@ manifest { name = 'scilus/nf-neuro-tutorial' description = """nf-neuro-tutorial is a Nextflow pipeline for processing neuroimaging data.""" version = '0.1dev' +} + +params.input = false +params.output = 'result' + +// ** subworkflow PREPROC_DIFF ** +params.preproc_dwi_run_denoising = true + +// ** Subworkflow PREPROC T1 ** +params.preproc_t1_run_denoising = true +params.preproc_t1_run_N4 = false +params.preproc_t1_run_resampling = false +params.preproc_t1_run_ants_bet = false +params.preproc_t1_run_synthbet = true +params.preproc_t1_run_crop = false + + +process { + + publishDir = [ + path: { "${params.output}/$meta.id/${task.process.replaceAll(':', '-')}" }, + mode: 'copy' + ] + + withName: "DENOISING_MPPCA" { + ext.extent = 3 + } + + withName: "BETCROP_SYNTHBET" { + memory = "4G" + ext.nocsf = false + } + + withName: "RECONST_DTIMETRICS" { + ext.ad = false + ext.evecs = false + ext.evals = false + ext.fa = true + ext.ga = false + ext.rgb = false + ext.md = false + ext.mode = false + ext.norm = false + ext.rd = false + ext.tensor = false + ext.nonphysical = false + ext.pulsation = false + ext.residual = false + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.max_dti_shell_value = 1200 + ext.run_qc = true + } + + withName: "STATS_METRICSINROI" { + ext.bin = true + ext.normalize_weights = false + } } \ No newline at end of file diff --git a/subworkflows/local/preproc_diff/main.nf b/subworkflows/local/preproc_diff/main.nf new file mode 100644 index 0000000..0099ef5 --- /dev/null +++ b/subworkflows/local/preproc_diff/main.nf @@ -0,0 +1,45 @@ +include { RECONST_DTIMETRICS } from '../../../modules/nf-neuro/reconst/dtimetrics/main' +include { DENOISING_MPPCA } from '../../../modules/nf-neuro/denoising/mppca/main' + +workflow PREPROC_DIFF { + + take: + ch_dwi // channel: [ val(meta), dwi, bval, bvec ] + + main: + ch_multiqc_files = Channel.empty() + + // ** Denoise DWI ** // + if (params.preproc_dwi_run_denoising) { + ch_dwi_bvalbvec = ch_dwi + .multiMap { meta, dwi, bval, bvec -> + dwi: [ meta, dwi ] + bvs_files: [ meta, bval, bvec ] + } + + ch_denoise_dwi = ch_dwi_bvalbvec.dwi + .map{ it + [[]] } + + DENOISING_MPPCA ( ch_denoise_dwi ) + + // Fetch specific output + ch_dwi = DENOISING_MPPCA.out.image + .join(ch_dwi_bvalbvec.bvs_files) + } + + // Input DTI update with DWI denoised output + input_dti = ch_dwi.map{ it + [[]] } + + // DTI-derived metrics + RECONST_DTIMETRICS( input_dti ) + ch_multiqc_files = ch_multiqc_files.mix(RECONST_DTIMETRICS.out.mqc) + + emit: + dwi = ch_dwi_bvalbvec.dwi // channel: [ val(meta), dwi-raw ] + dwi_denoised = DENOISING_MPPCA.out.image // channel: [ val(meta), dwi-after-mppca ] + bvs_files = ch_dwi_bvalbvec.bvs_files // channel: [ val(meta), bval, bvec ] + fa = RECONST_DTIMETRICS.out.fa // channel: [ val(meta), fa ] + md = RECONST_DTIMETRICS.out.md // channel: [ val(meta), md ] + mqc = ch_multiqc_files // channel: [ val(meta), mqc ] + +} \ No newline at end of file diff --git a/subworkflows/nf-neuro/preproc_t1/main.nf b/subworkflows/nf-neuro/preproc_t1/main.nf new file mode 100644 index 0000000..cc87f85 --- /dev/null +++ b/subworkflows/nf-neuro/preproc_t1/main.nf @@ -0,0 +1,159 @@ +// ** Importing modules from nf-neuro ** // +include { DENOISING_NLMEANS } from '../../../modules/nf-neuro/denoising/nlmeans/main' +include { PREPROC_N4 } from '../../../modules/nf-neuro/preproc/n4/main' +include { IMAGE_RESAMPLE } from '../../../modules/nf-neuro/image/resample/main' +include { BETCROP_ANTSBET } from '../../../modules/nf-neuro/betcrop/antsbet/main' +include { BETCROP_SYNTHBET} from '../../../modules/nf-neuro/betcrop/synthbet/main' +include { IMAGE_CROPVOLUME as IMAGE_CROPVOLUME_T1 } from '../../../modules/nf-neuro/image/cropvolume/main' +include { IMAGE_CROPVOLUME as IMAGE_CROPVOLUME_MASK } from '../../../modules/nf-neuro/image/cropvolume/main' + +params.preproc_t1_run_synthbet = false + +workflow PREPROC_T1 { + + take: + ch_image // channel: [ val(meta), image ] + ch_template // channel: [ val(meta), template ] , optional + ch_probability_map // channel: [ val(meta), probability-map, mask, initial-affine ] , optional + ch_mask_nlmeans // channel: [ val(meta), mask ] , optional + ch_ref_n4 // channel: [ val(meta), ref, ref-mask ] , optional + ch_ref_resample // channel: [ val(meta), ref ] , optional + ch_weights // channel: [ val(meta), weights ] , optional + + main: + + ch_versions = Channel.empty() + + if ( params.preproc_t1_run_denoising ) { + + // ** Denoising ** // + // Result : [ meta, image, mask | [] ] + // Steps : + // - join [ meta, image, mask | null ] + // - map [ meta, image, mask | [] ] + ch_nlmeans = ch_image + .join(ch_mask_nlmeans, remainder: true) + .map{ it[0..1] + [it[2] ?: []] } + + DENOISING_NLMEANS ( ch_nlmeans ) + ch_versions = ch_versions.mix(DENOISING_NLMEANS.out.versions.first()) + image_nlmeans = DENOISING_NLMEANS.out.image + } + else { + image_nlmeans = ch_image + } + + if ( params.preproc_t1_run_N4 ) { + // ** N4 correction ** // + // Result : [ meta, image, reference | [], mask | [] ] + // Steps : + // - join [ meta, image ] + [ reference, mask ] | [ reference, null ] | [ null ] + // - map [ meta, image, reference | [], mask | [] ] + // - join [ meta, image, reference | [], mask | [], nlmeans-mask | null ] + // - map [ meta, image, reference | [], mask | [] ] + ch_N4 = image_nlmeans + .join(ch_ref_n4, remainder: true) + .map{ it[0..1] + [it[2] ?: [], it[3] ?: []] } + .join(ch_mask_nlmeans, remainder: true) + .map{ it[0..2] + [it[3] ?: it[4] ?: []] } + + PREPROC_N4 ( ch_N4 ) + ch_versions = ch_versions.mix(PREPROC_N4.out.versions.first()) + image_N4 = PREPROC_N4.out.image + } + else { + image_N4 = image_nlmeans + } + + if ( params.preproc_t1_run_resampling ) { + // ** Resampling ** // + // Result : [ meta, image, reference | [] ] + // Steps : + // - join [ meta, image, reference | null ] + // - map [ meta, image, reference | [] ] + ch_resampling = image_N4 + .join(ch_ref_resample, remainder: true) + .map{ it[0..1] + [it[2] ?: []] } + + IMAGE_RESAMPLE ( ch_resampling ) + ch_versions = ch_versions.mix(IMAGE_RESAMPLE.out.versions.first()) + image_resample = IMAGE_RESAMPLE.out.image + } + else { + image_resample = image_N4 + } + + if ( params.preproc_t1_run_synthbet ) { + // ** SYNTHBET ** // + // Result : [ meta, image, weights | [] ] + // Steps : + // - join [ meta, image, weights | null ] + // - map [ meta, image, weights | [] ] + ch_bet = image_resample + .join(ch_weights, remainder: true) + .map{ it[0..1] + [it[2] ?: []] } + + BETCROP_SYNTHBET ( ch_bet ) + ch_versions = ch_versions.mix(BETCROP_SYNTHBET.out.versions.first()) + + // ** Setting BET output ** // + image_bet = BETCROP_SYNTHBET.out.bet_image + mask_bet = BETCROP_SYNTHBET.out.brain_mask + } + else if ( params.preproc_t1_run_ants_bet ) { + // ** ANTSBET ** // + // The template and probability maps are mandatory if running antsBET. Since the + // error message from nextflow when they are absent is either non-informative or + // missing, we use ifEmpty to provide a more informative one. + ch_bet = image_resample + .join(ch_template.ifEmpty{ error("ANTS BET needs a template") }) + .join(ch_probability_map.ifEmpty{ error("ANTS BET needs a tissue probability map") }) + .map{ it + [[], []] } + + BETCROP_ANTSBET ( ch_bet ) + ch_versions = ch_versions.mix(BETCROP_ANTSBET.out.versions.first()) + + // ** Setting BET output ** // + image_bet = BETCROP_ANTSBET.out.t1 + mask_bet = BETCROP_ANTSBET.out.mask + } + else { + image_bet = image_resample + mask_bet = Channel.empty() + } + + if ( params.preproc_t1_run_crop ) { + // ** Crop image ** // + ch_crop = image_bet + .map{ it + [[]] } + + IMAGE_CROPVOLUME_T1 ( ch_crop ) + ch_versions = ch_versions.mix(IMAGE_CROPVOLUME_T1.out.versions.first()) + image_crop = IMAGE_CROPVOLUME_T1.out.image + bbox = IMAGE_CROPVOLUME_T1.out.bounding_box + + // ** Crop mask ** // + ch_crop_mask = mask_bet + .join(IMAGE_CROPVOLUME_T1.out.bounding_box) + + IMAGE_CROPVOLUME_MASK ( ch_crop_mask ) + ch_versions = ch_versions.mix(IMAGE_CROPVOLUME_MASK.out.versions.first()) + mask_crop = IMAGE_CROPVOLUME_MASK.out.image + } + else { + image_crop = image_bet + mask_crop = Channel.empty() + bbox = Channel.empty() + } + + emit: + t1_final = image_crop // channel: [ val(meta), t1-preprocessed ] + mask_final = mask_crop // channel: [ val(meta), t1-mask ] + image_nlmeans = image_nlmeans // channel: [ val(meta), t1-after-denoise ] + image_N4 = image_N4 // channel: [ val(meta), t1-after-unbias ] + image_resample = image_resample // channel: [ val(meta), t1-after-resample ] + image_bet = image_bet // channel: [ val(meta), t1-after-bet ] + mask_bet = mask_bet // channel: [ val(meta), intermediary-mask ] + crop_box = bbox // channel: [ val(meta), bounding-box ] + versions = ch_versions // channel: [ versions.yml ] +} diff --git a/subworkflows/nf-neuro/preproc_t1/meta.yml b/subworkflows/nf-neuro/preproc_t1/meta.yml new file mode 100644 index 0000000..e736187 --- /dev/null +++ b/subworkflows/nf-neuro/preproc_t1/meta.yml @@ -0,0 +1,151 @@ +name: "preproc_t1" +description: | + Subworkflow for T1 image pre-processing, from denoising to brain extraction and cropping. + It requires three input channels, one with a raw T1 image you want to process (ch_image), + one with a T1 template (ch_template) and one with a brain probability mask (ch_probability_map). + The aim of the workflow is to carry out the first stages of pre-processing, through denoising, + N4 correction, resampling and brain extraction, right up to the cropping stage. + The resulting T1 is corrected, resampled, brain extracted and cropped. + You can retrieve the file after each step if you don't want to run the entire subworkflow. + The next steps would be to register the resulting T1-corrected image with the DWI-corrected image + with, for example, the REGISTRATION subworkflow. IMPORTANT : the subworkflow is only reproducible + with when running ANTs BET using a single thread. + ----------- Steps ----------- + Denoising (nlmeans, scil). + Used to remove the noise induced by the MRI acquisition, + enhance the signal to noise ratio and improve the image quality and following metrics. + The denoising is performed in the original spatial resolution and uses the nlmeans method. + N4 Image Intensities Bias Correction (N4BiasFieldCorrection, ANTs). + Used to normalize the image intensities and reduce biases (e.g. signal loss with distance in + multi-channel head coils), while preserving contrasts between tissues. + Resample (DIPY). + Resamples the T1 to an isotropic spatial resolution. The default is 1mm, a standard in humans which + usually facilitate registration with corrected DWI images. + This spatial resolution is modifiable in the configuration file. + Brain Extraction (ANTs - default, freesurfer). + Isolates the brain tissue voxels from the remaining image. Also creates a binary brain mask. + This brain extraction is required for the T1 to DWI Registration. IMPORTANT : when using ANTs, + brain extraction is reproducible only when run using a single thread. + Cropping (scil). + Crops the empty planes around the brain to optimize the next processing steps. + Subworkflow based on Tractoflow : https://www.sciencedirect.com/science/article/pii/S105381192030375X?via%3Dihub + +keywords: + - T1 + - preprocessing + - nlmeans + - n4 + - resample + - bet + - crop + +components: + - denoising/nlmeans + - preproc/n4 + - image/resample + - betcrop/antsbet + - betcrop/synthbet + - image/cropvolume + +input: + - ch_image: + type: file + description: | + The input channel containing the anatomical image. + Structure: [ val(meta), path(image) ] + pattern: "*.{nii,nii.gz}" + - ch_template: + type: file + description: | + The input channel containing the anatomical template for antsBET. + Structure: [ val(meta), path(image) ] + pattern: "*.{nii,nii.gz}" + - ch_probability_map: + type: file + description: | + The input channel containing the brain probability mask for antsBET, + with intensity range 1 (definitely brain) to 0 (definitely background). + Structure: [ val(meta), path(image) ] + pattern: "*.{nii,nii.gz}" + - ch_mask_nlmeans: + type: file + description: | + The input channel containing the brain mask for the nlmeans denoising. Optional + Structure: [ val(meta), path(mask) ] + pattern: "*.{nii,nii.gz}" + - ch_ref_n4: + type: file + description: | + The input channel containing the reference and the reference mask for the n4 correction. Optional + Structure: [ val(meta), path(ref), path(mask_ref) ] + pattern: "*.{nii,nii.gz}" + - ch_ref_resample: + type: file + description: | + The input channel containing the reference for the resampling. Optional + Structure: [ val(meta), path(ref) ] + pattern: "*.{nii,nii.gz}" + - ch_weights: + type: file + description: | + The input channel containing an alternative model weights for synthbet. Optional + Structure: [ val(meta), path (weights)] + pattern: "*.pt" +output: + - image_nlmeans: + type: file + description: | + Channel containing the anatomical image after the nlmeans denoising. + Structure: [ val(meta), path(image) ] + pattern: "*.{nii,nii.gz}" + - image_N4: + type: file + description: | + Channel containing the anatomical image after the N4 correction. + Structure: [ val(meta), path(image) ] + pattern: "*.{nii,nii.gz}" + - image_resample: + type: file + description: | + Channel containing the anatomical image after the resampling. + Structure: [ val(meta), path(image) ] + pattern: "*.{nii,nii.gz}" + - image_bet: + type: file + description: | + Channel containing the anatomical image after the brain-extraction. + Structure: [ val(meta), path(t1) ] + pattern: "*.{nii,nii.gz}" + - mask_bet: + type: file + description: | + Channel containing the binary mask after the brain-extraction. + Structure: [ val(meta), path(mask) ] + pattern: "*.{nii,nii.gz}" + - crop_box: + type: file + description: | + Channel containing the bouding box defining the limits of the crop. + Structure: [ val(meta), path(image) ] + pattern: "*.{pkl}" + - mask_final: + type: file + description: | + Channel containing the binary mask after the all preprocessing. + Structure: [ val(meta), path(mask) ] + pattern: "*.{nii,nii.gz}" + - t1_final: + type: file + description: | + Channel containing the anatomical image after the all preprocessing. + Structure: [ val(meta), path(image) ] + pattern: "*.{nii,nii.gz}" + - versions: + type: file + description: | + File containing software versions + Structure: [ path(versions.yml) ] + pattern: "versions.yml" + +authors: + - "@ThoumyreStanislas" diff --git a/subworkflows/nf-neuro/preproc_t1/tests/main.nf.test b/subworkflows/nf-neuro/preproc_t1/tests/main.nf.test new file mode 100644 index 0000000..ec2d95b --- /dev/null +++ b/subworkflows/nf-neuro/preproc_t1/tests/main.nf.test @@ -0,0 +1,341 @@ +nextflow_workflow { + + name "Test Subworkflow PREPROC_T1" + script "../main.nf" + workflow "PREPROC_T1" + + tag "subworkflows" + tag "subworkflows_nfcore" + tag "subworkflows/preproc_t1" + + tag "denoising/nlmeans" + tag "preproc/n4" + tag "image/resample" + tag "betcrop/antsbet" + tag "betcrop/synthbet" + tag "image/cropvolume" + + tag "load_test_data" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "antsbet.zip" , "T1w.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("preproc_t1_antsbet_error") { + config "./nextflow.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + antsbet: it.simpleName == "antsbet" + t1w: it.simpleName == "T1w" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/T1w.nii.gz") + ]} + input[1] = Channel.empty() + input[2] = Channel.empty() + input[3] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[4] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [], + [] + ]} + input[5] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[6] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + """ + } + } + + then { + assert workflow.failed + } + } + + test("preproc_t1_antsbet") { + config "./nextflow.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + antsbet: it.simpleName == "antsbet" + t1w: it.simpleName == "T1w" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/T1w.nii.gz") + ]} + input[1] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/t1_template.nii.gz") + ]} + input[2] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/t1_brain_probability_map.nii.gz"), + [], + [] + ]} + input[3] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[4] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [], + [] + ]} + input[5] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[6] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + """ + } + } + + then { + assertAll( + { assert workflow.success}, + { assert snapshot( + niftiMD5SUM(workflow.out.image_nlmeans.get(0).get(1)), + niftiMD5SUM(workflow.out.image_N4.get(0).get(1)), + niftiMD5SUM(workflow.out.image_resample.get(0).get(1)), + file(workflow.out.image_bet.get(0).get(1)).name, + file(workflow.out.mask_bet.get(0).get(1)).name, + file(workflow.out.crop_box.get(0).get(1)).name, + file(workflow.out.mask_final.get(0).get(1)).name, + file(workflow.out.t1_final.get(0).get(1)).name, + workflow.out.versions + ).match()} + ) + } + } + + test("preproc_t1_synthbet") { + config "./nextflow_synthbet.config" + + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + antsbet: it.simpleName == "antsbet" + t1w: it.simpleName == "T1w" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/T1w.nii.gz") + ]} + input[1] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[2] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[3] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [], + [], + [] + ]} + input[4] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [], + [] + ]} + input[5] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[6] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + """ + } + } + + then { + assertAll( + { assert workflow.success}, + { assert snapshot( + niftiMD5SUM(workflow.out.image_nlmeans.get(0).get(1)), + niftiMD5SUM(workflow.out.image_N4.get(0).get(1)), + niftiMD5SUM(workflow.out.image_resample.get(0).get(1)), + niftiMD5SUM(workflow.out.image_bet.get(0).get(1)), + niftiMD5SUM(workflow.out.mask_bet.get(0).get(1)), + workflow.out.crop_box, + niftiMD5SUM(workflow.out.mask_final.get(0).get(1)), + niftiMD5SUM(workflow.out.t1_final.get(0).get(1)), + workflow.out.versions + ).match()} + ) + } + } + + test("preproc_t1_quick") { + config "./nextflow_quick.config" + + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + antsbet: it.simpleName == "antsbet" + t1w: it.simpleName == "T1w" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/T1w.nii.gz") + ]} + input[1] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[2] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[3] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [], + [], + [] + ]} + input[4] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [], + [] + ]} + input[5] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[6] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + } + + test("preproc_t1_skip_all") { + config "./nextflow_skip_all.config" + + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + antsbet: it.simpleName == "antsbet" + t1w: it.simpleName == "T1w" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/T1w.nii.gz") + ]} + input[1] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[2] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[3] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [], + [], + [] + ]} + input[4] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [], + [] + ]} + input[5] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + input[6] = ch_split_test_data.antsbet.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + [] + ]} + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + } +} diff --git a/subworkflows/nf-neuro/preproc_t1/tests/main.nf.test.snap b/subworkflows/nf-neuro/preproc_t1/tests/main.nf.test.snap new file mode 100644 index 0000000..da31b7d --- /dev/null +++ b/subworkflows/nf-neuro/preproc_t1/tests/main.nf.test.snap @@ -0,0 +1,314 @@ +{ + "preproc_t1_quick": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_cropped.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "1": [ + + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test__denoised.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "3": [ + [ + { + "id": "test", + "single_end": false + }, + "test__denoised.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "4": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "5": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "6": [ + + ], + "7": [ + + ], + "8": [ + "versions.yml:md5,bdd934b4b8456060c36d6d97e4f30740", + "versions.yml:md5,bf4dd58c38dd4863ebfb9e78a94c3a20", + "versions.yml:md5,ea32c30f5320f720b2f5dc32ac2535ea" + ], + "crop_box": [ + + ], + "image_N4": [ + [ + { + "id": "test", + "single_end": false + }, + "test__denoised.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "image_bet": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "image_nlmeans": [ + [ + { + "id": "test", + "single_end": false + }, + "test__denoised.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "image_resample": [ + [ + { + "id": "test", + "single_end": false + }, + "test_resampled.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "mask_bet": [ + + ], + "mask_final": [ + + ], + "t1_final": [ + [ + { + "id": "test", + "single_end": false + }, + "test_cropped.nii.gz:md5,c507c9182cc410c298fad4a03540c0c9" + ] + ], + "versions": [ + "versions.yml:md5,bdd934b4b8456060c36d6d97e4f30740", + "versions.yml:md5,bf4dd58c38dd4863ebfb9e78a94c3a20", + "versions.yml:md5,ea32c30f5320f720b2f5dc32ac2535ea" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-14T10:02:29.972139887" + }, + "preproc_t1_synthbet": { + "content": [ + "test__denoised.nii.gz:md5:header,a7ee0e819007aba98d14f7a145b550e6,data,2e21433e2bcd4de2a6b6167c6233cd40", + "test__image_n4.nii.gz:md5:header,e7cfbd06624321d70cbd667a77315ba3,data,a81e98f32ed963c098ccb07486101898", + "test_resampled.nii.gz:md5:header,7628a07204938d640c3530fa3d76d2b7,data,a81e98f32ed963c098ccb07486101898", + "test__bet_image.nii.gz:md5:header,38a09a5addf7c1f13dae1121c562f3b5,data,74f76699e09839809038875af1d9ae6c", + "test__brain_mask.nii.gz:md5:header,8fc2b4ae979d881623dfd34d377d437d,data,2d61506dc4ab2f8093b731474ae7e45c", + [ + [ + { + "id": "test", + "single_end": false + }, + "test_t1_cropped_bbox.pkl:md5,522d2c44d3ad1058ea77457a263e39c8" + ] + ], + "test_cropped.nii.gz:md5:header,efab188f3700b5b29d4b4ef99cec1295,data,e551be653d402018fd73a4f708b1641e", + "test_t1_cropped.nii.gz:md5:header,efab188f3700b5b29d4b4ef99cec1295,data,8a93b9d76ead7dae4af4792b9cf70479", + [ + "versions.yml:md5,318cabe934be45528a25f52083d9c90d", + "versions.yml:md5,b979132991d8f72a3585465533bd5730", + "versions.yml:md5,bdd934b4b8456060c36d6d97e4f30740", + "versions.yml:md5,be3dbb0ac2589ad263d583018f339102", + "versions.yml:md5,bf4dd58c38dd4863ebfb9e78a94c3a20", + "versions.yml:md5,ea32c30f5320f720b2f5dc32ac2535ea" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-11-25T18:23:03.990173398" + }, + "preproc_t1_skip_all": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "1": [ + + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "3": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "4": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "5": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "6": [ + + ], + "7": [ + + ], + "8": [ + + ], + "crop_box": [ + + ], + "image_N4": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "image_bet": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "image_nlmeans": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "image_resample": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "mask_bet": [ + + ], + "mask_final": [ + + ], + "t1_final": [ + [ + { + "id": "test", + "single_end": false + }, + "T1w.nii.gz:md5,ce10054d30c0a0753c619d67d811fe32" + ] + ], + "versions": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-01-14T21:44:26.607236449" + }, + "preproc_t1_antsbet": { + "content": [ + "test__denoised.nii.gz:md5:header,a7ee0e819007aba98d14f7a145b550e6,data,2e21433e2bcd4de2a6b6167c6233cd40", + "test__image_n4.nii.gz:md5:header,e7cfbd06624321d70cbd667a77315ba3,data,a81e98f32ed963c098ccb07486101898", + "test_resampled.nii.gz:md5:header,7628a07204938d640c3530fa3d76d2b7,data,a81e98f32ed963c098ccb07486101898", + "test__t1_bet.nii.gz", + "test__t1_bet_mask.nii.gz", + "test_t1_cropped_bbox.pkl", + "test_cropped.nii.gz", + "test_t1_cropped.nii.gz", + [ + "versions.yml:md5,b979132991d8f72a3585465533bd5730", + "versions.yml:md5,bdd934b4b8456060c36d6d97e4f30740", + "versions.yml:md5,be3dbb0ac2589ad263d583018f339102", + "versions.yml:md5,bf4dd58c38dd4863ebfb9e78a94c3a20", + "versions.yml:md5,da278daafbe3afa8454021e2716dd205", + "versions.yml:md5,ea32c30f5320f720b2f5dc32ac2535ea" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-12-12T15:50:29.838128539" + } +} diff --git a/subworkflows/nf-neuro/preproc_t1/tests/nextflow.config b/subworkflows/nf-neuro/preproc_t1/tests/nextflow.config new file mode 100644 index 0000000..3a82000 --- /dev/null +++ b/subworkflows/nf-neuro/preproc_t1/tests/nextflow.config @@ -0,0 +1,18 @@ +process { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + withName: "IMAGE_RESAMPLE" { + ext.voxel_size = 1 + ext.interp = "lin" + } + withName: "IMAGE_CROPVOLUME_T1" { + ext.output_bbox = true + ext.first_suffix = "t1" + } +} + +params.preproc_t1_run_denoising = true +params.preproc_t1_run_N4 = true +params.preproc_t1_run_resampling = true +params.preproc_t1_run_ants_bet = true +params.preproc_t1_run_synthbet = false +params.preproc_t1_run_crop = true diff --git a/subworkflows/nf-neuro/preproc_t1/tests/nextflow_quick.config b/subworkflows/nf-neuro/preproc_t1/tests/nextflow_quick.config new file mode 100644 index 0000000..04551ce --- /dev/null +++ b/subworkflows/nf-neuro/preproc_t1/tests/nextflow_quick.config @@ -0,0 +1,14 @@ +process { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + withName: "IMAGE_RESAMPLE" { + ext.voxel_size = 1 + ext.interp = "lin" + } +} + +params.preproc_t1_run_denoising = true +params.preproc_t1_run_N4 = false +params.preproc_t1_run_resampling = true +params.preproc_t1_run_ants_bet = false +params.preproc_t1_run_synthbet = false +params.preproc_t1_run_crop = true diff --git a/subworkflows/nf-neuro/preproc_t1/tests/nextflow_skip_all.config b/subworkflows/nf-neuro/preproc_t1/tests/nextflow_skip_all.config new file mode 100644 index 0000000..4e87743 --- /dev/null +++ b/subworkflows/nf-neuro/preproc_t1/tests/nextflow_skip_all.config @@ -0,0 +1,14 @@ +process { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + withName: "BETCROP_SYNTHBET" { + memory = "8G" + ext.nocsf = true + } +} + +params.preproc_t1_run_denoising = false +params.preproc_t1_run_N4 = false +params.preproc_t1_run_resampling = false +params.preproc_t1_run_ants_bet = false +params.preproc_t1_run_synthbet = false +params.preproc_t1_run_crop = false diff --git a/subworkflows/nf-neuro/preproc_t1/tests/nextflow_synthbet.config b/subworkflows/nf-neuro/preproc_t1/tests/nextflow_synthbet.config new file mode 100644 index 0000000..a514910 --- /dev/null +++ b/subworkflows/nf-neuro/preproc_t1/tests/nextflow_synthbet.config @@ -0,0 +1,22 @@ +process { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + withName: "IMAGE_RESAMPLE" { + ext.voxel_size = 1 + ext.interp = "lin" + } + withName: "IMAGE_CROPVOLUME_T1" { + ext.output_bbox = true + ext.first_suffix = "t1" + } + withName: "BETCROP_SYNTHBET" { + memory = "8G" + ext.nocsf = true + } +} + +params.preproc_t1_run_denoising = true +params.preproc_t1_run_N4 = true +params.preproc_t1_run_resampling = true +params.preproc_t1_run_ants_bet = false +params.preproc_t1_run_synthbet = true +params.preproc_t1_run_crop = true diff --git a/subworkflows/nf-neuro/preproc_t1/tests/tags.yml b/subworkflows/nf-neuro/preproc_t1/tests/tags.yml new file mode 100644 index 0000000..033e4cd --- /dev/null +++ b/subworkflows/nf-neuro/preproc_t1/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/preproc_t1: + - subworkflows/nf-neuro/preproc_t1/**