diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b290e09..46699f0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,15 @@ "image": "nfcore/gitpod:latest", "remoteUser": "gitpod", "runArgs": ["--privileged"], - + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "containerEnv": { + "NFCORE_MODULES_GIT_REMOTE": "https://github.com/scilus/nf-neuro.git", + "NFCORE_MODULES_BRANCH": "main", + "NFCORE_SUBWORKFLOWS_GIT_REMOTE": "https://github.com/scilus/nf-neuro.git", + "NFCORE_SUBWORKFLOWS_BRANCH": "main" + }, // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. diff --git a/.editorconfig b/.editorconfig index 6d9b74c..790b791 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,13 +12,27 @@ indent_style = space indent_size = 2 # These files are edited and tested upstream in nf-core/modules -[/modules/nf-core/**] +[modules/nf-core/**] charset = unset end_of_line = unset insert_final_newline = unset trim_trailing_whitespace = unset indent_style = unset -[/subworkflows/nf-core/**] +[subworkflows/nf-core/**] +charset = unset +end_of_line = unset +insert_final_newline = unset +trim_trailing_whitespace = unset +indent_style = unset + +# Same for nf-neuro +[modules/nf-neuro/**] +charset = unset +end_of_line = unset +insert_final_newline = unset +trim_trailing_whitespace = unset +indent_style = unset +[subworkflows/nf-neuro/**] charset = unset end_of_line = unset insert_final_newline = unset diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cfb304..1509e8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,9 @@ jobs: # Only run on push if this is the nf-core dev branch (merged PRs) if: "${{ github.event_name != 'push' || (github.event_name == 'push' && github.repository == 'scilus/nf-tractoflow') }}" runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} strategy: + fail-fast: true matrix: NXF_VER: - "24.04.2" @@ -37,6 +39,10 @@ jobs: - "test" isMaster: - ${{ github.base_ref == 'master' }} + include: + - experimental: false + - experimental: true + NXF_VER: "latest-everything" # Exclude conda and singularity on dev exclude: - isMaster: false diff --git a/.gitignore b/.gitignore index a42ce01..de9dfa3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ testing/ testing* *.pyc null/ +node_modules/ +venv/ diff --git a/.nf-core.yml b/.nf-core.yml index c18be30..82672f2 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -8,6 +8,7 @@ lint: - .github/workflows/awstest.yml - .github/workflows/awsfulltest.yml - conf/igenomes.config + - conf/igenomes_ignored.config files_unchanged: - CODE_OF_CONDUCT.md - assets/nf-core-tractoflow_logo_light.png @@ -19,6 +20,9 @@ lint: nextflow_config: - manifest.name - manifest.homePage + schema_lint: false + schema_params: false + schema_description: false nf_core_version: 3.2.0 repository_type: pipeline template: diff --git a/.prettierignore b/.prettierignore index 903c9a1..edd29f0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,6 @@ email_template.html adaptivecard.json +slackreport.json .nextflow* work/ data/ diff --git a/README.md b/README.md index bc4b132..a074ee7 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Now, you can run the pipeline using: ```bash nextflow run scilus/nf-tractoflow \ -profile \ - --input samplesheet.csv \ + <--input|--bids> samplesheet.csv \ --outdir ``` diff --git a/assets/adaptivecard.json b/assets/adaptivecard.json index 6a0fc77..68dc84d 100644 --- a/assets/adaptivecard.json +++ b/assets/adaptivecard.json @@ -5,7 +5,7 @@ "contentType": "application/vnd.microsoft.card.adaptive", "contentUrl": null, "content": { - "\$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "msteams": { "width": "Full" }, @@ -50,12 +50,11 @@ "title": "Pipeline Configuration", "card": { "type": "AdaptiveCard", - "\$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "body": [ { "type": "FactSet", - "facts": [<% out << summary.collect{ k,v -> "{\"title\": \"$k\", \"value\" : \"$v\"}"}.join(",\n") %> - ] + "facts": [] } ] } diff --git a/assets/methods_description_template.yml b/assets/methods_description_template.yml index 89201ab..09d033e 100644 --- a/assets/methods_description_template.yml +++ b/assets/methods_description_template.yml @@ -3,11 +3,9 @@ description: "Suggested text and references to use when describing pipeline usag section_name: "scilus/nf-tractoflow Methods Description" section_href: "https://github.com/scilus/nf-tractoflow" plot_type: "html" -## TODO nf-core: Update the HTML below to your preferred methods description, e.g. add publication citation for this pipeline -## You inject any metadata in the Nextflow '${workflow}' object data: |

Methods

-

Data was processed using scilus/nf-tractoflow v${workflow.manifest.version} ${doi_text} of the nf-core collection of workflows (Ewels et al., 2020), utilising reproducible software environments from the Bioconda (Grüning et al., 2018) and Biocontainers (da Veiga Leprevost et al., 2017) projects.

+

Data was processed using scilus/nf-tractoflow v${workflow.manifest.version} ${doi_text} of the nf-neuro collection of workflows, part of nf-core (Ewels et al., 2020).

The pipeline was executed with Nextflow v${workflow.nextflow.version} (Di Tommaso et al., 2017) with the following command:

${workflow.commandLine}

${tool_citations}

@@ -15,8 +13,7 @@ data: |
diff --git a/assets/nf-core-nf-tractoflow_logo_light.png b/assets/nf-core-nf-tractoflow_logo_light.png new file mode 100644 index 0000000..57fdc83 Binary files /dev/null and b/assets/nf-core-nf-tractoflow_logo_light.png differ diff --git a/assets/samplesheet_raw.csv b/assets/samplesheet.csv similarity index 89% rename from assets/samplesheet_raw.csv rename to assets/samplesheet.csv index ac5b525..d978c23 100644 --- a/assets/samplesheet_raw.csv +++ b/assets/samplesheet.csv @@ -1,4 +1,4 @@ -subject,dwi,bval,bvec,sbref,rev_dwi,rev_bval,rev_bvec,rev_sbref,t1,wmparc,aparc_aseg -sub-01,/path/to/sub-01/dwi.nii.gz,/path/to/sub-01/dwi.bval,/path/to/sub-01/dwi.bvec,/path/to/sub-01/sbref.nii.gz,/path/to/sub-01/rev_dwi.nii.gz,/path/to/sub-01/rev_dwi.bval,/path/to/sub-01/rev_dwi.bvec,/path/to/sub-01/rev_sbref.nii.gz,/path/to/sub-01/t1.nii.gz,/path/to/sub-01/wmparc.nii.gz,/path/to/sub-01/aparc_aseg.nii.gz -sub-02,/path/to/sub-02/dwi.nii.gz,/path/to/sub-02/dwi.bval,/path/to/sub-02/dwi.bvec,,,,,/path/to/sub-02/rev_sbref.nii.gz,/path/to/sub-02/t1.nii.gz,, -sub-03,/path/to/sub-03/dwi.nii.gz,/path/to/sub-03/dwi.bval,/path/to/sub-03/dwi.bvec,,/path/to/sub-03/rev_dwi.nii.gz,/path/to/sub-03/rev_dwi.bval,/path/to/sub-03/rev_dwi.bvec,,/path/to/sub-03/t1.nii.gz,, +subject,dwi,bval,bvec,sbref,rev_dwi,rev_bval,rev_bvec,rev_sbref,t1,wmparc,aparc_aseg,lesion +sub-01,/path/to/sub-01/dwi.nii.gz,/path/to/sub-01/dwi.bval,/path/to/sub-01/dwi.bvec,/path/to/sub-01/sbref.nii.gz,/path/to/sub-01/rev_dwi.nii.gz,/path/to/sub-01/rev_dwi.bval,/path/to/sub-01/rev_dwi.bvec,/path/to/sub-01/rev_sbref.nii.gz,/path/to/sub-01/t1.nii.gz,/path/to/sub-01/wmparc.nii.gz,/path/to/sub-01/aparc_aseg.nii.gz,lesion.nii.gz +sub-02,/path/to/sub-02/dwi.nii.gz,/path/to/sub-02/dwi.bval,/path/to/sub-02/dwi.bvec,,,,,/path/to/sub-02/rev_sbref.nii.gz,/path/to/sub-02/t1.nii.gz,,, +sub-03,/path/to/sub-03/dwi.nii.gz,/path/to/sub-03/dwi.bval,/path/to/sub-03/dwi.bvec,,/path/to/sub-03/rev_dwi.nii.gz,/path/to/sub-03/rev_dwi.bval,/path/to/sub-03/rev_dwi.bvec,,/path/to/sub-03/t1.nii.gz,,, diff --git a/assets/samplesheet_bids.csv b/assets/samplesheet_bids.csv deleted file mode 100644 index 1b2ca1e..0000000 --- a/assets/samplesheet_bids.csv +++ /dev/null @@ -1,4 +0,0 @@ -database,bidsfolder,fsfolder,bidsignore -db1,/path/to/db1,, -db2,/path/to/db2,/path/to/db2/fsfolder, -db3,/path/to/db3,/path/to/db3/fsfolder,/path/to/db3/.bidsignore diff --git a/assets/schema_bids_input.json b/assets/schema_bids_input.json deleted file mode 100644 index 481d563..0000000 --- a/assets/schema_bids_input.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "https://raw.githubusercontent.com/AlexVCaron/nf-tractoflow/master/assets/schema_bids_input.json", - "title": "nf-tractoflow pipeline - params.input schema", - "description": "Schema for the file provided with params.input", - "type": "array", - "items": { - "type": "object", - "minProperties": 2, - "maxProperties": 4, - "properties": { - "database": { - "type": "string", - "pattern": "^\\S+$", - "errorMessage": "Name of the BIDS database to process", - "meta": ["db"] - }, - "bidsfolder": { - "type": "string", - "format": "dir-path", - "exists": true, - "pattern": "^\\S+$", - "errorMessage": "Path to the BIDS database, cannot contain spaces" - }, - "fsfolder": { - "type": "string", - "format": "dir-path", - "exists": true, - "pattern": "^\\S+$", - "errorMessage": "Path to the freesurfer BIDS database, cannot contain spaces" - }, - "bidsignore": { - "type": "string", - "format": "dir-path", - "exists": true, - "pattern": "^\\S+$", - "errorMessage": "Path to a bidsignore file, cannot contain spaces" - } - }, - "required": ["database", "bidsfolder"] - } -} diff --git a/assets/schema_raw_input.json b/assets/schema_input.json similarity index 90% rename from assets/schema_raw_input.json rename to assets/schema_input.json index 9b4d04f..3999906 100644 --- a/assets/schema_raw_input.json +++ b/assets/schema_input.json @@ -1,13 +1,13 @@ { - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "https://raw.githubusercontent.com/AlexVCaron/nf-tractoflow/master/assets/schema_raw_input.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/scilus/nf-tractoflow/master/assets/schema_input.json", "title": "nf-tractoflow pipeline - params.input schema", "description": "Schema for the file provided with params.input", "type": "array", "items": { "type": "object", "minProperties": 5, - "maxProperties": 12, + "maxProperties": 13, "properties": { "subject": { "type": "string", @@ -179,6 +179,21 @@ } ], "errorMessage": "Segmentation of the cortical parcellation in Nifti format, cannot contain spaces and must have extension '.nii' or '.nii.gz'" + }, + "lesion": { + "type": "string", + "anyOf": [ + { + "format": "file-path", + "exists": true, + "pattern": "^\\S+(\\.nii)?\\.gz$" + }, + { + "format": "url", + "mimetype": "application/gzip" + } + ], + "errorMessage": "Lesion mask or map in Nifti format, cannot contain spaces and must have extension '.nii' or '.nii.gz'" } }, "required": ["subject", "dwi", "bval", "bvec", "t1"] diff --git a/assets/templates/mni_152_sym_09c/b0/b0_brain_mask.nii.gz b/assets/templates/mni_152_sym_09c/b0/b0_brain_mask.nii.gz new file mode 100755 index 0000000..57e69f2 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/b0/b0_brain_mask.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/b0/b0_brain_probability_map.nii.gz b/assets/templates/mni_152_sym_09c/b0/b0_brain_probability_map.nii.gz new file mode 100755 index 0000000..7bc5d20 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/b0/b0_brain_probability_map.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/b0/b0_brain_registration_mask.nii.gz b/assets/templates/mni_152_sym_09c/b0/b0_brain_registration_mask.nii.gz new file mode 100755 index 0000000..ff2a694 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/b0/b0_brain_registration_mask.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/b0/b0_template.nii.gz b/assets/templates/mni_152_sym_09c/b0/b0_template.nii.gz new file mode 100755 index 0000000..f1bdc57 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/b0/b0_template.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/t1/t1_brain.nii.gz b/assets/templates/mni_152_sym_09c/t1/t1_brain.nii.gz new file mode 100755 index 0000000..a06e62d Binary files /dev/null and b/assets/templates/mni_152_sym_09c/t1/t1_brain.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/t1/t1_brain_mask.nii.gz b/assets/templates/mni_152_sym_09c/t1/t1_brain_mask.nii.gz new file mode 100755 index 0000000..83bac17 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/t1/t1_brain_mask.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/t1/t1_brain_probability_map.nii.gz b/assets/templates/mni_152_sym_09c/t1/t1_brain_probability_map.nii.gz new file mode 100755 index 0000000..fa59d92 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/t1/t1_brain_probability_map.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/t1/t1_brain_registration_mask.nii.gz b/assets/templates/mni_152_sym_09c/t1/t1_brain_registration_mask.nii.gz new file mode 100755 index 0000000..773fca8 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/t1/t1_brain_registration_mask.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/t1/t1_template.nii.gz b/assets/templates/mni_152_sym_09c/t1/t1_template.nii.gz new file mode 100755 index 0000000..07fc7b7 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/t1/t1_template.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/t2/t2_brain_mask.nii.gz b/assets/templates/mni_152_sym_09c/t2/t2_brain_mask.nii.gz new file mode 100755 index 0000000..c1d4aea Binary files /dev/null and b/assets/templates/mni_152_sym_09c/t2/t2_brain_mask.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/t2/t2_brain_probability_map.nii.gz b/assets/templates/mni_152_sym_09c/t2/t2_brain_probability_map.nii.gz new file mode 100755 index 0000000..f45d49f Binary files /dev/null and b/assets/templates/mni_152_sym_09c/t2/t2_brain_probability_map.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/t2/t2_brain_registration_mask.nii.gz b/assets/templates/mni_152_sym_09c/t2/t2_brain_registration_mask.nii.gz new file mode 100755 index 0000000..26e1176 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/t2/t2_brain_registration_mask.nii.gz differ diff --git a/assets/templates/mni_152_sym_09c/t2/t2_template.nii.gz b/assets/templates/mni_152_sym_09c/t2/t2_template.nii.gz new file mode 100755 index 0000000..f034819 Binary files /dev/null and b/assets/templates/mni_152_sym_09c/t2/t2_template.nii.gz differ diff --git a/assets/tests/raw_input_test.csv b/assets/tests/raw_input_test.csv index 18c8c8d..ad6cce3 100644 --- a/assets/tests/raw_input_test.csv +++ b/assets/tests/raw_input_test.csv @@ -1,2 +1,2 @@ subject,dwi,bval,bvec,sbref,rev_dwi,rev_bval,rev_bvec,rev_sbref,t1,wmparc,aparc_aseg -sub-test-1,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/2b/6f81ecdee186013956491fbda12e69,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/25/f1142227475e362e0381b42ec44433,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/ff/b5454f2212783f65e69b0fd7d242b1,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/09/442b32c4c459af4ba1b1628c6e2975,,,,,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/75/a767f8265ca41e497da70c97395057,, +sub-test-1,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/2b/6f81ecdee186013956491fbda12e69,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/25/f1142227475e362e0381b42ec44433,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/ff/b5454f2212783f65e69b0fd7d242b1,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/09/442b32c4c459af4ba1b1628c6e2975,,,,,https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/75/a767f8265ca41e497da70c97395057,,, diff --git a/conf/base.config b/conf/base.config index 9d3c608..51bc0dd 100644 --- a/conf/base.config +++ b/conf/base.config @@ -28,29 +28,29 @@ process { // See https://www.nextflow.io/docs/latest/config.html#config-process-selectors withLabel:process_single { cpus = { 1 } - memory = { 6.GB * task.attempt } + memory = { 4.GB * task.attempt } time = { 4.h * task.attempt } } withLabel:process_low { cpus = { 2 * task.attempt } - memory = { 12.GB * task.attempt } + memory = { 4.GB * task.attempt } time = { 4.h * task.attempt } } withLabel:process_medium { cpus = { 6 * task.attempt } - memory = { 36.GB * task.attempt } + memory = { 8.GB * task.attempt } time = { 8.h * task.attempt } } withLabel:process_high { cpus = { 12 * task.attempt } - memory = { 72.GB * task.attempt } + memory = { 12.GB * task.attempt } time = { 16.h * task.attempt } } withLabel:process_long { time = { 20.h * task.attempt } } withLabel:process_high_memory { - memory = { 200.GB * task.attempt } + memory = { 16.GB * task.attempt } } withLabel:error_ignore { errorStrategy = 'ignore' diff --git a/conf/modules.config b/conf/modules.config index d523779..35c74fb 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -10,124 +10,138 @@ ---------------------------------------------------------------------------------------- */ +params.preproc_dwi_run_denoising = params.run_dwi_denoising +params.preproc_dwi_run_degibbs = params.run_gibbs_correction +params.topup_eddy_run_topup = params.run_topup +params.topup_eddy_run_eddy = params.run_eddy +params.preproc_dwi_run_N4 = true +params.preproc_t1_run_denoising = params.run_t1_denoising +params.preproc_t1_run_N4 = true +params.preproc_t1_run_synthbet = false +params.preproc_t1_run_ants_bet = true +params.preproc_t1_run_crop = true +params.preproc_dwi_run_resampling = params.run_resample_dwi +params.preproc_t1_run_resampling = params.run_resample_t1 +params.run_pft = params.run_pft_tracking +params.frf_average_from_data = params.mean_frf + process { - publishDir = [ - path: { "${params.outdir}/${task.process.tokenize(":")[-1].tokenize("_")[0].toLowerCase()}" }, - mode: params.publish_dir_mode, - saveAs: { filename -> filename.equals("versions.yml") ? null : filename } - ] + withName: "!.*(IO_SAFECASTINPUTS|MULTIQC)" { + publishDir = [ + path: { "${params.outdir}/${meta.id}/${task.process.tokenize(":")[2..-1].findAll{ it != "RUN" }.join("/").toLowerCase()}" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals("versions.yml") ? null : filename } + ] + } /* SUBWORKFLOWS CONFIGURATION */ - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:DENOISE_DWI" { - ext.extent = params.dwi_noise_filter_patch_size + withName: ".*:RUN:PREPROC_DWI:DENOISE_DWI" { + cpus = params.processes_denoise_dwi + ext.extent = params.extent } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:DENOISE_REVDWI" { - ext.extent = params.dwi_noise_filter_patch_size + withName: ".*:RUN:PREPROC_DWI:DENOISE_REVDWI" { + cpus = params.processes_denoise_dwi + ext.extent = params.extent } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:UTILS_EXTRACTB0" { + withName: ".*:RUN:PREPROC_DWI:TOPUP_EDDY:UTILS_EXTRACTB0" { ext.b0_extraction_strategy = "mean" } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:TOPUP_EDDY:PREPROC_TOPUP" { - ext.prefix_topup = params.dwi_susceptibility_filter_output_prefix - ext.default_config_topup = params.dwi_susceptibility_filter_config_file - ext.encoding = "y" //FIXME - ext.readout = 0.062 //FIXME - ext.b0_thr_extract_b0 = params.dwi_b0_extract_threshold + withName: ".*:RUN:PREPROC_DWI:TOPUP_EDDY:PREPROC_TOPUP" { + ext.prefix_topup = params.prefix_topup + ext.default_config_topup = params.config_topup + ext.encoding = params.encoding_direction //FIXME : this is subject bound, pass through meta ? + ext.readout = params.readout //FIXME : this is subject bound, pass through meta ? + ext.b0_thr_extract_b0 = params.b0_thr_extract_b0 } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:TOPUP_EDDY:PREPROC_EDDY" { - ext.prefix_topup = params.dwi_susceptibility_filter_output_prefix - ext.slice_drop_flag = params.dwi_motion_and_eddy_filter_restore_slices - ext.bet_topup_before_eddy_f = params.dwi_motion_and_eddy_filter_bet_f_threshold - ext.eddy_cmd = params.dwi_motion_and_eddy_filter_command - ext.dilate_b0_mask_prelim_brain_extraction = params.dwi_mask_prelim_bet_dilation_radius - ext.bet_prelim_f = params.dwi_mask_prelim_bet_f_threshold - ext.b0_thr_extract_b0 = params.dwi_b0_extract_threshold - ext.encoding = "y" //FIXME - ext.readout = 0.062 //FIXME + withName: ".*:RUN:PREPROC_DWI:TOPUP_EDDY:PREPROC_EDDY" { + cpus = params.processes_eddy + + ext.prefix_topup = params.prefix_topup + ext.slice_drop_flag = params.use_slice_drop_correction + ext.bet_topup_before_eddy_f = params.bet_topup_before_eddy_f + ext.eddy_cmd = params.eddy_cmd + ext.dilate_b0_mask_prelim_brain_extraction = params.dilate_b0_mask_prelim_brain_extraction + ext.bet_prelim_f = params.bet_prelim_f + ext.b0_thr_extract_b0 = params.b0_thr_extract_b0 + ext.encoding = params.encoding_direction //FIXME : this is subject bound, pass through meta ? + ext.readout = params.readout //FIXME : this is subject bound, pass through meta ? } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:TOPUP_EDDY:UTILS_EXTRACTB0" { - ext.b0_extraction_strategy = "mean" - } - - - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:BETCROP_FSLBETCROP" { - ext.bet_f = params.dwi_mask_final_bet_f_threshold - ext.b0_thr = params.dwi_b0_extract_threshold + withName: ".*:RUN:PREPROC_DWI:BETCROP_FSLBETCROP" { + ext.bet_f = params.bet_dwi_final_f + ext.b0_thr = params.b0_thr_extract_b0 ext.crop = true ext.dilate = false } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:N4_DWI" { - ext.bspline_knot_per_voxel = params.dwi_bias_correction_bspline_knots_per_voxel - ext.shrink_factor = params.dwi_bias_correction_shrink_factor + withName: ".*:RUN:PREPROC_DWI:N4_DWI" { + ext.bspline_knot_per_voxel = 0.25 + ext.shrink_factor = 2 } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:RESAMPLE_DWI" { - ext.voxel_size = params.dwi_spatial_resample_resolution - ext.interp = params.dwi_spatial_resample_interpolation + withName: ".*:RUN:PREPROC_DWI:NORMALIZE_DWI" { + ext.dwi_shell_tolerance = params.dwi_shell_tolerance + ext.max_dti_shell_value = params.max_dti_shell_value + ext.dti_shells = params.dti_shells + ext.fa_mask_threshold = params.fa_mask_threshold + } + + withName: ".*:RUN:PREPROC_DWI:RESAMPLE_DWI" { + ext.voxel_size = params.dwi_resolution + ext.interp = params.dwi_interpolation ext.first_suffix = "dwi" } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:BETCROP_CROPVOLUME" { + withName: ".*:RUN:PREPROC_DWI:IMAGE_CROPVOLUME" { ext.output_bbox = false } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_DWI:RESAMPLE_MASK" { - ext.voxel_size = params.dwi_spatial_resample_resolution + withName: ".*:RUN:PREPROC_DWI:RESAMPLE_MASK" { + ext.voxel_size = params.dwi_resolution ext.interp = "nn" - ext.first_suffix = "mask" + ext.first_suffix = "dwi_mask" } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_T1:BETCROP_CROPVOLUME_T1" { + withName: ".*:RUN:PREPROC_T1:DENOISING_NLMEANS" { + cpus = params.processes_denoise_t1 + } + + withName: ".*:RUN:PREPROC_T1:IMAGE_CROPVOLUME_T1" { ext.output_bbox = true ext.first_suffix = "t1" } - withName: "NF_TRACTOFLOW:TRACTOFLOW:PREPROC_T1:IMAGE_RESAMPLE" { - ext.voxel_size = params.t1_spatial_resample_resolution - ext.interp = params.t1_spatial_resample_interpolation + withName: ".*:RUN:PREPROC_T1:IMAGE_RESAMPLE" { + ext.voxel_size = params.t1_resolution + ext.interp = params.t1_interpolation ext.first_suffix = "t1" } - //withName: "NF_TRACTOFLOW:TRACTOFLOW:T1_REGISTRATION:REGISTER_ANATTODWI" { - // Nothing to do ! - //} + withName: ".*:RUN:PREPROC_T1:BETCROP_ANTSBET" { + cpus = params.processes_brain_extraction_t1 + } - //withName: "NF_TRACTOFLOW:TRACTOFLOW:ANATOMICAL_SEGMENTATION:SEGMENTATION_FASTSEG" { + withName: ".*:RUN:T1_REGISTRATION:REGISTRATION_ANATTODWI" { + cpus = params.processes_registration + } + + //withName: ".*:RUN:ANATOMICAL_SEGMENTATION:SEGMENTATION_FASTSEG" { // Nothing to do ! //} - //withName: "NF_TRACTOFLOW:TRACTOFLOW:ANATOMICAL_SEGMENTATION:SEGMENTATION_FREESURFERSEG" { + //withName: ".*:RUN:ANATOMICAL_SEGMENTATION:SEGMENTATION_FREESURFERSEG" { // Nothing to do ! //} /* MODULES CONFIGURATION */ - withName: "NF_TRACTOFLOW:TRACTOFLOW:REGISTRATION_FA" { - ext.fa = true - ext.ad = false - ext.evecs = false - ext.evals = false - 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 - } - - withName: "NF_TRACTOFLOW:TRACTOFLOW:TRANSFORM_WMPARC" { + withName: ".*:RUN:TRANSFORM_WMPARC" { ext.dimensionality = 3 ext.image_type = 0 ext.interpolation = "NearestNeighbor" @@ -135,7 +149,7 @@ process { ext.default_val = 0 } - withName: "NF_TRACTOFLOW:TRACTOFLOW:TRANSFORM_APARC_ASEG" { + withName: ".*:RUN:TRANSFORM_APARC_ASEG" { ext.dimensionality = 3 ext.image_type = 0 ext.interpolation = "MultiLabel" @@ -143,49 +157,76 @@ process { ext.default_val = 0 } - withName: "NF_TRACTOFLOW:TRACTOFLOW:RECONST_FRF" { - ext.fa = params.dwi_fodf_fit_frf_max_fa_threshold - ext.fa_min = params.dwi_fodf_fit_frf_min_fa_threshold - ext.nvox_min = params.dwi_fodf_fit_frf_min_n_voxels - ext.roi_radius = params.dwi_fodf_fit_frf_roi_radius - ext.set_frf = params.dwi_fodf_fit_force_frf ? true : false - ext.manual_frf = params.dwi_fodf_fit_force_frf + withName: ".*:RUN:TRANSFORM_LESION_MASK" { + ext.dimensionality = 3 + ext.image_type = 0 + ext.interpolation = "NearestNeighbor" + ext.output_dtype = "uchar" + ext.default_val = 0 + } + + withName: ".*:RUN:TRANSFORM_BRAINMASK" { + ext.dimensionality = 3 + ext.image_type = 0 + ext.interpolation = "NearestNeighbor" + ext.output_dtype = "uchar" + ext.default_val = 0 + } + + withName: ".*:RECONST_SHSIGNAL" { + ext.dwi_shell_tolerance = params.dwi_shell_tolerance + ext.b0_thr_extract_b0 = params.b0_thr_extract_b0 + ext.shell_to_fit = params.sh_fitting_shells + ext.sh_order = params.sh_fitting_order + ext.sh_basis = params.sh_fitting_basis + } + + withName: ".*:RUN:RECONST_FRF" { + ext.fa = params.fa + ext.fa_min = params.min_fa + ext.nvox_min = params.min_nvox + ext.roi_radius = params.roi_radius + ext.set_frf = params.manual_frf ? true : false + ext.manual_frf = params.manual_frf } //withName: "RECONST_MEANFRF" { // Nothing to do ! //} - withName: "NF_TRACTOFLOW:TRACTOFLOW:RECONST_DTIMETRICS" { - ext.ad = true - ext.evecs = true - ext.evals = true - ext.fa = true - ext.ga = true - ext.rgb = true - ext.md = true - ext.mode = true - ext.norm = true - ext.rd = true - ext.tensor = true - ext.nonphysical = true - ext.pulsation = true - ext.residual = true - } - - withName: "NF_TRACTOFLOW:TRACTOFLOW:RECONST_FODF" { - ext.b0_thr_extract_b0 = params.dwi_b0_extract_threshold - ext.dwi_shell_tolerance = params.dwi_shell_extract_tolerance - ext.min_fodf_shell_value = params.dwi_fodf_fit_shell_min_bvalue - ext.fodf_shells = params.dwi_fodf_fit_shells - ext.sh_order = params.dwi_fodf_fit_order - ext.sh_basis = params.dwi_fodf_fit_basis - ext.fa_threshold = params.dwi_fodf_fit_peaks_ventricle_max_fa - ext.md_threshold = params.dwi_fodf_fit_peaks_ventricle_min_md - ext.relative_threshold = params.dwi_fodf_fit_peaks_relative_threshold - ext.fodf_metrics_a_factor = params.dwi_fodf_fit_peaks_absolute_factor - ext.absolute_peaks = true //FIXME - ext.processes = 4 //FIXME + withName: ".*:RUN:RECONST_DTIMETRICS" { + ext.ad = true + ext.evecs = true + ext.evals = true + ext.fa = true + ext.ga = true + ext.rgb = true + ext.md = true + ext.mode = true + ext.norm = true + ext.rd = true + ext.tensor = true + ext.nonphysical = true + ext.pulsation = true + ext.residual = true + ext.max_dti_shell_value = params.max_dti_shell_value + ext.dti_shells = params.dti_shells + } + + withName: ".*:RUN:RECONST_FODF" { + cpus = params.processes_fodf + + ext.b0_thr_extract_b0 = params.b0_thr_extract_b0 + ext.dwi_shell_tolerance = params.dwi_shell_tolerance + ext.min_fodf_shell_value = params.min_fodf_shell_value + ext.fodf_shells = params.fodf_shells + ext.sh_order = params.sh_order + ext.sh_basis = params.basis + ext.fa_threshold = params.max_fa_in_ventricle + ext.md_threshold = params.min_md_in_ventricle + ext.relative_threshold = params.relative_threshold + ext.fodf_metrics_a_factor = params.fodf_metrics_a_factor + ext.absolute_peaks = true ext.peaks = true ext.peak_indices = true ext.afd_max = true @@ -194,44 +235,48 @@ process { ext.nufo = true } - withName: "NF_TRACTOFLOW:TRACTOFLOW:TRACKING_PFTTRACKING" { - ext.pft_seeding_mask_type = params.fodf_pft_fit_seeding_type - ext.pft_fa_seeding_mask_threshold = params.fodf_pft_fit_seeding_fa_mask_threshold - ext.pft_seeding = params.fodf_pft_fit_seeding_strategy - ext.pft_nbr_seeds = params.fodf_pft_fit_seeding_n_seeds - ext.pft_algo = params.fodf_pft_fit_algorithm - ext.pft_step = params.fodf_pft_fit_step_size - ext.pft_theta = params.fodf_pft_fit_theta_max_deviation - ext.pft_sfthres = params.fodf_pft_fit_sf_threshold - ext.pft_sfthres_init = params.fodf_pft_fit_sf_initial_threshold - ext.pft_min_len = params.fodf_pft_fit_streamline_min_length - ext.pft_max_len = params.fodf_pft_fit_streamline_max_length - ext.pft_particles = params.fodf_pft_fit_filter_n_particles - ext.pft_back = params.fodf_pft_fit_filter_backward_step_size - ext.pft_front = params.fodf_pft_fit_filter_forward_step_size - ext.pft_random_seed = params.fodf_pft_fit_random_seed - ext.pft_compress_streamlines = params.fodf_pft_fit_compress_tractogram - ext.pft_compress_value = params.fodf_pft_fit_compress_max_displacement - ext.basis = params.dwi_fodf_fit_basis - } - - withName: "NF_TRACTOFLOW:TRACTOFLOW:TRACKING_LOCALTRACKING" { - ext.local_tracking_mask_type = params.fodf_local_fit_tracking_mask_type - ext.local_fa_tracking_mask_threshold = params.fodf_local_fit_tracking_mask_fa_threshold - ext.local_seeding_mask_type = params.fodf_local_fit_seeding_type - ext.local_fa_seeding_mask_threshold = params.fodf_local_fit_seeding_fa_threshold - ext.local_seeding = params.fodf_local_fit_seeding_strategy - ext.local_nbr_seeds = params.fodf_local_fit_seeding_n_seeds - ext.local_algo = params.fodf_local_fit_algorithm - ext.local_step = params.fodf_local_fit_step_size - ext.local_theta = params.fodf_local_fit_theta_max_deviation - ext.local_sfthres = params.fodf_local_fit_sf_threshold - ext.local_min_len = params.fodf_local_fit_streamline_min_length - ext.local_max_len = params.fodf_local_fit_streamline_max_length - ext.local_random_seed = params.fodf_local_fit_random_seed - ext.local_compress_streamlines = params.fodf_local_fit_compress_tractogram - ext.local_compress_value = params.fodf_local_fit_compress_max_displacement - ext.basis = params.dwi_fodf_fit_basis + withName: ".*:RUN:TRACKING_PFTTRACKING" { + ext.pft_seeding_mask_type = params.pft_seeding_mask_type + ext.pft_fa_seeding_mask_threshold = params.pft_fa_seeding_mask_threshold + ext.pft_seeding = params.pft_seeding + ext.pft_nbr_seeds = params.pft_nbr_seeds + ext.pft_algo = params.pft_algo + ext.pft_step = params.pft_step + ext.pft_theta = params.pft_theta + ext.pft_sfthres = params.pft_sfthres + ext.pft_sfthres_init = params.pft_sfthres_init + ext.pft_min_len = params.pft_min_len + ext.pft_max_len = params.pft_max_len + ext.pft_particles = params.pft_particles + ext.pft_back = params.pft_back + ext.pft_front = params.pft_front + ext.pft_random_seed = params.pft_random_seed + ext.pft_compress_streamlines = params.pft_compress_value > 0 + ext.pft_compress_value = params.pft_compress_value + ext.basis = params.basis + } + + withName: ".*:RUN:TRACKING_LOCALTRACKING" { + cpus = params.processes_local_tracking + + ext.enable_gpu = params.local_tracking_gpu + ext.gpu_batch_size = params.local_tracking_gpu ? params.local_batch_size_gpu : null + ext.local_tracking_mask_type = params.local_tracking_mask_type + ext.local_fa_tracking_mask_threshold = params.local_fa_tracking_mask_threshold + ext.local_seeding_mask_type = params.local_seeding_mask_type + ext.local_fa_seeding_mask_threshold = params.local_fa_seeding_mask_threshold + ext.local_seeding = params.local_seeding + ext.local_nbr_seeds = params.local_nbr_seeds + ext.local_algo = params.local_algo + ext.local_step = params.local_step + ext.local_theta = params.local_theta + ext.local_sfthres = params.local_sfthres + ext.local_min_len = params.local_min_len + ext.local_max_len = params.local_max_len + ext.local_random_seed = params.local_random_seed + ext.local_compress_streamlines = params.local_compress_value > 0 + ext.local_compress_value = params.local_compress_value + ext.basis = params.basis } withName: "MULTIQC" { diff --git a/conf/test.config b/conf/test.config index 7ffcb90..d7ef0c5 100644 --- a/conf/test.config +++ b/conf/test.config @@ -14,7 +14,7 @@ process { resourceLimits = [ cpus: 4, memory: '15.GB', - time: '1.h' + time: '2.h' ] } @@ -23,6 +23,16 @@ params { config_profile_description = 'Minimal test dataset to check pipeline function' // Input data - input = "https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/f4/f8165557bd75a90c4088d87cd8753d" + input = "https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/b9/e0f61087c5a0a8f892ac94ef452d23" + template_t1 = "${projectDir}/assets/templates/mni_152_sym_09c/t1" + sh_fitting_shells = "0 1000" + pft_seeding = "nt" + pft_nbr_seeds = 4000 + local_seeding = "nt" + local_nbr_seeds = 4000 + pft_back = 0.5 + pft_front = 0.5 + run_pft_tracking = true + run_local_tracking = true } diff --git a/conf/test_full.config b/conf/test_full.config index 7d64ed0..fbcf239 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -14,8 +14,16 @@ params { config_profile_name = 'Full test profile' config_profile_description = 'Full test dataset to check pipeline function' - // Input data for full size test - // TODO nf-core: Specify the paths to your full test data ( on nf-core/test-datasets or directly in repositories, e.g. SRA) - // TODO nf-core: Give any required params for the test so that command line flags are not needed - input = null + // Input data + input = "https://scil.usherbrooke.ca/scil_test_data/dvc-store/files/md5/c0/9dd7d33afb95068a253e3a49f810e2" + + sh_fitting_shells = "0 1000" + pft_seeding = "npv" + pft_nbr_seeds = 1 + local_seeding = "npv" + local_nbr_seeds = 1 + pft_back = 0.5 + pft_front = 0.5 + run_pft_tracking = true + run_local_tracking= true } diff --git a/docs.html b/docs.html new file mode 100644 index 0000000..4068033 --- /dev/null +++ b/docs.html @@ -0,0 +1,1751 @@ +

nf-tractoflow pipeline parameters

+

Human diffusion MRI processing and tractography

+

Input/output options

+

Define where the pipeline should find input data and save output data.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
input + Path to comma-separated file containing information about the input subjects files. +
+ Help + List of subjects to process. See assets/samplesheet_raw.csv. +
+
string
bids + Path to comma-separated file containing information about the input bids databases. +
+ Help + List of BIDS databases to load. See assets/samplesheet_bids.csv. +
+
string
bids_config + BIDS configuration file in json pre-generated by TractoFlow(Read_BIDS process). This option must be used only if + --bids did not work due to an issue in the BIDS folder. +
+ Help + BIDS configuration file in json pre-generated by TractoFlow(Read_BIDS process). This option must be used + only if --bids did not work due to an issue in the BIDS folder. +
+
string
bidsignore + If you want to ignore some subjects or some files, you can provide an extra bidsignore file. Check: + https://github.com/bids-standard/bids-validator#bidsignore +
+ Help + If you want to ignore some subjects or some files, you can provide an extra bidsignore file. Check: + https://github.com/bids-standard/bids-validator#bidsignore +
+
string
clean_bids + If set, it will remove all the participants that are missing any information. +
+ Help + If set, it will remove all the participants that are missing any information. +
+
boolean
b0_thr_extract_b0 + Consider all b-values below threshold as b=0 images. +
+ Help + Consider all b-values below threshold as b=0 images. +
+
integer10
dwi_shell_tolerance + Consider all b-values up to +-tolerance as belonging to the same shell. +
+ Help + Consider all b-values up to +-tolerance as belonging to the same shell. +
+
integer20
outdir + The output directory where the results will be saved. You have to use absolute paths to storage on Cloud + infrastructure. +
+ Help + The output directory where the results will be saved. You have to use absolute paths to storage on Cloud + infrastructure. +
+
string
email + Email address for completion summary. +
+ Help + Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when + the workflow exits. If set in your user config file (~/.nextflow/config) then you don't need to + specify this on the command line for every run. +
+
stringTrue
multiqc_title + MultiQC report title. Printed as page header, used for filename if not otherwise specified. +
+ Help + MultiQC report title. Printed as page header, used for filename if not otherwise specified. +
+
stringTrue
+

DWI preprocessing options

+

All parameters related to DWI preprocessing.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
bet_prelim_f + Fractional Intensity threshold for preliminary brain masking. +
+ Help + Fractional Intensity threshold for preliminary brain masking. +
+
number0.16
dilate_b0_mask_prelim_brain_extraction + Dilation radius applied to the preliminary mask to ensure whole brain coverage. +
+ Help + Dilation radius applied to the preliminary mask to ensure whole brain coverage. +
+
integer5
run_dwi_denoising + Run dwi denoising. +
+ Help + Run dwi denoising. +
+
booleanTrue
extent + Denoising block size. Rule: extent^3 >= number of directions. +
+ Help + Denoising block size. Rule: extent^3 >= number of directions. +
+
integer7
run_gibbs_correction + RRun Gibbs correction. +
+ Help + RRun Gibbs correction. +
+
boolean
run_topup + Run Topup. +
+ Help + Run Topup. +
+
booleanTrue
encoding_direction + Encoding direction of the DWI [x, y, z] +
+ Help + Encoding direction of the DWI [x, y, z] +
+
stringy
readout + Readout time +
+ Help + Readout time +
+
number0.062
config_topup + Configuration file to use for Topup. Can either be a full path to a file or the name of a configuration file in + Topup resources. + stringb02b0.cnfTrue
prefix_topupOutput prefix to use for susceptibility filtering output filenames.stringtopup_resultsTrue
run_eddy + Run Eddy. +
+ Help + Run Eddy. +
+
booleanTrue
eddy_cmd + Command to use to run Eddy. +
+ Help + Command to use to run Eddy. +
+
stringeddy_cpu
bet_topup_before_eddy_f + Fractional Intensity threshold for intermediate brain masking on Topup corrected b0 images. +
+ Help + Fractional Intensity threshold for intermediate brain masking on Topup corrected b0 images. +
+
number0.16
use_slice_drop_correction + If set, will use the slice drop correction option from Eddy. +
+ Help + If set, will use the slice drop correction option from Eddy. +
+
booleanTrue
bet_dwi_final_f + Fractional Intensity threshold for final brain masking. +
+ Help + Fractional Intensity threshold for final brain masking. +
+
number0.16
fa_mask_threshold + FA threshold to compute WM mask for normalization. +
+ Help + FA threshold to compute WM mask for normalization. +
+
number0.4
+

T1 preprocessing options

+

All parameters related to T1 preprocessing.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
run_t1_denoising + Run T1 denoising. +
+ Help + Run T1 denoising. +
+
booleanTrue
template_t1 + Path to the template for antsBrainExtraction. The folder must contain t1_template.nii.gz and + t1_brain_probability_map.nii.gz. The default path is the human_data folder in the singularity container +
+ Help + Path to the template for antsBrainExtraction. The folder must contain t1_template.nii.gz and + t1_brain_probability_map.nii.gz. The default path is the human_data folder in the singularity + container +
+
string/human-data/mni_152_sym_09c/t1
+

Resampling options

+

Resampling options for DWI and anatomical volumes.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
run_resample_dwi + Run resample DWI. +
+ Help + Run resample DWI. +
+
booleanTrue
dwi_resolution + DWI resolution. +
+ Help + DWI resolution. +
+
number1
dwi_interpolation + DWI interpolation method. +
+ Help + DWI interpolation method +
+
stringlin
run_resample_t1 + Run resample T1. +
+ Help + Run resample T1. +
+
booleanTrue
t1_resolution + T1 resolution. +
+ Help + T1 resolution. +
+
number1
t1_interpolation + T1 interpolation method. +
+ Help + T1 interpolation method. +
+
stringlin
+

DWI processing options

+

All parameters related to DWI processing (SH, DTI, fODF).

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
max_dti_shell_value + Maximum shell threshold to be consider as a DTI shell. This is the default behavior to select DTI shells. +
+ Help + Maximum shell threshold to be consider as a DTI shell. This is the default behavior to select DTI + shells. +
+
integer1200
dti_shells + Shells selected to compute the DTI metrics (generally b <= 1200). Please write them between quotes e.g. + (--dti_shells "0 300 1000"). If selected, it will overwrite max_dti_shell_value. +
+ Help + Shells selected to compute the DTI metrics (generally b <= 1200). Please write them between quotes e.g. + (--dti_shells "0 300 1000"). If selected, it will overwrite max_dti_shell_value. +
+
string
min_fodf_shell_value + Minimum shell threshold to be consider as a fODF shell. This is the default behaviour to select fODF shells. +
+ Help + Minimum shell threshold to be consider as a fODF shell. This is the default behaviour to select fODF + shells. +
+
integer700
fodf_shells + Shells selected to compute the fODF metrics (generally b >= 700). Please write them between quotes e.g. + (--fodf_shells "0 1000 2000"). If selected, it will overwrite min_fodf_shell_value. +
+ Help + Shells selected to compute the fODF metrics (generally b >= 700). Please write them between quotes e.g. + (--fodf_shells "0 1000 2000"). If selected, it will overwrite min_fodf_shell_value. +
+
string
fa + Initial FA threshold to compute the frf. +
+ Help + Initial FA threshold to compute the frf. +
+
number0.7
min_fa + Minimum FA threshold to compute the frf. +
+ Help + Minimum FA threshold to compute the frf. +
+
number0.5
min_nvox + Minimum number of voxels to compute the frf. +
+ Help + Minimum number of voxels to compute the frf. +
+
integer300
roi_radius + Region of interest radius to compute the frf. +
+ Help + Region of interest radius to compute the frf. +
+
integer20
manual_frf + Force a fiber response function. Supply as 3 values : --manual_frf "mean_b0 parallel_diff perpendicular_diff". +
+ Help + Force a fiber response function. Supply as 3 values : --manual_frf "mean_b0 parallel_diff + perpendicular_diff". +
+
string
mean_frf + Average the frf of all subjects. USE ONLY IF ALL OF SUBJECTS COME FROM THE SAME SCANNER AND ADHERE TO THE SAME + ACQUISITION PROTOCOL. +
+ Help + Average the frf of all subjects. USE ONLY IF ALL OF SUBJECTS COME FROM THE SAME SCANNER AND ADHERE TO THE + SAME ACQUISITION PROTOCOL. +
+
boolean
sh_order + Spherical harmonics order, must be even. Rules : sh_order=8 for 45 directions, sh_order=6 for 28 directions. +
+ Help + Spherical harmonics order, must be even. Rules : sh_order=8 for 45 directions, sh_order=6 for 28 + directions. +
+
integer8
basis + fODF basis. +
+ Help + fODF basis. +
+
stringdescoteaux07
fodf_metrics_a_factor + Multiplicative factor for AFD max in ventricles. +
+ Help + Multiplicative factor for AFD max in ventricles. +
+
number2
relative_threshold + Relative threshold on fODF amplitude in ]0,1]. +
+ Help + Relative threshold on fODF amplitude in ]0,1]. +
+
number0.1
max_fa_in_ventricle + Maximal threshold of FA to be considered as ventricles voxels. +
+ Help + Maximal threshold of FA to be considered as ventricles voxels. +
+
number0.1
min_md_in_ventricle + Minimal threshold of MD in mm2/s to be considered as ventricles voxels. +
+ Help + Minimal threshold of MD in mm2/s to be considered as ventricles voxels. +
+
number0.003
sh_fitting + Compute a Spherical Harmonics fit on the DWI, and output the SH coefficients in a Nifti file. +
+ Help + Compute a Spherical Harmonics fit on the DWI, and output the SH coefficients in a Nifti file. +
+
booleanTrue
sh_fitting_basis + SH basis used for the optional SH fitting. +
+ Help + SH basis used for the optional SH fitting. +
+
stringdescoteaux07
sh_fitting_order + SH order used for the optional SH fitting, must be an even number. Rules : sh_order=8 for 45 directions, + sh_order=6 for 28 directions +
+ Help + SH order used for the optional SH fitting, must be an even number. Rules : sh_order=8 for 45 directions, + sh_order=6 for 28 directions +
+
integer6
sh_fitting_shells + Shells selected to compute the SH fitting. Please write them between quotes e.g. (--sh_fitting_shells "0 1000"). + NOTE: SH fitting works only on single shell. You must include the b0 shell as well. +
+ Help + Shells selected to compute the SH fitting. Please write them between quotes e.g. (--sh_fitting_shells "0 + 1000"). NOTE: SH fitting works only on single shell. You must include the b0 shell as well. +
+
string
+

Particle Filtering Tractography options

+

All parameters related to particle filtering tractography on fODF fields.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
run_pft_tracking + Run Particle Filtering Tractography (PFT). +
+ Help + Run Particle Filtering Tractography (PFT). +
+
booleanTrue
pft_seeding_mask_type + [PFT] Seeding mask type. +
+ Help + [PFT] Seeding mask type. +
+
stringwm
pft_fa_seeding_mask_threshold + [PFT] FA threshold for FA seeding mask. +
+ Help + [PFT] FA threshold for FA seeding mask. +
+
number0.1
pft_algo + [PFT] Tracking algorithm. +
+ Help + [PFT] Tracking algorithm. +
+
stringprob
pft_seeding + [PFT] Seeding type. +
+ Help + [PFT] Seeding type. +
+
stringnpv
pft_nbr_seeds + [PFT] Number of seeds related to the seeding type param. +
+ Help + [PFT] Number of seeds related to the seeding type param. +
+
integer10
pft_step + [PFT] Step size. +
+ Help + [PFT] Step size. +
+
number0.5
pft_theta + [PFT] Maximum angle between 2 steps. +
+ Help + [PFT] Maximum angle between 2 steps. +
+
number20
pft_sfthresThreshold on spherical functions computed from fODF.number0.1True
pft_sfthres_initInitial threshold on spherical functions computed from fODF.number0.5True
pft_min_len + [PFT] Minimum length. +
+ Help + [PFT] Minimum length. +
+
number20
pft_max_len + [PFT] Maximum length . +
+ Help + [PFT] Maximum length . +
+
number200
pft_particlesNumber of particles propagated in case of premature termination.integer15True
pft_backLength of the backtracking steps of the particle filter in mm.number2True
pft_frontLength of the forward tracking steps of the particle filter in mm.number1True
pft_compress_value + [PFT] Compression error threshold. +
+ Help + [PFT] Compression error threshold. +
+
number0.2
pft_random_seed + [PFT] List of random seed numbers for the random number generator. Please write them as list separated using + comma WITHOUT SPACE e.g. (--pft_random_seed 0,1,2) +
+ Help + [PFT] List of random seed numbers for the random number generator. Please write them as list separated + using comma WITHOUT SPACE e.g. (--pft_random_seed 0,1,2) +
+
integer0
+

Local Tractography options

+

All parameters related to local tractography on fODF fields.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
run_local_tracking + Run Local Tracking. +
+ Help + Run Local Tracking. +
+
booleanTrue
local_seeding_mask_type + [LOCAL] seeding mask type. +
+ Help + [LOCAL] seeding mask type. +
+
stringwm
local_fa_seeding_mask_threshold + [LOCAL] FA threshold for FA seeding mask. +
+ Help + [LOCAL] FA threshold for FA seeding mask. +
+
number0.1
local_tracking_mask_type + [LOCAL] tracking mask type. +
+ Help + [LOCAL] tracking mask type. +
+
stringwm
local_fa_tracking_mask_threshold + [LOCAL] FA threshold for FA tracking mask. +
+ Help + [LOCAL] FA threshold for FA tracking mask. +
+
number0.1
local_algo + [LOCAL] Tracking algorithm. +
+ Help + [LOCAL] Tracking algorithm. +
+
stringprob
local_seeding + [LOCAL] Seeding type. +
+ Help + [LOCAL] Seeding type. +
+
stringnpv
local_nbr_seeds + [LOCAL] Number of seeds related to the seeding type param. +
+ Help + [LOCAL] Number of seeds related to the seeding type param. +
+
integer10
local_step + [LOCAL] Step size. +
+ Help + [LOCAL] Step size. +
+
number0.5
local_theta + [LOCAL] Maximum angle between 2 steps. +
+ Help + [LOCAL] Maximum angle between 2 steps. +
+
number20
local_sfthresThreshold on spherical functions computed from fODF.number0.1True
local_sfthres_initInitial threshold on spherical functions computed from fODF.number0.5True
local_min_len + [LOCAL] Minimum length. +
+ Help + [LOCAL] Minimum length. +
+
number20
local_max_len + [LOCAL] Maximum length. +
+ Help + [LOCAL] Maximum length. +
+
number200
local_compress_value + [LOCAL] Compression error threshold. +
+ Help + [LOCAL] Compression error threshold. +
+
number0.2
local_random_seed + [LOCAL] List of random seed numbers for the random number generator. Please write them as list separated using + comma WITHOUT SPACE e.g. (--local_random_seed 0,1,2). +
+ Help + [LOCAL] List of random seed numbers for the random number generator. Please write them as list separated + using comma WITHOUT SPACE e.g. (--local_random_seed 0,1,2). +
+
integer0
local_batch_size_gpu + [LOCAL-GPU] Approximate size of GPU batches (number of streamlines to track in parallel). +
+ Help + [LOCAL-GPU] Approximate size of GPU batches (number of streamlines to track in parallel). +
+
integer10000
local_tracking_gpuUse GPU trackingbooleanTrue
+

Institutional config options

+

Parameters used to describe centralised config profiles. These should not be edited.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
custom_config_versionGit commit id for Institutional configs.stringmasterTrue
custom_config_base + Base directory for Institutional configs. +
+ Help + If you're running offline, Nextflow will not be able to fetch the institutional config files from the + internet. If you don't need them, then this is not a problem. If you do need them, you should download the + files from the repo and tell Nextflow where to find them with this parameter. +
+
stringhttps://raw.githubusercontent.com/nf-core/configs/masterTrue
config_profile_nameInstitutional config name.stringTrue
config_profile_descriptionInstitutional config description.stringTrue
config_profile_contactInstitutional config contact information.stringTrue
config_profile_urlInstitutional config URL link.stringTrue
+

Max job request options

+

Set the top limit for requested resources for any single job.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
max_cpus + Maximum number of CPUs that can be requested for any single job. +
+ Help + Use to set an upper-limit for the CPU requirement for each process. Should be an integer e.g. + --max_cpus 1 +
+
integer16
max_memory + Maximum amount of memory that can be requested for any single job. +
+ Help + Use to set an upper-limit for the memory requirement for each process. Should be a string in the format + integer-unit e.g. --max_memory '8.GB' +
+
string128.GB
max_time + Maximum amount of time that can be requested for any single job. +
+ Help + Use to set an upper-limit for the time requirement for each process. Should be a string in the format + integer-unit e.g. --max_time '2.h' +
+
string240.h
+

Generic options

+

Less common options for the pipeline, typically set in a config file.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
versionDisplay version and exit.booleanTrue
publish_dir_mode + Method used to save pipeline results to output directory. +
+ Help + The Nextflow publishDir option specifies which intermediate files should be saved to the + output directory. This option tells the pipeline what method should be used to move these files. See + Nextflow docs for details. +
+
stringcopyTrue
email_on_fail + Email address for completion summary, only when pipeline fails. +
+ Help + An email address to send a summary email to when the pipeline is completed - ONLY sent if the pipeline does + not exit successfully. +
+
stringTrue
plaintext_emailSend plain-text email instead of HTML.booleanTrue
max_multiqc_email_sizeFile size limit when attaching MultiQC reports to summary emails.string25.MBTrue
monochrome_logsDo not use coloured log outputs.booleanTrue
multiqc_configCustom config file to supply to MultiQC.stringTrue
multiqc_logoCustom logo file to supply to MultiQC. File name must also be set in the MultiQC config filestringTrue
multiqc_methods_descriptionCustom MultiQC yaml file containing HTML including a methods description.stringTrue
validate_paramsBoolean whether to validate parameters against the schema at runtimebooleanTrueTrue
pipelines_testdata_base_pathBase URL or local path to location of pipeline test dataset filesstringhttps://raw.githubusercontent.com/nf-core/test-datasets/True
trace_report_suffix + Suffix to add to the trace report filename. Default is the date and time in the format yyyy-MM-dd_HH-mm-ss. + stringTrue
+

Other parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionTypeDefaultRequiredHidden
processes_denoise_dwi + Number of processes for DWI denoising. +
+ Help + Number of processes for DWI denoising. +
+
integer4
processes_eddy + Number of processes for Eddy and motion filtering. +
+ Help + Number of processes for Eddy and motion filtering. +
+
integer4
processes_fodf + Number of processes for fODF fitting. +
+ Help + Number of processes for fODF fitting. +
+
integer4
processes_local_tracking + Number of processes for local tractography. +
+ Help + Number of processes for local tractography. +
+
integer4
processes_denoise_t1 + Number of processes for T1 denoising +
+ Help + Number of processes for T1 denoising +
+
integer4
processes_brain_extraction_t1 + Number of processes for T1 brain extraction. +
+ Help + Number of processes for T1 brain extraction. +
+
integer4
processes_registration + Number of processes for registration task +
+ Help + Number of processes for registration task +
+
integer4
diff --git a/docs/images/nf-core-nf-tractoflow_logo_dark.png b/docs/images/nf-core-nf-tractoflow_logo_dark.png new file mode 100644 index 0000000..57fdc83 Binary files /dev/null and b/docs/images/nf-core-nf-tractoflow_logo_dark.png differ diff --git a/docs/images/nf-core-nf-tractoflow_logo_light.png b/docs/images/nf-core-nf-tractoflow_logo_light.png new file mode 100644 index 0000000..57fdc83 Binary files /dev/null and b/docs/images/nf-core-nf-tractoflow_logo_light.png differ diff --git a/docs/usage.md b/docs/usage.md index 5ba3771..bd62301 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -4,11 +4,52 @@ ## Introduction - +## BIDS input + +scilus/nf-tractoflow supports a **BIDS compliant dataset** in inputs, using the `--input` parameter. Optionally, the `--fsbids` input +parameter can be supplied to load a **freesurfer sidecar**, with precomputed parcellations and segmentations (`wmparc` and `aparc+aseg`). +If the `--bidsignore` input is given, its content will be used to filter both `--input` and `--fsbids`. Valid `--input` and `--fsbids` +inputs abide the following structure : + +``` + Assuming ─> FP : Forward Phase encoding + ─> RP : Reverse Phase encoding + + --input=/path/to/[bids] + | + ├── S1 + | ├── dwi + | | ├── *dir-FP_*dwi.nii.gz + | | ├── *dir-FP_*dwi.json + | | ├── *dir-FP_*dwi.bval + | | ├── *dir-FP_*dwi.bvec + | | ├── *dir-RP_*dwi.nii.gz (optional) + | | ├── *dir-RP_*dwi.json (optional) + | | ├── *dir-RP_*dwi.bval (optional) + | | └── *dir-RP_*dwi.bvec (optional) + | ├── anat + | | ├── *T1w.nii.gz + | | └── *T1w.json + | └── fmap + | ├── *epi.nii.gz (optional) + | └── *epi.json (optional) + └── S2 + ⋮ + + --fsbids=/path/to/[fsbids] + | + ├── S1 + | ├── mri + | | ├── *aparc+aseg.nii.gz + | | └── *wmparc.nii.gz + └── S2 + ⋮ + +``` ## Samplesheet input -You will need to create a samplesheet with information about the samples you would like to analyse before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 3 columns, and a header row as shown in the examples below. +You will need to create a samplesheet with information about the samples you would like to analyse before running the pipeline. Use this parameter to specify its location. It has to be a comma-separated file with 13 columns, and a header row as shown in the examples below. ```bash --input '[path to samplesheet file]' @@ -16,37 +57,47 @@ You will need to create a samplesheet with information about the samples you wou ### Multiple runs of the same sample -The `sample` identifiers have to be the same when you have re-sequenced the same sample more than once e.g. to increase sequencing depth. The pipeline will concatenate the raw reads before performing any downstream analysis. Below is an example for the same sample sequenced across 3 lanes: +Multiple runs of the same `sample` are supported, but their **sample ID** must differ. They will be processed and outputted +separately, no averaging or ensembling is performed by the pipeline. ```csv title="samplesheet.csv" -sample,fastq_1,fastq_2 -CONTROL_REP1,AEG588A1_S1_L002_R1_001.fastq.gz,AEG588A1_S1_L002_R2_001.fastq.gz -CONTROL_REP1,AEG588A1_S1_L003_R1_001.fastq.gz,AEG588A1_S1_L003_R2_001.fastq.gz -CONTROL_REP1,AEG588A1_S1_L004_R1_001.fastq.gz,AEG588A1_S1_L004_R2_001.fastq.gz +sample,dwi,bval,bvec,sbref,rev_dwi,rev_bval,rev_bvec,rev_sbref,t1,wmparc,apar_aseg,lesion +CONTROL_RUN1,ctrl_run-3_dwi.nii.gz,ctrl_run-3.bval,ctrl_run-3.bvec,,,,,ctrl_run-3_revb0.nii.gz,ctrl_run-3_t1.nii.gz,,, +CONTROL_RUN2,ctrl_run-2_dwi.nii.gz,ctrl_run-2.bval,ctrl_run-2.bvec,,,,,ctrl_run-2_revb0.nii.gz,ctrl_run-2_t1.nii.gz,,, +CONTROL_RUN3,ctrl_run-3_dwi.nii.gz,ctrl_run-3.bval,ctrl_run-3.bvec,,,,,ctrl_run-3_revb0.nii.gz,ctrl_run-3_t1.nii.gz,,, ``` ### Full samplesheet -The pipeline will auto-detect whether a sample is single- or paired-end using the information provided in the samplesheet. The samplesheet can have as many columns as you desire, however, there is a strict requirement for the first 3 columns to match those defined in the table below. - -A final samplesheet file consisting of both single- and paired-end data may look something like the one below. This is for 6 samples, where `TREATMENT_REP3` has been sequenced twice. +The pipeline detects almost all information required about your protocol from the data added in your samplesheet. Reverse phase +acquired images are determined by importance, using the **full DWI** if available, and deferring to the **single band** references +(sbref) if needed. It will load and use lesions masks when needed, as well as parcellations and segmentations from freesurfer, if provided. ```csv title="samplesheet.csv" -sample,fastq_1,fastq_2 -CONTROL_REP1,AEG588A1_S1_L002_R1_001.fastq.gz,AEG588A1_S1_L002_R2_001.fastq.gz -CONTROL_REP2,AEG588A2_S2_L002_R1_001.fastq.gz,AEG588A2_S2_L002_R2_001.fastq.gz -CONTROL_REP3,AEG588A3_S3_L002_R1_001.fastq.gz,AEG588A3_S3_L002_R2_001.fastq.gz -TREATMENT_REP1,AEG588A4_S4_L003_R1_001.fastq.gz, -TREATMENT_REP2,AEG588A5_S5_L003_R1_001.fastq.gz, -TREATMENT_REP3,AEG588A6_S6_L003_R1_001.fastq.gz, -TREATMENT_REP3,AEG588A6_S6_L004_R1_001.fastq.gz, +sample,dwi,bval,bvec,sbref,rev_dwi,rev_bval,rev_bvec,rev_sbref,t1,wmparc,aparc_aseg,lesion +CONTROL_RUN1,ctrl_run-3_dwi.nii.gz,ctrl_run-3.bval,ctrl_run-3.bvec,,,,,ctrl_run-3_revb0.nii.gz,ctrl_run-3_t1.nii.gz,,, +CONTROL_RUN2,ctrl_run-2_dwi.nii.gz,ctrl_run-2.bval,ctrl_run-2.bvec,,,,,ctrl_run-2_revb0.nii.gz,ctrl_run-2_t1.nii.gz,,, +CONTROL_RUN3,ctrl_run-3_dwi.nii.gz,ctrl_run-3.bval,ctrl_run-3.bvec,,,,,ctrl_run-3_revb0.nii.gz,ctrl_run-3_t1.nii.gz,,, +CONTROL_FDWI,ctrl_dwi.nii.gz,ctrl_dwi.bval,ctrl_dwi.bvec,,ctrl_rev.nii.gz,ctrl_rev.bval,ctrl_rev.bvec,,ctrl_t1.nii.gz,,, +PATIENT1,patient1_dwi.nii.gz,patient1.bval,patient1.bvec,,,,,patient1_revb0.nii.gz,patient1_t1.nii.gz,patient1_wmparc.nii.gz,patient1_aparc+aseg.nii.gz,patient1_lesions.nii.gz +PATIENT2,patient2_dwi.nii.gz,patient2.bval,patient2.bvec,,,,,patient2_revb0.nii.gz,patient2_t1.nii.gz,patient2_wmparc.nii.gz,patient2_aparc+aseg.nii.gz,patient2_lesions.nii.gz ``` -| Column | Description | -| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `sample` | Custom sample name. This entry will be identical for multiple sequencing libraries/runs from the same sample. Spaces in sample names are automatically converted to underscores (`_`). | -| `fastq_1` | Full path to FastQ file for Illumina short reads 1. File has to be gzipped and have the extension ".fastq.gz" or ".fq.gz". | -| `fastq_2` | Full path to FastQ file for Illumina short reads 2. File has to be gzipped and have the extension ".fastq.gz" or ".fq.gz". | +| Column | Description | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `sample` | Name for the sample/subject that will be used to identify it in the pipeline through processing. Spaces in sample names are automatically converted to underscores (`_`). | +| `dwi` | Input Diffusion Weighted Imaging 4D volume (.nii/.nii.gz). | +| `bval` | Input b-values (gradient weights) for each direction in the associated DWI 4D volume (.bval). | +| `bvec` | Input b-vectors (gradient directions) for each direction in the associated DWI 4D volume (.bvec). | +| `sbref` | Single-band acquired b0 reference for the DWI 4D volume (.nii/.nii.gz). | +| `rev_dwi` | Reverse phase acquired Difusion Weighted Imaging 4D volume (.nii/.nii.gz). | +| `rev_bval` | Input b-values (gradient weights) for each direction in the associated reverse phase acquired DWI 4D volume (.bval). | +| `rev_bvec` | Input b-vectors (gradient directions) for each direction in the associated reverse phase acquired DWI 4D volume (.bvec). | +| `rev_sbref` | Single-band acquired b0 reference for the reverse phase acquired DWI 4D volume (.nii/.nii.gz). | +| `t1` | Anatomical T1-weighted volume (.nii/.nii.gz). | +| `wmparc` | White Matter parcellation obtained from freesurfer (.nii/.nii.gz). | +| `aparc_aseg` | Brain parcellation and segmentation obtained from freesurfer (.nii/.nii.gz). | +| `lesion` | Lesions mask (.nii/.nii.gz). | An [example samplesheet](../assets/samplesheet.csv) has been provided with the pipeline. @@ -58,6 +109,12 @@ The typical command for running the pipeline is as follows: nextflow run scilus/nf-tractoflow --input ./samplesheet.csv --outdir ./results -profile docker ``` +If using `bids` : + +```bash +nextflow run scilus/nf-tractoflow --input /path/to/bids/database --outdir ./results -profile docker +``` + This will launch the pipeline with the `docker` configuration profile. See below for more information about profiles. Note that the pipeline will create the following files in your working directory: diff --git a/main.nf b/main.nf index fd2945c..fc1b28b 100644 --- a/main.nf +++ b/main.nf @@ -28,7 +28,14 @@ include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_nf-t workflow SCILUS_NF_TRACTOFLOW { take: - samplesheet // channel: samplesheet read in from --input + t1 // channel: t1 read in from --input + wmparc // channel: wmparc read in from --input + aparc_aseg // channel: aparc_aseg read in from --input + dwi_bval_bvec // channel: dwi_bval_bvec read in from --input + b0 // channel: b0 read in from --input + rev_dwi_bval_bvec // channel: rev_dwi_bval_bvec read in from --input + rev_b0 // channel: rev_b0 read in from --input + lesion // channel: lesion read in from --input main: @@ -36,7 +43,14 @@ workflow SCILUS_NF_TRACTOFLOW { // WORKFLOW: Run pipeline // NF_TRACTOFLOW ( - samplesheet + t1, + wmparc, + aparc_aseg, + dwi_bval_bvec, + b0, + rev_dwi_bval_bvec, + rev_b0, + lesion ) emit: multiqc_report = NF_TRACTOFLOW.out.multiqc_report // channel: /path/to/multiqc_report.html @@ -48,7 +62,6 @@ workflow SCILUS_NF_TRACTOFLOW { */ workflow { - main: // // SUBWORKFLOW: Run initialisation tasks @@ -66,7 +79,14 @@ workflow { // WORKFLOW: Run main workflow // SCILUS_NF_TRACTOFLOW ( - PIPELINE_INITIALISATION.out.samplesheet + PIPELINE_INITIALISATION.out.t1, + PIPELINE_INITIALISATION.out.wmparc, + PIPELINE_INITIALISATION.out.aparc_aseg, + PIPELINE_INITIALISATION.out.dwi_bval_bvec, + PIPELINE_INITIALISATION.out.b0, + PIPELINE_INITIALISATION.out.rev_dwi_bval_bvec, + PIPELINE_INITIALISATION.out.rev_b0, + PIPELINE_INITIALISATION.out.lesion ) // // SUBWORKFLOW: Run completion tasks @@ -77,7 +97,6 @@ workflow { params.plaintext_email, params.outdir, params.monochrome_logs, - params.hook_url, SCILUS_NF_TRACTOFLOW.out.multiqc_report ) } diff --git a/modules.json b/modules.json index 412f4dc..2d240d6 100644 --- a/modules.json +++ b/modules.json @@ -7,7 +7,7 @@ "nf-core": { "multiqc": { "branch": "master", - "git_sha": "f0719ae309075ae4a291533883847c3f7c441dad", + "git_sha": "81880787133db07d9b4c1febd152c090eb8325dc", "installed_by": ["modules"] } } @@ -32,152 +32,192 @@ } } }, - "https://github.com/scilus/nf-scil.git": { + "https://github.com/scilus/nf-neuro.git": { "modules": { - "nf-scil": { + "nf-neuro": { "betcrop/antsbet": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", + "branch": "main", + "git_sha": "b8949dd284432bbe1399dbc3f54cdf9191855f8e", "installed_by": ["preproc_t1"] }, - "betcrop/cropvolume": { - "branch": "review/alex_preproc_dwi", - "git_sha": "8a5ea101e53d2bb73618ca71bf9817df0e470e37", - "installed_by": ["preproc_dwi", "preproc_t1"] - }, "betcrop/fslbetcrop": { - "branch": "review/alex_preproc_dwi", - "git_sha": "de49903e49cabf71a942b43e6320617aa8dc8a43", - "installed_by": ["preproc_dwi"] + "branch": "main", + "git_sha": "d51aa53995a45009b8e077cfc41c7335a87ae1db", + "installed_by": ["preproc_dwi", "topup_eddy"] + }, + "betcrop/synthbet": { + "branch": "main", + "git_sha": "dca20370d97a69c6a91c80843f417206212568e6", + "installed_by": ["preproc_t1"] }, "denoising/mppca": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", + "branch": "main", + "git_sha": "2e222d18c89e5547a6bf5c0c74673baeb63bcd52", "installed_by": ["preproc_dwi"] }, "denoising/nlmeans": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", + "branch": "main", + "git_sha": "261a7e0606645eeaf863e401cb9fc99c130b3a19", "installed_by": ["preproc_t1"] }, + "image/cropvolume": { + "branch": "main", + "git_sha": "3e2e971f5bdaafcd5f72cb9c69f9d0b2a6f20de3", + "installed_by": ["preproc_dwi", "preproc_t1"] + }, "image/resample": { - "branch": "review/alex_preproc_dwi", - "git_sha": "85eebbc9e881e1ac930a22ceada63c44c099e1d3", - "installed_by": ["preproc_t1", "preproc_dwi"] + "branch": "main", + "git_sha": "36e010a236a0bd86334ab99b0cac4f7c4ff51532", + "installed_by": ["preproc_dwi", "preproc_t1"] }, "io/readbids": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", - "installed_by": ["modules"] + "branch": "main", + "git_sha": "8e03029b9ca8f73b35a48a53c9498725360d8b07", + "installed_by": ["io_bids"] }, "preproc/eddy": { "branch": "main", - "git_sha": "a275f58196407fb05d4d9730ca781f584b00048c", + "git_sha": "9b1c19bd5073aec628e4b9fe11bfc7510e038e09", "installed_by": ["topup_eddy"] }, + "preproc/gibbs": { + "branch": "main", + "git_sha": "6b380f7a9c594b71a4ee713b510ee38b2e40677b", + "installed_by": ["preproc_dwi"] + }, "preproc/n4": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", + "branch": "main", + "git_sha": "18273a2cef9ffdaf7088e305b9c4ebf4dd439079", "installed_by": ["preproc_dwi", "preproc_t1"] }, "preproc/normalize": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", + "branch": "main", + "git_sha": "a439fe5eda4249ba0c9d221ba04ede81c1c29f40", "installed_by": ["preproc_dwi"] }, "preproc/topup": { "branch": "main", - "git_sha": "8863c9b6f0757b9c41920b5222d10d2c580009d2", + "git_sha": "0228ee7a52df57340e4373fc278c71f4662c388a", "installed_by": ["topup_eddy"] }, "reconst/dtimetrics": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", - "installed_by": ["modules"] + "branch": "main", + "git_sha": "452075a707a9769b0509fc33a1051e8ba80799bf", + "installed_by": ["tractoflow"] }, "reconst/fodf": { - "branch": "review/alex_preproc_dwi", - "git_sha": "62979e1455a723b1f1a149a4353cbcc2dcd318ba", - "installed_by": ["modules"] + "branch": "main", + "git_sha": "452075a707a9769b0509fc33a1051e8ba80799bf", + "installed_by": ["tractoflow"] }, "reconst/frf": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", - "installed_by": ["modules"] + "branch": "main", + "git_sha": "452075a707a9769b0509fc33a1051e8ba80799bf", + "installed_by": ["tractoflow"] }, "reconst/meanfrf": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", + "branch": "main", + "git_sha": "a372865103842ddb9b240539e7ec147df9862604", + "installed_by": ["tractoflow"] + }, + "reconst/shsignal": { + "branch": "main", + "git_sha": "7e14d496291cdbd3bb44b38e65d563fa14f7478f", "installed_by": ["modules"] }, - "register/anattodwi": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", + "registration/anattodwi": { + "branch": "main", + "git_sha": "f9eba772a9198dbfca2cf03a71b1a07edbc598af", "installed_by": ["registration"] }, "registration/ants": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", + "branch": "main", + "git_sha": "8b713829fd96b11f3cee6c6f4d502848cbc392d1", "installed_by": ["registration"] }, "registration/antsapplytransforms": { - "branch": "review/alex_preproc_dwi", - "git_sha": "4c7e79daf65774c9f11937d0dcab117ae6eb64a1", - "installed_by": ["modules"] + "branch": "main", + "git_sha": "8b713829fd96b11f3cee6c6f4d502848cbc392d1", + "installed_by": ["tractoflow"] + }, + "registration/easyreg": { + "branch": "main", + "git_sha": "47cd7af16914614d41cee6099aa994d04a6f8380", + "installed_by": ["registration"] + }, + "registration/synthregistration": { + "branch": "main", + "git_sha": "a439fe5eda4249ba0c9d221ba04ede81c1c29f40", + "installed_by": ["registration"] }, "segmentation/fastseg": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", + "branch": "main", + "git_sha": "cb567ddcd631be744545dbac139a8b0ff21e55e8", "installed_by": ["anatomical_segmentation"] }, "segmentation/freesurferseg": { - "branch": "review/alex_preproc_dwi", - "git_sha": "62979e1455a723b1f1a149a4353cbcc2dcd318ba", + "branch": "main", + "git_sha": "72c629dd20bf72ae4c580717123ff57516d24f3d", + "installed_by": ["anatomical_segmentation"] + }, + "segmentation/synthseg": { + "branch": "main", + "git_sha": "b041bd20d966f5e87bb528f500ccd3cfb1245f3d", "installed_by": ["anatomical_segmentation"] }, "tracking/localtracking": { - "branch": "review/alex_preproc_dwi", - "git_sha": "62979e1455a723b1f1a149a4353cbcc2dcd318ba", - "installed_by": ["modules"] + "branch": "main", + "git_sha": "b8f82569a1160bf13138b6278c0e22caf88c4e22", + "installed_by": ["tractoflow"] }, "tracking/pfttracking": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", - "installed_by": ["modules"] + "branch": "main", + "git_sha": "12dd6cf0fa5cdac800c7960b3c42c960d5760907", + "installed_by": ["tractoflow"] }, "utils/extractb0": { - "branch": "review/alex_preproc_dwi", - "git_sha": "317f9f98b9b1faa93e72ff61560bb765d6fdc364", - "installed_by": ["topup_eddy", "preproc_dwi"] + "branch": "main", + "git_sha": "baf69f6658ab3874264d2081aeb5a2388f861bb2", + "installed_by": ["preproc_dwi", "topup_eddy"] } } }, "subworkflows": { - "nf-scil": { + "nf-neuro": { "anatomical_segmentation": { - "branch": "review/alex_preproc_dwi", - "git_sha": "f19275c7cd2c78b5201fd9707b00aa6e6730efa8", + "branch": "main", + "git_sha": "72c629dd20bf72ae4c580717123ff57516d24f3d", + "installed_by": ["tractoflow"] + }, + "io_bids": { + "branch": "main", + "git_sha": "9f9ad793400c5783e7766d3e5131a2a403300ac9", "installed_by": ["subworkflows"] }, "preproc_dwi": { "branch": "main", - "git_sha": "455be4fdaff99dcb1d81f8adff17da933f71cc87", - "installed_by": ["subworkflows"] + "git_sha": "e7ecaddd48ac16eebc751f076530e2de74efafd9", + "installed_by": ["tractoflow"] }, "preproc_t1": { - "branch": "review/alex_preproc_dwi", - "git_sha": "2d0ec09328b7ee6697406a177046f6482a3c2ba7", - "installed_by": ["subworkflows"] + "branch": "main", + "git_sha": "84317cd68567fa8651c120bd19028177f90d41ae", + "installed_by": ["tractoflow"] }, "registration": { - "branch": "review/alex_preproc_dwi", - "git_sha": "10d399e7b2dcdf6891e33170d2e7ba66b9147411", - "installed_by": ["subworkflows"] + "branch": "main", + "git_sha": "a439fe5eda4249ba0c9d221ba04ede81c1c29f40", + "installed_by": ["tractoflow"] }, "topup_eddy": { "branch": "main", - "git_sha": "7543d392b565b1a56885c813dd3c896163ce8fcc", + "git_sha": "e7ecaddd48ac16eebc751f076530e2de74efafd9", "installed_by": ["preproc_dwi"] + }, + "tractoflow": { + "branch": "main", + "git_sha": "a439fe5eda4249ba0c9d221ba04ede81c1c29f40", + "installed_by": ["subworkflows"] } } } diff --git a/modules/local/io/safecastinputs/main.nf b/modules/local/io/safecastinputs/main.nf index 4301fb5..bba99e1 100644 --- a/modules/local/io/safecastinputs/main.nf +++ b/modules/local/io/safecastinputs/main.nf @@ -1,38 +1,37 @@ -def safecast_filetype( f, awaited_ext ) { - if ( f.scheme in ["http", "https", "ftp", "s3"] ) { - def name = "$f.parent/${f.simpleName}.${awaited_ext}" - f.copyTo(name) - return name - } - def ext = f.name.replace(f.simpleName, "") - if ( ext == ".$awaited_ext" ) { - return "$f" - } - - error "File $f does not have the awaited extension $awaited_ext" -} - - process IO_SAFECASTINPUTS { label 'process_single' input: - tuple val(meta), path(dwi), path(bval), path(bvec), path(sbref), path(rev_dwi), path(rev_bval), path(rev_bvec), path(rev_sbref), path(t1), path(wmparc), path(aparc_aseg) + tuple val(meta), path(dwi, stageAs: "dwi"), path(bval, stageAs: "bval"), path(bvec, stageAs: "bvec"), path(sbref, stageAs: "sbref"), path(rev_dwi, stageAs: "rev"), path(rev_bval, stageAs: "rval"), path(rev_bvec, stageAs: "rvec"), path(rev_sbref, stageAs: "rbref"), path(t1, stageAs: "t1"), path(wmparc, stageAs: "wmparc"), path(aparc_aseg, stageAs: "aparc+aseg"), path(lesion, stageAs: "lesion") output: - tuple val(meta), path("$out_dwi"), path("$out_bval"), path("$out_bvec"), path("$out_sbref"), path("$out_rev_dwi"), path("$out_rev_bval"), path("$out_rev_bvec"), path("$out_rev_sbref"), path("$out_t1"), path("$out_wmparc"), path("$out_aparc_aseg"), emit: safe_inputs + tuple val(meta), path("$out_dwi"), path("$out_bval"), path("$out_bvec"), path("$out_sbref"), path("$out_rev_dwi"), path("$out_rev_bval"), path("$out_rev_bvec"), path("$out_rev_sbref"), path("$out_t1"), path("$out_wmparc"), path("$out_aparc_aseg"), path("$out_lesion"), emit: safe_inputs script: - out_dwi = dwi ? safecast_filetype(dwi, 'nii.gz') : "$dwi" - out_bval = bval ? safecast_filetype(bval, 'bval') : "$bval" - out_bvec = bvec ? safecast_filetype(bvec, 'bvec') : "$bvec" - out_sbref = sbref ? safecast_filetype(sbref, 'nii.gz') : "$sbref" - out_rev_dwi = rev_dwi ? safecast_filetype(rev_dwi, 'nii.gz') : "$rev_dwi" - out_rev_bval = rev_bval ? safecast_filetype(rev_bval, 'bval') : "$rev_bval" - out_rev_bvec = rev_bvec ? safecast_filetype(rev_bvec, 'bvec') : "$rev_bvec" - out_rev_sbref = rev_sbref ? safecast_filetype(rev_sbref, 'nii.gz') : "$rev_sbref" - out_t1 = t1 ? safecast_filetype(t1, 'nii.gz') : "$t1" - out_wmparc = wmparc ? safecast_filetype(wmparc, 'nii.gz') : "$wmparc" - out_aparc_aseg = aparc_aseg ? safecast_filetype(aparc_aseg, 'nii.gz') : "$aparc_aseg" + out_dwi = dwi ? "dwi.nii.gz" : "$dwi" + out_bval = bval ? "dwi.bval" : "$bval" + out_bvec = bvec ? "dwi.bvec" : "$bvec" + out_sbref = sbref ? "sbref.nii.gz" : "$sbref" + out_rev_dwi = rev_dwi ? "rev_dwi.nii.gz" : "$rev_dwi" + out_rev_bval = rev_bval ? "rev_dwi.bval" : "$rev_bval" + out_rev_bvec = rev_bvec ? "rev_dwi.bvec" : "$rev_bvec" + out_rev_sbref = rev_sbref ? "rev_sbref.nii.gz" : "$rev_sbref" + out_t1 = t1 ? "t1.nii.gz" : "$t1" + out_wmparc = wmparc ? "wmparc.nii.gz" : "$wmparc" + out_aparc_aseg = aparc_aseg ? "aparc+aseg.nii.gz" : "$aparc_aseg" + out_lesion = lesion ? "lesion.nii.gz" : "$lesion" """ + [ -f "$dwi" ] && ln -sf $dwi dwi.nii.gz + [ -f "$bval" ] && ln -sf $bval dwi.bval + [ -f "$bvec" ] && ln -sf $bvec dwi.bvec + [ -f "$sbref" ] && ln -sf $sbref sbref.nii.gz + [ -f "$rev_dwi" ] && ln -sf $rev_dwi rev_dwi.nii.gz + [ -f "$rev_bval" ] && ln -sf $rev_bval rev_dwi.bval + [ -f "$rev_bvec" ] && ln -sf $rev_bvec rev_dwi.bvec + [ -f "$rev_sbref" ] && ln -sf $rev_sbref rev_sbref.nii.gz + [ -f "$t1" ] && ln -sf $t1 t1.nii.gz + [ -f "$wmparc" ] && ln -sf $wmparc wmparc.nii.gz + [ -f "$aparc_aseg" ] && ln -sf $aparc_aseg aparc+aseg.nii.gz + [ -f "$lesion" ] && ln -sf $lesion lesion.nii.gz + exit 0 """ } diff --git a/modules/nf-core/multiqc/environment.yml b/modules/nf-core/multiqc/environment.yml index a27122c..c3b3413 100644 --- a/modules/nf-core/multiqc/environment.yml +++ b/modules/nf-core/multiqc/environment.yml @@ -1,3 +1,5 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json channels: - conda-forge - bioconda diff --git a/modules/nf-core/multiqc/main.nf b/modules/nf-core/multiqc/main.nf index 58d9313..b39b91d 100644 --- a/modules/nf-core/multiqc/main.nf +++ b/modules/nf-core/multiqc/main.nf @@ -4,7 +4,7 @@ process MULTIQC { conda "${moduleDir}/environment.yml" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/multiqc:1.27--pyhdfd78af_0' : - 'biocontainers/multiqc:1.27--pyhdfd78af_0' }" + 'multiqc/multiqc:v1.27' }" input: path multiqc_files, stageAs: "?/*" 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-scil/betcrop/antsbet/meta.yml b/modules/nf-neuro/betcrop/antsbet/meta.yml similarity index 71% rename from modules/nf-scil/betcrop/antsbet/meta.yml rename to modules/nf-neuro/betcrop/antsbet/meta.yml index d0f58cb..e85bfcb 100644 --- a/modules/nf-scil/betcrop/antsbet/meta.yml +++ b/modules/nf-neuro/betcrop/antsbet/meta.yml @@ -1,18 +1,15 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json name: "betcrop_antsbet" description: Perform Brain extraction using antsBrainExtraction.sh on T1 image. keywords: - T1 - BET - ants + - scilpy tools: - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" - - "MRtrix3": - description: "Toolbox for image processing, analysis and visualisation of dMRI." - homepage: "https://mrtrix.readthedocs.io/en/latest/" - "ants": description: "Advanced Normalization Tools." homepage: "https://github.com/ANTsX/ANTs" @@ -36,9 +33,25 @@ input: - tissues_probabilities: type: file - description: Brain probability mask, with intensity range 1 (definitely brain) to 0 (definitely background). + 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 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/fslbetcrop/environment.yml b/modules/nf-neuro/betcrop/fslbetcrop/environment.yml new file mode 100644 index 0000000..48df4a1 --- /dev/null +++ b/modules/nf-neuro/betcrop/fslbetcrop/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: betcrop_fslbetcrop diff --git a/modules/nf-scil/betcrop/fslbetcrop/main.nf b/modules/nf-neuro/betcrop/fslbetcrop/main.nf similarity index 60% rename from modules/nf-scil/betcrop/fslbetcrop/main.nf rename to modules/nf-neuro/betcrop/fslbetcrop/main.nf index b3a6113..54fc9dd 100755 --- a/modules/nf-scil/betcrop/fslbetcrop/main.nf +++ b/modules/nf-neuro/betcrop/fslbetcrop/main.nf @@ -4,8 +4,8 @@ process BETCROP_FSLBETCROP { label 'process_single' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: tuple val(meta), path(image), path(bval), path(bvec) @@ -22,7 +22,7 @@ process BETCROP_FSLBETCROP { script: def prefix = task.ext.prefix ?: "${meta.id}" - def b0_thr = task.ext.b0_thr ? "--b0_thr " + task.ext.b0_thr : "" + def b0_thr = task.ext.b0_thr ? "--b0_threshold " + task.ext.b0_thr : "" def bet_f = task.ext.bet_f ? "-f " + task.ext.bet_f : "" def size_dil = task.ext.size_dil ? task.ext.size_dil : "" @@ -33,35 +33,35 @@ process BETCROP_FSLBETCROP { if [[ -f "$bval" ]] then - scil_extract_b0.py $image $bval $bvec ${prefix}__b0.nii.gz --mean \ - $b0_thr --force_b0_threshold + scil_dwi_extract_b0.py $image $bval $bvec ${prefix}__b0.nii.gz --mean \ + $b0_thr --skip_b0_check bet ${prefix}__b0.nii.gz ${prefix}__image_bet.nii.gz -m -R $bet_f - scil_image_math.py convert ${prefix}__image_bet_mask.nii.gz ${prefix}__image_bet_mask.nii.gz --data_type uint8 -f + scil_volume_math.py convert ${prefix}__image_bet_mask.nii.gz ${prefix}__image_bet_mask.nii.gz --data_type uint8 -f mrcalc $image ${prefix}__image_bet_mask.nii.gz -mult ${prefix}__image_bet.nii.gz -quiet -nthreads 1 -force else bet $image ${prefix}__image_bet.nii.gz -m -R $bet_f - scil_image_math.py convert ${prefix}__image_bet_mask.nii.gz ${prefix}__image_bet_mask.nii.gz --data_type uint8 -f + scil_volume_math.py convert ${prefix}__image_bet_mask.nii.gz ${prefix}__image_bet_mask.nii.gz --data_type uint8 -f fi if [ "$task.ext.crop" = "true" ]; then - scil_crop_volume.py ${prefix}__image_bet.nii.gz ${prefix}__image_bet.nii.gz -f \ - --output_bbox ${prefix}__image_boundingBox.pkl -f - scil_crop_volume.py ${prefix}__image_bet_mask.nii.gz ${prefix}__image_bet_mask.nii.gz -f\ - --input_bbox ${prefix}__image_boundingBox.pkl -f - scil_image_math.py convert ${prefix}__image_bet_mask.nii.gz ${prefix}__image_bet_mask.nii.gz \ + scil_volume_crop.py ${prefix}__image_bet.nii.gz ${prefix}__image_bet.nii.gz -f \ + --output_bbox ${prefix}__image_boundingBox.pkl + scil_volume_crop.py ${prefix}__image_bet_mask.nii.gz ${prefix}__image_bet_mask.nii.gz -f\ + --input_bbox ${prefix}__image_boundingBox.pkl + scil_volume_math.py convert ${prefix}__image_bet_mask.nii.gz ${prefix}__image_bet_mask.nii.gz \ --data_type uint8 -f fi if [ "$task.ext.dilate" = "true" ]; then - scil_image_math.py dilation ${prefix}__image_bet_mask.nii.gz $size_dil ${prefix}__image_bet_mask.nii.gz --data_type uint8 -f + scil_volume_math.py dilation ${prefix}__image_bet_mask.nii.gz $size_dil ${prefix}__image_bet_mask.nii.gz --data_type uint8 -f fi cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(mrcalc -version 2>&1 | sed -n 's/== mrcalc \\([0-9.]\\+\\).*/\\1/p') fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') @@ -69,25 +69,31 @@ process BETCROP_FSLBETCROP { """ stub: - def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" """ - scil_extract_b0.py -h - bet -h - scil_image_math.py -h - mrcalc -h - scil_crop_volume.py -h - touch ${prefix}__image_bet.nii.gz touch ${prefix}__image_bet_mask.nii.gz touch ${prefix}__image_boundingBox.pkl cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(mrcalc -version 2>&1 | sed -n 's/== mrcalc \\([0-9.]\\+\\).*/\\1/p') fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') END_VERSIONS + + function handle_code () { + local code=\$? + ignore=( 1 ) + exit \$([[ " \${ignore[@]} " =~ " \$code " ]] && echo 0 || echo \$code) + } + trap 'handle_code' ERR + + bet + scil_dwi_extract_b0.py -h + scil_volume_math.py -h + mrcalc -h + scil_volume_crop.py -h """ } diff --git a/modules/nf-scil/betcrop/fslbetcrop/meta.yml b/modules/nf-neuro/betcrop/fslbetcrop/meta.yml similarity index 93% rename from modules/nf-scil/betcrop/fslbetcrop/meta.yml rename to modules/nf-neuro/betcrop/fslbetcrop/meta.yml index f2775ac..7b31826 100755 --- a/modules/nf-scil/betcrop/fslbetcrop/meta.yml +++ b/modules/nf-neuro/betcrop/fslbetcrop/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "betcrop_fslbetcrop" description: Perform Brain extraction using FSL BET followed by cropping empty planes around the data. keywords: @@ -7,11 +7,12 @@ keywords: - T1 - BET - Crop + - scilpy tools: - "FSL": - description: "FSL Toolbox and Scilpy Toolbox" + description: "FSL Toolbox" homepage: "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki" - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" - "MRtrix3": diff --git a/modules/nf-neuro/betcrop/fslbetcrop/tests/main.nf.test b/modules/nf-neuro/betcrop/fslbetcrop/tests/main.nf.test new file mode 100644 index 0000000..c89409d --- /dev/null +++ b/modules/nf-neuro/betcrop/fslbetcrop/tests/main.nf.test @@ -0,0 +1,139 @@ +nextflow_process { + + name "Test Process BETCROP_FSLBETCROP" + script "../main.nf" + process "BETCROP_FSLBETCROP" + + tag "modules" + tag "modules_nfcore" + tag "betcrop" + tag "betcrop/fslbetcrop" + + 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("betcrop - fslbetcrop - dwi") { + + config "./nextflow.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/dwi.nii.gz"), + file("\${test_data_directory}/dwi/dwi.bval"), + file("\${test_data_directory}/dwi/dwi.bvec") + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("betcrop - fslbetcrop - t1") { + + config "./nextflow_t1.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() } + ) + } + + } + + test("betcrop - fslbetcrop - t1 - dilate") { + + config "./nextflow_t1_dilate.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() } + ) + } + + } + + test("betcrop - fslbetcrop - stub-run") { + + options "-stub-run" + + 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/dwi.nii.gz"), + file("\${test_data_directory}/dwi/dwi.bval"), + file("\${test_data_directory}/dwi/dwi.bvec") + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + + } + +} diff --git a/subworkflows/nf-scil/anatomical_segmentation/tests/main.nf.test.snap b/modules/nf-neuro/betcrop/fslbetcrop/tests/main.nf.test.snap similarity index 56% rename from subworkflows/nf-scil/anatomical_segmentation/tests/main.nf.test.snap rename to modules/nf-neuro/betcrop/fslbetcrop/tests/main.nf.test.snap index 2bce0d1..47640ec 100644 --- a/subworkflows/nf-scil/anatomical_segmentation/tests/main.nf.test.snap +++ b/modules/nf-neuro/betcrop/fslbetcrop/tests/main.nf.test.snap @@ -1,5 +1,17 @@ { - "anatomical_segmentation - freesurferseg": { + "betcrop - fslbetcrop - stub-run": { + "content": [ + [ + "versions.yml:md5,cc45ab23921525536874337c1c88e5cf" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.1" + }, + "timestamp": "2024-12-12T10:33:04.50811" + }, + "betcrop - fslbetcrop - dwi": { "content": [ { "0": [ @@ -8,7 +20,7 @@ "id": "test", "single_end": false }, - "test__mask_wm.nii.gz:md5,2dc635526adb6ef185300acc326ead13" + "test__image_bet.nii.gz:md5,6a3960fdbf0f4dc6e52e1a240aab3e77" ] ], "1": [ @@ -17,7 +29,7 @@ "id": "test", "single_end": false }, - "test__mask_gm.nii.gz:md5,247106ed727133078fe262b0b8244071" + "test__image_bet_mask.nii.gz:md5,aad98ede91402c7f8494d5af1900ac93" ] ], "2": [ @@ -26,69 +38,51 @@ "id": "test", "single_end": false }, - "test__mask_csf.nii.gz:md5,b92db7e454c95afadea320a143912ce8" + "test__image_boundingBox.pkl:md5,7a2508ff4c6ee3a0c16828411f42ea6a" ] ], "3": [ - - ], - "4": [ - - ], - "5": [ - - ], - "6": [ - "versions.yml:md5,e7c6099100bcaa3691e03060e8c16f64" - ], - "csf_map": [ - + "versions.yml:md5,6759fc7721f27034dd14647de8b4987f" ], - "csf_mask": [ + "bbox": [ [ { "id": "test", "single_end": false }, - "test__mask_csf.nii.gz:md5,b92db7e454c95afadea320a143912ce8" + "test__image_boundingBox.pkl:md5,7a2508ff4c6ee3a0c16828411f42ea6a" ] ], - "gm_map": [ - - ], - "gm_mask": [ + "image": [ [ { "id": "test", "single_end": false }, - "test__mask_gm.nii.gz:md5,247106ed727133078fe262b0b8244071" + "test__image_bet.nii.gz:md5,6a3960fdbf0f4dc6e52e1a240aab3e77" ] ], - "versions": [ - "versions.yml:md5,e7c6099100bcaa3691e03060e8c16f64" - ], - "wm_map": [ - - ], - "wm_mask": [ + "mask": [ [ { "id": "test", "single_end": false }, - "test__mask_wm.nii.gz:md5,2dc635526adb6ef185300acc326ead13" + "test__image_bet_mask.nii.gz:md5,aad98ede91402c7f8494d5af1900ac93" ] + ], + "versions": [ + "versions.yml:md5,6759fc7721f27034dd14647de8b4987f" ] } ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.4" }, - "timestamp": "2024-04-12T11:32:06.5329" + "timestamp": "2024-10-29T11:39:49.11235624" }, - "anatomical_segmentation - fslfast": { + "betcrop - fslbetcrop - t1": { "content": [ { "0": [ @@ -97,7 +91,7 @@ "id": "test", "single_end": false }, - "test__mask_wm.nii.gz:md5,f849e268007a747714e342162f13f631" + "test__image_bet.nii.gz:md5,3c983126d6226c914030e8d6a5c63f6e" ] ], "1": [ @@ -106,7 +100,7 @@ "id": "test", "single_end": false }, - "test__mask_gm.nii.gz:md5,39396421c6d1d32ea7943a5ec7a49dce" + "test__image_bet_mask.nii.gz:md5,c5d967b9ef5ffb31b543fecb87ef8479" ] ], "2": [ @@ -115,102 +109,119 @@ "id": "test", "single_end": false }, - "test__mask_csf.nii.gz:md5,d0fbc088059f17faff38fbcd33257d27" + "test__image_boundingBox.pkl:md5,8eb94c27b3db4e2f7e233eff697597fc" ] ], "3": [ + "versions.yml:md5,6759fc7721f27034dd14647de8b4987f" + ], + "bbox": [ [ { "id": "test", "single_end": false }, - "test__map_wm.nii.gz:md5,1d5a9655d4e3c832b14c83e9424b00fc" + "test__image_boundingBox.pkl:md5,8eb94c27b3db4e2f7e233eff697597fc" ] ], - "4": [ + "image": [ [ { "id": "test", "single_end": false }, - "test__map_gm.nii.gz:md5,f7e51eb14af645c687db5d2c6a4f5bf4" + "test__image_bet.nii.gz:md5,3c983126d6226c914030e8d6a5c63f6e" ] ], - "5": [ + "mask": [ [ { "id": "test", "single_end": false }, - "test__map_csf.nii.gz:md5,9c14d8f70e69d641d9c214d0d52b3663" + "test__image_bet_mask.nii.gz:md5,c5d967b9ef5ffb31b543fecb87ef8479" ] ], - "6": [ - "versions.yml:md5,a7590fcbc409b182d1a6d85fa8743d89" - ], - "csf_map": [ + "versions": [ + "versions.yml:md5,6759fc7721f27034dd14647de8b4987f" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-29T11:40:06.612888953" + }, + "betcrop - fslbetcrop - t1 - dilate": { + "content": [ + { + "0": [ [ { "id": "test", "single_end": false }, - "test__map_csf.nii.gz:md5,9c14d8f70e69d641d9c214d0d52b3663" + "test__image_bet.nii.gz:md5,3c983126d6226c914030e8d6a5c63f6e" ] ], - "csf_mask": [ + "1": [ [ { "id": "test", "single_end": false }, - "test__mask_csf.nii.gz:md5,d0fbc088059f17faff38fbcd33257d27" + "test__image_bet_mask.nii.gz:md5,78a93e32e7ff3093649509e59256645b" ] ], - "gm_map": [ + "2": [ [ { "id": "test", "single_end": false }, - "test__map_gm.nii.gz:md5,f7e51eb14af645c687db5d2c6a4f5bf4" + "test__image_boundingBox.pkl:md5,8eb94c27b3db4e2f7e233eff697597fc" ] ], - "gm_mask": [ + "3": [ + "versions.yml:md5,6759fc7721f27034dd14647de8b4987f" + ], + "bbox": [ [ { "id": "test", "single_end": false }, - "test__mask_gm.nii.gz:md5,39396421c6d1d32ea7943a5ec7a49dce" + "test__image_boundingBox.pkl:md5,8eb94c27b3db4e2f7e233eff697597fc" ] ], - "versions": [ - "versions.yml:md5,a7590fcbc409b182d1a6d85fa8743d89" - ], - "wm_map": [ + "image": [ [ { "id": "test", "single_end": false }, - "test__map_wm.nii.gz:md5,1d5a9655d4e3c832b14c83e9424b00fc" + "test__image_bet.nii.gz:md5,3c983126d6226c914030e8d6a5c63f6e" ] ], - "wm_mask": [ + "mask": [ [ { "id": "test", "single_end": false }, - "test__mask_wm.nii.gz:md5,f849e268007a747714e342162f13f631" + "test__image_bet_mask.nii.gz:md5,78a93e32e7ff3093649509e59256645b" ] + ], + "versions": [ + "versions.yml:md5,6759fc7721f27034dd14647de8b4987f" ] } ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.4" }, - "timestamp": "2024-04-12T11:28:23.344882" + "timestamp": "2024-10-29T11:40:26.52204924" } } \ No newline at end of file diff --git a/modules/nf-neuro/betcrop/fslbetcrop/tests/nextflow.config b/modules/nf-neuro/betcrop/fslbetcrop/tests/nextflow.config new file mode 100644 index 0000000..1a9b08b --- /dev/null +++ b/modules/nf-neuro/betcrop/fslbetcrop/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + withName: "BETCROP_FSLBETCROP" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.bet_f = 0.16 + ext.b0_threshold = 10 + ext.crop = true + ext.dilate = false + } +} diff --git a/modules/nf-neuro/betcrop/fslbetcrop/tests/nextflow_t1.config b/modules/nf-neuro/betcrop/fslbetcrop/tests/nextflow_t1.config new file mode 100644 index 0000000..68e581b --- /dev/null +++ b/modules/nf-neuro/betcrop/fslbetcrop/tests/nextflow_t1.config @@ -0,0 +1,8 @@ +process { + withName: "BETCROP_FSLBETCROP" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.bet_f = 0.5 + ext.crop = true + ext.dilate = false + } +} diff --git a/modules/nf-neuro/betcrop/fslbetcrop/tests/nextflow_t1_dilate.config b/modules/nf-neuro/betcrop/fslbetcrop/tests/nextflow_t1_dilate.config new file mode 100644 index 0000000..a02ac50 --- /dev/null +++ b/modules/nf-neuro/betcrop/fslbetcrop/tests/nextflow_t1_dilate.config @@ -0,0 +1,9 @@ +process { + withName: "BETCROP_FSLBETCROP" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.bet_f = 0.5 + ext.size_dil = 5 + ext.crop = true + ext.dilate = true + } +} diff --git a/modules/nf-neuro/betcrop/fslbetcrop/tests/tags.yml b/modules/nf-neuro/betcrop/fslbetcrop/tests/tags.yml new file mode 100644 index 0000000..c8d38af --- /dev/null +++ b/modules/nf-neuro/betcrop/fslbetcrop/tests/tags.yml @@ -0,0 +1,2 @@ +betcrop/fslbetcrop: + - "modules/nf-neuro/betcrop/fslbetcrop/**" 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-scil/denoising/mppca/main.nf b/modules/nf-neuro/denoising/mppca/main.nf similarity index 64% rename from modules/nf-scil/denoising/mppca/main.nf rename to modules/nf-neuro/denoising/mppca/main.nf index 2963622..4eb498d 100755 --- a/modules/nf-scil/denoising/mppca/main.nf +++ b/modules/nf-neuro/denoising/mppca/main.nf @@ -3,12 +3,10 @@ process DENOISING_MPPCA { tag "$meta.id" label 'process_medium' - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + container "mrtrix3/mrtrix3:latest" input: - tuple val(meta), path(dwi) + tuple val(meta), path(dwi), path(mask) output: tuple val(meta), path("*_dwi_denoised.nii.gz") , emit: image @@ -20,19 +18,22 @@ process DENOISING_MPPCA { 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 -nthreads $task.cpus - fslmaths ${prefix}_dwi_denoised.nii.gz -thr 0 ${prefix}_dwi_denoised.nii.gz + 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') - fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') END_VERSIONS """ @@ -42,14 +43,13 @@ process DENOISING_MPPCA { """ dwidenoise -h - fslmaths -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') - fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') END_VERSIONS """ } diff --git a/modules/nf-scil/denoising/mppca/meta.yml b/modules/nf-neuro/denoising/mppca/meta.yml similarity index 86% rename from modules/nf-scil/denoising/mppca/meta.yml rename to modules/nf-neuro/denoising/mppca/meta.yml index c5d6f24..5ce90fe 100755 --- a/modules/nf-scil/denoising/mppca/meta.yml +++ b/modules/nf-neuro/denoising/mppca/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# 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: @@ -12,9 +12,6 @@ tools: - "MRtrix3": description: "Toolbox for image processing, analysis and visualisation of dMRI." homepage: "https://mrtrix.readthedocs.io/en/latest/" - - "FSL": - description: "FSL Toolbox and Scilpy Toolbox" - homepage: "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki" input: - meta: @@ -28,6 +25,11 @@ input: 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 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-scil/denoising/nlmeans/main.nf b/modules/nf-neuro/denoising/nlmeans/main.nf similarity index 70% rename from modules/nf-scil/denoising/nlmeans/main.nf rename to modules/nf-neuro/denoising/nlmeans/main.nf index c478098..964b4b4 100755 --- a/modules/nf-scil/denoising/nlmeans/main.nf +++ b/modules/nf-neuro/denoising/nlmeans/main.nf @@ -4,8 +4,8 @@ process DENOISING_NLMEANS { label 'process_medium' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: tuple val(meta), path(image), path(mask) @@ -19,6 +19,7 @@ process DENOISING_NLMEANS { 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"] @@ -27,11 +28,11 @@ process DENOISING_NLMEANS { export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 - scil_run_nlmeans.py $image ${prefix}_denoised.nii.gz 1 ${args.join(" ")} + scil_denoising_nlmeans.py $image ${prefix}__denoised.nii.gz $ncoils ${args.join(" ")} cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ @@ -39,13 +40,13 @@ process DENOISING_NLMEANS { def prefix = task.ext.prefix ?: "${meta.id}" """ - scil_run_nlmeans.py -h + scil_denoising_nlmeans.py -h touch ${prefix}_denoised.nii.gz cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ } diff --git a/modules/nf-scil/denoising/nlmeans/meta.yml b/modules/nf-neuro/denoising/nlmeans/meta.yml similarity index 94% rename from modules/nf-scil/denoising/nlmeans/meta.yml rename to modules/nf-neuro/denoising/nlmeans/meta.yml index c35355e..23d7d1b 100755 --- a/modules/nf-scil/denoising/nlmeans/meta.yml +++ b/modules/nf-neuro/denoising/nlmeans/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# 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: @@ -8,7 +8,7 @@ keywords: - nlmeans - scilpy tools: - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" 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/subworkflows/nf-scil/anatomical_segmentation/tests/nextflow.config b/modules/nf-neuro/denoising/nlmeans/tests/nextflow.config old mode 100644 new mode 100755 similarity index 98% rename from subworkflows/nf-scil/anatomical_segmentation/tests/nextflow.config rename to modules/nf-neuro/denoising/nlmeans/tests/nextflow.config index 8730f1c..0293c16 --- a/subworkflows/nf-scil/anatomical_segmentation/tests/nextflow.config +++ b/modules/nf-neuro/denoising/nlmeans/tests/nextflow.config @@ -1,5 +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-scil/betcrop/cropvolume/main.nf b/modules/nf-neuro/image/cropvolume/main.nf similarity index 73% rename from modules/nf-scil/betcrop/cropvolume/main.nf rename to modules/nf-neuro/image/cropvolume/main.nf index 7462f79..08c5d38 100755 --- a/modules/nf-scil/betcrop/cropvolume/main.nf +++ b/modules/nf-neuro/image/cropvolume/main.nf @@ -1,11 +1,11 @@ -process BETCROP_CROPVOLUME { +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_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: tuple val(meta), path(image), path(bounding_box) @@ -26,27 +26,31 @@ process BETCROP_CROPVOLUME { def output_bbox = task.ext.output_bbox ? "--output_bbox ${prefix}_${suffix}_bbox.pkl" : "" """ - scil_crop_volume.py $image ${prefix}_${suffix}.nii.gz $input_bbox $output_bbox + scil_volume_crop.py $image ${prefix}_${suffix}.nii.gz $input_bbox $output_bbox cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + 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}_cropped" : "cropped" """ - scil_crop_volume.py -h + 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: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ } diff --git a/modules/nf-scil/betcrop/cropvolume/meta.yml b/modules/nf-neuro/image/cropvolume/meta.yml similarity index 93% rename from modules/nf-scil/betcrop/cropvolume/meta.yml rename to modules/nf-neuro/image/cropvolume/meta.yml index 85bff2b..df13e95 100755 --- a/modules/nf-scil/betcrop/cropvolume/meta.yml +++ b/modules/nf-neuro/image/cropvolume/meta.yml @@ -1,13 +1,13 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json -name: "betcrop_cropvolume" +# 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": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" 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-scil/image/resample/main.nf b/modules/nf-neuro/image/resample/main.nf similarity index 68% rename from modules/nf-scil/image/resample/main.nf rename to modules/nf-neuro/image/resample/main.nf index 6beb8a5..75fb5ba 100755 --- a/modules/nf-scil/image/resample/main.nf +++ b/modules/nf-neuro/image/resample/main.nf @@ -4,15 +4,15 @@ process IMAGE_RESAMPLE { label 'process_high_memory' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: - tuple val(meta), path(image), path(ref) /* optional, value = [] */ + tuple val(meta), path(image), path(ref) /* optional, input = [] */ output: - tuple val(meta), path("*_resampled.nii.gz"), emit: image - path "versions.yml" , emit: versions + tuple val(meta), path("*_resampled.nii.gz") , emit: image + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when @@ -33,28 +33,28 @@ process IMAGE_RESAMPLE { export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 - scil_resample_volume.py $image ${prefix}_${suffix}.nii.gz \ + 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: 1.6.0 + 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_resample_volume.py -h + scil_volume_resample.py -h - touch ${prefix}__resampled.nii.gz + touch ${prefix}_${suffix}.nii.gz cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ } diff --git a/modules/nf-scil/image/resample/meta.yml b/modules/nf-neuro/image/resample/meta.yml similarity index 98% rename from modules/nf-scil/image/resample/meta.yml rename to modules/nf-neuro/image/resample/meta.yml index c940d50..83aabf0 100755 --- a/modules/nf-scil/image/resample/meta.yml +++ b/modules/nf-neuro/image/resample/meta.yml @@ -23,7 +23,7 @@ input: description: Nifti image file to resample to pattern: "*.{nii,nii.gz}" - - reference: + - ref: type: file description: Nifti reference image file pattern: "*.{nii,nii.gz}" 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/io/readbids/environment.yml b/modules/nf-neuro/io/readbids/environment.yml new file mode 100644 index 0000000..41b6e9d --- /dev/null +++ b/modules/nf-neuro/io/readbids/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: io_readbids diff --git a/modules/nf-neuro/io/readbids/main.nf b/modules/nf-neuro/io/readbids/main.nf new file mode 100644 index 0000000..9f11251 --- /dev/null +++ b/modules/nf-neuro/io/readbids/main.nf @@ -0,0 +1,53 @@ +process IO_READBIDS { + 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: + path(bids_folder) + path(fs_folder) + path(bids_ignore) + + output: + path("tractoflow_bids_struct.json") , emit: bidsstructure + path "versions.yml" , emit: versions + + + when: + task.ext.when == null || task.ext.when + + script: + fs_folder = fs_folder ? "--fs $fs_folder" : '' + bids_ignore = bids_ignore ? "--bids_ignore $bids_ignore" : '' + def readout = task.ext.readout ? "--readout " + task.ext.readout : "--readout 0.062" + def clean_flag = task.ext.clean_bids ? "--clean " : '' + + """ + scil_bids_validate.py $bids_folder tractoflow_bids_struct.json\ + $readout \ + $clean_flag\ + $fs_folder\ + $bids_ignore\ + -v + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ + + + stub: + """ + scil_bids_validate.py -h + + touch tractoflow_bids_struct.json + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ +} diff --git a/modules/nf-scil/io/readbids/meta.yml b/modules/nf-neuro/io/readbids/meta.yml similarity index 57% rename from modules/nf-scil/io/readbids/meta.yml rename to modules/nf-neuro/io/readbids/meta.yml index 698f072..c4609cf 100644 --- a/modules/nf-scil/io/readbids/meta.yml +++ b/modules/nf-neuro/io/readbids/meta.yml @@ -1,19 +1,16 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json name: "io_readbids" -description: read bids file +description: Module reading bids specification for an input folder. keywords: - - sort - - example - - genomics + - Bids + - Input + - Structure tools: - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" input: - # Only when we have meta - - bids_folder: type: directory description: Folder containing bids file @@ -24,10 +21,22 @@ input: description: Folder containing fs file pattern: "*" - - bidsignore: + - bids_ignore: type: file - description: bidsignore file - pattern: "*.{bidsignore}" + description: bids_ignore file + pattern: "*.{bids_ignore}" + +args: + - readout: + type: float + description: Readout time to add in metadata. + default: 0.062 + - clean_bids: + type: boolean + description: | + If set to true, will remove participants with missing files + from the final BIDS layout. + default: false output: - versions: @@ -35,7 +44,7 @@ output: description: File containing software versions pattern: "versions.yml" - - tractoflow_bids_struct.json: + - bidsstructure: type: file description: json file containing tractoflow bids structure pattern: "*.{json}" diff --git a/modules/nf-neuro/io/readbids/tests/main.nf.test b/modules/nf-neuro/io/readbids/tests/main.nf.test new file mode 100644 index 0000000..4099cf7 --- /dev/null +++ b/modules/nf-neuro/io/readbids/tests/main.nf.test @@ -0,0 +1,50 @@ +nextflow_process { + + name "Test Process IO_READBIDS" + script "../main.nf" + process "IO_READBIDS" + + tag "modules" + tag "modules_nfcore" + tag "io" + tag "io/readbids" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + test("io - readbids - folder") { + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "bids.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + file("\${test_data_directory}/i_bids", checkIfExists: true) + ] + } + input[1] = [] + input[2] = [] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-neuro/io/readbids/tests/main.nf.test.snap b/modules/nf-neuro/io/readbids/tests/main.nf.test.snap new file mode 100644 index 0000000..3edae7a --- /dev/null +++ b/modules/nf-neuro/io/readbids/tests/main.nf.test.snap @@ -0,0 +1,25 @@ +{ + "io - readbids - folder": { + "content": [ + { + "0": [ + "tractoflow_bids_struct.json:md5,58e0494c51d30eb3494f7c9198986bb9" + ], + "1": [ + "versions.yml:md5,53cf301d7d6076acbc0691d3e7e52437" + ], + "bidsstructure": [ + "tractoflow_bids_struct.json:md5,58e0494c51d30eb3494f7c9198986bb9" + ], + "versions": [ + "versions.yml:md5,53cf301d7d6076acbc0691d3e7e52437" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T19:43:56.536400136" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/io/readbids/tests/nextflow.config b/modules/nf-neuro/io/readbids/tests/nextflow.config new file mode 100644 index 0000000..9fcb3b5 --- /dev/null +++ b/modules/nf-neuro/io/readbids/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.readout = 0.062 + ext.clean_bids = true +} diff --git a/modules/nf-neuro/io/readbids/tests/tags.yml b/modules/nf-neuro/io/readbids/tests/tags.yml new file mode 100644 index 0000000..a526e69 --- /dev/null +++ b/modules/nf-neuro/io/readbids/tests/tags.yml @@ -0,0 +1,2 @@ +io/readbids: + - "modules/nf-neuro/io/readbids/**" diff --git a/modules/nf-neuro/preproc/eddy/environment.yml b/modules/nf-neuro/preproc/eddy/environment.yml new file mode 100644 index 0000000..5009b00 --- /dev/null +++ b/modules/nf-neuro/preproc/eddy/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: preproc_eddy diff --git a/modules/nf-scil/preproc/eddy/main.nf b/modules/nf-neuro/preproc/eddy/main.nf similarity index 51% rename from modules/nf-scil/preproc/eddy/main.nf rename to modules/nf-neuro/preproc/eddy/main.nf index 641f933..64b2dd0 100644 --- a/modules/nf-scil/preproc/eddy/main.nf +++ b/modules/nf-neuro/preproc/eddy/main.nf @@ -3,25 +3,25 @@ process PREPROC_EDDY { label 'process_high' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - "https://scil.usherbrooke.ca/containers/scilus_2.0.0.sif": - "scilus/scilus:2.0.0"}" + "https://scil.usherbrooke.ca/containers/scilus_latest.sif": + "scilus/scilus:latest"}" input: tuple val(meta), path(dwi), path(bval), path(bvec), path(rev_dwi), path(rev_bval), path(rev_bvec), path(corrected_b0s), path(topup_fieldcoef), path(topup_movpart) output: - tuple val(meta), path("*__dwi_corrected.nii.gz"), emit: dwi_corrected - tuple val(meta), path("*__bval_eddy"), emit: bval_corrected - tuple val(meta), path("*__dwi_eddy_corrected.bvec"), emit: bvec_corrected - tuple val(meta), path("*__b0_bet_mask.nii.gz"), emit: b0_mask - - path "versions.yml" , emit: versions + tuple val(meta), path("*__dwi_corrected.nii.gz") , emit: dwi_corrected + tuple val(meta), path("*__dwi_eddy_corrected.bval") , emit: bval_corrected + tuple val(meta), path("*__dwi_eddy_corrected.bvec") , emit: bvec_corrected + tuple val(meta), path("*__b0_bet_mask.nii.gz") , emit: b0_mask + tuple val(meta), path("*__dwi_eddy_mqc.gif") , emit: dwi_eddy_mqc, optional:true + tuple val(meta), path("*__rev_dwi_eddy_mqc.gif") , emit: rev_dwi_eddy_mqc, optional:true + 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 slice_drop_flag = task.ext.slice_drop_correction ? "--slice_drop_correction " : "" def bet_topup_before_eddy_f = task.ext.bet_topup_before_eddy_f ?: "" @@ -33,12 +33,14 @@ process PREPROC_EDDY { def eddy_cmd = task.ext.eddy_cmd ? task.ext.eddy_cmd : "eddy_cpu" def bet_prelim_f = task.ext.bet_prelim_f ? task.ext.bet_prelim_f : "" def extra_args = task.ext.extra_args ?: "" + def run_qc = task.ext.run_qc ? task.ext.run_qc : false """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus export OMP_NUM_THREADS=$task.cpus export OPENBLAS_NUM_THREADS=1 export ANTS_RANDOM_SEED=7468 + export MRTRIX_RNG_SEED=12345 orig_bval=$bval # Concatenate DWIs @@ -92,54 +94,122 @@ process PREPROC_EDDY { $slice_drop_flag fi - echo "--very_verbose $extra_args" >> eddy.sh + echo "--very_verbose $extra_args --nthr=$task.cpus" >> eddy.sh sh eddy.sh - scil_volume_math.py lower_threshold dwi_eddy_corrected.nii.gz 0 ${prefix}__dwi_corrected.nii.gz + scil_volume_math.py lower_clip dwi_eddy_corrected.nii.gz 0 ${prefix}__dwi_corrected.nii.gz if [[ \$number_rev_dwi -eq 0 ]] then mv dwi_eddy_corrected.eddy_rotated_bvecs ${prefix}__dwi_eddy_corrected.bvec - mv \${orig_bval} ${prefix}__bval_eddy + mv \${orig_bval} ${prefix}__dwi_eddy_corrected.bval else - scil_gradients_validate_correct_eddy.py dwi_eddy_corrected.eddy_rotated_bvecs \${bval} \${number_rev_dwi} ${prefix}__dwi_eddy_corrected.bvec ${prefix}__bval_eddy + scil_gradients_validate_correct_eddy.py dwi_eddy_corrected.eddy_rotated_bvecs \${bval} \${number_rev_dwi} ${prefix}__dwi_eddy_corrected.bvec ${prefix}__dwi_eddy_corrected.bval fi + if $run_qc; + then + extract_dim=\$(mrinfo ${dwi} -size) + read sagittal_dim coronal_dim axial_dim fourth_dim <<< "\${extract_dim}" + + # Get the middle slice + coronal_dim=\$((\$coronal_dim / 2)) + axial_dim=\$((\$axial_dim / 2)) + sagittal_dim=\$((\$sagittal_dim / 2)) + + viz_params="--display_slice_number --display_lr --size 256 256" + rev_dwi="" + if [[ -f "$rev_dwi" ]]; + then + scil_dwi_powder_average.py ${rev_dwi} ${prefix}__dwi_eddy_corrected.bval ${prefix}__rev_dwi_powder_average.nii.gz + scil_volume_math.py normalize_max ${prefix}__rev_dwi_powder_average.nii.gz ${prefix}__rev_dwi_powder_average_norm.nii.gz + rev_dwi="rev_dwi" + fi + scil_dwi_powder_average.py ${dwi} ${prefix}__dwi_eddy_corrected.bval ${prefix}__dwi_powder_average.nii.gz + scil_dwi_powder_average.py ${prefix}__dwi_corrected.nii.gz ${prefix}__dwi_eddy_corrected.bval ${prefix}__dwi_corrected_powder_average.nii.gz + scil_volume_math.py normalize_max ${prefix}__dwi_powder_average.nii.gz ${prefix}__dwi_powder_average_norm.nii.gz + scil_volume_math.py normalize_max ${prefix}__dwi_corrected_powder_average.nii.gz ${prefix}__dwi_corrected_powder_average_norm.nii.gz + + for image in dwi_corrected dwi \${rev_dwi} + do + scil_viz_volume_screenshot.py ${prefix}__\${image}_powder_average_norm.nii.gz ${prefix}__\${image}_coronal.png \${viz_params} --slices \${coronal_dim} --axis coronal + scil_viz_volume_screenshot.py ${prefix}__\${image}_powder_average_norm.nii.gz ${prefix}__\${image}_axial.png \${viz_params} --slices \${axial_dim} --axis axial + scil_viz_volume_screenshot.py ${prefix}__\${image}_powder_average_norm.nii.gz ${prefix}__\${image}_sagittal.png \${viz_params} --slices \${sagittal_dim} --axis sagittal + + if [ \$image == "dwi_corrected" ] || [ \$image == "rev_dwi" ] + then + title="After" + else + title="Before" + fi + + convert +append ${prefix}__\${image}_coronal_slice_\${coronal_dim}.png \ + ${prefix}__\${image}_axial_slice_\${axial_dim}.png \ + ${prefix}__\${image}_sagittal_slice_\${sagittal_dim}.png \ + ${prefix}__\${image}.png + + convert -annotate +20+230 "\${title}" -fill white -pointsize 30 ${prefix}__\${image}.png ${prefix}__\${image}.png + done + + if [[ -f "$rev_dwi" ]]; + then + convert -delay 10 -loop 0 -morph 10 \ + ${prefix}__rev_dwi.png ${prefix}__dwi_corrected.png ${prefix}__rev_dwi.png \ + ${prefix}__rev_dwi_eddy_mqc.gif + fi + + convert -delay 10 -loop 0 -morph 10 \ + ${prefix}__dwi.png ${prefix}__dwi_corrected.png ${prefix}__dwi.png \ + ${prefix}__dwi_eddy_mqc.gif + + rm -rf *png + rm -rf *powder_average* + fi cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 2.0.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(dwidenoise -version 2>&1 | sed -n 's/== dwidenoise \\([0-9.]\\+\\).*/\\1/p') fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') + imagemagick: \$(convert -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') END_VERSIONS """ stub: - def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" """ - scil_volume_math.py -h - maskfilter -h - bet -h - scil_dwi_extract_b0.py -h - scil_gradients_validate_correct_eddy.py -h - scil_dwi_concatenate.py -h - mrconvert -h - scil_dwi_prepare_eddy_command.py -h - scil_header_print_info.py -h - touch ${prefix}__dwi_corrected.nii.gz - touch ${prefix}__bval_eddy + touch ${prefix}__dwi_eddy_mqc.gif + touch ${prefix}__rev_dwi_eddy_mqc.gif + touch ${prefix}__dwi_eddy_corrected.bval touch ${prefix}__dwi_eddy_corrected.bvec touch ${prefix}__b0_bet_mask.nii.gz - cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 2.0.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(dwidenoise -version 2>&1 | sed -n 's/== dwidenoise \\([0-9.]\\+\\).*/\\1/p') fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') - + imagemagick: \$(convert -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') END_VERSIONS + + function handle_code () { + local code=\$? + ignore=( 1 ) + exit \$([[ " \${ignore[@]} " =~ " \$code " ]] && echo 0 || echo \$code) + } + trap 'handle_code' ERR + + scil_volume_math.py -h + maskfilter -h + bet -h + scil_dwi_extract_b0.py -h + scil_gradients_validate_correct_eddy.py -h + scil_dwi_concatenate.py -h + mrconvert -h + scil_dwi_prepare_eddy_command.py -h + scil_header_print_info.py -h + scil_viz_volume_screenshot -h + convert """ } diff --git a/modules/nf-scil/preproc/eddy/meta.yml b/modules/nf-neuro/preproc/eddy/meta.yml similarity index 59% rename from modules/nf-scil/preproc/eddy/meta.yml rename to modules/nf-neuro/preproc/eddy/meta.yml index 83d0d31..d6ead8b 100644 --- a/modules/nf-scil/preproc/eddy/meta.yml +++ b/modules/nf-neuro/preproc/eddy/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "preproc_eddy" description: Apply Eddy (and Topup if already run) keywords: @@ -69,6 +69,53 @@ input: description: Text file - topup movpart pattern: "*__movpar.txt" +args: + - slice_drop_flag: + type: boolean + description: "If set, will activate eddy's outlier correction, which includes slice drop correction." + default: false + - bet_topup_before_eddy_f: + type: float + description: Fractional intensity threshold for BET before running topup + default: 0.16 + - prefix_topup: + type: string + description: Prefix for the topup output. + default: "topup_results" + - b0_thr_extract_b0: + type: int + description: Threshold under which b-values are considered to be b0s. + default: 10 + - encoding: + type: string + description: Encoding direction of the forward DWI. + default: "y" + choices: "x, y or z" + - readout: + type: float + description: Total readout time from the DICOM metadata. + default: 0.062 + - dilate_b0_mask_prelim_brain_extraction: + type: int + description: Number of times to repeatedly apply the filter. + default: 5 + - eddy_cmd: + type: string + description: Command to run Eddy. + default: "eddy_cpu" + - bet_prelim_f: + type: float + description: Fractional intensity threshold for BET if TOPUP has not been run. + default: 0.16 + - extra_args: + type: string + description: Extra arguments for Eddy. + default: "" + - run_qc: + type: boolean + description: Run QC for Eddy. + default: false + output: #Only when we have meta - meta: @@ -97,6 +144,15 @@ output: description: Nifti volume - Mask for b0 corrected pattern: "*__b0_bet_mask.nii.gz" + - dwi_eddy_mqc: + type: file + description: .gif file containing quality control image for the eddy process. Made for use in MultiQC report. + pattern: "*_dwi_eddy_mqc.gif" + - rev_dwi_eddy_mqc: + type: file + description: .gif file containing quality control image for the eddy process for the rev_dwi. Made for use in MultiQC report. + pattern: "*_rev_dwi_eddy_mqc.gif" + - versions: type: file description: File containing software versions diff --git a/modules/nf-neuro/preproc/eddy/tests/main.nf.test b/modules/nf-neuro/preproc/eddy/tests/main.nf.test new file mode 100644 index 0000000..dec67d6 --- /dev/null +++ b/modules/nf-neuro/preproc/eddy/tests/main.nf.test @@ -0,0 +1,129 @@ +nextflow_process { + + name "Test Process PREPROC_EDDY" + script "../main.nf" + process "PREPROC_EDDY" + + tag "modules" + tag "modules_nfcore" + tag "preproc" + tag "preproc/eddy" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + config "./nextflow.config" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "topup_eddy_light.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("eddy_full") { + 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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/sub-01__corrected_b0s.nii.gz", checkIfExists: true), + file("\${test_data_directory}/topup_results_fieldcoef.nii.gz", checkIfExists: true), + file("\${test_data_directory}/topup_results_movpar.txt", checkIfExists: true) + ] + } + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.dwi_corrected.get(0).get(1)).name, + process.out.bval_corrected, + file(process.out.bvec_corrected.get(0).get(1)).name, + process.out.b0_mask, + file(process.out.dwi_eddy_mqc.get(0).get(1)).name, + file(process.out.rev_dwi_eddy_mqc.get(0).get(1)).name, + process.out.versions + ).match() } + ) + } + } + + test("eddy_light") { + 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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true), + [], + [], + [], + file("\${test_data_directory}/sub-01__corrected_b0s.nii.gz", checkIfExists: true), + file("\${test_data_directory}/topup_results_fieldcoef.nii.gz", checkIfExists: true), + file("\${test_data_directory}/topup_results_movpar.txt", checkIfExists: true)]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.dwi_corrected.get(0).get(1)).name, + process.out.bval_corrected, + file(process.out.bvec_corrected.get(0).get(1)).name, + process.out.b0_mask, + file(process.out.dwi_eddy_mqc.get(0).get(1)).name, + process.out.versions + ).match() } + ) + } + } + + test("eddy - stub-run") { + options "-stub-run" + 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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/sub-01__corrected_b0s.nii.gz", checkIfExists: true), + file("\${test_data_directory}/topup_results_fieldcoef.nii.gz", checkIfExists: true), + file("\${test_data_directory}/topup_results_movpar.txt", checkIfExists: true) + ] + } + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + } +} diff --git a/modules/nf-neuro/preproc/eddy/tests/main.nf.test.snap b/modules/nf-neuro/preproc/eddy/tests/main.nf.test.snap new file mode 100644 index 0000000..06d3257 --- /dev/null +++ b/modules/nf-neuro/preproc/eddy/tests/main.nf.test.snap @@ -0,0 +1,81 @@ +{ + "eddy_full": { + "content": [ + "test__dwi_corrected.nii.gz", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_eddy_corrected.bval:md5,4c61c53078316c31b4d5daf446a3d6ac" + ] + ], + "test__dwi_eddy_corrected.bvec", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__b0_bet_mask.nii.gz:md5,3e6bf62a42cb7f1f2c8e0d3024fb9fcb" + ] + ], + "test__dwi_eddy_mqc.gif", + "test__rev_dwi_eddy_mqc.gif", + [ + "versions.yml:md5,95220463b44e9e5778729aaa87c09b3f" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-27T14:57:09.691201694" + }, + "eddy_light": { + "content": [ + "test__dwi_corrected.nii.gz", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_eddy_corrected.bval:md5,8192e97b08e8032382397bb844b31892" + ] + ], + "test__dwi_eddy_corrected.bvec", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__b0_bet_mask.nii.gz:md5,3e6bf62a42cb7f1f2c8e0d3024fb9fcb" + ] + ], + "test__dwi_eddy_mqc.gif", + [ + "versions.yml:md5,95220463b44e9e5778729aaa87c09b3f" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-27T14:58:37.693170403" + }, + "eddy - stub-run": { + "content": [ + [ + "versions.yml:md5,95220463b44e9e5778729aaa87c09b3f" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.4" + }, + "timestamp": "2025-02-23T21:27:53.190124319" + } +} \ No newline at end of file diff --git a/modules/nf-scil/preproc/eddy/tests/nextflow.config b/modules/nf-neuro/preproc/eddy/tests/nextflow.config similarity index 94% rename from modules/nf-scil/preproc/eddy/tests/nextflow.config rename to modules/nf-neuro/preproc/eddy/tests/nextflow.config index fe36c21..a8eb014 100644 --- a/modules/nf-scil/preproc/eddy/tests/nextflow.config +++ b/modules/nf-neuro/preproc/eddy/tests/nextflow.config @@ -11,5 +11,6 @@ process { ext.dilate_b0_mask_prelim_brain_extraction = 5 ext.bet_prelim_f = 0.16 ext.extra_args = "--flm=linear --niter=2" + ext.run_qc = true } } diff --git a/modules/nf-neuro/preproc/eddy/tests/tags.yml b/modules/nf-neuro/preproc/eddy/tests/tags.yml new file mode 100644 index 0000000..c1b5cb4 --- /dev/null +++ b/modules/nf-neuro/preproc/eddy/tests/tags.yml @@ -0,0 +1,2 @@ +preproc/eddy: + - "modules/nf-neuro/preproc/eddy/**" diff --git a/modules/nf-neuro/preproc/gibbs/environment.yml b/modules/nf-neuro/preproc/gibbs/environment.yml new file mode 100644 index 0000000..2ab9495 --- /dev/null +++ b/modules/nf-neuro/preproc/gibbs/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: preproc_gibbs diff --git a/modules/nf-neuro/preproc/gibbs/main.nf b/modules/nf-neuro/preproc/gibbs/main.nf new file mode 100644 index 0000000..1744e5d --- /dev/null +++ b/modules/nf-neuro/preproc/gibbs/main.nf @@ -0,0 +1,43 @@ +process PREPROC_GIBBS { + tag "$meta.id" + label 'process_single' + + container "mrtrix3/mrtrix3:latest" + + input: + tuple val(meta), path(dwi) + + output: + tuple val(meta), path("*dwi_gibbs_corrected.nii.gz"), emit: dwi + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + mrdegibbs $dwi ${prefix}__dwi_gibbs_corrected.nii.gz -nthreads 1 + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + mrtrix: \$(mrdegibbs -version 2>&1 | sed -n 's/== mrdegibbs \\([0-9.]\\+\\).*/\\1/p') + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + mrdegibbs -h + + touch ${prefix}__dwi_gibbs_corrected.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + mrtrix: \$(mrdegibbs -version 2>&1 | sed -n 's/== mrdegibbs \\([0-9.]\\+\\).*/\\1/p') + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/preproc/gibbs/meta.yml b/modules/nf-neuro/preproc/gibbs/meta.yml new file mode 100644 index 0000000..ca13b4d --- /dev/null +++ b/modules/nf-neuro/preproc/gibbs/meta.yml @@ -0,0 +1,45 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json +name: "preproc_gibbs" +description: Remove Gibbs Ringing Artifacts +keywords: + - correction + - Gibbs + - artefact + +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 image file to correct + pattern: "*.{nii,nii.gz}" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - dwi: + type: file + description: DWI image Gibbs corrected + pattern: "*.{nii,nii.gz}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@arnaudbore" diff --git a/modules/nf-neuro/preproc/gibbs/tests/main.nf.test b/modules/nf-neuro/preproc/gibbs/tests/main.nf.test new file mode 100644 index 0000000..cbb9920 --- /dev/null +++ b/modules/nf-neuro/preproc/gibbs/tests/main.nf.test @@ -0,0 +1,48 @@ +nextflow_process { + + name "Test Process PREPROC_GIBBS" + script "../main.nf" + process "PREPROC_GIBBS" + + tag "modules" + tag "modules_nfcore" + tag "preproc" + tag "preproc/gibbs" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + config "./nextflow.config" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "topup_eddy_light.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("preproc - gibbs") { + 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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true) + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-neuro/preproc/gibbs/tests/main.nf.test.snap b/modules/nf-neuro/preproc/gibbs/tests/main.nf.test.snap new file mode 100644 index 0000000..7a33b1d --- /dev/null +++ b/modules/nf-neuro/preproc/gibbs/tests/main.nf.test.snap @@ -0,0 +1,37 @@ +{ + "preproc - gibbs": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_gibbs_corrected.nii.gz:md5,f15ce978797cd4342089e16204a40436" + ] + ], + "1": [ + "versions.yml:md5,65c58dca4630ca36c724dd823f437585" + ], + "dwi": [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_gibbs_corrected.nii.gz:md5,f15ce978797cd4342089e16204a40436" + ] + ], + "versions": [ + "versions.yml:md5,65c58dca4630ca36c724dd823f437585" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-08T17:18:35.063226" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/preproc/gibbs/tests/nextflow.config b/modules/nf-neuro/preproc/gibbs/tests/nextflow.config new file mode 100644 index 0000000..6198e49 --- /dev/null +++ b/modules/nf-neuro/preproc/gibbs/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + withName: "PREPROC_GIBBS" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + } +} diff --git a/modules/nf-neuro/preproc/gibbs/tests/tags.yml b/modules/nf-neuro/preproc/gibbs/tests/tags.yml new file mode 100644 index 0000000..ae41734 --- /dev/null +++ b/modules/nf-neuro/preproc/gibbs/tests/tags.yml @@ -0,0 +1,2 @@ +preproc/gibbs: + - "modules/nf-neuro/preproc/gibbs/**" 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-scil/preproc/n4/main.nf b/modules/nf-neuro/preproc/n4/main.nf old mode 100755 new mode 100644 similarity index 85% rename from modules/nf-scil/preproc/n4/main.nf rename to modules/nf-neuro/preproc/n4/main.nf index 0b06bc0..c39d42b --- a/modules/nf-scil/preproc/n4/main.nf +++ b/modules/nf-neuro/preproc/n4/main.nf @@ -4,8 +4,8 @@ process PREPROC_N4 { label "process_high_memory" container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + '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) @@ -39,7 +39,7 @@ process PREPROC_N4 { -b [\${knot_spacing}, 3] \ -s $shrink_factor - scil_apply_bias_field_on_dwi.py $image bias_field_ref.nii.gz\ + scil_dwi_apply_bias_field.py $image bias_field_ref.nii.gz\ ${prefix}__image_n4.nii.gz --mask $ref_mask -f else @@ -50,7 +50,7 @@ process PREPROC_N4 { cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + 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 """ @@ -59,14 +59,14 @@ process PREPROC_N4 { def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" """ - N4BiasFieldCorrection.py -h - scil_apply_bias_field_on_dwi -h + N4BiasFieldCorrection -h + scil_dwi_apply_bias_field.py -h touch ${prefix}__image_n4.nii.gz cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + 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-scil/preproc/n4/meta.yml b/modules/nf-neuro/preproc/n4/meta.yml old mode 100755 new mode 100644 similarity index 95% rename from modules/nf-scil/preproc/n4/meta.yml rename to modules/nf-neuro/preproc/n4/meta.yml index 5966bab..a473851 --- a/modules/nf-scil/preproc/n4/meta.yml +++ b/modules/nf-neuro/preproc/n4/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# 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: @@ -7,7 +7,7 @@ keywords: - N4 - bias field tools: - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" 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/preproc/normalize/environment.yml b/modules/nf-neuro/preproc/normalize/environment.yml new file mode 100644 index 0000000..69e4629 --- /dev/null +++ b/modules/nf-neuro/preproc/normalize/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: preproc_normalize diff --git a/modules/nf-scil/preproc/normalize/main.nf b/modules/nf-neuro/preproc/normalize/main.nf old mode 100755 new mode 100644 similarity index 73% rename from modules/nf-scil/preproc/normalize/main.nf rename to modules/nf-neuro/preproc/normalize/main.nf index 31862e0..b24adbb --- a/modules/nf-scil/preproc/normalize/main.nf +++ b/modules/nf-neuro/preproc/normalize/main.nf @@ -3,11 +3,11 @@ process PREPROC_NORMALIZE { label 'process_single' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: - tuple val(meta), path(dwi), path(mask), path(bval), path(bvec) + tuple val(meta), path(dwi), path(bval), path(bvec), path(mask) output: tuple val(meta), path("*dwi_normalized.nii.gz") , emit: dwi @@ -25,25 +25,25 @@ process PREPROC_NORMALIZE { def dti_info = task.ext.dti_shells ?: "\$(cut -d ' ' --output-delimiter=\$'\\n' -f 1- $bval | awk -F' ' '{v=int(\$1)}{if(v<=$max_dti_shell_value)print v}' | sort | uniq)" """ - export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 - export OMP_NUM_THREADS=1 + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus + export OMP_NUM_THREADS=$task.cpus export OPENBLAS_NUM_THREADS=1 - scil_extract_dwi_shell.py $dwi $bval $bvec $dti_info dwi_dti.nii.gz \ + scil_dwi_extract_shell.py $dwi $bval $bvec $dti_info dwi_dti.nii.gz \ bval_dti bvec_dti $dwi_shell_tolerance - scil_compute_dti_metrics.py dwi_dti.nii.gz bval_dti bvec_dti --mask $mask \ - --not_all --fa fa.nii.gz --force_b0_threshold + scil_dti_metrics.py dwi_dti.nii.gz bval_dti bvec_dti --mask $mask \ + --not_all --fa fa.nii.gz --skip_b0_check mrthreshold fa.nii.gz ${prefix}_fa_wm_mask.nii.gz $fa_mask_threshold \ - -nthreads 1 + -nthreads $task.cpus dwinormalise individual $dwi ${prefix}_fa_wm_mask.nii.gz \ - ${prefix}__dwi_normalized.nii.gz -fslgrad $bvec $bval -nthreads 1 + ${prefix}__dwi_normalized.nii.gz -fslgrad $bvec $bval -nthreads $task.cpus cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(dwidenoise -version 2>&1 | sed -n 's/== dwidenoise \\([0-9.]\\+\\).*/\\1/p') END_VERSIONS """ @@ -53,8 +53,8 @@ process PREPROC_NORMALIZE { def prefix = task.ext.prefix ?: "${meta.id}" """ - scil_extract_dwi_shell.py -h - scil_compute_dti_metrics.py -h + scil_dwi_extract_shell.py -h + scil_dti_metrics.py -h mrthreshold -h dwinormalise -h @@ -63,7 +63,7 @@ process PREPROC_NORMALIZE { cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(dwidenoise -version 2>&1 | sed -n 's/== dwidenoise \\([0-9.]\\+\\).*/\\1/p') END_VERSIONS """ diff --git a/modules/nf-scil/preproc/normalize/meta.yml b/modules/nf-neuro/preproc/normalize/meta.yml old mode 100755 new mode 100644 similarity index 93% rename from modules/nf-scil/preproc/normalize/meta.yml rename to modules/nf-neuro/preproc/normalize/meta.yml index 4eb3629..cff489c --- a/modules/nf-scil/preproc/normalize/meta.yml +++ b/modules/nf-neuro/preproc/normalize/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "preproc_normalize" description: Normalize DWI using MRtrix3 (in WM) keywords: @@ -8,7 +8,7 @@ keywords: - scilpy - MRtrix3 tools: - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" - "MRtrix3": @@ -35,12 +35,12 @@ input: - bval: type: file description: Text file containing b-values - pattern: "*.{.bval}" + pattern: "*.bval" - bvec: type: file description: Text file containing b-vectors - pattern: "*.{.bvec}" + pattern: "*.bvec" output: - meta: diff --git a/modules/nf-neuro/preproc/normalize/tests/main.nf.test b/modules/nf-neuro/preproc/normalize/tests/main.nf.test new file mode 100644 index 0000000..c8bc8e8 --- /dev/null +++ b/modules/nf-neuro/preproc/normalize/tests/main.nf.test @@ -0,0 +1,76 @@ +nextflow_process { + + name "Test Process PREPROC_NORMALIZE" + script "../main.nf" + process "PREPROC_NORMALIZE" + + tag "modules" + tag "modules_nfcore" + tag "preproc" + tag "preproc/normalize" + + 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 - normalize") { + config "./nextflow.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/dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bval", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/dwi/mask.nii.gz", checkIfExists: true) + ]} + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("preproc - normalize - dti shells") { + config "./nextflow_with_dti_shells.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/dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bval", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/dwi/mask.nii.gz", checkIfExists: true) + ]} + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-neuro/preproc/normalize/tests/main.nf.test.snap b/modules/nf-neuro/preproc/normalize/tests/main.nf.test.snap new file mode 100644 index 0000000..3159d53 --- /dev/null +++ b/modules/nf-neuro/preproc/normalize/tests/main.nf.test.snap @@ -0,0 +1,108 @@ +{ + "preproc - normalize - dti shells": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_normalized.nii.gz:md5,0d1ec91a9fdac4448bd7690fbf93a960" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test_fa_wm_mask.nii.gz:md5,9056862830ea449d4c4cf01752b51944" + ] + ], + "2": [ + "versions.yml:md5,de4076b60c7dbf985efd96d5e6dc3faf" + ], + "dwi": [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_normalized.nii.gz:md5,0d1ec91a9fdac4448bd7690fbf93a960" + ] + ], + "mask": [ + [ + { + "id": "test", + "single_end": false + }, + "test_fa_wm_mask.nii.gz:md5,9056862830ea449d4c4cf01752b51944" + ] + ], + "versions": [ + "versions.yml:md5,de4076b60c7dbf985efd96d5e6dc3faf" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-12T02:10:28.526169" + }, + "preproc - normalize": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_normalized.nii.gz:md5,0644f1faa8ecc7890747439800f7a616" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test_fa_wm_mask.nii.gz:md5,2b5c00d69e49c77bfec11f4cc1da2b8e" + ] + ], + "2": [ + "versions.yml:md5,de4076b60c7dbf985efd96d5e6dc3faf" + ], + "dwi": [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_normalized.nii.gz:md5,0644f1faa8ecc7890747439800f7a616" + ] + ], + "mask": [ + [ + { + "id": "test", + "single_end": false + }, + "test_fa_wm_mask.nii.gz:md5,2b5c00d69e49c77bfec11f4cc1da2b8e" + ] + ], + "versions": [ + "versions.yml:md5,de4076b60c7dbf985efd96d5e6dc3faf" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-12T02:10:08.116591" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/preproc/normalize/tests/nextflow.config b/modules/nf-neuro/preproc/normalize/tests/nextflow.config new file mode 100755 index 0000000..9d8998b --- /dev/null +++ b/modules/nf-neuro/preproc/normalize/tests/nextflow.config @@ -0,0 +1,11 @@ + +process { + + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + + withName: "PREPROC_NORMALIZE" { + ext.dwi_shell_tolerance = '20' + ext.fa_mask_threshold = '0.4' + ext.max_dti_shell_value = '1200' + } +} diff --git a/modules/nf-neuro/preproc/normalize/tests/nextflow_with_dti_shells.config b/modules/nf-neuro/preproc/normalize/tests/nextflow_with_dti_shells.config new file mode 100755 index 0000000..e054b21 --- /dev/null +++ b/modules/nf-neuro/preproc/normalize/tests/nextflow_with_dti_shells.config @@ -0,0 +1,9 @@ + +process { + + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + + withName: "PREPROC_NORMALIZE" { + ext.dti_shells = '0 1000' + } +} diff --git a/modules/nf-neuro/preproc/normalize/tests/tags.yml b/modules/nf-neuro/preproc/normalize/tests/tags.yml new file mode 100644 index 0000000..a6e9818 --- /dev/null +++ b/modules/nf-neuro/preproc/normalize/tests/tags.yml @@ -0,0 +1,2 @@ +preproc/normalize: + - "modules/nf-neuro/preproc/normalize/**" diff --git a/modules/nf-neuro/preproc/topup/environment.yml b/modules/nf-neuro/preproc/topup/environment.yml new file mode 100644 index 0000000..e7bbee3 --- /dev/null +++ b/modules/nf-neuro/preproc/topup/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: preproc_topup diff --git a/modules/nf-neuro/preproc/topup/main.nf b/modules/nf-neuro/preproc/topup/main.nf new file mode 100644 index 0000000..f501664 --- /dev/null +++ b/modules/nf-neuro/preproc/topup/main.nf @@ -0,0 +1,159 @@ +process PREPROC_TOPUP { + 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(dwi), path(bval), path(bvec), path(b0), path(rev_dwi), path(rev_bval), path(rev_bvec), path(rev_b0) + val config_topup + + output: + tuple val(meta), path("*__corrected_b0s.nii.gz"), emit: topup_corrected_b0s + tuple val(meta), path("*_fieldcoef.nii.gz") , emit: topup_fieldcoef + tuple val(meta), path("*_movpar.txt") , emit: topup_movpart + tuple val(meta), path("*__rev_b0_warped.nii.gz"), emit: rev_b0_warped + tuple val(meta), path("*__rev_b0_mean.nii.gz") , emit: rev_b0_mean + tuple val(meta), path("*__b0_mean.nii.gz") , emit: b0_mean + tuple val(meta), path("*_b0_topup_mqc.gif") , emit: mqc , optional: true + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + + def prefix_topup = task.ext.prefix_topup ? task.ext.prefix_topup : "" + config_topup = config_topup ?: task.ext.default_config_topup + def encoding = task.ext.encoding ? task.ext.encoding : "" + def readout = task.ext.readout ? task.ext.readout : "" + def b0_thr_extract_b0 = task.ext.b0_thr_extract_b0 ? task.ext.b0_thr_extract_b0 : "" + def run_qc = task.ext.run_qc ? task.ext.run_qc : false + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + export ANTS_RANDOM_SEED=7468 + + if [[ -f "$b0" ]]; + then + scil_volume_math.py concatenate $b0 $b0 ${prefix}__concatenated_b0.nii.gz --data_type float32 + scil_volume_math.py mean ${prefix}__concatenated_b0.nii.gz ${prefix}__b0_mean.nii.gz + else + scil_dwi_extract_b0.py $dwi $bval $bvec ${prefix}__b0_mean.nii.gz --mean --b0_threshold $b0_thr_extract_b0 --skip_b0_check + fi + + if [[ -f "$rev_b0" ]]; + then + scil_volume_math.py concatenate $rev_b0 $rev_b0 ${prefix}__concatenated_rev_b0.nii.gz --data_type float32 + scil_volume_math.py mean ${prefix}__concatenated_rev_b0.nii.gz ${prefix}__rev_b0_mean.nii.gz + else + scil_dwi_extract_b0.py $rev_dwi $rev_bval $rev_bvec ${prefix}__rev_b0_mean.nii.gz --mean --b0_threshold $b0_thr_extract_b0 --skip_b0_check + fi + + antsRegistrationSyNQuick.sh -d 3 -f ${prefix}__b0_mean.nii.gz -m ${prefix}__rev_b0_mean.nii.gz -o output -t r -e 1 + mv outputWarped.nii.gz ${prefix}__rev_b0_warped.nii.gz + scil_dwi_prepare_topup_command.py ${prefix}__b0_mean.nii.gz ${prefix}__rev_b0_warped.nii.gz\ + --config $config_topup\ + --encoding_direction $encoding\ + --readout $readout --out_prefix $prefix_topup\ + --out_script + sh topup.sh + cp corrected_b0s.nii.gz ${prefix}__corrected_b0s.nii.gz + + # QC + if $run_qc; + then + extract_dim=\$(mrinfo ${prefix}__b0_mean.nii.gz -size) + read sagittal_dim axial_dim coronal_dim <<< "\${extract_dim}" + + # Get the middle slice + coronal_dim=\$((\$coronal_dim / 2)) + axial_dim=\$((\$axial_dim / 2)) + sagittal_dim=\$((\$sagittal_dim / 2)) + + fslsplit ${prefix}__corrected_b0s.nii.gz ${prefix}__ -t + for image in b0_mean rev_b0_mean 0000 0001; + do + viz_params="--display_slice_number --display_lr --size 256 256" + scil_volume_math.py normalize_max ${prefix}__\${image}.nii.gz ${prefix}__\${image}_norm.nii.gz + scil_viz_volume_screenshot.py ${prefix}__\${image}_norm.nii.gz ${prefix}__\${image}_coronal.png \${viz_params} --slices \${coronal_dim} --axis coronal + scil_viz_volume_screenshot.py ${prefix}__\${image}_norm.nii.gz ${prefix}__\${image}_axial.png \${viz_params} --slices \${axial_dim} --axis axial + scil_viz_volume_screenshot.py ${prefix}__\${image}_norm.nii.gz ${prefix}__\${image}_sagittal.png \${viz_params} --slices \${sagittal_dim} --axis sagittal + + if [ \$image == "b0_mean" ] || [ \$image == "rev_b0_mean" ]; + then + title="Before" + else + title="After" + fi + + convert +append ${prefix}__\${image}_coronal_slice_\${coronal_dim}.png \ + ${prefix}__\${image}_axial_slice_\${axial_dim}.png \ + ${prefix}__\${image}_sagittal_slice_\${sagittal_dim}.png \ + ${prefix}__\${image}.png + convert -annotate +20+230 "\${title}" -fill white -pointsize 30 ${prefix}__\${image}.png ${prefix}__\${image}.png + done + + convert -delay 10 -loop 0 -morph 10 \ + ${prefix}__b0_mean.png ${prefix}__0000.png ${prefix}__b0_mean.png \ + ${prefix}__b0_topup_mqc.gif + + convert -delay 10 -loop 0 -morph 10 \ + ${prefix}__rev_b0_mean.png ${prefix}__0001.png ${prefix}__rev_b0_mean.png \ + ${prefix}__rev_b0_topup_mqc.gif + fi + + rm -rf *png + rm -rf *norm* + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + antsRegistration: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\+\\).*/\\1/') + fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') + imagemagick: \$(convert -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + def prefix_topup = task.ext.prefix_topup ? task.ext.prefix_topup : "" + + """ + touch ${prefix}__corrected_b0s.nii.gz + touch ${prefix}__rev_b0_warped.nii.gz + touch ${prefix}__rev_b0_mean.nii.gz + touch ${prefix}__b0_mean.nii.gz + touch ${prefix}__rev_b0_topup_mqc.gif + touch ${prefix}__b0_topup_mqc.gif + touch ${prefix_topup}_fieldcoef.nii.gz + touch ${prefix_topup}_movpar.txt + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + antsRegistration: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\+\\).*/\\1/') + fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') + imagemagick: \$(convert -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + END_VERSIONS + + function handle_code () { + local code=\$? + ignore=( 1 ) + exit \$([[ " \${ignore[@]} " =~ " \$code " ]] && echo 0 || echo \$code) + } + trap 'handle_code' ERR + + scil_volume_math.py -h + scil_dwi_extract_b0.py -h + antsRegistrationSyNQuick.sh + scil_dwi_prepare_topup_command.py -h + """ +} diff --git a/modules/nf-scil/preproc/topup/meta.yml b/modules/nf-neuro/preproc/topup/meta.yml similarity index 73% rename from modules/nf-scil/preproc/topup/meta.yml rename to modules/nf-neuro/preproc/topup/meta.yml index 5bcc451..5e79e81 100755 --- a/modules/nf-scil/preproc/topup/meta.yml +++ b/modules/nf-neuro/preproc/topup/meta.yml @@ -1,16 +1,20 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "preproc_topup" description: Prepare data and apply FSL topup keywords: - DWI - distorsion - topup + tools: + - "ANTs": + description: "Advanced Normalization Tools (ANTs) for image processing." + homepage: "http://stnava.github.io/ANTs/" - "FSL": description: "FSL Toolbox and Scilpy Toolbox" homepage: "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki" - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" @@ -67,6 +71,25 @@ input: description: topup config file. See https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide#Configuration_files pattern: "*cnf" +args: + - encoding: + type: string + description: Encoding direction of the forward DWI. + choices: "x, y or z" + default: "y" + - readout: + type: float + description: Total readout time from the DICOM metadata. + default: 0.062 + - b0_thr_extract_b0: + type: int + description: Threshold under which b-values are considered to be b0s. + default: 10 + - run_qc: + type: boolean + description: Run quality control. + default: true + output: #Only when we have meta - meta: @@ -105,9 +128,14 @@ output: description: Nifti volume - b0 mean pattern: "*__b0_mean.nii.gz" + - mqc: + type: file + description: .gif file containing quality control image for the topup process (Comparison with dwi and rev_dwi). Made for use in MultiQC report. + pattern: "*_b0_topup_mqc.gif" + - versions: type: file - description: File containing software versions + description: File containing software versions. pattern: "versions.yml" authors: diff --git a/modules/nf-neuro/preproc/topup/tests/main.nf.test b/modules/nf-neuro/preproc/topup/tests/main.nf.test new file mode 100644 index 0000000..f6e1043 --- /dev/null +++ b/modules/nf-neuro/preproc/topup/tests/main.nf.test @@ -0,0 +1,151 @@ +nextflow_process { + + name "Test Process PREPROC_TOPUP" + script "../main.nf" + process "PREPROC_TOPUP" + + tag "modules" + tag "modules_nfcore" + tag "preproc" + tag "preproc/topup" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + config "./nextflow.config" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "topup_eddy_light.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("topup_full") { + 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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_sbref.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_sbref.nii.gz", checkIfExists: true)]} + input[1] = Channel.value('') + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.topup_corrected_b0s.get(0).get(1)).name, + file(process.out.topup_fieldcoef.get(0).get(1)).name, + file(process.out.topup_movpart.get(0).get(1)).name, + file(process.out.rev_b0_warped.get(0).get(1)).name, + process.out.rev_b0_mean, + process.out.b0_mean, + file(process.out.mqc.get(0).get(1).get(0)).name, + file(process.out.mqc.get(0).get(1).get(1)).name, + process.out.versions + ).match() } + ) + } + } + + test("topup_light") { + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.flatMap { test_data_directory -> + [ + [ + [ id:'test1', single_end:false ], + file("\${test_data_directory}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true), + [], + [], + [], + [], + file("\${test_data_directory}/sub-01_dir-PA_sbref.nii.gz", checkIfExists: true) + ], + [ + [ id:'test2', single_end:false ], + file("\${test_data_directory}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true), + [], + [], + [], + [], + file("\${test_data_directory}/sub-01_dir-PA_sbref.nii.gz", checkIfExists: true) + ] + ] + } + input[1] = Channel.value('') + """ + } + } + then { + assertAll( + { assert process.success }, + { assert process.out.topup_corrected_b0s.size() == 2 }, + { assert process.out.topup_fieldcoef.size() == 2 }, + { assert process.out.topup_movpart.size() == 2 }, + { assert process.out.rev_b0_warped.size() == 2 }, + { assert process.out.rev_b0_mean.size() == 2 }, + { assert process.out.b0_mean.size() == 2 }, + { assert snapshot( + file(process.out.topup_corrected_b0s.get(0).get(1)).name, + file(process.out.topup_fieldcoef.get(0).get(1)).name, + file(process.out.topup_movpart.get(0).get(1)).name, + file(process.out.rev_b0_warped.get(0).get(1)).name, + process.out.rev_b0_mean, + process.out.b0_mean, + file(process.out.mqc.get(0).get(1).get(0)).name, + file(process.out.mqc.get(0).get(1).get(1)).name, + process.out.versions + ).match() } + ) + } + } + + test("topup - stub-run") { + options "-stub-run" + 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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_sbref.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-PA_sbref.nii.gz", checkIfExists: true)]} + input[1] = Channel.value('') + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + } +} diff --git a/modules/nf-neuro/preproc/topup/tests/main.nf.test.snap b/modules/nf-neuro/preproc/topup/tests/main.nf.test.snap new file mode 100644 index 0000000..701c006 --- /dev/null +++ b/modules/nf-neuro/preproc/topup/tests/main.nf.test.snap @@ -0,0 +1,101 @@ +{ + "topup_full": { + "content": [ + "test__corrected_b0s.nii.gz", + "topup_results_fieldcoef.nii.gz", + "topup_results_movpar.txt", + "test__rev_b0_warped.nii.gz", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__rev_b0_mean.nii.gz:md5,a0c63d7a8a234d85eeb63045ae2a8f60" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__b0_mean.nii.gz:md5,f50e652bcdd1d45f7a4c592df7eb851a" + ] + ], + "test__b0_topup_mqc.gif", + "test__rev_b0_topup_mqc.gif", + [ + "versions.yml:md5,94e2c9feaffa49fdd4fd59ac95fd251f" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.4" + }, + "timestamp": "2025-02-23T18:16:40.661701215" + }, + "topup - stub-run": { + "content": [ + [ + "versions.yml:md5,94e2c9feaffa49fdd4fd59ac95fd251f" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.4" + }, + "timestamp": "2025-02-20T19:58:59.31028257" + }, + "topup_light": { + "content": [ + "test1__corrected_b0s.nii.gz", + "topup_results_fieldcoef.nii.gz", + "topup_results_movpar.txt", + "test1__rev_b0_warped.nii.gz", + [ + [ + { + "id": "test1", + "single_end": false + }, + "test1__rev_b0_mean.nii.gz:md5,a0c63d7a8a234d85eeb63045ae2a8f60" + ], + [ + { + "id": "test2", + "single_end": false + }, + "test2__rev_b0_mean.nii.gz:md5,a0c63d7a8a234d85eeb63045ae2a8f60" + ] + ], + [ + [ + { + "id": "test1", + "single_end": false + }, + "test1__b0_mean.nii.gz:md5,91288914a80e7866ef8b08a363d617f9" + ], + [ + { + "id": "test2", + "single_end": false + }, + "test2__b0_mean.nii.gz:md5,91288914a80e7866ef8b08a363d617f9" + ] + ], + "test1__b0_topup_mqc.gif", + "test1__rev_b0_topup_mqc.gif", + [ + "versions.yml:md5,94e2c9feaffa49fdd4fd59ac95fd251f", + "versions.yml:md5,94e2c9feaffa49fdd4fd59ac95fd251f" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.4" + }, + "timestamp": "2025-02-23T18:17:42.609518864" + } +} \ No newline at end of file diff --git a/modules/nf-scil/preproc/topup/tests/nextflow.config b/modules/nf-neuro/preproc/topup/tests/nextflow.config similarity index 89% rename from modules/nf-scil/preproc/topup/tests/nextflow.config rename to modules/nf-neuro/preproc/topup/tests/nextflow.config index d90454e..a5e2472 100644 --- a/modules/nf-scil/preproc/topup/tests/nextflow.config +++ b/modules/nf-neuro/preproc/topup/tests/nextflow.config @@ -5,5 +5,6 @@ process { ext.encoding = "y" ext.readout = 0.062 ext.b0_thr_extract_b0 = 10 + ext.run_qc = true } } diff --git a/modules/nf-neuro/preproc/topup/tests/tags.yml b/modules/nf-neuro/preproc/topup/tests/tags.yml new file mode 100644 index 0000000..1df37d5 --- /dev/null +++ b/modules/nf-neuro/preproc/topup/tests/tags.yml @@ -0,0 +1,2 @@ +preproc/topup: + - "modules/nf-neuro/preproc/topup/**" diff --git a/modules/nf-neuro/reconst/dtimetrics/environment.yml b/modules/nf-neuro/reconst/dtimetrics/environment.yml new file mode 100644 index 0000000..5fed550 --- /dev/null +++ b/modules/nf-neuro/reconst/dtimetrics/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: reconst_dtimetrics diff --git a/modules/nf-scil/reconst/dtimetrics/main.nf b/modules/nf-neuro/reconst/dtimetrics/main.nf old mode 100755 new mode 100644 similarity index 56% rename from modules/nf-scil/reconst/dtimetrics/main.nf rename to modules/nf-neuro/reconst/dtimetrics/main.nf index 1724f4c..c992930 --- a/modules/nf-scil/reconst/dtimetrics/main.nf +++ b/modules/nf-neuro/reconst/dtimetrics/main.nf @@ -4,8 +4,8 @@ process RECONST_DTIMETRICS { label 'process_single' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_latest.sif': + 'scilus/scilus:latest' }" input: tuple val(meta), path(dwi), path(bval), path(bvec), path(b0mask) @@ -30,6 +30,7 @@ process RECONST_DTIMETRICS { tuple val(meta), path("*__tensor.nii.gz") , emit: tensor, optional: true tuple val(meta), path("*__nonphysical.nii.gz") , emit: nonphysical, optional: true tuple val(meta), path("*__pulsation_std_dwi.nii.gz") , emit: pulsation_std_dwi, optional: true + tuple val(meta), path("*__pulsation_std_b0.nii.gz") , emit: pulsation_std_b0, optional: true tuple val(meta), path("*__residual.nii.gz") , emit: residual, optional: true tuple val(meta), path("*__residual_iqr_residuals.npy") , emit: residual_iqr_residuals, optional: true tuple val(meta), path("*__residual_mean_residuals.npy") , emit: residual_mean_residuals, optional: true @@ -37,6 +38,7 @@ process RECONST_DTIMETRICS { tuple val(meta), path("*__residual_q3_residuals.npy") , emit: residual_q3_residuals, optional: true tuple val(meta), path("*__residual_residuals_stats.png") , emit: residual_residuals_stats, optional: true tuple val(meta), path("*__residual_std_residuals.npy") , emit: residual_std_residuals, optional: true + tuple val(meta), path("*__dti_mqc.png") , emit: mqc , optional: true path "versions.yml" , emit: versions when: @@ -45,9 +47,14 @@ process RECONST_DTIMETRICS { script: def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" - def mask =[] + def dwi_shell_tolerance = task.ext.dwi_shell_tolerance ? "--tolerance " + task.ext.dwi_shell_tolerance : "" + def max_dti_shell_value = task.ext.max_dti_shell_value ?: 1500 + def b0_thr_extract_b0 = task.ext.b0_thr_extract_b0 ?: 10 + def b0_threshold = task.ext.b0_thr_extract_b0 ? "--b0_threshold $task.ext.b0_thr_extract_b0" : "" + def dti_shells = task.ext.dti_shells ?: "\$(cut -d ' ' --output-delimiter=\$'\\n' -f 1- $bval | awk -F' ' '{v=int(\$1)}{if(v<=$max_dti_shell_value|| v<=$b0_thr_extract_b0)print v}' | sort | uniq)" + def run_qc = task.ext.run_qc ?: false - if (b0mask) mask += ["--mask $b0mask"] + if ( b0mask ) args += " --mask $b0mask" if ( task.ext.ad ) args += " --ad ${prefix}__ad.nii.gz" if ( task.ext.evecs ) args += " --evecs ${prefix}__evecs.nii.gz" if ( task.ext.evals ) args += " --evals ${prefix}__evals.nii.gz" @@ -60,29 +67,83 @@ process RECONST_DTIMETRICS { if ( task.ext.rd ) args += " --rd ${prefix}__rd.nii.gz" if ( task.ext.tensor ) args += " --tensor ${prefix}__tensor.nii.gz" if ( task.ext.nonphysical ) args += " --non-physical ${prefix}__nonphysical.nii.gz" - if ( task.ext.pulsation ) args += " --pulsation ${prefix}__pulsation_std_dwi.nii.gz" + if ( task.ext.pulsation ) args += " --pulsation ${prefix}__pulsation.nii.gz" if ( task.ext.residual ) args += " --residual ${prefix}__residual.nii.gz" - """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 - scil_compute_dti_metrics.py $dwi $bval $bvec ${mask.join(" ")} --not_all $args -f --force_b0_threshold + scil_dwi_extract_shell.py $dwi $bval $bvec $dti_shells \ + dwi_dti_shells.nii.gz bval_dti_shells bvec_dti_shells \ + $dwi_shell_tolerance -f + + scil_dti_metrics.py dwi_dti_shells.nii.gz bval_dti_shells bvec_dti_shells \ + --not_all $args $b0_threshold -f + + if [ "$run_qc" = true ] && [ "$args" != '' ]; + then + mv ${prefix}__residual_residuals_stats.png ${prefix}__residual_residuals_stats.png_bk + + nii_files=\$(echo "$args" | awk '{for(i=1; i versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list --disable-pip-version-check --no-python-version-warning | grep scilpy | tr -s ' ' | cut -d' ' -f2) + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') + imagemagick: \$(convert -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') END_VERSIONS """ stub: - def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" """ - scil_compute_dti_metrics.py -h + scil_dwi_extract_shell.py -h + scil_dti_metrics.py -h touch ${prefix}__ad.nii.gz touch ${prefix}__evecs.nii.gz @@ -103,6 +164,7 @@ process RECONST_DTIMETRICS { touch ${prefix}__tensor.nii.gz touch ${prefix}__nonphysical.nii.gz touch ${prefix}__pulsation_std_dwi.nii.gz + touch ${prefix}__pulsation_std_b0.nii.gz touch ${prefix}__residual.nii.gz touch ${prefix}__residual_iqr_residuals.npy touch ${prefix}__residual_mean_residuals.npy @@ -110,10 +172,12 @@ process RECONST_DTIMETRICS { touch ${prefix}__residual_q3_residuals.npy touch ${prefix}__residual_residuals_stats.png touch ${prefix}__residual_std_residuals.npy + touch ${prefix}__dti_mqc.png cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list --disable-pip-version-check --no-python-version-warning | grep scilpy | tr -s ' ' | cut -d' ' -f2) + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') END_VERSIONS """ } diff --git a/modules/nf-scil/reconst/dtimetrics/meta.yml b/modules/nf-neuro/reconst/dtimetrics/meta.yml old mode 100755 new mode 100644 similarity index 65% rename from modules/nf-scil/reconst/dtimetrics/meta.yml rename to modules/nf-neuro/reconst/dtimetrics/meta.yml index 75ba395..da919a6 --- a/modules/nf-scil/reconst/dtimetrics/meta.yml +++ b/modules/nf-neuro/reconst/dtimetrics/meta.yml @@ -1,5 +1,3 @@ ---- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json name: "reconst_dtimetrics" description: Script to compute all of the Diffusion Tensor Imaging (DTI) metrics @@ -9,7 +7,7 @@ keywords: - tensor - scilpy tools: - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" @@ -40,147 +38,210 @@ input: description: Nifti b0 volume file used to mask the input image. pattern: "*.{nii,nii.gz}" +args: + - dwi_shell_tolerance: + type: int + description: "Tolerance for the shell detection algorithm." + default: 20 + - max_dti_shell_value: + type: int + description: "Maximum value for the DTI shell." + default: 1500 + - b0_thr_extract_b0: + type: int + description: "Threshold for the b0 extraction." + default: 0 + - b0_threshold: + type: int + description: "Threshold for the b0 mask." + default: 0 + - dti_shells: + type: string + description: "List of b-values to use for the DTI computation." + default: "0 1000" + - b0mask: + type: boolean + description: "Use the b0 mask to mask the input image." + default: true + - ad: + type: boolean + description: "Compute the axial diffusivity." + default: true + - evecs: + type: boolean + description: "Compute the eigenvectors of the tensor." + default: true + - evals: + type: boolean + description: "Compute the eigenvalues of the tensor." + default: true + - fa: + type: boolean + description: "Compute the fractional anisotropy." + default: true + - ga: + type: boolean + description: "Compute the geodesic anisotropy." + default: true + - rgb: + type: boolean + description: "Compute the colored fractional anisotropy." + default: true + - md: + type: boolean + description: "Compute the mean diffusivity." + default: true + - mode: + type: boolean + description: "Compute the mode." + default: true + - norm: + type: boolean + description: "Compute the tensor norm." + default: true + - rd: + type: boolean + description: "Compute the radial diffusivity." + default: true + - tensor: + type: boolean + description: "Compute the tensor coefficients." + default: true + - nonphysical: + type: boolean + description: "Compute the nonphysical voxels." + default: true + - pulsation: + type: boolean + description: "Compute the pulsation." + default: true + - residual: + type: boolean + description: "Compute the residual." + default: true + output: - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - ad: type: file description: Output filename for the axial diffusivity. pattern: "*__ad.{nii,nii.gz}" - - evecs: type: file description: Output filename for the eigenvectors of the tensor. pattern: "*__evecs.{nii,nii.gz}" - - evecs_v1: type: file description: Output filename for the first eigenvector. pattern: "*__evecs_v1.{nii,nii.gz}" - - evecs_v2: type: file description: Output filename for the second eigenvector. pattern: "*__evecs_v2.{nii,nii.gz}" - - - evec_v3: + - evecs_v3: type: file description: Output filename for the third eigenvector. pattern: "*__evecs_v3.{nii,nii.gz}" - - evals: type: file description: Output filename for the eigenvalues of the tensor. pattern: "*__evals.{nii,nii.gz}" - - evals_e1: type: file description: Output filename for the first eigenvalue. pattern: "*__evals_e1.{nii,nii.gz}" - - evals_e2: type: file description: Output filename for the second eigenvalue. pattern: "*__evals_e2.{nii,nii.gz}" - - evals_e3: type: file description: Output filename for the third eigenvalue. pattern: "*__evals_e3.{nii,nii.gz}" - - fa: type: file description: Output filename for the fractional anisotropy. pattern: "*__fa.{nii,nii.gz}" - - ga: type: file description: Output filename for the geodesic anisotropy. pattern: "*__ga.{nii,nii.gz}" - - rgb: type: file description: Output filename for the colored fractional anisotropy. pattern: "*__rgb.{nii,nii.gz}" - - md: type: file description: Output filename for the mean diffusivity. pattern: "*__md.{nii,nii.gz}" - - mode: type: file description: Output filename for the mode. pattern: "*__mode.{nii,nii.gz}" - - norm: type: file description: Output filename for the tensor norm. pattern: "*__norm.{nii,nii.gz}" - - rd: type: file description: Output filename for the radial diffusivity. pattern: "*__rd.{nii,nii.gz}" - - tensor: type: file description: Output filename for the tensor coefficients. pattern: "*__tensor.{nii,nii.gz}" - - nonphysical: type: file description: Output filename for the voxels with physically implausible signals where the mean of b=0 images is below one or more diffusion-weighted images. pattern: "*__nonphysical.{nii,nii.gz}" - - pulsation_std_dwi: type: file - description: Standard deviation map across all diffusion-weighted images - and across b=0 images if more than one is available.Shows - pulsation and misalignment artifacts. + description: Standard deviation map across all diffusion-weighted images. + Shows pulsation and misalignment artifacts. pattern: "*__pulsation_std_dwi.{nii,nii.gz}" - + - pulsation_std_b0: + type: file + description: Standard deviation map across b=0 images if more than one + is available. Shows pulsation and misalignment artifacts. + pattern: "*__pulsation_std_b0.{nii,nii.gz}" - residual: type: file description: Output filename for the map of the residual of the tensor fit. pattern: "*__residual.{nii,nii.gz}" - - residual_iqr_residuals: type: file description: Output filename for the interquartile range of the residual of the tensor fit. pattern: "*__residual_iqr_residuals.npy" - - residual_mean_residuals: type: file description: Output filename for the mean of the residual of the tensor fit. pattern: "*__residual_mean_residuals.npy" - - residual_q1_residuals: type: file description: Output filename for the firt quartile of the residual of the tensor fit. pattern: "*__residual_q1_residuals.npy" - - residual_q3_residuals: type: file description: Output filename for the third quartile of the residual of the tensor fit. pattern: "*__residual_q3_residuals.npy" - - residual_residuals_stats: type: file description: Output filename for the all statistics of the residual of the tensor fit. pattern: "*__residual_residuals_stats.png" - - residual_std_residuals: type: file description: Output filename for the standard deviation of the residual of the tensor fit. pattern: "*__residual_std_residuals.npy" - + - mqc: + type: file + pattern: "*__dti_mqc.png" + description: .png file containing screenshots of some DTI metrics. Made for use in MultiQC report. - versions: type: file description: File containing software versions diff --git a/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test b/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test new file mode 100644 index 0000000..ff88cee --- /dev/null +++ b/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test @@ -0,0 +1,156 @@ +nextflow_process { + + name "Test Process RECONST_DTIMETRICS" + script "../main.nf" + process "RECONST_DTIMETRICS" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "reconst" + tag "reconst/dtimetrics" + + 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( [ "DWIss1000-dir32.zip", "segmentation.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + 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 { + 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(process.out.versions).match() } + ) + } + } +} diff --git a/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test.snap b/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test.snap new file mode 100644 index 0000000..6064273 --- /dev/null +++ b/modules/nf-neuro/reconst/dtimetrics/tests/main.nf.test.snap @@ -0,0 +1,109 @@ +{ + "reconst - stub-run dtimetrics": { + "content": [ + [ + "versions.yml:md5,c7a6b86e604d3b25340611cec27c758a" + ] + ], + "meta": { + "nf-test": "0.9.0", + "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" + } +} diff --git a/modules/nf-neuro/reconst/dtimetrics/tests/nextflow.config b/modules/nf-neuro/reconst/dtimetrics/tests/nextflow.config new file mode 100644 index 0000000..c7dd643 --- /dev/null +++ b/modules/nf-neuro/reconst/dtimetrics/tests/nextflow.config @@ -0,0 +1,22 @@ +process { + withName: "RECONST_DTIMETRICS" { + ext.ad = true + ext.evecs = true + ext.evals = true + ext.fa = true + ext.ga = true + ext.rgb = true + ext.md = true + ext.mode = true + ext.norm = true + ext.rd = true + ext.tensor = true + ext.nonphysical = true + ext.pulsation = true + ext.residual = true + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.max_dti_shell_value = 1200 + ext.run_qc = true + } +} 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..af5f007 --- /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 = false + } +} diff --git a/modules/nf-neuro/reconst/dtimetrics/tests/tags.yml b/modules/nf-neuro/reconst/dtimetrics/tests/tags.yml new file mode 100644 index 0000000..6510e1d --- /dev/null +++ b/modules/nf-neuro/reconst/dtimetrics/tests/tags.yml @@ -0,0 +1,2 @@ +reconst/dtimetrics: + - "modules/nf-neuro/reconst/dtimetrics/**" diff --git a/modules/nf-neuro/reconst/fodf/environment.yml b/modules/nf-neuro/reconst/fodf/environment.yml new file mode 100644 index 0000000..1463549 --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: reconst_fodf diff --git a/modules/nf-neuro/reconst/fodf/main.nf b/modules/nf-neuro/reconst/fodf/main.nf new file mode 100644 index 0000000..98b1989 --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/main.nf @@ -0,0 +1,165 @@ + +process RECONST_FODF { + 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(dwi), path(bval), path(bvec), path(mask), path(fa), path(md), path(wm_frf), path(gm_frf), path(csf_frf) + + output: + tuple val(meta), path("*__fodf.nii.gz") , emit: fodf, optional: true + tuple val(meta), path("*__wm_fodf.nii.gz") , emit: wm_fodf, optional: true + tuple val(meta), path("*__gm_fodf.nii.gz") , emit: gm_fodf, optional: true + tuple val(meta), path("*__csf_fodf.nii.gz") , emit: csf_fodf, optional: true + tuple val(meta), path("*__vf.nii.gz") , emit: vf, optional: true + tuple val(meta), path("*__vf_rgb.nii.gz") , emit: vf_rgb, optional: true + tuple val(meta), path("*__peaks.nii.gz") , emit: peaks, optional: true + tuple val(meta), path("*__peak_values.nii.gz") , emit: peak_values, optional: true + tuple val(meta), path("*__peak_indices.nii.gz") , emit: peak_indices, optional: true + tuple val(meta), path("*__afd_max.nii.gz") , emit: afd_max, optional: true + tuple val(meta), path("*__afd_total.nii.gz") , emit: afd_total, optional: true + tuple val(meta), path("*__afd_sum.nii.gz") , emit: afd_sum, optional: true + tuple val(meta), path("*__nufo.nii.gz") , emit: nufo, optional: true + tuple val(meta), path("*__ventricles_mask.nii.gz"), emit: vent_mask, optional: true + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + + def dwi_shell_tolerance = task.ext.dwi_shell_tolerance ? "--tolerance " + task.ext.dwi_shell_tolerance : "" + def min_fodf_shell_value = task.ext.min_fodf_shell_value ?: 100 /* Default value for min_fodf_shell_value */ + def b0_thr_extract_b0 = task.ext.b0_thr_extract_b0 ?: 10 /* Default value for b0_thr_extract_b0 */ + def fodf_shells = task.ext.fodf_shells ? "0 " + task.ext.fodf_shells : "\$(cut -d ' ' --output-delimiter=\$'\\n' -f 1- $bval | awk -F' ' '{v=int(\$1)}{if(v>=$min_fodf_shell_value|| v<=$b0_thr_extract_b0)print v}' | sort | uniq)" + def sh_order = task.ext.sh_order ? "--sh_order " + task.ext.sh_order : "" + def sh_basis = task.ext.sh_basis ? "--sh_basis " + task.ext.sh_basis : "" + def set_method = task.ext.method ? task.ext.method : "ssst" + def processes = task.cpus > 1 ? "--processes " + task.cpus : "" + def set_mask = mask ? "--mask $mask" : "" + def relative_threshold = task.ext.relative_threshold ? "--rt " + task.ext.relative_threshold : "" + def fodf_metrics_a_factor = task.ext.fodf_metrics_a_factor ? task.ext.fodf_metrics_a_factor : 2.0 + def fa_threshold = task.ext.fa_threshold ? "--fa_t " + task.ext.fa_threshold : "" + def md_threshold = task.ext.md_threshold ? "--md_t " + task.ext.md_threshold : "" + def absolute_peaks = task.ext.absolute_peaks ? "--abs_peaks_and_values" : "" + + /* if (set_method != "ssst_fodf" || set_method != "msmt_fodf") error "ERROR";*/ + if ( task.ext.wm_fodf ) wm_fodf = "--wm_out_fODF ${prefix}__wm_fodf.nii.gz" else wm_fodf = "" + if ( task.ext.gm_fodf ) gm_fodf = "--gm_out_fODF ${prefix}__gm_fodf.nii.gz" else gm_fodf = "" + if ( task.ext.csf_fodf ) csf_fodf = "--csf_out_fODF ${prefix}__csf_fodf.nii.gz" else csf_fodf = "" + if ( task.ext.vf) vf = "--vf ${prefix}__vf.nii.gz" else vf = "" + if ( task.ext.vf_rgb) vf_rgb = "--vf_rgb ${prefix}__vf_rgb.nii.gz" else vf_rgb = "" + + if ( task.ext.peaks ) peaks = "--peaks ${prefix}__peaks.nii.gz" else peaks = "" + if ( task.ext.peak_values ) peak_values = "--peak_values ${prefix}__peak_values.nii.gz" else peak_values = "" + if ( task.ext.peak_indices ) peak_indices = "--peak_indices ${prefix}__peak_indices.nii.gz" else peak_indices = "" + if ( task.ext.afd_max ) afd_max = "--afd_max ${prefix}__afd_max.nii.gz" else afd_max = "" + if ( task.ext.afd_total ) afd_total = "--afd_total ${prefix}__afd_total.nii.gz" else afd_total = "" + if ( task.ext.afd_sum ) afd_sum = "--afd_sum ${prefix}__afd_sum.nii.gz" else afd_sum = "" + if ( task.ext.nufo ) nufo = "--nufo ${prefix}__nufo.nii.gz" else nufo = "" + if ( task.ext.ventricles_mask ) vent_mask = "--mask_output ${prefix}__ventricles_mask.nii.gz" else vent_mask = "" + + def run_fodf_metrics = [ + task.ext.peaks, task.ext.peak_values, task.ext.peak_indices, task.ext.afd_max, + task.ext.afd_sum, task.ext.afd_total, task.ext.nufo + ].any() + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + scil_dwi_extract_shell.py $dwi $bval $bvec $fodf_shells \ + dwi_fodf_shells.nii.gz bval_fodf_shells bvec_fodf_shells \ + $dwi_shell_tolerance -f + + if [ "$set_method" = "ssst" ] + then + + scil_fodf_ssst.py dwi_fodf_shells.nii.gz bval_fodf_shells bvec_fodf_shells $wm_frf ${prefix}__fodf.nii.gz \ + $sh_order $sh_basis --b0_threshold $b0_thr_extract_b0 \ + $set_mask $processes + + elif [ "$set_method" = "msmt" ] + then + + scil_fodf_msmt.py dwi_fodf_shells.nii.gz bval_fodf_shells bvec_fodf_shells \ + $wm_frf $gm_frf $csf_frf \ + $sh_order $sh_basis $set_mask $processes $dwi_shell_tolerance \ + --not_all $wm_fodf $gm_fodf $csf_fodf $vf $vf_rgb + + cp ${prefix}__wm_fodf.nii.gz ${prefix}__fodf.nii.gz + + fi + + if $run_fodf_metrics + then + + scil_fodf_max_in_ventricles.py ${prefix}__fodf.nii.gz $fa $md \ + --max_value_output ventricles_fodf_max_value.txt $sh_basis \ + $fa_threshold $md_threshold $vent_mask -f + + echo "Maximal peak value in ventricle in file : \$(cat ventricles_fodf_max_value.txt)" + + a_factor=$fodf_metrics_a_factor + v_max=\$(sed -E 's/([+-]?[0-9.]+)[eE]\\+?(-?)([0-9]+)/(\\1*10^\\2\\3)/g' <<<"\$(cat ventricles_fodf_max_value.txt)") + + echo "Maximal peak value in ventricles : \${v_max}" + + a_threshold=\$(echo "scale=10; \${a_factor} * \${v_max}" | bc) + if (( \$(echo "\${a_threshold} <= 0" | bc -l) )); then + a_threshold=1E-10 + fi + + echo "Computing fodf metrics with absolute threshold : \${a_threshold}" + + scil_fodf_metrics.py ${prefix}__fodf.nii.gz \ + $set_mask $sh_basis $absolute_peaks \ + $peaks $peak_values $peak_indices \ + $afd_max $afd_total \ + $afd_sum $nufo $processes \ + $relative_threshold --not_all --at \${a_threshold} + fi + + 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_dwi_extract_shell.py -h + scil_fodf_ssst.py -h + scil_fodf_msmt.py -h + scil_fodf_max_in_ventricles.py -h + scil_fodf_metrics.py -h + + touch ${prefix}__fodf.nii.gz + touch ${prefix}__wm_fodf.nii.gz + touch ${prefix}__gm_fodf.nii.gz + touch ${prefix}__csf_fodf.nii.gz + touch ${prefix}__vf.nii.gz + touch ${prefix}__vf_rgb.nii.gz + touch ${prefix}__peaks.nii.gz + touch ${prefix}__peak_values.nii.gz + touch ${prefix}__peak_indices.nii.gz + touch ${prefix}__afd_max.nii.gz + touch ${prefix}__afd_total.nii.gz + touch ${prefix}__afd_sum.nii.gz + touch ${prefix}__nufo.nii.gz + touch ${prefix}__ventricles_mask.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/reconst/fodf/meta.yml b/modules/nf-neuro/reconst/fodf/meta.yml new file mode 100644 index 0000000..ab3c5d7 --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/meta.yml @@ -0,0 +1,161 @@ +--- +name: "reconst_fodf" +description: Perform FODF reconstruction and compute FODF metrics from a dwi volume for a selected number of shells. + Note that multiple choices of FODF reconstruction method are available through the method argument. + The single-shell single-tissue (ssst) method is performed by selecting only one shell (task.ext.fodf_shells) + and choosing the ssst method (task.ext.method). + The multi-shell single-tissue (msst) method (default) is performed by selecting multiple shells (task.ext.fodf_shells) + and choosing the ssst method (task.ext.method). Both ssst and msst are expected to output only WM FODF. + The multi-shell multi-tissue (msmt) method is performed by selecting multiple shells (task.ext.fodf_shells) + and choosing the msmt method (task.ext.method). The msmt method is expected to output WM FODF, GM FODF and CSF FODF, + along with volume fraction (vf) maps. In every case, chosen FODF metrics are outputed. +keywords: + - FODF + - Local Model + - Reconst +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 ]` + + - dwi: + type: file + description: Nifti DWI volume to reconstruct FODF from. + pattern: "*.{nii,nii.gz}" + + - bval: + type: file + description: B-values in FSL format. + pattern: "*.bval" + + - bvec: + type: file + description: B-vectors in FSL format. + pattern: "*.bvec" + + - mask: + type: file + description: B0 mask. + pattern: "*.{nii,nii.gz}" + + - fa: + type: file + description: FA map. + pattern: "*.{nii,nii.gz}" + + - md: + type: file + description: MD map. + pattern: "*.{nii,nii.gz}" + + - wm_frf: + type: file + description: Fiber Response Function (FRF) for the white matter (WM). + In the case of single-shell method, this should be the only input FRF. + In the case of multi-shell method, gm_frf and csf_frf are also mandatory. + pattern: "*.txt" + + - gm_frf: + type: file + description: Fiber Response Function (FRF) for the grey matter (GM). + Only use with multi-shell method, along with wm_frf and csf_frf. + pattern: "*.txt" + + - csf_frf: + type: file + description: Fiber Response Function (FRF) for the CSF. + Only use with multi-shell method, along with wm_frf and gm_frf. + pattern: "*.txt" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - fodf: + type: file + description: FODF map. + pattern: "*fodf.nii.gz" + + - wm_fodf: + type: file + description: WM FODF map. + pattern: "*wm_fodf.nii.gz" + + - gm_fodf: + type: file + description: GM FODF map. + pattern: "*gm_fodf.nii.gz" + + - csf_fodf: + type: file + description: CSF FODF map. + pattern: "*csf_fodf.nii.gz" + + - vf: + type: file + description: Volume fraction map. + pattern: "*vf.nii.gz" + + - vf_rgb: + type: file + description: Volume fraction map in rgb. + pattern: "*vf_rgb.nii.gz" + + - peaks: + type: file + description: Peaks file. + pattern: "*peaks.nii.gz" + + - peak_values: + type: file + description: Peak values file. + pattern: "*peak_values.nii.gz" + + - peak_indices: + type: file + description: Peak indices file. + pattern: "*peak_indices.nii.gz" + + - afd_max: + type: file + description: Maximum Apparent Fiber Density (AFDmax) map. + pattern: "*afd_max.nii.gz" + + - afd_total: + type: file + description: Total Apparent Fiber Density (AFDtotal) map. + pattern: "*afd_total.nii.gz" + + - afd_sum: + type: file + description: Sum of all Apparent Fiber Density (Afdsum) map. + pattern: "*afd_sum.nii.gz" + + - nufo: + type: file + description: Number of Fiber Orientation (NuFO) map. + pattern: "*nufo.nii.gz" + + - vent_mask: + type: file + description: Ventricule mask estimated from an MD and FA threshold. + pattern: "*ventricles_mask.nii.gz" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@gagnonanthony" + - "@karanphil" diff --git a/modules/nf-neuro/reconst/fodf/tests/main.nf.test b/modules/nf-neuro/reconst/fodf/tests/main.nf.test new file mode 100644 index 0000000..a86fef5 --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/tests/main.nf.test @@ -0,0 +1,304 @@ +nextflow_process { + + name "Test Process RECONST_FODF" + script "../main.nf" + process "RECONST_FODF" + + tag "modules" + tag "modules_nfcore" + tag "reconst" + tag "reconst/fodf" + + 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( [ + "DWIss2000-dir60.zip", + "DWIms1000-2000-dir10-10.zip", + "segmentation.zip", + "dti.zip", + "responses.zip" + ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("reconst - fodf") { + + config "./nextflow.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + ssst: it.simpleName == "DWIss2000-dir60" + segmentation: it.simpleName == "segmentation" + dti: it.simpleName == "dti" + responses: it.simpleName == "responses" + } + ch_dwi = ch_split_test_data.ssst.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") + ] + } + ch_metrics = ch_split_test_data.dti.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/fa.nii.gz"), + file("\${test_data_directory}/md.nii.gz") + ] + } + ch_response = ch_split_test_data.responses.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/DWIss1000-dir32.txt") + ] + } + input[0] = ch_dwi + .join(ch_mask) + .join(ch_metrics) + .join(ch_response) + .map{ it + [[], []] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + niftiMD5SUM(process.out.fodf.get(0).get(1), 6), + file(process.out.peaks.get(0).get(1)).name, + niftiMD5SUM(process.out.peak_values.get(0).get(1), 6), + file(process.out.peak_indices.get(0).get(1)).name, + process.out.afd_max, + process.out.afd_total, + niftiMD5SUM(process.out.afd_sum.get(0).get(1), 6), + process.out.nufo, + process.out.versions).match()} + ) + } + + } + + test("reconst - fodf_with_shells") { + + config "./nextflow_shells.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + msmt: it.simpleName == "DWIms1000-2000-dir10-10" + segmentation: it.simpleName == "segmentation" + dti: it.simpleName == "dti" + responses: it.simpleName == "responses" + } + ch_dwi = ch_split_test_data.msmt.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") + ] + } + ch_metrics = ch_split_test_data.dti.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/fa.nii.gz") , + file("\${test_data_directory}/md.nii.gz") + ] + } + ch_response = ch_split_test_data.responses.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/DWIss1000-dir32.txt") + ] + } + input[0] = ch_dwi + .join(ch_mask) + .join(ch_metrics) + .join(ch_response) + .map{ it + [[], []] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + niftiMD5SUM(process.out.fodf.get(0).get(1), 6), + file(process.out.peaks.get(0).get(1)).name, + niftiMD5SUM(process.out.peak_values.get(0).get(1), 6), + file(process.out.peak_indices.get(0).get(1)).name, + process.out.afd_max, + process.out.afd_total, + niftiMD5SUM(process.out.afd_sum.get(0).get(1), 6), + process.out.nufo, + process.out.versions).match()} + ) + } + + } + + test("reconst - fodf_no_metrics") { + + config "./nextflow_no_metrics.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + ssst: it.simpleName == "DWIss2000-dir60" + segmentation: it.simpleName == "segmentation" + dti: it.simpleName == "dti" + responses: it.simpleName == "responses" + } + ch_dwi = ch_split_test_data.ssst.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") + ] + } + ch_metrics = ch_split_test_data.dti.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/fa.nii.gz"), + file("\${test_data_directory}/md.nii.gz") + ] + } + ch_response = ch_split_test_data.responses.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/DWIss1000-dir32.txt") + ] + } + input[0] = ch_dwi + .join(ch_mask) + .join(ch_metrics) + .join(ch_response) + .map{ it + [[], []] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + niftiMD5SUM(process.out.fodf.get(0).get(1), 6), + process.out.versions).match()} + ) + } + + } + + test("reconst - fodf_msmt") { + + config "./nextflow_msmt.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + msmt: it.simpleName == "DWIms1000-2000-dir10-10" + segmentation: it.simpleName == "segmentation" + dti: it.simpleName == "dti" + responses: it.simpleName == "responses" + } + ch_dwi = ch_split_test_data.msmt.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") + ] + } + ch_metrics = ch_split_test_data.dti.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/fa.nii.gz"), + file("\${test_data_directory}/md.nii.gz") + ] + } + ch_response = ch_split_test_data.responses.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/DWIms1000-2000-dir32-60_wm.txt"), + file("\${test_data_directory}/DWIms1000-2000-dir32-60_gm.txt"), + file("\${test_data_directory}/DWIms1000-2000-dir32-60_csf.txt") + ] + } + input[0] = ch_dwi + .join(ch_mask) + .join(ch_metrics) + .join(ch_response) + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.fodf.get(0).get(1)).name, + file(process.out.wm_fodf.get(0).get(1)).name, + file(process.out.gm_fodf.get(0).get(1)).name, + file(process.out.csf_fodf.get(0).get(1)).name, + file(process.out.vf.get(0).get(1)).name, + file(process.out.vf_rgb.get(0).get(1)).name, + file(process.out.peaks.get(0).get(1)).name, + file(process.out.peak_values.get(0).get(1)).name, + file(process.out.peak_indices.get(0).get(1)).name, + file(process.out.afd_max.get(0).get(1)).name, + file(process.out.afd_total.get(0).get(1)).name, + file(process.out.afd_sum.get(0).get(1)).name, + file(process.out.nufo.get(0).get(1)).name, + process.out.vent_mask, + process.out.versions).match()} + ) + } + + } + +} diff --git a/modules/nf-neuro/reconst/fodf/tests/main.nf.test.snap b/modules/nf-neuro/reconst/fodf/tests/main.nf.test.snap new file mode 100644 index 0000000..faf49a4 --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/tests/main.nf.test.snap @@ -0,0 +1,130 @@ +{ + "reconst - fodf_msmt": { + "content": [ + "test__fodf.nii.gz", + "test__wm_fodf.nii.gz", + "test__gm_fodf.nii.gz", + "test__csf_fodf.nii.gz", + "test__vf.nii.gz", + "test__vf_rgb.nii.gz", + "test__peaks.nii.gz", + "test__peak_values.nii.gz", + "test__peak_indices.nii.gz", + "test__afd_max.nii.gz", + "test__afd_total.nii.gz", + "test__afd_sum.nii.gz", + "test__nufo.nii.gz", + [ + [ + { + "id": "test" + }, + "test__ventricles_mask.nii.gz:md5,848409863defb87ed4e29e8e50e5dbc8" + ] + ], + [ + "versions.yml:md5,f80e57f2c24a4a1fd403de1d132ec6e7" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-11-12T20:01:53.02243736" + }, + "reconst - fodf": { + "content": [ + "test__fodf.nii.gz:md5:header,f3c67b648a14b8d66f776cb1a9eb4a59,data,0a96b14bb47bb45c37f59263fe2afb0e", + "test__peaks.nii.gz", + "test__peak_values.nii.gz:md5:header,f55799c7ffe93b1148a313f0aade0f5f,data,7e233cedc0ee283661827bdd74f0487a", + "test__peak_indices.nii.gz", + [ + [ + { + "id": "test" + }, + "test__afd_max.nii.gz:md5,c3852145d531399374e961bd1c0ee3f6" + ] + ], + [ + [ + { + "id": "test" + }, + "test__afd_total.nii.gz:md5,bbdaf17c5c2fa76991c72ed59ca6961b" + ] + ], + "test__afd_sum.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,43434d4570ffb0582e00405a49015929", + [ + [ + { + "id": "test" + }, + "test__nufo.nii.gz:md5,830e269e3c49d0c58f8556bab264c249" + ] + ], + [ + "versions.yml:md5,f80e57f2c24a4a1fd403de1d132ec6e7" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-11-18T16:12:56.211434796" + }, + "reconst - fodf_no_metrics": { + "content": [ + "test__fodf.nii.gz:md5:header,f3c67b648a14b8d66f776cb1a9eb4a59,data,0a96b14bb47bb45c37f59263fe2afb0e", + [ + "versions.yml:md5,f80e57f2c24a4a1fd403de1d132ec6e7" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-11-18T16:14:51.395786266" + }, + "reconst - fodf_with_shells": { + "content": [ + "test__fodf.nii.gz:md5:header,f3c67b648a14b8d66f776cb1a9eb4a59,data,4bb09d0b3bb0acad56d82083404ff353", + "test__peaks.nii.gz", + "test__peak_values.nii.gz:md5:header,f55799c7ffe93b1148a313f0aade0f5f,data,45554369edd640abd2a92b55324c4087", + "test__peak_indices.nii.gz", + [ + [ + { + "id": "test" + }, + "test__afd_max.nii.gz:md5,9afadd7cdfc05792303d39ec51c5970b" + ] + ], + [ + [ + { + "id": "test" + }, + "test__afd_total.nii.gz:md5,6a4602c474ac3fb59af4003bb32c66d9" + ] + ], + "test__afd_sum.nii.gz:md5:header,c9e720b9a8acfe45a617e6486ebb8de0,data,3ae12eebe0c67c89b3f63653691544e8", + [ + [ + { + "id": "test" + }, + "test__nufo.nii.gz:md5,dc98763318fbbe2368a4da919829ea3b" + ] + ], + [ + "versions.yml:md5,f80e57f2c24a4a1fd403de1d132ec6e7" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-11-18T20:38:22.999203708" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/reconst/fodf/tests/nextflow.config b/modules/nf-neuro/reconst/fodf/tests/nextflow.config new file mode 100644 index 0000000..137cd98 --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/tests/nextflow.config @@ -0,0 +1,20 @@ +process { + withName: "RECONST_FODF" { + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.min_fodf_shell_value = 700 + ext.sh_order = 2 + ext.sh_basis = "descoteaux07" + ext.fa_threshold = 0.1 + ext.md_threshold = 0.003 + ext.relative_threshold = 0.1 + ext.absolute_peaks = true + ext.peaks = true + ext.peak_values = true + ext.peak_indices = true + ext.afd_max = true + ext.afd_total = true + ext.afd_sum = true + ext.nufo = true + } +} diff --git a/modules/nf-neuro/reconst/fodf/tests/nextflow_msmt.config b/modules/nf-neuro/reconst/fodf/tests/nextflow_msmt.config new file mode 100644 index 0000000..89bab1d --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/tests/nextflow_msmt.config @@ -0,0 +1,27 @@ +process { + withName: "RECONST_FODF" { + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.min_fodf_shell_value = 100 + ext.sh_order = 2 + ext.sh_basis = "descoteaux07" + ext.fa_threshold = 0.1 + ext.md_threshold = 0.003 + ext.relative_threshold = 0.1 + ext.absolute_peaks = true + ext.peaks = true + ext.peak_values = true + ext.peak_indices = true + ext.afd_max = true + ext.afd_total = true + ext.afd_sum = true + ext.nufo = true + ext.wm_fodf = true + ext.gm_fodf = true + ext.csf_fodf = true + ext.vf = true + ext.vf_rgb = true + ext.method = "msmt" + ext.ventricles_mask = true + } +} diff --git a/modules/nf-neuro/reconst/fodf/tests/nextflow_no_metrics.config b/modules/nf-neuro/reconst/fodf/tests/nextflow_no_metrics.config new file mode 100644 index 0000000..7f4893e --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/tests/nextflow_no_metrics.config @@ -0,0 +1,20 @@ +process { + withName: "RECONST_FODF" { + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.min_fodf_shell_value = 700 + ext.sh_order = 2 + ext.sh_basis = "descoteaux07" + ext.fa_threshold = 0.1 + ext.md_threshold = 0.003 + ext.relative_threshold = 0.1 + ext.absolute_peaks = true + ext.peaks = false + ext.peak_values = false + ext.peak_indices = false + ext.afd_max = false + ext.afd_total = false + ext.afd_sum = false + ext.nufo = false + } +} diff --git a/modules/nf-neuro/reconst/fodf/tests/nextflow_shells.config b/modules/nf-neuro/reconst/fodf/tests/nextflow_shells.config new file mode 100644 index 0000000..fe8b7f2 --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/tests/nextflow_shells.config @@ -0,0 +1,21 @@ +process { + withName: "RECONST_FODF" { + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.min_fodf_shell_value = 700 + ext.sh_order = 2 + ext.sh_basis = "descoteaux07" + ext.fa_threshold = 0.1 + ext.md_threshold = 0.003 + ext.relative_threshold = 0.1 + ext.absolute_peaks = true + ext.peaks = true + ext.peak_values = true + ext.peak_indices = true + ext.afd_max = true + ext.afd_total = true + ext.afd_sum = true + ext.nufo = true + ext.fodf_shells = "1000 2000" + } +} diff --git a/modules/nf-neuro/reconst/fodf/tests/tags.yml b/modules/nf-neuro/reconst/fodf/tests/tags.yml new file mode 100644 index 0000000..6030697 --- /dev/null +++ b/modules/nf-neuro/reconst/fodf/tests/tags.yml @@ -0,0 +1,2 @@ +reconst/fodf: + - "modules/nf-neuro/reconst/fodf/**" diff --git a/modules/nf-neuro/reconst/frf/environment.yml b/modules/nf-neuro/reconst/frf/environment.yml new file mode 100644 index 0000000..c8d6225 --- /dev/null +++ b/modules/nf-neuro/reconst/frf/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: reconst_frf diff --git a/modules/nf-neuro/reconst/frf/main.nf b/modules/nf-neuro/reconst/frf/main.nf new file mode 100644 index 0000000..28863aa --- /dev/null +++ b/modules/nf-neuro/reconst/frf/main.nf @@ -0,0 +1,124 @@ + + +process RECONST_FRF { + 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:dev' }" + + input: + tuple val(meta), path(dwi), path(bval), path(bvec), path(mask), path(wm_mask), path(gm_mask), path(csf_mask) + + output: + tuple val(meta), path("*__frf.txt") , emit: frf, optional: true + tuple val(meta), path("*__wm_frf.txt") , emit: wm_frf, optional: true + tuple val(meta), path("*__gm_frf.txt") , emit: gm_frf, optional: true + tuple val(meta), path("*__csf_frf.txt") , emit: csf_frf, optional: true + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + def fa = task.ext.fa ? "--fa " + task.ext.fa : "" + def fa_min = task.ext.fa_min ? "--min_fa " + task.ext.fa_min : "" + def nvox_min = task.ext.nvox_min ? "--min_nvox " + task.ext.nvox_min : "" + def roi_radius = task.ext.roi_radius ? "--roi_radii " + task.ext.roi_radius : "" + def dwi_shell_tolerance = task.ext.dwi_shell_tolerance ? "--tolerance " + task.ext.dwi_shell_tolerance : "" + def max_dti_shell_value = task.ext.max_dti_shell_value ?: 1500 + def min_fodf_shell_value = task.ext.min_fodf_shell_value ?: 100 + def b0_thr_extract_b0 = task.ext.b0_thr_extract_b0 ?: 10 + def dti_shells = task.ext.dti_shells ?: "\$(cut -d ' ' --output-delimiter=\$'\\n' -f 1- $bval | awk -F' ' '{v=int(\$1)}{if(v<=$max_dti_shell_value|| v<=$b0_thr_extract_b0)print v}' | sort | uniq)" + def fodf_shells = task.ext.fodf_shells ? "0 " + task.ext.fodf_shells : "\$(cut -d ' ' --output-delimiter=\$'\\n' -f 1- $bval | awk -F' ' '{v=int(\$1)}{if(v>=$min_fodf_shell_value|| v<=$b0_thr_extract_b0)print v}' | sort | uniq)" + def set_method = task.ext.method ? task.ext.method : "ssst" + def precision = task.ext.precision ? "--precision " + task.ext.precision : "" + + def fa_thr_wm = task.ext.fa_thr_wm ? "--fa_thr_wm " + task.ext.fa_thr_wm : "" + def fa_thr_gm = task.ext.fa_thr_gm ? "--fa_thr_gm " + task.ext.fa_thr_gm : "" + def fa_thr_csf = task.ext.fa_thr_csf ? "--fa_thr_csf " + task.ext.fa_thr_csf : "" + def md_thr_wm = task.ext.md_thr_wm ? "--md_thr_wm " + task.ext.md_thr_wm : "" + def md_thr_gm = task.ext.md_thr_gm ? "--md_thr_gm " + task.ext.md_thr_gm : "" + def md_thr_csf = task.ext.md_thr_csf ? "--md_thr_csf " + task.ext.md_thr_csf : "" + + def fix_frf = task.ext.manual_frf ? task.ext.manual_frf : "" + def fix_wm_frf = task.ext.manual_wm_frf ? task.ext.manual_wm_frf : "" + def fix_gm_frf = task.ext.manual_gm_frf ? task.ext.manual_gm_frf : "" + def fix_csf_frf = task.ext.manual_csf_frf ? task.ext.manual_csf_frf : "" + def set_mask = mask ? "--mask $mask" : "" + def set_wm_mask = wm_mask ? "--mask_wm $wm_mask" : "" + def set_gm_mask = gm_mask ? "--mask_gm $gm_mask" : "" + def set_csf_mask = csf_mask ? "--mask_csf $csf_mask" : "" + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + if [ "$set_method" = "ssst" ] + then + + scil_dwi_extract_shell.py $dwi $bval $bvec $dti_shells \ + dwi_dti_shells.nii.gz bval_dti_shells bvec_dti_shells \ + $dwi_shell_tolerance -f -v + + scil_frf_ssst.py dwi_dti_shells.nii.gz bval_dti_shells bvec_dti_shells ${prefix}__frf.txt \ + $set_mask $fa $fa_min $nvox_min $roi_radius --b0_threshold $b0_thr_extract_b0 $precision -v + + if ( "$task.ext.set_frf" = true ); then + scil_frf_set_diffusivities.py ${prefix}__frf.txt "${fix_frf}" \ + ${prefix}__frf.txt $precision -f -v + fi + + elif [ "$set_method" = "msmt" ] + then + + scil_dwi_extract_shell.py $dwi $bval $bvec $fodf_shells \ + dwi_fodf_shells.nii.gz bval_fodf_shells bvec_fodf_shells \ + $dwi_shell_tolerance -f -v + + scil_frf_msmt.py dwi_fodf_shells.nii.gz bval_fodf_shells bvec_fodf_shells \ + ${prefix}__wm_frf.txt ${prefix}__gm_frf.txt ${prefix}__csf_frf.txt \ + $set_mask $set_wm_mask $set_gm_mask $set_csf_mask $fa_thr_wm $fa_thr_gm \ + $fa_thr_csf $md_thr_wm $md_thr_gm $md_thr_csf $nvox_min $roi_radius \ + $dwi_shell_tolerance --dti_bval_limit $max_dti_shell_value $precision -v + + if ( "$task.ext.set_frf" = true ); then + scil_frf_set_diffusivities.py ${prefix}__wm_frf.txt "${fix_wm_frf}" \ + ${prefix}__wm_frf.txt $precision -f -v + scil_frf_set_diffusivities.py ${prefix}__gm_frf.txt "${fix_gm_frf}" \ + ${prefix}__gm_frf.txt $precision -f -v + scil_frf_set_diffusivities.py ${prefix}__csf_frf.txt "${fix_csf_frf}" \ + ${prefix}__csf_frf.txt $precision -f -v + fi + + fi + + 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_dwi_extract_shell.py -h + scil_frf_ssst.py -h + scil_frf_set_diffusivities.py -h + scil_frf_msmt.py -h + + touch ${prefix}__frf.txt + touch ${prefix}__wm_frf.txt + touch ${prefix}__gm_frf.txt + touch ${prefix}__csf_frf.txt + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ +} diff --git a/modules/nf-scil/reconst/frf/meta.yml b/modules/nf-neuro/reconst/frf/meta.yml old mode 100755 new mode 100644 similarity index 50% rename from modules/nf-scil/reconst/frf/meta.yml rename to modules/nf-neuro/reconst/frf/meta.yml index fb2f481..3a9d612 --- a/modules/nf-scil/reconst/frf/meta.yml +++ b/modules/nf-neuro/reconst/frf/meta.yml @@ -1,7 +1,12 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "reconst_frf" -description: Compute a single Fiber Response Function from a DWI. +description: Compute a Fiber Response Function (FRF) from a DWI. + Note that multiple types of FRF are available through the method argument. + The single-shell single-tissue (ssst) FRF is performed by choosing the ssst method (task.ext.method). + This is expected to output a single WM FRF. + The multi-shell multi-tissue (msmt) FRF is performed by choosing the msmt method (task.ext.method). + This is expected to output a WM FRF, a GM FRF and a CSF FRF, each containing values for every shells. keywords: - Fiber Orientation Distribution Function - Diffusion MRI @@ -42,6 +47,21 @@ input: description: Nifti b0 binary mask. pattern: "*.{nii,nii.gz}" + - wm_mask: + type: file + description: Nifti binary WM mask. + pattern: "*.{nii,nii.gz}" + + - gm_mask: + type: file + description: Nifti binary GM mask. + pattern: "*.{nii,nii.gz}" + + - csf_mask: + type: file + description: Nifti binary CSF mask. + pattern: "*.{nii,nii.gz}" + output: - meta: type: map @@ -51,7 +71,22 @@ output: - frf: type: file - description: Fiber Response Function (FRF). + description: Fiber Response Function (FRF), in the case of ssst_frf. + pattern: "*.txt" + + - wm_frf: + type: file + description: WM Fiber Response Function (FRF), in the case of msmt_frf. + pattern: "*.txt" + + - gm_frf: + type: file + description: GM Fiber Response Function (FRF), in the case of msmt_frf. + pattern: "*.txt" + + - csf_frf: + type: file + description: CSF Fiber Response Function (FRF), in the case of msmt_frf. pattern: "*.txt" - versions: @@ -61,3 +96,4 @@ output: authors: - "@Manonedde" + - "@karanphil" diff --git a/modules/nf-neuro/reconst/frf/tests/main.nf.test b/modules/nf-neuro/reconst/frf/tests/main.nf.test new file mode 100644 index 0000000..9a81131 --- /dev/null +++ b/modules/nf-neuro/reconst/frf/tests/main.nf.test @@ -0,0 +1,249 @@ +nextflow_process { + + name "Test Process RECONST_FRF" + script "../main.nf" + process "RECONST_FRF" + + tag "modules" + tag "modules_nfcore" + tag "reconst" + tag "reconst/frf" + + 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" , "commit_amico.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("reconst - frf") { + + config "./nextflow.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + ssst: it.simpleName == "heavy" + msmt: it.simpleName == "commit_amico" + } + input[0] = ch_split_test_data.ssst.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/dwi/dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bval", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/dwi/mask.nii.gz", checkIfExists: true), + [], + [], + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("reconst - frf_no_mask") { + + config "./nextflow.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + ssst: it.simpleName == "heavy" + msmt: it.simpleName == "commit_amico" + } + input[0] = ch_split_test_data.ssst.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/dwi/dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bval", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bvec", checkIfExists: true), + [], + [], + [], + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("reconst - frf_set_frf") { + + config "./nextflow_set_frf.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + ssst: it.simpleName == "heavy" + msmt: it.simpleName == "commit_amico" + } + input[0] = ch_split_test_data.ssst.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/dwi/dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bval", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/dwi/mask.nii.gz", checkIfExists: true), + [], + [], + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("reconst - frf_set_frf_no_mask") { + + config "./nextflow_set_frf.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + ssst: it.simpleName == "heavy" + msmt: it.simpleName == "commit_amico" + } + input[0] = ch_split_test_data.ssst.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/dwi/dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bval", checkIfExists: true), + file("\${test_data_directory}/dwi/dwi.bvec", checkIfExists: true), + [], + [], + [], + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("reconst - frf_msmt") { + + config "./nextflow_msmt.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + ssst: it.simpleName == "heavy" + msmt: it.simpleName == "commit_amico" + } + input[0] = ch_split_test_data.msmt.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi.bval", checkIfExists: true), + file("\${test_data_directory}/dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/mask.nii.gz", checkIfExists: true), + [], + [], + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("reconst - frf_msmt_set_frf") { + + config "./nextflow_msmt_set_frf.config" + + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + ssst: it.simpleName == "heavy" + msmt: it.simpleName == "commit_amico" + } + input[0] = ch_split_test_data.msmt.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/dwi.bval", checkIfExists: true), + file("\${test_data_directory}/dwi.bvec", checkIfExists: true), + file("\${test_data_directory}/mask.nii.gz", checkIfExists: true), + [], + [], + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-neuro/reconst/frf/tests/main.nf.test.snap b/modules/nf-neuro/reconst/frf/tests/main.nf.test.snap new file mode 100644 index 0000000..f4e75f7 --- /dev/null +++ b/modules/nf-neuro/reconst/frf/tests/main.nf.test.snap @@ -0,0 +1,368 @@ +{ + "reconst - frf_msmt": { + "content": [ + { + "0": [ + + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test__wm_frf.txt:md5,4ba04389a01fadc6986b53b225c3fde0" + ] + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test__gm_frf.txt:md5,e627436ce8b59ccab2bf34937f9ba3ce" + ] + ], + "3": [ + [ + { + "id": "test", + "single_end": false + }, + "test__csf_frf.txt:md5,0f7188e638225f25637f8abfc5e32de1" + ] + ], + "4": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "csf_frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__csf_frf.txt:md5,0f7188e638225f25637f8abfc5e32de1" + ] + ], + "frf": [ + + ], + "gm_frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__gm_frf.txt:md5,e627436ce8b59ccab2bf34937f9ba3ce" + ] + ], + "versions": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "wm_frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__wm_frf.txt:md5,4ba04389a01fadc6986b53b225c3fde0" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-11-28T20:54:23.833965559" + }, + "reconst - frf_set_frf_no_mask": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__frf.txt:md5,216a64bf3d00e4ca66e1b7f0df0dabb3" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "csf_frf": [ + + ], + "frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__frf.txt:md5,216a64bf3d00e4ca66e1b7f0df0dabb3" + ] + ], + "gm_frf": [ + + ], + "versions": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "wm_frf": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-11-28T20:54:17.732032899" + }, + "reconst - frf": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__frf.txt:md5,5174bb98e1ea39d559cd605c4b2ec48e" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "csf_frf": [ + + ], + "frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__frf.txt:md5,5174bb98e1ea39d559cd605c4b2ec48e" + ] + ], + "gm_frf": [ + + ], + "versions": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "wm_frf": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-11-28T20:53:55.867095265" + }, + "reconst - frf_set_frf": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__frf.txt:md5,1d0f31a4b209e4a624ad8ad77b91ff38" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "csf_frf": [ + + ], + "frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__frf.txt:md5,1d0f31a4b209e4a624ad8ad77b91ff38" + ] + ], + "gm_frf": [ + + ], + "versions": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "wm_frf": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-11-28T20:54:10.018700009" + }, + "reconst - frf_no_mask": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__frf.txt:md5,3432466fbca7f1f1df92e8634b75fcb6" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "csf_frf": [ + + ], + "frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__frf.txt:md5,3432466fbca7f1f1df92e8634b75fcb6" + ] + ], + "gm_frf": [ + + ], + "versions": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "wm_frf": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-11-28T20:54:02.707061504" + }, + "reconst - frf_msmt_set_frf": { + "content": [ + { + "0": [ + + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test__wm_frf.txt:md5,5e797b4721417dcef35f870462577660" + ] + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test__gm_frf.txt:md5,85bf905f0953f48bbb622c9bb179692b" + ] + ], + "3": [ + [ + { + "id": "test", + "single_end": false + }, + "test__csf_frf.txt:md5,7ba2403272a2ee2d2878a93ae44a42e8" + ] + ], + "4": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "csf_frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__csf_frf.txt:md5,7ba2403272a2ee2d2878a93ae44a42e8" + ] + ], + "frf": [ + + ], + "gm_frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__gm_frf.txt:md5,85bf905f0953f48bbb622c9bb179692b" + ] + ], + "versions": [ + "versions.yml:md5,0ebf33e30da5fc7e67dee8889822322e" + ], + "wm_frf": [ + [ + { + "id": "test", + "single_end": false + }, + "test__wm_frf.txt:md5,5e797b4721417dcef35f870462577660" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.0" + }, + "timestamp": "2024-11-28T20:54:31.726195565" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/reconst/frf/tests/nextflow.config b/modules/nf-neuro/reconst/frf/tests/nextflow.config new file mode 100644 index 0000000..371acb8 --- /dev/null +++ b/modules/nf-neuro/reconst/frf/tests/nextflow.config @@ -0,0 +1,13 @@ +process { + withName: "RECONST_FRF" { + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.max_dti_shell_value = 1200 + ext.fa = 0.7 + ext.fa_min = 0.5 + ext.nvox_min = 300 + ext.roi_radius = 20 + ext.set_frf = false + ext.precision = 8 + } +} diff --git a/modules/nf-neuro/reconst/frf/tests/nextflow_msmt.config b/modules/nf-neuro/reconst/frf/tests/nextflow_msmt.config new file mode 100644 index 0000000..15b0076 --- /dev/null +++ b/modules/nf-neuro/reconst/frf/tests/nextflow_msmt.config @@ -0,0 +1,14 @@ +process { + withName: "RECONST_FRF" { + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.max_dti_shell_value = 1200 + ext.fa = 0.7 + ext.fa_min = 0.5 + ext.nvox_min = 100 + ext.roi_radius = 20 + ext.set_frf = false + ext.method = "msmt" + ext.precision = 8 + } +} diff --git a/modules/nf-neuro/reconst/frf/tests/nextflow_msmt_set_frf.config b/modules/nf-neuro/reconst/frf/tests/nextflow_msmt_set_frf.config new file mode 100644 index 0000000..045c926 --- /dev/null +++ b/modules/nf-neuro/reconst/frf/tests/nextflow_msmt_set_frf.config @@ -0,0 +1,17 @@ +process { + withName: "RECONST_FRF" { + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.max_dti_shell_value = 1200 + ext.fa = 0.7 + ext.fa_min = 0.5 + ext.nvox_min = 100 + ext.roi_radius = 20 + ext.set_frf = true + ext.manual_wm_frf = "15,4,4,12,3,3,9,3,3" + ext.manual_gm_frf = "8,7,7,7,6,6,5,5,5" + ext.manual_csf_frf = "17,17,17,10,10,10,8,8,8" + ext.method = "msmt" + ext.precision = 8 + } +} diff --git a/modules/nf-neuro/reconst/frf/tests/nextflow_set_frf.config b/modules/nf-neuro/reconst/frf/tests/nextflow_set_frf.config new file mode 100644 index 0000000..ebb57fc --- /dev/null +++ b/modules/nf-neuro/reconst/frf/tests/nextflow_set_frf.config @@ -0,0 +1,14 @@ +process { + withName: "RECONST_FRF" { + ext.b0_thr_extract_b0 = 10 + ext.dwi_shell_tolerance = 50 + ext.max_dti_shell_value = 1200 + ext.fa = 0.7 + ext.fa_min = 0.5 + ext.nvox_min = 300 + ext.roi_radius = 20 + ext.set_frf = true + ext.manual_frf = "15,4,4" + ext.precision = 8 + } +} diff --git a/modules/nf-neuro/reconst/frf/tests/tags.yml b/modules/nf-neuro/reconst/frf/tests/tags.yml new file mode 100644 index 0000000..8a20c45 --- /dev/null +++ b/modules/nf-neuro/reconst/frf/tests/tags.yml @@ -0,0 +1,2 @@ +reconst/frf: + - "modules/nf-neuro/reconst/frf/**" diff --git a/modules/nf-neuro/reconst/meanfrf/environment.yml b/modules/nf-neuro/reconst/meanfrf/environment.yml new file mode 100644 index 0000000..5a02cfa --- /dev/null +++ b/modules/nf-neuro/reconst/meanfrf/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: reconst_meanfrf diff --git a/modules/nf-scil/reconst/meanfrf/main.nf b/modules/nf-neuro/reconst/meanfrf/main.nf old mode 100755 new mode 100644 similarity index 58% rename from modules/nf-scil/reconst/meanfrf/main.nf rename to modules/nf-neuro/reconst/meanfrf/main.nf index 10d2ed3..39f35a5 --- a/modules/nf-scil/reconst/meanfrf/main.nf +++ b/modules/nf-neuro/reconst/meanfrf/main.nf @@ -4,14 +4,14 @@ process RECONST_MEANFRF { label 'process_single' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: - path(frf_list) + tuple val(prefix), path(frf_list) output: - path("mean_frf.txt") , emit: meanfrf + tuple val(prefix), path("*mean_frf.txt") , emit: meanfrf path "versions.yml" , emit: versions when: @@ -23,23 +23,23 @@ process RECONST_MEANFRF { export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 - scil_compute_mean_frf.py $frf_list mean_frf.txt + scil_frf_mean.py $frf_list ${prefix}_mean_frf.txt cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ stub: """ - scil_compute_mean_frf.py -h + scil_frf_mean.py -h - touch mean_frf.txt + touch ${prefix}_mean_frf.txt cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ } diff --git a/modules/nf-scil/reconst/meanfrf/meta.yml b/modules/nf-neuro/reconst/meanfrf/meta.yml old mode 100755 new mode 100644 similarity index 85% rename from modules/nf-scil/reconst/meanfrf/meta.yml rename to modules/nf-neuro/reconst/meanfrf/meta.yml index 23ad360..316550f --- a/modules/nf-scil/reconst/meanfrf/meta.yml +++ b/modules/nf-neuro/reconst/meanfrf/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "reconst_meanfrf" description: Compute the mean Fiber Response Function from a set of individually computed Response Functions. @@ -10,11 +10,14 @@ keywords: - Average tools: - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" input: + - label: + type: string + description: Label for the output file, used as prefix. - frf_list: type: list description: List of individual Fiber Response Function (FRF) path. diff --git a/modules/nf-neuro/reconst/meanfrf/tests/main.nf.test b/modules/nf-neuro/reconst/meanfrf/tests/main.nf.test new file mode 100644 index 0000000..0b228c0 --- /dev/null +++ b/modules/nf-neuro/reconst/meanfrf/tests/main.nf.test @@ -0,0 +1,72 @@ +nextflow_process { + + name "Test Process RECONST_MEANFRF" + script "../main.nf" + process "RECONST_MEANFRF" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "reconst" + tag "reconst/meanfrf" + + 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( [ "processing.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("reconst - meanfrf") { + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory + .map{ test_data_directory -> [ + "test", + [ + file("\${test_data_directory}/frf.txt"), + file("\${test_data_directory}/mfrf.txt"), + file("\${test_data_directory}/nfrf.txt") + ] + ]} + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("reconst - meanfrf - stub") { + options "-stub-run" + + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory + .map{ test_data_directory -> ["test", []] } + """ + } + } + + then { + assert process.success + } + + } +} diff --git a/modules/nf-neuro/reconst/meanfrf/tests/main.nf.test.snap b/modules/nf-neuro/reconst/meanfrf/tests/main.nf.test.snap new file mode 100644 index 0000000..02a42fc --- /dev/null +++ b/modules/nf-neuro/reconst/meanfrf/tests/main.nf.test.snap @@ -0,0 +1,31 @@ +{ + "reconst - meanfrf": { + "content": [ + { + "0": [ + [ + "test", + "test_mean_frf.txt:md5,92f823e2fb057161b9d6a046a589c0cc" + ] + ], + "1": [ + "versions.yml:md5,0726c38b1e767e3e19f34b087306a364" + ], + "meanfrf": [ + [ + "test", + "test_mean_frf.txt:md5,92f823e2fb057161b9d6a046a589c0cc" + ] + ], + "versions": [ + "versions.yml:md5,0726c38b1e767e3e19f34b087306a364" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.2" + }, + "timestamp": "2024-12-17T01:44:09.840777788" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/reconst/meanfrf/tests/nextflow.config b/modules/nf-neuro/reconst/meanfrf/tests/nextflow.config new file mode 100644 index 0000000..bc3f701 --- /dev/null +++ b/modules/nf-neuro/reconst/meanfrf/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + withName: "RECONST_MEANFRF" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + } +} diff --git a/modules/nf-neuro/reconst/meanfrf/tests/tags.yml b/modules/nf-neuro/reconst/meanfrf/tests/tags.yml new file mode 100644 index 0000000..09cdc2f --- /dev/null +++ b/modules/nf-neuro/reconst/meanfrf/tests/tags.yml @@ -0,0 +1,2 @@ +reconst/meanfrf: + - "modules/nf-neuro/reconst/meanfrf/**" diff --git a/modules/nf-neuro/reconst/shsignal/environment.yml b/modules/nf-neuro/reconst/shsignal/environment.yml new file mode 100644 index 0000000..a07236a --- /dev/null +++ b/modules/nf-neuro/reconst/shsignal/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: reconst_shsignal diff --git a/modules/nf-neuro/reconst/shsignal/main.nf b/modules/nf-neuro/reconst/shsignal/main.nf new file mode 100644 index 0000000..006c98d --- /dev/null +++ b/modules/nf-neuro/reconst/shsignal/main.nf @@ -0,0 +1,65 @@ +process RECONST_SHSIGNAL { + 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(dwi), path(bval), path(bvec), path(mask) /* optional, value = [] */ + + output: + tuple val(meta), path("*__dwi_sh.nii.gz") , emit: sh_signal + 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 max_shell_bvalue = task.ext.max_shell_bvalue ?: 1500 + def b0_thr_extract_b0 = task.ext.b0_thr_extract_b0 ?: 10 + def b0_threshold = task.ext.b0_thr_extract_b0 ? "--b0_threshold $task.ext.b0_thr_extract_b0" : "" + def shell_to_fit = task.ext.shell_to_fit ?: "\$(cut -d ' ' --output-delimiter=\$'\\n' -f 1- $bval | awk -F' ' '{v=int(\$1)}{if(v<=$max_shell_bvalue|| v<=$b0_thr_extract_b0)print v}' | sort | uniq)" + def sh_order = task.ext.sh_order ? "--sh_order $task.ext.sh_order" : "" + def sh_basis = task.ext.sh_basis ? "--sh_basis $task.ext.sh_basis" : "" + def smoothing = task.ext.smoothing ? "--smooth $task.ext.smoothing" : "" + def attenuation_only = task.ext.fit_attenuation_only ? "--use_attenuation" : "" + + if ( mask ) args += " --mask $mask" + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + scil_dwi_extract_shell.py $dwi $bval $bvec $shell_to_fit \ + dwi_sh_shells.nii.gz bval_sh_shells bvec_sh_shells -f + + scil_dwi_to_sh.py dwi_sh_shells.nii.gz bval_sh_shells bvec_sh_shells \ + ${prefix}__dwi_sh.nii.gz \ + $sh_order $sh_basis $smoothing \ + $attenuation_only $b0_threshold $args + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list --disable-pip-version-check --no-python-version-warning | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + scil_dwi_extract_shell.py -h + scil_dwi_to_sh.py -h + + touch ${prefix}__dwi_sh.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + scilpy: \$(pip list --disable-pip-version-check --no-python-version-warning | grep scilpy | tr -s ' ' | cut -d' ' -f2) + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/reconst/shsignal/meta.yml b/modules/nf-neuro/reconst/shsignal/meta.yml new file mode 100644 index 0000000..13cd4fb --- /dev/null +++ b/modules/nf-neuro/reconst/shsignal/meta.yml @@ -0,0 +1,95 @@ +--- +name: "reconst_shsignal" +description: Compute the SH coefficient directly on the raw DWI signal +keywords: + - sh + - dwi + - signal +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 ]` + + - dwi: + type: file + description: Nifti DWI volume used to extract DTI metrics. + pattern: "*.{nii,nii.gz}" + + - bval: + type: file + description: B-values in FSL format. + pattern: "*.bval" + + - bvec: + type: file + description: B-vectors in FSL format. + pattern: "*.bvec" + + - mask: + type: file + description: Nifti volume file used to mask the input image. + pattern: "*.{nii,nii.gz}" + +args: + - dwi_shell_tolerance: + type: int + description: Volumes with b-values that differ by this tolerance are considered of same weighting. + default: 20 + - b0_thr_extract_b0: + type: int + description: Volumes with b-values up to this threshold are considered b0 (unweighted). + default: 10 + - shell_to_fit: + type: int + description: Only use volumes with this b-value. Required if input is multi-shell. + default: null + - sh_order: + type: int + description: Truncate spherical harmonics coefficients to this order. + default: 4 + - sh_basis: + type: string + description: | + Spherical harmonics basis to use. Options : + - descoteaux07 + - tournier07 + - descoteaux07_legacy + - tournier07_legacy + default: "descoteaux07_legacy" + - smoothing: + type: float + description: Regularization coefficient used for the spherical harmonics fit. + default: 0.006 + - attenuation_only: + type: boolean + description: If enabled, divide the weighted volumes by the b0 before fitting. + default: false + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1', single_end:false ]` + + - sh_signal: + type: file + description: Output filename for the SH coefficients. + pattern: "*__dwi_sh.{nii,nii.gz}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@AlexVCaron" +maintainers: + - "@AlexVCaron" diff --git a/modules/nf-neuro/reconst/shsignal/tests/main.nf.test b/modules/nf-neuro/reconst/shsignal/tests/main.nf.test new file mode 100644 index 0000000..2a9c55a --- /dev/null +++ b/modules/nf-neuro/reconst/shsignal/tests/main.nf.test @@ -0,0 +1,111 @@ +nextflow_process { + + name "Test Process RECONST_SHSIGNAL" + script "../main.nf" + process "RECONST_SHSIGNAL" + + tag "modules" + tag "modules_nfcore" + tag "reconst" + tag "reconst/shsignal" + + 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( [ "DWIss1000-dir32.zip", "segmentation.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("fit single shell signal") { + 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.sh_signal.get(0).get(1), 6), + process.out.versions + ).match() } + ) + } + + } + + test("fit sh - stub") { + + options "-stub-run" + + 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(process.out.versions).match() } + ) + } + + } + +} diff --git a/modules/nf-neuro/reconst/shsignal/tests/main.nf.test.snap b/modules/nf-neuro/reconst/shsignal/tests/main.nf.test.snap new file mode 100644 index 0000000..8e4cb87 --- /dev/null +++ b/modules/nf-neuro/reconst/shsignal/tests/main.nf.test.snap @@ -0,0 +1,27 @@ +{ + "fit single shell signal": { + "content": [ + "test__dwi_sh.nii.gz:md5:header,76a9953111f97786bd14a451c61529e7,data,9d8c694457c72375d075ce29838ec91e", + [ + "versions.yml:md5,173eabd26e11dab936f8e29ee3f01d8c" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-27T02:07:47.742388106" + }, + "fit sh - stub": { + "content": [ + [ + "versions.yml:md5,173eabd26e11dab936f8e29ee3f01d8c" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-26T21:48:34.631443809" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/reconst/shsignal/tests/tags.yml b/modules/nf-neuro/reconst/shsignal/tests/tags.yml new file mode 100644 index 0000000..0fd701a --- /dev/null +++ b/modules/nf-neuro/reconst/shsignal/tests/tags.yml @@ -0,0 +1,2 @@ +reconst/shsignal: + - "modules/nf-neuro/reconst/shsignal/**" diff --git a/modules/nf-neuro/registration/anattodwi/environment.yml b/modules/nf-neuro/registration/anattodwi/environment.yml new file mode 100644 index 0000000..7efa0ac --- /dev/null +++ b/modules/nf-neuro/registration/anattodwi/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: registration_anattodwi diff --git a/modules/nf-neuro/registration/anattodwi/main.nf b/modules/nf-neuro/registration/anattodwi/main.nf new file mode 100644 index 0000000..7f05140 --- /dev/null +++ b/modules/nf-neuro/registration/anattodwi/main.nf @@ -0,0 +1,135 @@ +process REGISTRATION_ANATTODWI { + 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:latest' }" + + input: + tuple val(meta), path(t1), path(b0), path(metric) + + output: + tuple val(meta), path("*0GenericAffine.mat") , emit: affine + tuple val(meta), path("*1Warp.nii.gz") , emit: warp + tuple val(meta), path("*1InverseWarp.nii.gz") , emit: inverse_warp + tuple val(meta), path("*t1_warped.nii.gz") , emit: t1_warped + tuple val(meta), path("*_registration_anattodwi_mqc.gif") , emit: mqc, optional: true + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + + def run_qc = task.ext.run_qc ? task.ext.run_qc : false + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + export ANTS_RANDOM_SEED=1234 + + antsRegistration --dimensionality 3 --float 0\ + --output [output,outputWarped.nii.gz,outputInverseWarped.nii.gz]\ + --interpolation Linear --use-histogram-matching 0\ + --winsorize-image-intensities [0.005,0.995]\ + --initial-moving-transform [$b0,$t1,1]\ + --transform Rigid['0.2']\ + --metric MI[$b0,$t1,1,32,Regular,0.25]\ + --convergence [500x250x125x50,1e-6,10] --shrink-factors 8x4x2x1\ + --smoothing-sigmas 3x2x1x0\ + --transform Affine['0.2']\ + --metric MI[$b0,$t1,1,32,Regular,0.25]\ + --convergence [500x250x125x50,1e-6,10] --shrink-factors 8x4x2x1\ + --smoothing-sigmas 3x2x1x0\ + --transform SyN[0.1,3,0]\ + --metric MI[$b0,$t1,1,32]\ + --metric CC[$metric,$t1,1,4]\ + --convergence [50x25x10,1e-6,10] --shrink-factors 4x2x1\ + --smoothing-sigmas 3x2x1 + + mv outputWarped.nii.gz ${prefix}__t1_warped.nii.gz + mv output0GenericAffine.mat ${prefix}__output0GenericAffine.mat + mv output1InverseWarp.nii.gz ${prefix}__output1InverseWarp.nii.gz + mv output1Warp.nii.gz ${prefix}__output1Warp.nii.gz + + ### ** QC ** ### + if $run_qc; + then + # Extract dimensions. + dim=\$(mrinfo ${prefix}__t1_warped.nii.gz -size) + read sagittal_dim coronal_dim axial_dim <<< "\${dim}" + + # Get middle slices. + coronal_mid=\$((\$coronal_dim / 2)) + sagittal_mid=\$((\$sagittal_dim / 2)) + axial_mid=\$((\$axial_dim / 2)) + + # Set viz params. + viz_params="--display_slice_number --display_lr --size 256 256" + + # Iterate over images. + for image in t1_warped b0; + do + scil_viz_volume_screenshot.py *\${image}.nii.gz \${image}_coronal.png \ + --slices \$coronal_mid --axis coronal \$viz_params + scil_viz_volume_screenshot.py *\${image}.nii.gz \${image}_sagittal.png \ + --slices \$sagittal_mid --axis sagittal \$viz_params + scil_viz_volume_screenshot.py *\${image}.nii.gz \${image}_axial.png \ + --slices \$axial_mid --axis axial \$viz_params + + if [ \$image != b0 ]; + then + title="Warped T1" + else + title="Reference B0" + fi + + convert +append \${image}_coronal*.png \${image}_axial*.png \ + \${image}_sagittal*.png \${image}_mosaic.png + convert -annotate +20+230 "\${title}" -fill white -pointsize 30 \ + \${image}_mosaic.png \${image}_mosaic.png + + # Clean up. + rm \${image}_coronal*.png \${image}_sagittal*.png \${image}_axial*.png + done + + # Create GIF. + convert -delay 10 -loop 0 -morph 10 \ + t1_warped_mosaic.png b0_mosaic.png t1_warped_mosaic.png \ + ${prefix}_registration_anattodwi_mqc.gif + + # Clean up. + rm t1_warped_mosaic.png b0_mosaic.png + fi + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\.[0-9]+\\.[0-9]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') + imagemagick: \$(convert -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + antsRegistration -h + + touch ${prefix}__t1_warped.nii.gz + touch ${prefix}__output0GenericAffine.mat + touch ${prefix}__output1InverseWarp.nii.gz + touch ${prefix}__output1Warp.nii.gz + touch ${prefix}__registration_anattodwi_mqc.gif + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\.[0-9]+\\.[0-9]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') + imagemagick: \$(convert -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + END_VERSIONS + """ +} diff --git a/modules/nf-scil/register/anattodwi/meta.yml b/modules/nf-neuro/registration/anattodwi/meta.yml similarity index 82% rename from modules/nf-scil/register/anattodwi/meta.yml rename to modules/nf-neuro/registration/anattodwi/meta.yml index d47e51c..1142ebe 100644 --- a/modules/nf-scil/register/anattodwi/meta.yml +++ b/modules/nf-neuro/registration/anattodwi/meta.yml @@ -1,6 +1,6 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json -name: "register_anattodwi" +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json +name: "registration_anattodwi" description: Anatomical image registration on a diffusion image. keywords: - nifti @@ -15,6 +15,12 @@ tools: documentation: "http://stnava.github.io/ANTsDoc/" doi: "10.1016/j.neuroimage.2010.09.025" +args: + - run_qc: + type: boolean + description: "Run quality control for the registration process" + default: false + input: - meta: type: map @@ -59,6 +65,11 @@ output: description: Anatomical T1 warped to dwi space pattern: "*.{nii,nii.gz}" + - mqc: + type: file + description: .gif file containing quality control image for the registration process. Made for use in MultiQC report. + pattern: "*.gif" + - versions: type: file description: File containing software versions diff --git a/modules/nf-neuro/registration/anattodwi/tests/main.nf.test b/modules/nf-neuro/registration/anattodwi/tests/main.nf.test new file mode 100644 index 0000000..546969c --- /dev/null +++ b/modules/nf-neuro/registration/anattodwi/tests/main.nf.test @@ -0,0 +1,114 @@ +nextflow_process { + + name "Test Process REGISTRATION_ANATTODWI" + script "../main.nf" + process "REGISTRATION_ANATTODWI" + + tag "modules" + tag "modules_nfcore" + tag "registration" + tag "registration/anattodwi" + + 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( [ "T1w.zip", "b0.zip", "dti.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("registration - anattodwi") { + config "./nextflow.config" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + dti: it.simpleName == "dti" + } + ch_t1w = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/T1w.nii.gz") + ] + } + ch_b0 = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + ch_fa = ch_split_test_data.dti.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/fa.nii.gz") + ] + } + input[0] = ch_t1w + .join(ch_b0) + .join(ch_fa) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("registration - anattodwi -stub-run") { + options "-stub-run" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + dti: it.simpleName == "dti" + } + ch_t1w = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/T1w.nii.gz") + ] + } + ch_b0 = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + ch_fa = ch_split_test_data.dti.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/fa.nii.gz") + ] + } + input[0] = ch_t1w + .join(ch_b0) + .join(ch_fa) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + } +} diff --git a/modules/nf-neuro/registration/anattodwi/tests/main.nf.test.snap b/modules/nf-neuro/registration/anattodwi/tests/main.nf.test.snap new file mode 100644 index 0000000..aebfc5f --- /dev/null +++ b/modules/nf-neuro/registration/anattodwi/tests/main.nf.test.snap @@ -0,0 +1,121 @@ +{ + "registration - anattodwi": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output0GenericAffine.mat:md5,6eef07f9004fb96e57ae0404ffef174c" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output1Warp.nii.gz:md5,e0995852e251bee5521572a4495a0bd6" + ] + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output1InverseWarp.nii.gz:md5,28694d2b41f5209ab8f1acbb219014fe" + ] + ], + "3": [ + [ + { + "id": "test", + "single_end": false + }, + "test__t1_warped.nii.gz:md5,9c34f424900adea263e39ed1f37a793f" + ] + ], + "4": [ + [ + { + "id": "test", + "single_end": false + }, + "test_registration_anattodwi_mqc.gif:md5,74a7cf404c443726487683c23bd926c0" + ] + ], + "5": [ + "versions.yml:md5,f368bc76bf8d1f1a68caea3597b0c65b" + ], + "affine": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output0GenericAffine.mat:md5,6eef07f9004fb96e57ae0404ffef174c" + ] + ], + "inverse_warp": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output1InverseWarp.nii.gz:md5,28694d2b41f5209ab8f1acbb219014fe" + ] + ], + "mqc": [ + [ + { + "id": "test", + "single_end": false + }, + "test_registration_anattodwi_mqc.gif:md5,74a7cf404c443726487683c23bd926c0" + ] + ], + "t1_warped": [ + [ + { + "id": "test", + "single_end": false + }, + "test__t1_warped.nii.gz:md5,9c34f424900adea263e39ed1f37a793f" + ] + ], + "versions": [ + "versions.yml:md5,f368bc76bf8d1f1a68caea3597b0c65b" + ], + "warp": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output1Warp.nii.gz:md5,e0995852e251bee5521572a4495a0bd6" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-20T14:58:42.372471" + }, + "registration - anattodwi -stub-run": { + "content": [ + [ + "versions.yml:md5,f368bc76bf8d1f1a68caea3597b0c65b" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-20T14:58:49.169195" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/registration/anattodwi/tests/nextflow.config b/modules/nf-neuro/registration/anattodwi/tests/nextflow.config new file mode 100644 index 0000000..68043db --- /dev/null +++ b/modules/nf-neuro/registration/anattodwi/tests/nextflow.config @@ -0,0 +1,6 @@ +process { + withName: "REGISTRATION_ANATTODWI" { + ext.cpus = 1 + ext.run_qc = true + } +} diff --git a/modules/nf-neuro/registration/anattodwi/tests/tags.yml b/modules/nf-neuro/registration/anattodwi/tests/tags.yml new file mode 100644 index 0000000..d09cc3f --- /dev/null +++ b/modules/nf-neuro/registration/anattodwi/tests/tags.yml @@ -0,0 +1,2 @@ +registration/anattodwi: + - "modules/nf-neuro/registration/anattodwi/**" diff --git a/modules/nf-neuro/registration/ants/environment.yml b/modules/nf-neuro/registration/ants/environment.yml new file mode 100644 index 0000000..2c00919 --- /dev/null +++ b/modules/nf-neuro/registration/ants/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: registration_ants diff --git a/modules/nf-neuro/registration/ants/main.nf b/modules/nf-neuro/registration/ants/main.nf new file mode 100644 index 0000000..ad33fbe --- /dev/null +++ b/modules/nf-neuro/registration/ants/main.nf @@ -0,0 +1,147 @@ + +process REGISTRATION_ANTS { + tag "$meta.id" + label 'process_medium' + + 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(fixedimage), path(movingimage), path(mask) //** optional, input = [] **// + + output: + tuple val(meta), path("*_warped.nii.gz") , emit: image + tuple val(meta), path("*__output0Warp.nii.gz") , emit: warp, optional:true + tuple val(meta), path("*__output1GenericAffine.mat") , emit: affine + tuple val(meta), path("*__output1InverseWarp.nii.gz") , emit: inverse_warp, optional: true + tuple val(meta), path("*__output0InverseAffine.mat") , emit: inverse_affine + tuple val(meta), path("*_registration_ants_mqc.gif") , emit: mqc, optional: true + 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 suffix_qc = task.ext.suffix_qc ? "${task.ext.suffix_qc}" : "" + def ants = task.ext.quick ? "antsRegistrationSyNQuick.sh " : "antsRegistrationSyN.sh " + def dimension = task.ext.dimension ? "-d " + task.ext.dimension : "-d 3" + def transform = task.ext.transform ? task.ext.transform : "s" + def seed = task.ext.random_seed ? " -e " + task.ext.random_seed : "-e 1234" + def run_qc = task.ext.run_qc ? task.ext.run_qc : false + + if ( task.ext.threads ) args += "-n " + task.ext.threads + if ( task.ext.initial_transform ) args += " -i " + task.ext.initial_transform + if ( task.ext.histogram_bins ) args += " -r " + task.ext.histogram_bins + if ( task.ext.spline_distance ) args += " -s " + task.ext.spline_distance + if ( task.ext.gradient_step ) args += " -g " + task.ext.gradient_step + if ( task.ext.mask ) args += " -x $mask" + if ( task.ext.type ) args += " -p " + task.ext.type + if ( task.ext.histogram_matching ) args += " -j " + task.ext.histogram_matching + if ( task.ext.repro_mode ) args += " -y " + task.ext.repro_mode + if ( task.ext.collapse_output ) args += " -z " + task.ext.collapse_output + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + $ants $dimension -f $fixedimage -m $movingimage -o output -t $transform $args $seed + + mv outputWarped.nii.gz ${prefix}__warped.nii.gz + mv output0GenericAffine.mat ${prefix}__output1GenericAffine.mat + + if [ $transform != "t" ] && [ $transform != "r" ] && [ $transform != "a" ]; + then + mv output1InverseWarp.nii.gz ${prefix}__output1InverseWarp.nii.gz + mv output1Warp.nii.gz ${prefix}__output0Warp.nii.gz + fi + + antsApplyTransforms -d 3 -i $fixedimage -r $movingimage -o Linear[output.mat]\ + -t [${prefix}__output1GenericAffine.mat,1] + + mv output.mat ${prefix}__output0InverseAffine.mat + + ### ** QC ** ### + if $run_qc; + then + mv $fixedimage fixedimage.nii.gz + extract_dim=\$(mrinfo fixedimage.nii.gz -size) + read sagittal_dim coronal_dim axial_dim <<< "\${extract_dim}" + + # Get the middle slice + coronal_dim=\$((\$coronal_dim / 2)) + axial_dim=\$((\$axial_dim / 2)) + sagittal_dim=\$((\$sagittal_dim / 2)) + + # Set viz params. + viz_params="--display_slice_number --display_lr --size 256 256" + # Iterate over images. + for image in fixedimage warped; + do + scil_viz_volume_screenshot.py *\${image}.nii.gz \${image}_coronal.png \ + --slices \$coronal_dim --axis coronal \$viz_params + scil_viz_volume_screenshot.py *\${image}.nii.gz \${image}_sagittal.png \ + --slices \$sagittal_dim --axis sagittal \$viz_params + scil_viz_volume_screenshot.py *\${image}.nii.gz \${image}_axial.png \ + --slices \$axial_dim --axis axial \$viz_params + if [ \$image != fixedimage ]; + then + title="T1 Warped" + else + title="fixedimage" + fi + convert +append \${image}_coronal*.png \${image}_axial*.png \ + \${image}_sagittal*.png \${image}_mosaic.png + convert -annotate +20+230 "\${title}" -fill white -pointsize 30 \ + \${image}_mosaic.png \${image}_mosaic.png + # Clean up. + rm \${image}_coronal*.png \${image}_sagittal*.png \${image}_axial*.png + done + # Create GIF. + convert -delay 10 -loop 0 -morph 10 \ + warped_mosaic.png fixedimage_mosaic.png warped_mosaic.png \ + ${prefix}_${suffix_qc}_registration_ants_mqc.gif + # Clean up. + rm *_mosaic.png + fi + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\.[0-9]+\\.[0-9]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') + imagemagick: \$(magick -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + touch ${prefix}__t1_warped.nii.gz + touch ${prefix}__output1GenericAffine.mat + touch ${prefix}__output0InverseAffine.mat + touch ${prefix}__output1InverseWarp.nii.gz + touch ${prefix}__output0Warp.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\.[0-9]+\\.[0-9]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') + imagemagick: \$(magick -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + END_VERSIONS + + function handle_code () { + local code=\$? + ignore=( 1 ) + exit \$([[ " \${ignore[@]} " =~ " \$code " ]] && echo 0 || echo \$code) + } + trap 'handle_code' ERR + + antsRegistrationSyNQuick.sh -h + antsApplyTransforms -h + """ +} diff --git a/modules/nf-scil/registration/ants/meta.yml b/modules/nf-neuro/registration/ants/meta.yml similarity index 88% rename from modules/nf-scil/registration/ants/meta.yml rename to modules/nf-neuro/registration/ants/meta.yml index 58f295d..aeb5502 100644 --- a/modules/nf-scil/registration/ants/meta.yml +++ b/modules/nf-neuro/registration/ants/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "registration_ants" description: Image registration with antsRegistrationSyN or antsRegistrationSyNQuick keywords: @@ -59,6 +59,11 @@ output: description: Tuple, Transformation files to warp trk (nii InverseWarp, mat file) pattern: "*.{nii,nii.gz,mat}" + - mqc: + type: file + description: .gif file containing quality control image for the registration process. Made for use in MultiQC report. + pattern: "*.gif" + - versions: type: file description: File containing software versions diff --git a/modules/nf-neuro/registration/ants/tests/main.nf.test b/modules/nf-neuro/registration/ants/tests/main.nf.test new file mode 100644 index 0000000..2f18ce3 --- /dev/null +++ b/modules/nf-neuro/registration/ants/tests/main.nf.test @@ -0,0 +1,170 @@ +nextflow_process { + + name "Test Process REGISTRATION_ANTS" + script "../main.nf" + process "REGISTRATION_ANTS" + + tag "modules" + tag "modules_nfcore" + tag "registration" + tag "registration/ants" + + 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( [ "T1w.zip", "b0.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("registration - ants") { + config "./nextflow.config" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + T1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + } + ch_T1w = ch_split_test_data.T1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz") + ] + } + ch_b0 = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + input[0] = ch_b0 + .join(ch_T1w) + .map{ it + [[]] } + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("registration - ants - quick") { + config "./nextflow_quick.config" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + T1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + } + ch_T1w = ch_split_test_data.T1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz") + ] + } + ch_b0 = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + input[0] = ch_b0 + .join(ch_T1w) + .map{ it + [[]] } + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("registration - ants - options") { + config "./nextflow_options.config" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + T1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + } + ch_T1w = ch_split_test_data.T1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz"), + file("\${test_data_directory}/T1w_mask.nii.gz") + ] + } + ch_b0 = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + input[0] = ch_b0 + .join(ch_T1w) + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("registration - ants - stub") { + options "-stub-run" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + T1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + } + ch_T1w = ch_split_test_data.T1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz"), + file("\${test_data_directory}/T1w_mask.nii.gz") + ] + } + ch_b0 = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + input[0] = ch_b0 + .join(ch_T1w) + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + } +} diff --git a/modules/nf-neuro/registration/ants/tests/main.nf.test.snap b/modules/nf-neuro/registration/ants/tests/main.nf.test.snap new file mode 100644 index 0000000..f1ba276 --- /dev/null +++ b/modules/nf-neuro/registration/ants/tests/main.nf.test.snap @@ -0,0 +1,323 @@ +{ + "registration - ants": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__warped.nii.gz:md5,4382217c04988eb01ed8f99916d7631c" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test__output0Warp.nii.gz:md5,6fee747df2c30bb48cf2b6897b5d3e17" + ] + ], + "2": [ + [ + { + "id": "test" + }, + "test__output1GenericAffine.mat:md5,8e987eb08cc568478c37ae79a8824f8b" + ] + ], + "3": [ + [ + { + "id": "test" + }, + "test__output1InverseWarp.nii.gz:md5,1608837de13478dff81fe51ac1720d28" + ] + ], + "4": [ + [ + { + "id": "test" + }, + "test__output0InverseAffine.mat:md5,d342d5f1c35a53cadcdf2d49a25f5ce8" + ] + ], + "5": [ + + ], + "6": [ + "versions.yml:md5,2c26609651ebb851765327617e7aec96" + ], + "affine": [ + [ + { + "id": "test" + }, + "test__output1GenericAffine.mat:md5,8e987eb08cc568478c37ae79a8824f8b" + ] + ], + "image": [ + [ + { + "id": "test" + }, + "test__warped.nii.gz:md5,4382217c04988eb01ed8f99916d7631c" + ] + ], + "inverse_affine": [ + [ + { + "id": "test" + }, + "test__output0InverseAffine.mat:md5,d342d5f1c35a53cadcdf2d49a25f5ce8" + ] + ], + "inverse_warp": [ + [ + { + "id": "test" + }, + "test__output1InverseWarp.nii.gz:md5,1608837de13478dff81fe51ac1720d28" + ] + ], + "mqc": [ + + ], + "versions": [ + "versions.yml:md5,2c26609651ebb851765327617e7aec96" + ], + "warp": [ + [ + { + "id": "test" + }, + "test__output0Warp.nii.gz:md5,6fee747df2c30bb48cf2b6897b5d3e17" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-24T07:52:30.173552689" + }, + "registration - ants - quick": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__warped.nii.gz:md5,81358dd2f3223f567f4101e61490ae0f" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test__output0Warp.nii.gz:md5,24e6f53eaa3e4476bb627c374bc809ca" + ] + ], + "2": [ + [ + { + "id": "test" + }, + "test__output1GenericAffine.mat:md5,48c8479a403e0c0ec29c69d37bb4c91a" + ] + ], + "3": [ + [ + { + "id": "test" + }, + "test__output1InverseWarp.nii.gz:md5,24e6f53eaa3e4476bb627c374bc809ca" + ] + ], + "4": [ + [ + { + "id": "test" + }, + "test__output0InverseAffine.mat:md5,25d0b8a83a922f2e4e89c6a86258af15" + ] + ], + "5": [ + [ + { + "id": "test" + }, + "test_T1_to_DWI_registration_ants_mqc.gif:md5,ebf58acb9c3f3b9bc8c5a08105d4ceff" + ] + ], + "6": [ + "versions.yml:md5,2c26609651ebb851765327617e7aec96" + ], + "affine": [ + [ + { + "id": "test" + }, + "test__output1GenericAffine.mat:md5,48c8479a403e0c0ec29c69d37bb4c91a" + ] + ], + "image": [ + [ + { + "id": "test" + }, + "test__warped.nii.gz:md5,81358dd2f3223f567f4101e61490ae0f" + ] + ], + "inverse_affine": [ + [ + { + "id": "test" + }, + "test__output0InverseAffine.mat:md5,25d0b8a83a922f2e4e89c6a86258af15" + ] + ], + "inverse_warp": [ + [ + { + "id": "test" + }, + "test__output1InverseWarp.nii.gz:md5,24e6f53eaa3e4476bb627c374bc809ca" + ] + ], + "mqc": [ + [ + { + "id": "test" + }, + "test_T1_to_DWI_registration_ants_mqc.gif:md5,ebf58acb9c3f3b9bc8c5a08105d4ceff" + ] + ], + "versions": [ + "versions.yml:md5,2c26609651ebb851765327617e7aec96" + ], + "warp": [ + [ + { + "id": "test" + }, + "test__output0Warp.nii.gz:md5,24e6f53eaa3e4476bb627c374bc809ca" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-03-06T16:44:46.474693596" + }, + "registration - ants - options": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__warped.nii.gz:md5,7cb6839d4af18d9d37a1bd9d07c8c57b" + ] + ], + "1": [ + + ], + "2": [ + [ + { + "id": "test" + }, + "test__output1GenericAffine.mat:md5,c3a48d19c815206de2b140f505969317" + ] + ], + "3": [ + + ], + "4": [ + [ + { + "id": "test" + }, + "test__output0InverseAffine.mat:md5,f473c1e548be1540cbf757202ffd5984" + ] + ], + "5": [ + [ + { + "id": "test" + }, + "test__registration_ants_mqc.gif:md5,23a92899290210c2fed8dbd9ed6b946b" + ] + ], + "6": [ + "versions.yml:md5,2c26609651ebb851765327617e7aec96" + ], + "affine": [ + [ + { + "id": "test" + }, + "test__output1GenericAffine.mat:md5,c3a48d19c815206de2b140f505969317" + ] + ], + "image": [ + [ + { + "id": "test" + }, + "test__warped.nii.gz:md5,7cb6839d4af18d9d37a1bd9d07c8c57b" + ] + ], + "inverse_affine": [ + [ + { + "id": "test" + }, + "test__output0InverseAffine.mat:md5,f473c1e548be1540cbf757202ffd5984" + ] + ], + "inverse_warp": [ + + ], + "mqc": [ + [ + { + "id": "test" + }, + "test__registration_ants_mqc.gif:md5,23a92899290210c2fed8dbd9ed6b946b" + ] + ], + "versions": [ + "versions.yml:md5,2c26609651ebb851765327617e7aec96" + ], + "warp": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-03-06T16:44:58.966002773" + }, + "registration - ants - stub": { + "content": [ + [ + "versions.yml:md5,2c26609651ebb851765327617e7aec96" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-24T07:52:56.165561569" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/registration/ants/tests/nextflow.config b/modules/nf-neuro/registration/ants/tests/nextflow.config new file mode 100644 index 0000000..3750dac --- /dev/null +++ b/modules/nf-neuro/registration/ants/tests/nextflow.config @@ -0,0 +1,6 @@ +process { + withName: "REGISTRATION_ANTS" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.repro_mode = 1 + } +} diff --git a/modules/nf-neuro/registration/ants/tests/nextflow_options.config b/modules/nf-neuro/registration/ants/tests/nextflow_options.config new file mode 100644 index 0000000..88c9a90 --- /dev/null +++ b/modules/nf-neuro/registration/ants/tests/nextflow_options.config @@ -0,0 +1,16 @@ +process { + withName: "REGISTRATION_ANTS" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.quick = true + ext.run_qc = true + ext.threads = 1 + ext.transform = "r" + ext.histogram_bins = 4 + ext.spline_distance = 26 + ext.gradient_step = 0.1 + ext.histogram_matching = 0 + ext.repro_mode = 0 + ext.collapse_output = 0 + ext.random_seed = 1234 + } +} diff --git a/modules/nf-neuro/registration/ants/tests/nextflow_quick.config b/modules/nf-neuro/registration/ants/tests/nextflow_quick.config new file mode 100644 index 0000000..9fd98e5 --- /dev/null +++ b/modules/nf-neuro/registration/ants/tests/nextflow_quick.config @@ -0,0 +1,9 @@ +process { + withName: "REGISTRATION_ANTS" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.quick = true + ext.repro_mode = 1 + ext.run_qc = true + ext.suffix_qc = "T1_to_DWI" + } +} diff --git a/modules/nf-neuro/registration/ants/tests/tags.yml b/modules/nf-neuro/registration/ants/tests/tags.yml new file mode 100644 index 0000000..8aa857e --- /dev/null +++ b/modules/nf-neuro/registration/ants/tests/tags.yml @@ -0,0 +1,2 @@ +registration/ants: + - "modules/nf-neuro/registration/ants/**" diff --git a/modules/nf-neuro/registration/antsapplytransforms/environment.yml b/modules/nf-neuro/registration/antsapplytransforms/environment.yml new file mode 100644 index 0000000..ef969e4 --- /dev/null +++ b/modules/nf-neuro/registration/antsapplytransforms/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: registration_antsapplytransforms diff --git a/modules/nf-neuro/registration/antsapplytransforms/main.nf b/modules/nf-neuro/registration/antsapplytransforms/main.nf new file mode 100644 index 0000000..fd24ea1 --- /dev/null +++ b/modules/nf-neuro/registration/antsapplytransforms/main.nf @@ -0,0 +1,114 @@ +process REGISTRATION_ANTSAPPLYTRANSFORMS { + tag "$meta.id" + label 'process_low' + + 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(image), path(reference), path(warp), path(affine) + + output: + tuple val(meta), path("*__warped.nii.gz") , emit: warped_image + tuple val(meta), path("*_registration_antsapplytransforms_mqc.gif") , emit: mqc, optional: true + 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}__warped" : "warped" + def suffix_qc = task.ext.suffix_qc ? "${task.ext.suffix_qc}" : "" + + def dimensionality = task.ext.dimensionality ? "-d " + task.ext.dimensionality : "" + def image_type = task.ext.image_type ? "-e " + task.ext.image_type : "" + def interpolation = task.ext.interpolation ? "-n " + task.ext.interpolation : "" + def output_dtype = task.ext.output_dtype ? "-u " + task.ext.output_dtype : "" + def default_val = task.ext.default_val ? "-f " + task.ext.default_val : "" + def run_qc = task.ext.run_qc ? task.ext.run_qc : false + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + antsApplyTransforms $dimensionality\ + -i $image\ + -r $reference\ + -o ${prefix}__${suffix}.nii.gz\ + $interpolation\ + -t $warp $affine\ + $image_type\ + $default_val\ + $output_dtype + + ### ** QC ** ### + if $run_qc; + then + mv $reference reference.nii.gz + extract_dim=\$(mrinfo ${prefix}__${suffix}.nii.gz -size) + read sagittal_dim coronal_dim axial_dim <<< "\${extract_dim}" + + # Get the middle slice + coronal_dim=\$((\$coronal_dim / 2)) + axial_dim=\$((\$axial_dim / 2)) + sagittal_dim=\$((\$sagittal_dim / 2)) + + # Set viz params. + viz_params="--display_slice_number --display_lr --size 256 256" + # Iterate over images. + for image in reference warped; + do + scil_viz_volume_screenshot.py *\${image}.nii.gz \${image}_coronal.png \ + --slices \$coronal_dim --axis coronal \$viz_params + scil_viz_volume_screenshot.py *\${image}.nii.gz \${image}_sagittal.png \ + --slices \$sagittal_dim --axis sagittal \$viz_params + scil_viz_volume_screenshot.py *\${image}.nii.gz \${image}_axial.png \ + --slices \$axial_dim --axis axial \$viz_params + if [ \$image != reference ]; + then + title="Transformed" + else + title="Reference" + fi + convert +append \${image}_coronal*.png \${image}_axial*.png \ + \${image}_sagittal*.png \${image}_mosaic.png + convert -annotate +20+230 "\${title}" -fill white -pointsize 30 \ + \${image}_mosaic.png \${image}_mosaic.png + # Clean up. + rm \${image}_coronal*.png \${image}_sagittal*.png \${image}_axial*.png + done + # Create GIF. + convert -delay 10 -loop 0 -morph 10 \ + warped_mosaic.png reference_mosaic.png warped_mosaic.png \ + ${prefix}_${suffix_qc}_registration_antsapplytransforms_mqc.gif + # Clean up. + rm *_mosaic.png + fi + cat <<-END_VERSIONS > versions.yml + "${task.process}": + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\.[0-9]+\\.[0-9]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') + imagemagick: \$(magick -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + def suffix = task.ext.first_suffix ? "${task.ext.first_suffix}__warped" : "warped" + + """ + antsApplyTransforms -h + + touch ${prefix}__${suffix}.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9]+\\.[0-9]+\\.[0-9]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | sed -n 's/== mrinfo \\([0-9.]\\+\\).*/\\1/p') + imagemagick: \$(magick -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + END_VERSIONS + """ +} diff --git a/modules/nf-scil/registration/antsapplytransforms/meta.yml b/modules/nf-neuro/registration/antsapplytransforms/meta.yml similarity index 63% rename from modules/nf-scil/registration/antsapplytransforms/meta.yml rename to modules/nf-neuro/registration/antsapplytransforms/meta.yml index cd28936..a4e2594 100644 --- a/modules/nf-scil/registration/antsapplytransforms/meta.yml +++ b/modules/nf-neuro/registration/antsapplytransforms/meta.yml @@ -27,10 +27,15 @@ input: description: Reference image for registration pattern: "*.{nii.nii.gz}" - - transform: + - warp: type: file - description: file or tuple of files, transformation file(s) to warp image or trk (*mat or [nii Warp, mat file]). If a rigid or affine transformation needs to be inverted before being applied, use antsApplyTransforms with the -o Linear[inversedTransform,1], as this module does not handles it. - pattern: "*.{nii,nii.gz,mat}" + description: Warp transformation file to warp image or trk. + pattern: "*.{nii,nii.gz}" + + - affine: + type: file + description: Affine or rigid transformation file to warp image or trk (*mat). If a rigid or affine transformation needs to be inverted before being applied, use antsApplyTransforms with the -o Linear[inversedTransform,1], as this module does not handles it. + pattern: "*.mat" output: - meta: @@ -39,11 +44,16 @@ output: Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - warped: + - warped_image: type: file description: Warped image pattern: "*.{nii.nii.gz}" + - mqc: + type: file + description: .gif file containing quality control image for the registration process. Made for use in MultiQC report. + pattern: "*.gif" + - versions: type: file description: File containing software versions diff --git a/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test b/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test new file mode 100644 index 0000000..8447aa3 --- /dev/null +++ b/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test @@ -0,0 +1,80 @@ +nextflow_process { + + name "Test Process REGISTRATION_ANTSAPPLYTRANSFORMS" + script "../main.nf" + process "REGISTRATION_ANTSAPPLYTRANSFORMS" + + tag "modules" + tag "modules_nfcore" + tag "registration" + tag "registration/antsapplytransforms" + + 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( [ "registration.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("registration - antsapplytransforms") { + + config "./nextflow.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}/b0.nii.gz"), + file("\${test_data_directory}/mni_masked_2x2x2.nii.gz"), + file("\${test_data_directory}/output1Warp.nii.gz"), + file("\${test_data_directory}/output0GenericAffine.mat") + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("registration - antsapplytransforms - stub-run") { + + options "-stub-run" + + config "./nextflow.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}/b0.nii.gz"), + file("\${test_data_directory}/mni_masked_2x2x2.nii.gz"), + file("\${test_data_directory}/output1Warp.nii.gz"), + file("\${test_data_directory}/output0GenericAffine.mat") + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + } +} diff --git a/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test.snap b/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test.snap new file mode 100644 index 0000000..0e4edc5 --- /dev/null +++ b/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test.snap @@ -0,0 +1,67 @@ +{ + "registration - antsapplytransforms": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__b0__warped.nii.gz:md5,3ae49aacab66fb2378da92155453b1be" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0_to_template_registration_antsapplytransforms_mqc.gif:md5,2e97314c320a27e4271c960c50895dfb" + ] + ], + "2": [ + "versions.yml:md5,3414a40c02551052589b50e4eb35a3da" + ], + "mqc": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0_to_template_registration_antsapplytransforms_mqc.gif:md5,2e97314c320a27e4271c960c50895dfb" + ] + ], + "versions": [ + "versions.yml:md5,3414a40c02551052589b50e4eb35a3da" + ], + "warped_image": [ + [ + { + "id": "test", + "single_end": false + }, + "test__b0__warped.nii.gz:md5,3ae49aacab66fb2378da92155453b1be" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-03-06T16:07:48.527748453" + }, + "registration - antsapplytransforms - stub-run": { + "content": [ + [ + "versions.yml:md5,3414a40c02551052589b50e4eb35a3da" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-23T16:23:22.94059003" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/registration/antsapplytransforms/tests/nextflow.config b/modules/nf-neuro/registration/antsapplytransforms/tests/nextflow.config new file mode 100644 index 0000000..8e50f45 --- /dev/null +++ b/modules/nf-neuro/registration/antsapplytransforms/tests/nextflow.config @@ -0,0 +1,12 @@ +process { + withName: "REGISTRATION_ANTSAPPLYTRANSFORMS" { + ext.interpolation = "linear" + ext.first_suffix = "b0" + ext.dimensionality = 3 + ext.image_type = 0 + ext.output_dtype = "float" + ext.default_val = 0 + ext.run_qc = true + ext.suffix_qc = "b0_to_template" + } +} diff --git a/modules/nf-neuro/registration/antsapplytransforms/tests/tags.yml b/modules/nf-neuro/registration/antsapplytransforms/tests/tags.yml new file mode 100644 index 0000000..e539b44 --- /dev/null +++ b/modules/nf-neuro/registration/antsapplytransforms/tests/tags.yml @@ -0,0 +1,2 @@ +registration/antsapplytransforms: + - "modules/nf-neuro/registration/antsapplytransforms/**" diff --git a/modules/nf-neuro/registration/easyreg/environment.yml b/modules/nf-neuro/registration/easyreg/environment.yml new file mode 100644 index 0000000..22fbcae --- /dev/null +++ b/modules/nf-neuro/registration/easyreg/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: registration_easyreg diff --git a/modules/nf-neuro/registration/easyreg/main.nf b/modules/nf-neuro/registration/easyreg/main.nf new file mode 100644 index 0000000..e81c209 --- /dev/null +++ b/modules/nf-neuro/registration/easyreg/main.nf @@ -0,0 +1,82 @@ + + +process REGISTRATION_EASYREG { + tag "$meta.id" + label 'process_single' + label 'process_high' + + container "freesurfer/freesurfer:7.4.1" + + input: + tuple val(meta), path(reference), path(floating), path(ref_segmentation), path(flo_segmentation) + + output: + tuple val(meta), path("*_reference_registered.nii.gz") , emit: ref_reg + tuple val(meta), path("*_floating_registered.nii.gz") , emit: flo_reg + tuple val(meta), path("*_reference_segmentation.nii.gz") , emit: ref_seg, optional: true + tuple val(meta), path("*_floating_segmentation.nii.gz") , emit: flo_seg, optional: true + tuple val(meta), path("*_forward_field.nii.gz") , emit: fwd_field, optional: true + tuple val(meta), path("*_backward_field.nii.gz") , emit: bak_field, optional: true + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + def field = task.ext.field ? "--fwd_field ${prefix}_forward_field.nii.gz --bak_field ${prefix}_backward_field.nii.gz " : "" + def threads = task.ext.threads ? "--threads " + task.ext.threads : "" + def affine = task.ext.affine ? "--affine_only " : "" + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + if [[ -f "$ref_segmentation" ]]; + then + reference_segmentation=$ref_segmentation + else + reference_segmentation="${prefix}_reference_segmentation.nii.gz" + fi + + if [[ -f "$flo_segmentation" ]]; + then + floating_segmentation=$flo_segmentation + else + floating_segmentation="${prefix}_floating_segmentation.nii.gz" + fi + + mri_easyreg \ + --ref $reference --flo $floating \ + --ref_seg \${reference_segmentation} \ + --flo_seg \${floating_segmentation} \ + --flo_reg ${prefix}_floating_registered.nii.gz \ + --ref_reg ${prefix}_reference_registered.nii.gz \ + $field $threads $affine + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + freesurfer: 7.4.1 + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + mri_easyreg -h + + touch ${prefix}_reference_registered.nii.gz + touch ${prefix}_floating_registered.nii.gz + touch ${prefix}_reference_segmentation.nii.gz + touch ${prefix}_floating_segmentation.nii.gz + touch ${prefix}_forward_field.nii.gz + touch ${prefix}_backward_field.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + freesurfer: 7.4.1 + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/registration/easyreg/meta.yml b/modules/nf-neuro/registration/easyreg/meta.yml new file mode 100644 index 0000000..d0325ab --- /dev/null +++ b/modules/nf-neuro/registration/easyreg/meta.yml @@ -0,0 +1,91 @@ +--- +name: "registration_easyreg" +description: Image registration and SynthSeg v2 segmentation with easyreg from freesurfer +keywords: + - nifti + - registration + - segmentation + - easyreg + - 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:'sample1', single_end:false ]` + + - reference: + type: file + description: the reference image in .nii(.gz) or .mgz format (note that, since the method is symmetric, the choice of reference vs floating is arbitrary). + pattern: "*.{nii,nii.gz,mgz}" + + - floating: + type: file + description: the floating image in .nii(.gz) or .mgz format. + pattern: "*.{nii,nii.gz,mgz}" + + - ref_segmentation: + type: file + description: file with the SynthSeg v2 (non-robust) segmentation + parcellation of the reference image. + If it does not exist, EasyReg will create it. If it already exists (e.g., from a previous EasyReg run), then EasyReg will read it from disk (which is faster than segmenting). + pattern: "*.{nii,nii.gz}" + + - flo_segmentation: + type: file + description: file with the SynthSeg v2 (non-robust) segmentation + parcellation of the floating image. + If it does not exist, EasyReg will create it. If it already exists (e.g., from a previous EasyReg run), then EasyReg will read it from disk (which is faster than segmenting). + pattern: "*.{nii,nii.gz}" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1', single_end:false ]` + + - ref_reg: + type: file + description: this is the file where the deformed (registered) reference image is written. + pattern: "*_reference_registered.nii.gz" + + - flo_reg: + type: file + description: this is the file where the deformed (registered) floating image is written. + pattern: "*_floating_registered.nii.gz" + + - ref_seg: + type: file + description: file with the SynthSeg v2 (non-robust) segmentation + parcellation of the reference image. Will produce image only if not passed as input. + pattern: "*_reference_segmentation.nii.gz" + + - flo_seg: + type: file + description: file with the SynthSeg v2 (non-robust) segmentation + parcellation of the floating image. Will produce image only if not passed as input. + pattern: "*_floating_segmentation.nii.gz" + + - fwd_field: + type: file + description: + this is the file where the forward deformation field is written. The deformation includes both the affine and nonlinear components. + Must be a nifti (.nii/.nii.gz) or .mgz file; it is encoded as the real world (RAS) coordinates of the target location for each voxel. + pattern: "*_forward_field.nii.gz" + + - bak_field: + type: file + description: this is the file where the backward deformation field is written. It must also be a nifty or mgz file. + pattern: "*_backward_field.nii.gz" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@ThoumyreStanislas" +maintainers: + - "@ThoumyreStanislas" diff --git a/modules/nf-neuro/registration/easyreg/tests/main.nf.test b/modules/nf-neuro/registration/easyreg/tests/main.nf.test new file mode 100644 index 0000000..68966fa --- /dev/null +++ b/modules/nf-neuro/registration/easyreg/tests/main.nf.test @@ -0,0 +1,72 @@ +nextflow_process { + + name "Test Process REGISTRATION_EASYREG" + script "../main.nf" + process "REGISTRATION_EASYREG" + + tag "modules" + tag "modules_nfcore" + tag "registration" + tag "registration/easyreg" + + 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( [ "freesurfer.zip", "registration.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("registration - easyreg") { + config "./nextflow.config" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1: it.simpleName == "freesurfer" + b0: it.simpleName == "registration" + } + ch_t1 = ch_split_test_data.t1.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/t1.nii.gz") + ] + } + ch_b0 = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/b0.nii.gz"), + [], + [] + ] + } + input[0] = ch_t1 + .join(ch_b0) + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.ref_seg.get(0).get(1)).name, + file(process.out.flo_seg.get(0).get(1)).name, + niftiMD5SUM(process.out.ref_reg.get(0).get(1), 6), + niftiMD5SUM(process.out.flo_reg.get(0).get(1), 6), + niftiMD5SUM(process.out.fwd_field.get(0).get(1), 6), + niftiMD5SUM(process.out.bak_field.get(0).get(1), 6), + process.out.versions + ).match() } + ) + } + } +} diff --git a/modules/nf-neuro/registration/easyreg/tests/main.nf.test.snap b/modules/nf-neuro/registration/easyreg/tests/main.nf.test.snap new file mode 100644 index 0000000..abb8538 --- /dev/null +++ b/modules/nf-neuro/registration/easyreg/tests/main.nf.test.snap @@ -0,0 +1,20 @@ +{ + "registration - easyreg": { + "content": [ + "test_reference_segmentation.nii.gz", + "test_floating_segmentation.nii.gz", + "test_reference_registered.nii.gz:md5:header,c5e41f89848f91c53a9a7be44970d4b1,data,1501221fe23cd62bfdafb33367cadf4d", + "test_floating_registered.nii.gz:md5:header,ec5893cd9ea024e630c4444bd914c331,data,d75ae3fc935cd5cc70ea6797a4c70775", + "test_forward_field.nii.gz:md5:header,0db0a80786ff39864cc17506ed1a0146,data,e4fa6c626729cbf236c34680e5c76df4", + "test_backward_field.nii.gz:md5:header,74c92ee4cd3c4abfecf3da6cdb1d4650,data,8f26afa5c3de21469466213498542486", + [ + "versions.yml:md5,4661210880c42a986923e8257f64c760" + ] + ], + "meta": { + "nf-test": "0.9.0-rc1", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-12T16:01:24.00559" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/registration/easyreg/tests/nextflow.config b/modules/nf-neuro/registration/easyreg/tests/nextflow.config new file mode 100644 index 0000000..71494b4 --- /dev/null +++ b/modules/nf-neuro/registration/easyreg/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + memory = '10G' + withName: "REGISTRATION_EASYREG" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.field = true + ext.affine = true + ext.threads = 1 + } +} diff --git a/modules/nf-neuro/registration/easyreg/tests/tags.yml b/modules/nf-neuro/registration/easyreg/tests/tags.yml new file mode 100644 index 0000000..a39b10e --- /dev/null +++ b/modules/nf-neuro/registration/easyreg/tests/tags.yml @@ -0,0 +1,2 @@ +registration/easyreg: + - "modules/nf-neuro/registration/easyreg/**" diff --git a/modules/nf-neuro/registration/synthregistration/environment.yml b/modules/nf-neuro/registration/synthregistration/environment.yml new file mode 100644 index 0000000..13b71ee --- /dev/null +++ b/modules/nf-neuro/registration/synthregistration/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: registration_synthregistration diff --git a/modules/nf-neuro/registration/synthregistration/main.nf b/modules/nf-neuro/registration/synthregistration/main.nf new file mode 100644 index 0000000..87eb32c --- /dev/null +++ b/modules/nf-neuro/registration/synthregistration/main.nf @@ -0,0 +1,62 @@ +process REGISTRATION_SYNTHREGISTRATION { + tag "$meta.id" + label 'process_high' + + container "freesurfer/synthmorph:4" + containerOptions { + (workflow.containerEngine == 'docker') ? '--entrypoint "" --env PYTHONPATH="/freesurfer/env/lib/python3.11/site-packages"' : "--env PYTHONPATH='/freesurfer/env/lib/python3.11/site-packages'" + } + + input: + tuple val(meta), path(moving), path(fixed) + + output: + tuple val(meta), path("*__output_warped.nii.gz") , emit: warped_image + tuple val(meta), path("*__deform_warp.nii.gz") , emit: warp + tuple val(meta), path("*__affine_warp.lta") , emit: affine + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + + def affine = task.ext.affine ? "-m " + task.ext.affine : "-m affine" + def warp = task.ext.warp ? "-m " + task.ext.warp : "-m deform" + def header = task.ext.header ? "-H" : "" + def gpu = task.ext.gpu ? "-g" : "" + def lambda = task.ext.lambda ? "-r " + task.ext.lambda : "" + def steps = task.ext.steps ? "-n " + task.ext.steps : "" + def extent = task.ext.extent ? "-e " + task.ext.extent : "" + def weight = task.ext.weight ? "-w " + task.ext.weight : "" + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + mri_synthmorph -j $task.cpus ${affine} -t ${prefix}__affine_warp.lta $moving $fixed + mri_synthmorph -j $task.cpus ${warp} ${gpu} ${lambda} ${steps} ${extent} ${weight} -i ${prefix}__affine_warp.lta -t ${prefix}__deform_warp.nii.gz -o ${prefix}__output_warped.nii.gz $moving $fixed + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + synthmoprh: 4 + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + mri_synthmorph -h + + touch ${prefix}__output_warped.nii.gz + touch ${prefix}__deform_warp.nii.gz + touch ${prefix}__affine_warp.lta + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + synthmoprh: 4 + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/registration/synthregistration/meta.yml b/modules/nf-neuro/registration/synthregistration/meta.yml new file mode 100644 index 0000000..6b5f46c --- /dev/null +++ b/modules/nf-neuro/registration/synthregistration/meta.yml @@ -0,0 +1,56 @@ +--- +name: "registration_synthregistration" +description: Perform registration using SynthMorph from Freesurfer. Outputs transforms in Freesurfer format .lta for affine and .nii.gz (synthmorph also supports .mgz) for deform, both in RAS orientation. Conversion to other formats is done using lta_convert and mri_warp_convert respectively, which support a wide range of conversion formats and orientations, such as ANTs ans FSL. To convert the output of this module, use the registration/convert module successively to it. Note that tests using synthmorph are non-reproductible. +keywords: + - Registration + - Brain imaging + - MRI + - Synthetic + - AI + - CNN +tools: + - "Freesurfer": + description: "Freesurfer Synthmorph" + homepage: "https://martinos.org/malte/synthmorph/" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - moving: + type: file + description: Nifti volume moving for registration + pattern: "*.{nii,nii.gz}" + + - fixed: + type: file + description: Nifti volume fixed for registration + pattern: "*.{nii,nii.gz}" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - warped_image: + type: file + description: Warped image + pattern: "*.{nii,nii.gz}" + + - transfo_image: + type: list + description: Tuple, Transformation files to warp images (nii.gz warp, lta file) + pattern: "*.{nii,nii.gz,lta}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@anroy1" diff --git a/modules/nf-neuro/registration/synthregistration/tests/main.nf.test b/modules/nf-neuro/registration/synthregistration/tests/main.nf.test new file mode 100644 index 0000000..33dfb38 --- /dev/null +++ b/modules/nf-neuro/registration/synthregistration/tests/main.nf.test @@ -0,0 +1,56 @@ +nextflow_process { + + name "Test Process REGISTRATION_SYNTHREGISTRATION" + script "../main.nf" + process "REGISTRATION_SYNTHREGISTRATION" + + tag "modules" + tag "modules_nfcore" + tag "registration" + tag "registration/synthregistration" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + // ** Assertion only uses file name since there is a discrepancy when tests are ran remotly. ** // + // ** Synthmorph is an AI model and freesurfer does not control for randomness in computation yet, both in terms of seeding and operation ordering.** // + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "processing.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("registration - synthregistration") { + config "./nextflow.config" + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory + .map{ test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/mni_masked_2x2x2.nii.gz"), + file("\${test_data_directory}/b0_mean.nii.gz") + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.warped_image.get(0).get(1)).name, + file(process.out.warp.get(0).get(1)).name, + file(process.out.affine.get(0).get(1)).name, + process.out.versions + ).match() } + ) + } + } +} diff --git a/modules/nf-neuro/registration/synthregistration/tests/main.nf.test.snap b/modules/nf-neuro/registration/synthregistration/tests/main.nf.test.snap new file mode 100644 index 0000000..6c00cc7 --- /dev/null +++ b/modules/nf-neuro/registration/synthregistration/tests/main.nf.test.snap @@ -0,0 +1,17 @@ +{ + "registration - synthregistration": { + "content": [ + "test__output_warped.nii.gz", + "test__deform_warp.nii.gz", + "test__affine_warp.lta", + [ + "versions.yml:md5,6f946408922f25f1a8182d35615b6d7b" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.04.3" + }, + "timestamp": "2024-12-18T13:10:28.733204" + } +} diff --git a/modules/nf-neuro/registration/synthregistration/tests/nextflow.config b/modules/nf-neuro/registration/synthregistration/tests/nextflow.config new file mode 100644 index 0000000..58ab598 --- /dev/null +++ b/modules/nf-neuro/registration/synthregistration/tests/nextflow.config @@ -0,0 +1,10 @@ +process { + withName: "REGISTRATION_SYNTHREGISTRATION" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + memory = "40G" + ext.affine = "affine" + ext.warp = "deform" + ext.lambda = 0.9 + ext.steps = 9 + } +} diff --git a/modules/nf-neuro/registration/synthregistration/tests/tags.yml b/modules/nf-neuro/registration/synthregistration/tests/tags.yml new file mode 100644 index 0000000..e06c6bf --- /dev/null +++ b/modules/nf-neuro/registration/synthregistration/tests/tags.yml @@ -0,0 +1,2 @@ +registration/synthregistration: + - "modules/nf-neuro/registration/synthregistration/**" diff --git a/modules/nf-neuro/segmentation/fastseg/environment.yml b/modules/nf-neuro/segmentation/fastseg/environment.yml new file mode 100644 index 0000000..e9adba9 --- /dev/null +++ b/modules/nf-neuro/segmentation/fastseg/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: segmentation_fastseg diff --git a/modules/nf-scil/segmentation/fastseg/main.nf b/modules/nf-neuro/segmentation/fastseg/main.nf similarity index 69% rename from modules/nf-scil/segmentation/fastseg/main.nf rename to modules/nf-neuro/segmentation/fastseg/main.nf index 0725c15..339ee44 100644 --- a/modules/nf-scil/segmentation/fastseg/main.nf +++ b/modules/nf-neuro/segmentation/fastseg/main.nf @@ -1,14 +1,13 @@ - process SEGMENTATION_FASTSEG { tag "$meta.id" label 'process_single' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: - tuple val(meta), path(image) + tuple val(meta), path(image), path(lesion) output: tuple val(meta), path("*mask_wm.nii.gz") , emit: wm_mask @@ -32,16 +31,21 @@ process SEGMENTATION_FASTSEG { fast -t 1 -n 3\ -H 0.1 -I 4 -l 20.0 -g -o t1.nii.gz $image - scil_image_math.py convert t1_seg_2.nii.gz ${prefix}__mask_wm.nii.gz --data_type uint8 - scil_image_math.py convert t1_seg_1.nii.gz ${prefix}__mask_gm.nii.gz --data_type uint8 - scil_image_math.py convert t1_seg_0.nii.gz ${prefix}__mask_csf.nii.gz --data_type uint8 + 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 + if [[ -f "$lesion" ]]; + then + scil_volume_math.py union ${prefix}__mask_wm.nii.gz $lesion ${prefix}__mask_wm.nii.gz --data_type uint8 -f + fi + cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: 2.0.2 fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') END_VERSIONS """ @@ -50,9 +54,6 @@ process SEGMENTATION_FASTSEG { def prefix = task.ext.prefix ?: "${meta.id}" """ - fast -h - scil_image_math.py -h - touch ${prefix}__mask_wm.nii.gz touch ${prefix}__mask_gm.nii.gz touch ${prefix}__mask_csf.nii.gz @@ -62,8 +63,18 @@ process SEGMENTATION_FASTSEG { cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: 2.0.0 fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') END_VERSIONS + + function handle_code () { + local code=\$? + ignore=( 1 ) + exit \$([[ " \${ignore[@]} " =~ " \$code " ]] && echo 0 || echo \$code) + } + trap 'handle_code' ERR + + fast -h + scil_volume_math.py -h """ } diff --git a/modules/nf-scil/segmentation/fastseg/meta.yml b/modules/nf-neuro/segmentation/fastseg/meta.yml similarity index 83% rename from modules/nf-scil/segmentation/fastseg/meta.yml rename to modules/nf-neuro/segmentation/fastseg/meta.yml index 67f383c..84c0e1f 100644 --- a/modules/nf-scil/segmentation/fastseg/meta.yml +++ b/modules/nf-neuro/segmentation/fastseg/meta.yml @@ -1,7 +1,7 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "segmentation_fastseg" -description: Perform Brain Tissues Segmentation using FSL fast on a T1 image. +description: Perform Brain Tissues Segmentation using FSL fast on a T1 image. Optionally, a binary mask of lesion can be add to correct the white matter mask. keywords: - Segmentation - T1 @@ -26,6 +26,11 @@ input: description: Nifti T1 volume to segment into tissue maps. pattern: "*.{nii,nii.gz}" + - lesion: + type: file + description: Nifti lesion volume to correct the white matter with a lesion mask. The lesion mask must be a binary mask. + pattern: "*.{nii,nii.gz}" + output: - meta: type: map diff --git a/modules/nf-neuro/segmentation/fastseg/tests/main.nf.test b/modules/nf-neuro/segmentation/fastseg/tests/main.nf.test new file mode 100644 index 0000000..c588800 --- /dev/null +++ b/modules/nf-neuro/segmentation/fastseg/tests/main.nf.test @@ -0,0 +1,113 @@ +nextflow_process { + + name "Test Process SEGMENTATION_FASTSEG" + script "../main.nf" + process "SEGMENTATION_FASTSEG" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "segmentation" + tag "segmentation/fastseg" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + config "./nextflow.config" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "T1w.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("segmentation - fastseg") { + 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}/T1w.nii.gz", checkIfExists: true,), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + niftiMD5SUM(process.out.wm_mask.get(0).get(1)), + niftiMD5SUM(process.out.gm_mask.get(0).get(1)), + niftiMD5SUM(process.out.csf_mask.get(0).get(1)), + niftiMD5SUM(process.out.wm_map.get(0).get(1)), + niftiMD5SUM(process.out.gm_map.get(0).get(1)), + niftiMD5SUM(process.out.csf_map.get(0).get(1)), + process.out.versions + ).match() } + ) + } + } + test("segmentation - fastseg - lesion") { + 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}/T1w.nii.gz", checkIfExists: true,), + file("\${test_data_directory}/T1w_mask.nii.gz", checkIfExists: true) + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + niftiMD5SUM(process.out.wm_mask.get(0).get(1)), + niftiMD5SUM(process.out.gm_mask.get(0).get(1)), + niftiMD5SUM(process.out.csf_mask.get(0).get(1)), + niftiMD5SUM(process.out.wm_map.get(0).get(1)), + niftiMD5SUM(process.out.gm_map.get(0).get(1)), + niftiMD5SUM(process.out.csf_map.get(0).get(1)), + process.out.versions + ).match() } + ) + } + } + test("segmentation - fastseg - stub-run") { + options "-stub-run" + 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}/T1w.nii.gz", checkIfExists: true,), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.versions).match() } + ) + } + } +} diff --git a/modules/nf-neuro/segmentation/fastseg/tests/main.nf.test.snap b/modules/nf-neuro/segmentation/fastseg/tests/main.nf.test.snap new file mode 100644 index 0000000..f3ab1eb --- /dev/null +++ b/modules/nf-neuro/segmentation/fastseg/tests/main.nf.test.snap @@ -0,0 +1,50 @@ +{ + "segmentation - fastseg - lesion": { + "content": [ + "test__mask_wm.nii.gz:md5:header,18e02970ee42367e0ee9f81b36cde160,data,e4d00ba5559d32119050e6afe77b627c", + "test__mask_gm.nii.gz:md5:header,cb2d88714675fc7d8c450de4db5227c6,data,d713974dbe87760182112e10f96a7f82", + "test__mask_csf.nii.gz:md5:header,cb2d88714675fc7d8c450de4db5227c6,data,29599fab55e3ee75318cef36563684a4", + "test__map_wm.nii.gz:md5:header,58dc3623694060dace951ba8073d5e01,data,62a268dbc74814d62ed9f9596b5a72e5", + "test__map_gm.nii.gz:md5:header,58dc3623694060dace951ba8073d5e01,data,420e7664f81a5899f7007f71c08f28d8", + "test__map_csf.nii.gz:md5:header,58dc3623694060dace951ba8073d5e01,data,2bde307c6dae36a5fc00eba7a2ca7933", + [ + "versions.yml:md5,d42da2620c5aa82292375b0f9d8d7092" + ] + ], + "meta": { + "nf-test": "0.9.0-rc1", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-04T20:12:08.928978" + }, + "segmentation - fastseg": { + "content": [ + "test__mask_wm.nii.gz:md5:header,cb2d88714675fc7d8c450de4db5227c6,data,08b5c8665f0a07c182eb63f045d01855", + "test__mask_gm.nii.gz:md5:header,cb2d88714675fc7d8c450de4db5227c6,data,d713974dbe87760182112e10f96a7f82", + "test__mask_csf.nii.gz:md5:header,cb2d88714675fc7d8c450de4db5227c6,data,29599fab55e3ee75318cef36563684a4", + "test__map_wm.nii.gz:md5:header,58dc3623694060dace951ba8073d5e01,data,62a268dbc74814d62ed9f9596b5a72e5", + "test__map_gm.nii.gz:md5:header,58dc3623694060dace951ba8073d5e01,data,420e7664f81a5899f7007f71c08f28d8", + "test__map_csf.nii.gz:md5:header,58dc3623694060dace951ba8073d5e01,data,2bde307c6dae36a5fc00eba7a2ca7933", + [ + "versions.yml:md5,d42da2620c5aa82292375b0f9d8d7092" + ] + ], + "meta": { + "nf-test": "0.9.0-rc1", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-04T20:03:24.730234" + }, + "segmentation - fastseg - stub-run": { + "content": [ + [ + "versions.yml:md5,e9c2afb5537207544dc1b54cbdc389a4" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.1" + }, + "timestamp": "2024-12-12T14:01:36.729872" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/segmentation/fastseg/tests/nextflow.config b/modules/nf-neuro/segmentation/fastseg/tests/nextflow.config new file mode 100644 index 0000000..0293c16 --- /dev/null +++ b/modules/nf-neuro/segmentation/fastseg/tests/nextflow.config @@ -0,0 +1,3 @@ +process { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } +} diff --git a/modules/nf-neuro/segmentation/fastseg/tests/tags.yml b/modules/nf-neuro/segmentation/fastseg/tests/tags.yml new file mode 100644 index 0000000..db7f18c --- /dev/null +++ b/modules/nf-neuro/segmentation/fastseg/tests/tags.yml @@ -0,0 +1,2 @@ +segmentation/fastseg: + - "modules/nf-neuro/segmentation/fastseg/**" diff --git a/modules/nf-neuro/segmentation/freesurferseg/environment.yml b/modules/nf-neuro/segmentation/freesurferseg/environment.yml new file mode 100644 index 0000000..a2fd55f --- /dev/null +++ b/modules/nf-neuro/segmentation/freesurferseg/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: segmentation_freesurferseg diff --git a/modules/nf-scil/segmentation/freesurferseg/main.nf b/modules/nf-neuro/segmentation/freesurferseg/main.nf old mode 100755 new mode 100644 similarity index 60% rename from modules/nf-scil/segmentation/freesurferseg/main.nf rename to modules/nf-neuro/segmentation/freesurferseg/main.nf index 2cee624..06b1b1c --- a/modules/nf-scil/segmentation/freesurferseg/main.nf +++ b/modules/nf-neuro/segmentation/freesurferseg/main.nf @@ -1,14 +1,13 @@ - process SEGMENTATION_FREESURFERSEG { tag "$meta.id" label 'process_single' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: - tuple val(meta), path(aparc_aseg), path(wmparc) + tuple val(meta), path(aparc_aseg), path(wmparc), path(lesion) output: tuple val(meta), path("*__mask_wm.nii.gz") , emit: wm_mask @@ -34,22 +33,22 @@ process SEGMENTATION_FREESURFERSEG { mrconvert -datatype int16 $aparc_aseg aparc+aseg_int16.nii.gz -force -nthreads 1 mrconvert -datatype int16 $wmparc wmparc_int16.nii.gz -force -nthreads 1 - scil_split_volume_by_labels.py wmparc_int16.nii.gz --scilpy_lut freesurfer_desikan_killiany --out_dir wmparc_desikan - scil_split_volume_by_labels.py wmparc_int16.nii.gz --scilpy_lut freesurfer_subcortical --out_dir wmparc_subcortical - scil_split_volume_by_labels.py aparc+aseg_int16.nii.gz --scilpy_lut freesurfer_subcortical --out_dir aparc+aseg_subcortical + scil_labels_split_volume_from_lut.py wmparc_int16.nii.gz --scilpy_lut freesurfer_desikan_killiany --out_dir wmparc_desikan + scil_labels_split_volume_from_lut.py wmparc_int16.nii.gz --scilpy_lut freesurfer_subcortical --out_dir wmparc_subcortical + scil_labels_split_volume_from_lut.py aparc+aseg_int16.nii.gz --scilpy_lut freesurfer_subcortical --out_dir aparc+aseg_subcortical - scil_image_math.py union wmparc_desikan/*\ + scil_volume_math.py union wmparc_desikan/*\ wmparc_subcortical/right-cerebellum-cortex.nii.gz\ wmparc_subcortical/left-cerebellum-cortex.nii.gz\ mask_cortex_m.nii.gz -f - scil_image_math.py union wmparc_subcortical/corpus-callosum-*\ + scil_volume_math.py union wmparc_subcortical/corpus-callosum-*\ aparc+aseg_subcortical/*white-matter*\ wmparc_subcortical/brain-stem.nii.gz\ aparc+aseg_subcortical/*ventraldc*\ mask_wm_m.nii.gz -f - scil_image_math.py union wmparc_subcortical/*thalamus*\ + scil_volume_math.py union wmparc_subcortical/*thalamus*\ wmparc_subcortical/*putamen*\ wmparc_subcortical/*pallidum*\ wmparc_subcortical/*hippocampus*\ @@ -59,31 +58,36 @@ process SEGMENTATION_FREESURFERSEG { wmparc_subcortical/*plexus*\ mask_nuclei_m.nii.gz -f - scil_image_math.py union wmparc_subcortical/*-lateral-ventricle.nii.gz\ + scil_volume_math.py union wmparc_subcortical/*-lateral-ventricle.nii.gz\ wmparc_subcortical/*-inferior-lateral-ventricle.nii.gz\ wmparc_subcortical/cerebrospinal-fluid.nii.gz\ wmparc_subcortical/*th-ventricle.nii.gz\ mask_csf_1_m.nii.gz -f - scil_image_math.py lower_threshold mask_wm_m.nii.gz 0.1\ + scil_volume_math.py lower_threshold mask_wm_m.nii.gz 0.1\ ${prefix}__mask_wm_bin.nii.gz -f - scil_image_math.py lower_threshold mask_cortex_m.nii.gz 0.1\ + scil_volume_math.py lower_threshold mask_cortex_m.nii.gz 0.1\ ${prefix}__mask_gm.nii.gz -f - scil_image_math.py lower_threshold mask_nuclei_m.nii.gz 0.1\ + scil_volume_math.py lower_threshold mask_nuclei_m.nii.gz 0.1\ ${prefix}__mask_nuclei_bin.nii.gz -f - scil_image_math.py lower_threshold mask_csf_1_m.nii.gz 0.1\ + scil_volume_math.py lower_threshold mask_csf_1_m.nii.gz 0.1\ ${prefix}__mask_csf.nii.gz -f - scil_image_math.py addition ${prefix}__mask_wm_bin.nii.gz\ + scil_volume_math.py addition ${prefix}__mask_wm_bin.nii.gz\ ${prefix}__mask_nuclei_bin.nii.gz\ ${prefix}__mask_wm.nii.gz --data_type int16 - scil_image_math.py convert ${prefix}__mask_wm.nii.gz ${prefix}__mask_wm.nii.gz --data_type uint8 -f - scil_image_math.py convert ${prefix}__mask_gm.nii.gz ${prefix}__mask_gm.nii.gz --data_type uint8 -f - scil_image_math.py convert ${prefix}__mask_csf.nii.gz ${prefix}__mask_csf.nii.gz --data_type uint8 -f + scil_volume_math.py convert ${prefix}__mask_wm.nii.gz ${prefix}__mask_wm.nii.gz --data_type uint8 -f + scil_volume_math.py convert ${prefix}__mask_gm.nii.gz ${prefix}__mask_gm.nii.gz --data_type uint8 -f + scil_volume_math.py convert ${prefix}__mask_csf.nii.gz ${prefix}__mask_csf.nii.gz --data_type uint8 -f + + if [[ -f "$lesion" ]]; + then + scil_volume_math.py union ${prefix}__mask_wm.nii.gz $lesion ${prefix}__mask_wm.nii.gz --data_type uint8 -f + fi cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(mrconvert -version 2>&1 | sed -n 's/== mrconvert \\([0-9.]\\+\\).*/\\1/p') END_VERSIONS """ @@ -92,8 +96,8 @@ process SEGMENTATION_FREESURFERSEG { def prefix = task.ext.prefix ?: "${meta.id}" """ mrconvert -h - scil_image_math.py -h - scil_split_volume_by_labels.py -h + scil_volume_math.py -h + scil_labels_split_volume_from_lut.py -h touch ${prefix}__mask_wm.nii.gz touch ${prefix}__mask_gm.nii.gz @@ -101,7 +105,7 @@ process SEGMENTATION_FREESURFERSEG { cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(mrconvert -version 2>&1 | sed -n 's/== mrconvert \\([0-9.]\\+\\).*/\\1/p') END_VERSIONS """ diff --git a/modules/nf-scil/segmentation/freesurferseg/meta.yml b/modules/nf-neuro/segmentation/freesurferseg/meta.yml old mode 100755 new mode 100644 similarity index 82% rename from modules/nf-scil/segmentation/freesurferseg/meta.yml rename to modules/nf-neuro/segmentation/freesurferseg/meta.yml index cb1663a..ff48d50 --- a/modules/nf-scil/segmentation/freesurferseg/meta.yml +++ b/modules/nf-neuro/segmentation/freesurferseg/meta.yml @@ -1,6 +1,6 @@ --- name: "segmentation_freesurferseg" -description: Performs Brain Tissues Segmentation using the FreeSurfer aparc and wmparc output files. +description: Performs Brain Tissues Segmentation using the FreeSurfer aparc and wmparc output files. Optionally, a binary mask of lesion can be add to correct the white matter mask. keywords: - FreeSurfer - Segmentation @@ -30,6 +30,11 @@ input: description: FreeSurfer WM parcellation output file. pattern: "*.mgz" + - lesion: + type: file + description: Nifti lesion volume to correct the white matter with a lesion mask. The lesion mask must be a binary mask. + pattern: "*.{nii,nii.gz}" + output: - meta: type: map diff --git a/modules/nf-neuro/segmentation/freesurferseg/tests/main.nf.test b/modules/nf-neuro/segmentation/freesurferseg/tests/main.nf.test new file mode 100644 index 0000000..425e474 --- /dev/null +++ b/modules/nf-neuro/segmentation/freesurferseg/tests/main.nf.test @@ -0,0 +1,76 @@ +nextflow_process { + + name "Test Process SEGMENTATION_FREESURFERSEG" + script "../main.nf" + process "SEGMENTATION_FREESURFERSEG" + + tag "modules" + tag "modules_nfcore" + tag "segmentation" + tag "segmentation/freesurferseg" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + config "./nextflow.config" + + 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("segmentation - freesurferseg") { + 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}/freesurfer/aparc_aseg.nii.gz", checkIfExists: true), + file("\${test_data_directory}/freesurfer/wmparc.nii.gz", checkIfExists: true), + [] + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("segmentation - freesurferseg - lesion") { + 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}/freesurfer/aparc_aseg.nii.gz", checkIfExists: true), + file("\${test_data_directory}/freesurfer/wmparc.nii.gz", checkIfExists: true), + [], + ] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-scil/segmentation/fastseg/tests/main.nf.test.snap b/modules/nf-neuro/segmentation/freesurferseg/tests/main.nf.test.snap similarity index 58% rename from modules/nf-scil/segmentation/fastseg/tests/main.nf.test.snap rename to modules/nf-neuro/segmentation/freesurferseg/tests/main.nf.test.snap index c22d56d..d2c78d1 100644 --- a/modules/nf-scil/segmentation/fastseg/tests/main.nf.test.snap +++ b/modules/nf-neuro/segmentation/freesurferseg/tests/main.nf.test.snap @@ -1,5 +1,5 @@ { - "segmentation - fastseg": { + "segmentation - freesurferseg - lesion": { "content": [ { "0": [ @@ -8,7 +8,7 @@ "id": "test", "single_end": false }, - "test__mask_wm.nii.gz:md5,f849e268007a747714e342162f13f631" + "test__mask_wm.nii.gz:md5,2dc635526adb6ef185300acc326ead13" ] ], "1": [ @@ -17,7 +17,7 @@ "id": "test", "single_end": false }, - "test__mask_gm.nii.gz:md5,39396421c6d1d32ea7943a5ec7a49dce" + "test__mask_gm.nii.gz:md5,247106ed727133078fe262b0b8244071" ] ], "2": [ @@ -26,102 +26,119 @@ "id": "test", "single_end": false }, - "test__mask_csf.nii.gz:md5,d0fbc088059f17faff38fbcd33257d27" + "test__mask_csf.nii.gz:md5,b92db7e454c95afadea320a143912ce8" ] ], "3": [ + "versions.yml:md5,6aaa50346ccfbb298945a071cd3df37e" + ], + "csf_mask": [ [ { "id": "test", "single_end": false }, - "test__map_wm.nii.gz:md5,1d5a9655d4e3c832b14c83e9424b00fc" + "test__mask_csf.nii.gz:md5,b92db7e454c95afadea320a143912ce8" ] ], - "4": [ + "gm_mask": [ [ { "id": "test", "single_end": false }, - "test__map_gm.nii.gz:md5,f7e51eb14af645c687db5d2c6a4f5bf4" + "test__mask_gm.nii.gz:md5,247106ed727133078fe262b0b8244071" ] ], - "5": [ + "versions": [ + "versions.yml:md5,6aaa50346ccfbb298945a071cd3df37e" + ], + "wm_mask": [ [ { "id": "test", "single_end": false }, - "test__map_csf.nii.gz:md5,9c14d8f70e69d641d9c214d0d52b3663" + "test__mask_wm.nii.gz:md5,2dc635526adb6ef185300acc326ead13" ] - ], - "6": [ - "versions.yml:md5,6accf12cf290105dc058afe63197fec7" - ], - "csf_map": [ + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-11-22T18:04:33.444424121" + }, + "segmentation - freesurferseg": { + "content": [ + { + "0": [ [ { "id": "test", "single_end": false }, - "test__map_csf.nii.gz:md5,9c14d8f70e69d641d9c214d0d52b3663" + "test__mask_wm.nii.gz:md5,2dc635526adb6ef185300acc326ead13" ] ], - "csf_mask": [ + "1": [ [ { "id": "test", "single_end": false }, - "test__mask_csf.nii.gz:md5,d0fbc088059f17faff38fbcd33257d27" + "test__mask_gm.nii.gz:md5,247106ed727133078fe262b0b8244071" ] ], - "gm_map": [ + "2": [ [ { "id": "test", "single_end": false }, - "test__map_gm.nii.gz:md5,f7e51eb14af645c687db5d2c6a4f5bf4" + "test__mask_csf.nii.gz:md5,b92db7e454c95afadea320a143912ce8" ] ], - "gm_mask": [ + "3": [ + "versions.yml:md5,6aaa50346ccfbb298945a071cd3df37e" + ], + "csf_mask": [ [ { "id": "test", "single_end": false }, - "test__mask_gm.nii.gz:md5,39396421c6d1d32ea7943a5ec7a49dce" + "test__mask_csf.nii.gz:md5,b92db7e454c95afadea320a143912ce8" ] ], - "versions": [ - "versions.yml:md5,6accf12cf290105dc058afe63197fec7" - ], - "wm_map": [ + "gm_mask": [ [ { "id": "test", "single_end": false }, - "test__map_wm.nii.gz:md5,1d5a9655d4e3c832b14c83e9424b00fc" + "test__mask_gm.nii.gz:md5,247106ed727133078fe262b0b8244071" ] ], + "versions": [ + "versions.yml:md5,6aaa50346ccfbb298945a071cd3df37e" + ], "wm_mask": [ [ { "id": "test", "single_end": false }, - "test__mask_wm.nii.gz:md5,f849e268007a747714e342162f13f631" + "test__mask_wm.nii.gz:md5,2dc635526adb6ef185300acc326ead13" ] ] } ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.04.4" }, - "timestamp": "2024-04-08T17:01:49.065542" + "timestamp": "2024-11-22T18:02:15.492951853" } } \ No newline at end of file diff --git a/modules/nf-neuro/segmentation/freesurferseg/tests/nextflow.config b/modules/nf-neuro/segmentation/freesurferseg/tests/nextflow.config new file mode 100644 index 0000000..0293c16 --- /dev/null +++ b/modules/nf-neuro/segmentation/freesurferseg/tests/nextflow.config @@ -0,0 +1,3 @@ +process { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } +} diff --git a/modules/nf-neuro/segmentation/freesurferseg/tests/tags.yml b/modules/nf-neuro/segmentation/freesurferseg/tests/tags.yml new file mode 100644 index 0000000..3e2b9f8 --- /dev/null +++ b/modules/nf-neuro/segmentation/freesurferseg/tests/tags.yml @@ -0,0 +1,2 @@ +segmentation/freesurferseg: + - "modules/nf-neuro/segmentation/freesurferseg/**" diff --git a/modules/nf-neuro/segmentation/synthseg/environment.yml b/modules/nf-neuro/segmentation/synthseg/environment.yml new file mode 100644 index 0000000..2e10848 --- /dev/null +++ b/modules/nf-neuro/segmentation/synthseg/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: segmentation_synthseg diff --git a/modules/nf-neuro/segmentation/synthseg/main.nf b/modules/nf-neuro/segmentation/synthseg/main.nf new file mode 100644 index 0000000..a74c365 --- /dev/null +++ b/modules/nf-neuro/segmentation/synthseg/main.nf @@ -0,0 +1,169 @@ +process SEGMENTATION_SYNTHSEG { + tag "$meta.id" + label 'process_medium' + + container "freesurfer/freesurfer:7.4.1" + + input: + tuple val(meta), path(image), path(lesion), path(fs_license) + + output: + 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 + tuple val(meta), path("*__seg.nii.gz") , emit: seg, optional: true + tuple val(meta), path("*__aparc_aseg.nii.gz") , emit: aparc_aseg, optional: true + tuple val(meta), path("*__resampled_image.nii.gz") , emit: resample, optional: true + tuple val(meta), path("*__volume.csv") , emit: volume, optional: true + tuple val(meta), path("*__qc_score.csv") , emit: qc_score, optional: true + 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 gpu = task.ext.gpu ? "" : "--cpu" + def gm_parc = task.ext.gm_parc ? "--parc" : "" + def robust = task.ext.robust ? "--robust" : "" + def fast = task.ext.fast ? "--fast" : "" + def ct = task.ext.ct ? "--ct" : "" + def output_resample = task.ext.output_resample ? "--resample ${prefix}__resampled_image.nii.gz" : "" + def output_volume = task.ext.output_volume ? "--vol ${prefix}__volume.csv" : "" + def output_qc_score = task.ext.output_qc_score ? "--qc ${prefix}__qc_score.csv" : "" + def crop = task.ext.crop ? "--crop " + task.ext.crop: "" + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + cp $fs_license \$FREESURFER_HOME/license.txt + + mri_synthseg --i $image --o seg.nii.gz --threads $task.cpus --post posteriors.nii.gz $gpu $robust $fast $ct $output_resample $output_volume $output_qc_score $crop + + if [[ -n "$gm_parc" ]]; + then + # Cortical grey matter parcellation + mv seg.nii.gz ${prefix}__aparc_aseg.nii.gz + + # WM Mask + mri_binarize --i ${prefix}__aparc_aseg.nii.gz \ + --match 2 7 10 12 13 16 28 41 46 49 51 52 60 \ + --o ${prefix}__mask_wm.nii.gz + + # GM Mask + mri_binarize --i ${prefix}__aparc_aseg.nii.gz \ + --match 8 11 17 18 26 47 50 53 54 58 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 \ + --o ${prefix}__mask_gm.nii.gz + + # CSF Mask + mri_binarize --i ${prefix}__aparc_aseg.nii.gz \ + --match 4 5 14 15 24 43 44 \ + --o ${prefix}__mask_csf.nii.gz + + else + mv seg.nii.gz ${prefix}__seg.nii.gz + + # WM Mask + mri_binarize --i ${prefix}__seg.nii.gz \ + --match 2 7 10 12 13 16 28 41 46 49 51 52 60 \ + --o ${prefix}__mask_wm.nii.gz + + # GM Mask + mri_binarize --i ${prefix}__seg.nii.gz \ + --match 3 8 11 17 18 26 42 47 50 53 54 58 \ + --o ${prefix}__mask_gm.nii.gz + + # CSF Mask + mri_binarize --i ${prefix}__seg.nii.gz \ + --match 4 5 14 15 24 43 44 \ + --o ${prefix}__mask_csf.nii.gz + fi + + # Posteriors 4D slices associated with wm, gm and csf masks. + wm_map_indices=(1 5 7 9 10 13 18 19 23 25 27 28 32) + gm_map_indices=(2 6 8 14 15 17 20 24 26 29 30 31) + csf_map_indices=(3 4 11 12 16 21 22) + + # Generating concat command + wm_map_command="mri_concat --sum --o ${prefix}__map_wm.nii.gz" + gm_map_command="mri_concat --sum --o ${prefix}__map_gm.nii.gz" + csf_map_command="mri_concat --sum --o ${prefix}__map_csf.nii.gz" + + # Extracting wm slices for wm map + for i in "\${wm_map_indices[@]}"; do + mri_convert -nth "\$i" posteriors.nii.gz wm_map_slice_\${i}.nii.gz + wm_map_command="\$wm_map_command --i wm_map_slice_\${i}.nii.gz" + done + + # Extracting gm slices for gm map + for i in "\${gm_map_indices[@]}"; do + mri_convert -nth "\$i" posteriors.nii.gz gm_map_slice_\${i}.nii.gz + gm_map_command="\$gm_map_command --i gm_map_slice_\${i}.nii.gz" + done + + # Extracting csf slices for csf map + for i in "\${csf_map_indices[@]}"; do + mri_convert -nth "\$i" posteriors.nii.gz csf_map_slice_\${i}.nii.gz + csf_map_command="\$csf_map_command --i csf_map_slice_\${i}.nii.gz" + done + + eval \${wm_map_command} + eval \${gm_map_command} + eval \${csf_map_command} + + rm wm_map_slice_*.nii.gz + rm gm_map_slice_*.nii.gz + rm csf_map_slice_*.nii.gz + + if [[ -f "$lesion" ]]; + then + mri_binarize --i ${prefix}__mask_wm.nii.gz --merge $lesion --min 0.5 --o ${prefix}__mask_wm.nii.gz + fi + + mri_convert -i ${prefix}__mask_wm.nii.gz --out_data_type uchar -o ${prefix}__mask_wm.nii.gz + mri_convert -i ${prefix}__mask_gm.nii.gz --out_data_type uchar -o ${prefix}__mask_gm.nii.gz + mri_convert -i ${prefix}__mask_csf.nii.gz --out_data_type uchar -o ${prefix}__mask_csf.nii.gz + + rm \$FREESURFER_HOME/license.txt + + 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 args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + mri_synthseg -h + mri_binarize -h + mri_convert -h + + touch ${prefix}__mask_wm.nii.gz + touch ${prefix}__mask_gm.nii.gz + touch ${prefix}__mask_csf.nii.gz + touch ${prefix}__map_wm.nii.gz + touch ${prefix}__map_gm.nii.gz + touch ${prefix}__map_csf.nii.gz + touch ${prefix}__seg.nii.gz + touch ${prefix}__aparc_aseg.nii.gz + touch ${prefix}__resampled_image.nii.gz + touch ${prefix}__volume.csv + touch ${prefix}__qc_score.csv + + + 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 + """ +} diff --git a/modules/nf-neuro/segmentation/synthseg/meta.yml b/modules/nf-neuro/segmentation/synthseg/meta.yml new file mode 100644 index 0000000..67ad014 --- /dev/null +++ b/modules/nf-neuro/segmentation/synthseg/meta.yml @@ -0,0 +1,107 @@ +name: "segmentation_synthseg" +description: Perform Brain Tissues Segmentation using Freesurfer synthseg on a T1 image. Optionally, a binary mask of lesion can be add to correct the white matter mask. Note that tests using synthseg are non-reproductible. +keywords: + - Segmentation + - Freesurfer + - Tissues + - Synthetic + - AI + - CNN +tools: + - "Freesurfer": + description: "An open source neuroimaging toolkit for processing, analyzing, and visualizing human brain MR images." + homepage: "https://surfer.nmr.mgh.harvard.edu/" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1', single_end:false ]` + + - image: + type: file + description: Nifti T1 volume to segment into tissue maps. + pattern: "*.{nii,nii.gz}" + + - lesion: + type: file + description: Nifti lesion volume to correct the white matter with a lesion mask. The lesion mask must be a binary mask. + pattern: "*.{nii,nii.gz}" + + - fs_license: + type: file + description: The path to your FreeSurfer license. To get one, go to https://surfer.nmr.mgh.harvard.edu/registration.html. Optional. If you have already set your license as prescribed by Freesurfer (copied to a .license file in your $FREESURFER_HOME), this is not required. + pattern: "*.txt" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1', single_end:false ]` + + - wm_mask: + type: file + description: Nifti WM mask volume. + pattern: "*.{nii,nii.gz}" + + - gm_mask: + type: file + description: Nifti GM mask volume. + pattern: "*.{nii,nii.gz}" + + - csf_mask: + type: file + description: Nifti CSF mask volume. + pattern: "*.{nii,nii.gz}" + + - wm_map: + type: file + description: Nifti WM map volume. + pattern: "*.{nii,nii.gz}" + + - gm_map: + type: file + description: Nifti GM map volume. + pattern: "*.{nii,nii.gz}" + + - csf_map: + type: file + description: Nifti CSF map volume. + pattern: "*.{nii,nii.gz}" + + - seg: + type: file + description: (optional) Nifti cortical segmentation volume. + pattern: "*.{nii,nii.gz}" + + - aparc_aseg: + type: file + description: (optional) Nifti cortical parcellation volume and segmentation. + pattern: "*.{nii,nii.gz}" + + - resample: + type: file + description: (optional) in order to return segmentations at 1mm resolution, the input images are internally resampled (except if they already are at 1mm). Use this optional flag to save the resampled images. This must be the same type as --i. + pattern: "*.{nii,nii.gz}" + + - volume: + type: file + description: (optional) Output CSV file with volumes for all structures and subjects. + pattern: "*.csv" + + - qc_score: + type: file + description: (optional) Output CSV file with qc scores for all subjects. + pattern: "*.csv" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@anroy1" +maintainers: + - "@anroy1" diff --git a/modules/nf-neuro/segmentation/synthseg/tests/main.nf.test b/modules/nf-neuro/segmentation/synthseg/tests/main.nf.test new file mode 100644 index 0000000..c53287c --- /dev/null +++ b/modules/nf-neuro/segmentation/synthseg/tests/main.nf.test @@ -0,0 +1,171 @@ +nextflow_process { + + name "Test Process SEGMENTATION_SYNTHSEG" + script "../main.nf" + process "SEGMENTATION_SYNTHSEG" + + tag "modules" + tag "modules_nfcore" + tag "segmentation" + tag "segmentation/synthseg" + + 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", "freesurfer.zip", "processing.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + +// ** Assertion only uses file name since there is a discrepancy when tests are ran remotly. ** // +// ** Synthseg's direct output was emitted as an output to help assess its non-reproductibility ** // +// ** Synthseg is an AI model and freesurfer does not control for randomness in computation yet, both in terms of seeding and operation ordering.** // + + test("segmentation - synthseg - basic") { + config "./nextflow_basic.config" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + heavy: it.simpleName == "heavy" + freesurfer: it.simpleName == "freesurfer" + } + ch_anat = ch_split_test_data.heavy.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/anat/anat_image.nii.gz"), + [] + ] + } + ch_license = ch_split_test_data.freesurfer.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/license.txt") + ] + } + input[0] = ch_anat + .join(ch_license) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.wm_mask.get(0).get(1)).name, + file(process.out.gm_mask.get(0).get(1)).name, + file(process.out.csf_mask.get(0).get(1)).name, + file(process.out.wm_map.get(0).get(1)).name, + file(process.out.gm_map.get(0).get(1)).name, + file(process.out.csf_map.get(0).get(1)).name, + file(process.out.seg.get(0).get(1)).name, + process.out.versions + ).match() } + ) + } + } + + test("segmentation - synthseg - options") { + config "./nextflow_options.config" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + processing: it.simpleName == "processing" + freesurfer: it.simpleName == "freesurfer" + } + ch_anat = ch_split_test_data.processing.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/mni_masked_2x2x2.nii.gz"), + [] + ] + } + ch_license = ch_split_test_data.freesurfer.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/license.txt") + ] + } + input[0] = ch_anat + .join(ch_license) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.wm_mask.get(0).get(1)).name, + file(process.out.gm_mask.get(0).get(1)).name, + file(process.out.csf_mask.get(0).get(1)).name, + file(process.out.wm_map.get(0).get(1)).name, + file(process.out.gm_map.get(0).get(1)).name, + file(process.out.csf_map.get(0).get(1)).name, + file(process.out.aparc_aseg.get(0).get(1)).name, + niftiMD5SUM(process.out.resample.get(0).get(1)), + process.out.volume, + process.out.qc_score, + process.out.versions + ).match() } + ) + } + } + + test("segmentation - synthseg - lesion") { + config "./nextflow_basic.config" + when { + process { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + heavy: it.simpleName == "heavy" + freesurfer: it.simpleName == "freesurfer" + } + ch_anat = ch_split_test_data.heavy.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/anat/anat_image.nii.gz"), + file("\${test_data_directory}/anat/anat_mask.nii.gz") + ] + } + ch_license = ch_split_test_data.freesurfer.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/license.txt") + ] + } + input[0] = ch_anat + .join(ch_license) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.wm_mask.get(0).get(1)).name, + file(process.out.gm_mask.get(0).get(1)).name, + file(process.out.csf_mask.get(0).get(1)).name, + file(process.out.wm_map.get(0).get(1)).name, + file(process.out.gm_map.get(0).get(1)).name, + file(process.out.csf_map.get(0).get(1)).name, + file(process.out.seg.get(0).get(1)).name, + process.out.versions + ).match() } + ) + } + } +} diff --git a/modules/nf-neuro/segmentation/synthseg/tests/main.nf.test.snap b/modules/nf-neuro/segmentation/synthseg/tests/main.nf.test.snap new file mode 100644 index 0000000..bb1c838 --- /dev/null +++ b/modules/nf-neuro/segmentation/synthseg/tests/main.nf.test.snap @@ -0,0 +1,66 @@ +{ + "segmentation - synthseg - options": { + "content": [ + "test__mask_wm.nii.gz", + "test__mask_gm.nii.gz", + "test__mask_csf.nii.gz", + "test__map_wm.nii.gz", + "test__map_gm.nii.gz", + "test__map_csf.nii.gz", + "test__aparc_aseg.nii.gz", + "test__resampled_image.nii.gz:md5:header,728dfc231a3dfcf567ac640930f68629,data,1f02837025def8b0701f79549c2955e1", + [ + + ], + [ + + ], + [ + "versions.yml:md5,b65418e9ce2ab1cce590b1f659e18c1d" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-12T03:42:10.76796495" + }, + "segmentation - synthseg - basic": { + "content": [ + "test__mask_wm.nii.gz", + "test__mask_gm.nii.gz", + "test__mask_csf.nii.gz", + "test__map_wm.nii.gz", + "test__map_gm.nii.gz", + "test__map_csf.nii.gz", + "test__seg.nii.gz", + [ + "versions.yml:md5,b65418e9ce2ab1cce590b1f659e18c1d" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-12T03:35:07.078841663" + }, + "segmentation - synthseg - lesion": { + "content": [ + "test__mask_wm.nii.gz", + "test__mask_gm.nii.gz", + "test__mask_csf.nii.gz", + "test__map_wm.nii.gz", + "test__map_gm.nii.gz", + "test__map_csf.nii.gz", + "test__seg.nii.gz", + [ + "versions.yml:md5,b65418e9ce2ab1cce590b1f659e18c1d" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-12T03:50:05.836734582" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/segmentation/synthseg/tests/nextflow_basic.config b/modules/nf-neuro/segmentation/synthseg/tests/nextflow_basic.config new file mode 100644 index 0000000..286dd59 --- /dev/null +++ b/modules/nf-neuro/segmentation/synthseg/tests/nextflow_basic.config @@ -0,0 +1,5 @@ +process { + memory = "16G" + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.fast = true +} diff --git a/modules/nf-neuro/segmentation/synthseg/tests/nextflow_options.config b/modules/nf-neuro/segmentation/synthseg/tests/nextflow_options.config new file mode 100644 index 0000000..4f2ddb9 --- /dev/null +++ b/modules/nf-neuro/segmentation/synthseg/tests/nextflow_options.config @@ -0,0 +1,10 @@ +process { + memory = "16G" + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.robust = true + ext.output_resample = true + ext.output_vol = true + ext.output_qc = true + ext.crop = 150 + ext.gm_parc = true +} diff --git a/modules/nf-neuro/segmentation/synthseg/tests/tags.yml b/modules/nf-neuro/segmentation/synthseg/tests/tags.yml new file mode 100644 index 0000000..d6461c7 --- /dev/null +++ b/modules/nf-neuro/segmentation/synthseg/tests/tags.yml @@ -0,0 +1,2 @@ +segmentation/synthseg: + - "modules/nf-neuro/segmentation/synthseg/**" diff --git a/modules/nf-neuro/tracking/localtracking/environment.yml b/modules/nf-neuro/tracking/localtracking/environment.yml new file mode 100644 index 0000000..11b9ffd --- /dev/null +++ b/modules/nf-neuro/tracking/localtracking/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: tracking_localtracking diff --git a/modules/nf-scil/tracking/localtracking/main.nf b/modules/nf-neuro/tracking/localtracking/main.nf old mode 100755 new mode 100644 similarity index 73% rename from modules/nf-scil/tracking/localtracking/main.nf rename to modules/nf-neuro/tracking/localtracking/main.nf index 6a607b5..279bb2e --- a/modules/nf-scil/tracking/localtracking/main.nf +++ b/modules/nf-neuro/tracking/localtracking/main.nf @@ -1,11 +1,10 @@ process TRACKING_LOCALTRACKING { 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_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: tuple val(meta), path(wm), path(fodf), path(fa) @@ -15,6 +14,8 @@ process TRACKING_LOCALTRACKING { tuple val(meta), path("*__local_tracking_config.json"), emit: config tuple val(meta), path("*__local_seeding_mask.nii.gz"), emit: seedmask tuple val(meta), path("*__local_tracking_mask.nii.gz"), emit: trackmask + tuple val(meta), path("*__local_tracking_mqc.png"), emit: mqc, optional: true + tuple val(meta), path("*__local_tracking_stats.json"), emit: global_mqc, optional: true path "versions.yml" , emit: versions when: @@ -43,40 +44,47 @@ process TRACKING_LOCALTRACKING { def gpu_batch_size = task.ext.gpu_batch_size ? "--batch_size " + task.ext.gpu_batch_size : "" def enable_gpu = task.ext.enable_gpu ? "--use_gpu $gpu_batch_size" : "" + def run_qc = task.ext.run_qc ? task.ext.run_qc : false + """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 if [ "${local_tracking_mask}" == "wm" ]; then - scil_image_math.py convert $wm ${prefix}__local_tracking_mask.nii.gz \ + scil_volume_math.py convert $wm ${prefix}__local_tracking_mask.nii.gz \ --data_type uint8 -f + cp $wm tmp_anat_qc.nii.gz + elif [ "${local_tracking_mask}" == "fa" ]; then - scil_image_math.py lower_threshold $fa \ + scil_volume_math.py lower_threshold $fa \ $local_fa_tracking_mask_threshold \ ${prefix}__local_tracking_mask.nii.gz \ --data_type uint8 -f + cp $fa tmp_anat_qc.nii.gz fi if [ "${local_seeding_mask}" == "wm" ]; then - scil_image_math.py convert $wm ${prefix}__local_seeding_mask.nii.gz \ + scil_volume_math.py convert $wm ${prefix}__local_seeding_mask.nii.gz \ --data_type uint8 -f + elif [ "${local_seeding_mask}" == "fa" ]; then - scil_image_math.py lower_threshold $fa \ + scil_volume_math.py lower_threshold $fa \ $local_fa_seeding_mask_threshold \ ${prefix}__local_seeding_mask.nii.gz \ --data_type uint8 -f fi - scil_compute_local_tracking.py $fodf ${prefix}__local_seeding_mask.nii.gz ${prefix}__local_tracking_mask.nii.gz tmp.trk $enable_gpu\ + scil_tracking_local.py $fodf ${prefix}__local_seeding_mask.nii.gz \ + ${prefix}__local_tracking_mask.nii.gz tmp.trk $enable_gpu\ $local_algo $local_seeding $local_nbr_seeds\ $local_random_seed $local_step $local_theta\ $local_sfthres $local_min_len\ - $local_max_len $compress $basis + $local_max_len $compress $basis -f - scil_remove_invalid_streamlines.py tmp.trk\ + scil_tractogram_remove_invalid.py tmp.trk\ ${prefix}__local_tracking.trk\ - --remove_single_point + --remove_single_point -f cat <<-TRACKING_INFO > ${prefix}__local_tracking_config.json {"algorithm": "${task.ext.local_algo}", @@ -97,27 +105,37 @@ process TRACKING_LOCALTRACKING { "sh_basis": "${task.ext.basis}"} TRACKING_INFO + if $run_qc; + then + scil_viz_bundle_screenshot_mosaic.py tmp_anat_qc.nii.gz ${prefix}__local_tracking.trk\ + ${prefix}__local_tracking_mqc.png --opacity_background 1 --light_screenshot + scil_tractogram_print_info.py ${prefix}__local_tracking.trk >> ${prefix}__local_tracking_stats.json + fi + rm -f tmp_anat_qc.nii.gz + cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ stub: def prefix = task.ext.prefix ?: "${meta.id}" """ - scil_compute_local_tracking.py -h - scil_remove_invalid_streamlines.py -h - scil_image_math -h + scil_tracking_local.py -h + scil_tractogram_remove_invalid.py -h + scil_volume_math.py -h touch ${prefix}__local_tracking.trk touch ${prefix}__local_tracking_config.json touch ${prefix}__local_seeding_mask.nii.gz touch ${prefix}__local_tracking_mask.nii.gz + touch ${prefix}__local_tracking_mqc.png + touch ${prefix}__local_tracking_stats.json cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ } diff --git a/modules/nf-scil/tracking/localtracking/meta.yml b/modules/nf-neuro/tracking/localtracking/meta.yml old mode 100755 new mode 100644 similarity index 80% rename from modules/nf-scil/tracking/localtracking/meta.yml rename to modules/nf-neuro/tracking/localtracking/meta.yml index 7d9fbea..e83e255 --- a/modules/nf-scil/tracking/localtracking/meta.yml +++ b/modules/nf-neuro/tracking/localtracking/meta.yml @@ -6,7 +6,7 @@ keywords: - Local tracking - Diffusion MRI tools: - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" @@ -59,6 +59,16 @@ output: description: Nifti tracking mask pattern: "*.{nii,nii.gz}" + - mqc: + type: file + description: PNG file containing quality control image for tracking. Made for use in MultiQC report. + pattern: "*local_tracking_mqc.png" + + - global_mqc: + type: file + description: JSON file containing basic measurements of whole tractogram. Made for use in MultiQC report. + pattern: "*local_tracking_stats.json" + - versions: type: file description: File containing software versions diff --git a/modules/nf-neuro/tracking/localtracking/tests/main.nf.test b/modules/nf-neuro/tracking/localtracking/tests/main.nf.test new file mode 100644 index 0000000..ad99a14 --- /dev/null +++ b/modules/nf-neuro/tracking/localtracking/tests/main.nf.test @@ -0,0 +1,83 @@ +nextflow_process { + + name "Test Process TRACKING_LOCALTRACKING" + script "../main.nf" + process "TRACKING_LOCALTRACKING" + + tag "modules" + tag "modules_nfcore" + tag "tracking" + tag "tracking/localtracking" + + 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( [ "tracking.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("tracking - localtracking - fa") { + config "./nextflow_fa.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}/map_wm.nii.gz"), + file("\${test_data_directory}/fodf.nii.gz"), + file("\${test_data_directory}/fa.nii.gz") + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.trk.get(0).get(1)).name, + process.out.config, + process.out.seedmask, + process.out.trackmask, + process.out.versions).match() } + ) + } + } + + test("tracking - localtracking - wm") { + config "./nextflow_wm.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}/map_wm.nii.gz"), + file("\${test_data_directory}/fodf.nii.gz"), + file("\${test_data_directory}/fa.nii.gz") + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.trk.get(0).get(1)).name, + process.out.config, + process.out.seedmask, + process.out.trackmask, + process.out.versions).match() } + ) + } + } + +} diff --git a/modules/nf-neuro/tracking/localtracking/tests/main.nf.test.snap b/modules/nf-neuro/tracking/localtracking/tests/main.nf.test.snap new file mode 100644 index 0000000..85d15ec --- /dev/null +++ b/modules/nf-neuro/tracking/localtracking/tests/main.nf.test.snap @@ -0,0 +1,82 @@ +{ + "tracking - localtracking - wm": { + "content": [ + "test__local_tracking.trk", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__local_tracking_config.json:md5,5eac37e6262a85d8d1b33c92e1f35796" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__local_seeding_mask.nii.gz:md5,6180aeda681e4443e56287ef1e1aee83" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__local_tracking_mask.nii.gz:md5,6180aeda681e4443e56287ef1e1aee83" + ] + ], + [ + "versions.yml:md5,0088e7190e395324acbb8fce5c474750" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T19:38:16.835402429" + }, + "tracking - localtracking - fa": { + "content": [ + "test__local_tracking.trk", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__local_tracking_config.json:md5,a082f8d2f2b92cb3f2912a796942a786" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__local_seeding_mask.nii.gz:md5,0484a2e44ff153caa79819a8646cc33c" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__local_tracking_mask.nii.gz:md5,0484a2e44ff153caa79819a8646cc33c" + ] + ], + [ + "versions.yml:md5,0088e7190e395324acbb8fce5c474750" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T19:37:54.047881093" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/tracking/localtracking/tests/nextflow_fa.config b/modules/nf-neuro/tracking/localtracking/tests/nextflow_fa.config new file mode 100644 index 0000000..ba45d25 --- /dev/null +++ b/modules/nf-neuro/tracking/localtracking/tests/nextflow_fa.config @@ -0,0 +1,21 @@ +process { + withName: "TRACKING_LOCALTRACKING" { + ext.local_fa_tracking_mask_threshold = 0.1 + ext.local_fa_seeding_mask_threshold = 0.1 + ext.local_random_seed = 1234 + ext.local_compress_streamlines = true + ext.local_compress_value = 0.2 + ext.local_algo = "prob" + ext.local_seeding = "nt" + ext.local_nbr_seeds = 100 + ext.local_step = 1.0 + ext.local_seeding_mask_type = "fa" + ext.local_tracking_mask_type = "fa" + ext.local_theta = 20 + ext.local_sfthres = 0.1 + ext.local_min_len = 3 + ext.local_max_len = 40 + ext.basis = "descoteaux07" + ext.run_qc = true + } +} diff --git a/modules/nf-neuro/tracking/localtracking/tests/nextflow_wm.config b/modules/nf-neuro/tracking/localtracking/tests/nextflow_wm.config new file mode 100644 index 0000000..0bf106f --- /dev/null +++ b/modules/nf-neuro/tracking/localtracking/tests/nextflow_wm.config @@ -0,0 +1,20 @@ +process { + withName: "TRACKING_LOCALTRACKING" { + ext.local_fa_tracking_mask_threshold = 0.1 + ext.local_random_seed = 1234 + ext.local_compress_streamlines = true + ext.local_compress_value = 0.2 + ext.local_algo = "prob" + ext.local_seeding = "nt" + ext.local_nbr_seeds = 100 + ext.local_step = 1.0 + ext.local_seeding_mask_type = "wm" + ext.local_tracking_mask_type = "wm" + ext.local_theta = 20 + ext.local_sfthres = 0.1 + ext.local_min_len = 3 + ext.local_max_len = 40 + ext.basis = "descoteaux07" + ext.run_qc = true + } +} diff --git a/modules/nf-neuro/tracking/localtracking/tests/tags.yml b/modules/nf-neuro/tracking/localtracking/tests/tags.yml new file mode 100644 index 0000000..a0d0e9c --- /dev/null +++ b/modules/nf-neuro/tracking/localtracking/tests/tags.yml @@ -0,0 +1,2 @@ +tracking/localtracking: + - "modules/nf-neuro/tracking/localtracking/**" diff --git a/modules/nf-neuro/tracking/pfttracking/environment.yml b/modules/nf-neuro/tracking/pfttracking/environment.yml new file mode 100644 index 0000000..5802940 --- /dev/null +++ b/modules/nf-neuro/tracking/pfttracking/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: tracking_pfttracking diff --git a/modules/nf-scil/tracking/pfttracking/main.nf b/modules/nf-neuro/tracking/pfttracking/main.nf old mode 100755 new mode 100644 similarity index 74% rename from modules/nf-scil/tracking/pfttracking/main.nf rename to modules/nf-neuro/tracking/pfttracking/main.nf index 09086b0..021bb76 --- a/modules/nf-scil/tracking/pfttracking/main.nf +++ b/modules/nf-neuro/tracking/pfttracking/main.nf @@ -1,23 +1,23 @@ process TRACKING_PFTTRACKING { 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_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: tuple val(meta), path(wm), path(gm), path(csf), path(fodf), path(fa) output: - tuple val(meta), path("*__pft_tracking.trk") , emit: trk tuple val(meta), path("*__pft_tracking_config.json") , emit: config - tuple val(meta), path("*__map_include.nii.gz") , emit: include - tuple val(meta), path("*__map_exclude.nii.gz") , emit: exclude + tuple val(meta), path("*__map_include.nii.gz") , emit: includes + tuple val(meta), path("*__map_exclude.nii.gz") , emit: excludes tuple val(meta), path("*__pft_seeding_mask.nii.gz") , emit: seeding + tuple val(meta), path("*__pft_tracking_mqc.png") , emit: mqc, optional: true + tuple val(meta), path("*__pft_tracking_stats.json") , emit: global_mqc, optional: true path "versions.yml" , emit: versions when: @@ -45,40 +45,44 @@ process TRACKING_PFTTRACKING { def pft_front = task.ext.pft_front ? "--forward " + task.ext.pft_front : "" def basis = task.ext.basis ? "--sh_basis " + task.ext.basis : "" + def run_qc = task.ext.run_qc ? task.ext.run_qc : false + """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 - scil_compute_maps_for_particle_filter_tracking.py $wm $gm $csf \ + scil_tracking_pft_maps.py $wm $gm $csf \ --include ${prefix}__map_include.nii.gz \ --exclude ${prefix}__map_exclude.nii.gz \ --interface ${prefix}__interface.nii.gz -f + cp $wm tmp_anat_qc.nii.gz + if [ "${pft_seeding_mask}" == "wm" ]; then - scil_image_math.py convert $wm ${prefix}__mask_wm.nii.gz \ + scil_volume_math.py convert $wm ${prefix}__pft_seeding_mask.nii.gz \ --data_type uint8 -f - scil_image_math.py union ${prefix}__mask_wm.nii.gz \ - ${prefix}__interface.nii.gz ${prefix}__pft_seeding_mask.nii.gz\ + scil_volume_math.py union ${prefix}__pft_seeding_mask.nii.gz \ + ${prefix}__interface.nii.gz ${prefix}__pft_seeding_mask.nii.gz \ --data_type uint8 -f elif [ "${pft_seeding_mask}" == "interface" ]; then cp ${prefix}__interface.nii.gz ${prefix}__pft_seeding_mask.nii.gz elif [ "${pft_seeding_mask}" == "fa" ]; then - mrcalc $fa $pft_fa_threshold -ge ${prefix}__pft_seeding_mask.nii.gz\ + mrcalc $fa $pft_fa_threshold -ge ${prefix}__pft_seeding_mask.nii.gz \ -datatype uint8 -force fi - scil_compute_pft.py $fodf ${prefix}__pft_seeding_mask.nii.gz \ - ${prefix}__map_include.nii.gz ${prefix}__map_exclude.nii.gz tmp.trk\ + scil_tracking_pft.py $fodf ${prefix}__pft_seeding_mask.nii.gz \ + ${prefix}__map_include.nii.gz ${prefix}__map_exclude.nii.gz tmp.trk \ $pft_algo $pft_seeding_type $pft_nbr_seeds \ - $pft_random_seed $pft_step $pft_theta\ - $pft_sfthres $pft_sfthres_init $pft_min_len $pft_max_len\ - $pft_particles $pft_back $pft_front $compress $basis + $pft_random_seed $pft_step $pft_theta \ + $pft_sfthres $pft_sfthres_init $pft_min_len $pft_max_len \ + $pft_particles $pft_back $pft_front $compress $basis -f - scil_remove_invalid_streamlines.py tmp.trk ${prefix}__pft_tracking.trk\ - --remove_single_point + scil_tractogram_remove_invalid.py tmp.trk ${prefix}__pft_tracking.trk \ + --remove_single_point -f cat <<-TRACKING_INFO > ${prefix}__pft_tracking_config.json {"algorithm": "${task.ext.pft_algo}", @@ -100,9 +104,17 @@ process TRACKING_PFTTRACKING { "sh_basis": "${task.ext.basis}"} TRACKING_INFO + if $run_qc; + then + scil_viz_bundle_screenshot_mosaic.py tmp_anat_qc.nii.gz ${prefix}__pft_tracking.trk\ + ${prefix}__pft_tracking_mqc.png --opacity_background 1 --light_screenshot + scil_tractogram_print_info.py ${prefix}__pft_tracking.trk >> ${prefix}__pft_tracking_stats.json + fi + rm -f tmp_anat_qc.nii.gz + cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(mrcalc -version 2>&1 | sed -n 's/== mrcalc \\([0-9.]\\+\\).*/\\1/p') END_VERSIONS """ @@ -111,21 +123,23 @@ process TRACKING_PFTTRACKING { def prefix = task.ext.prefix ?: "${meta.id}" """ - scil_compute_pft.py -h - scil_compute_maps_for_particle_filter_tracking.py -h - scil_image_math.py -h + scil_tracking_pft.py -h + scil_tracking_pft_maps.py -h + scil_volume_math.py -h mrcalc -h - scil_remove_invalid_streamlines.py -h + scil_tractogram_remove_invalid.py -h touch ${prefix}__map_include.nii.gz touch ${prefix}__map_exclude.nii.gz touch ${prefix}__pft_seeding_mask.nii.gz touch ${prefix}__pft_tracking.trk touch ${prefix}__pft_tracking_config.json + touch ${prefix}__pft_tracking_mqc.png + touch ${prefix}__pft_tracking_stats.json cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: \$(pip list | grep scilpy | tr -s ' ' | cut -d' ' -f2) mrtrix: \$(mrcalc -version 2>&1 | sed -n 's/== mrcalc \\([0-9.]\\+\\).*/\\1/p') END_VERSIONS """ diff --git a/modules/nf-scil/tracking/pfttracking/meta.yml b/modules/nf-neuro/tracking/pfttracking/meta.yml old mode 100755 new mode 100644 similarity index 84% rename from modules/nf-scil/tracking/pfttracking/meta.yml rename to modules/nf-neuro/tracking/pfttracking/meta.yml index e28c245..9194df2 --- a/modules/nf-scil/tracking/pfttracking/meta.yml +++ b/modules/nf-neuro/tracking/pfttracking/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "tracking_pfttracking" description: Compute include and exclude maps, and the seeding mask from partial volume estimation (PVE) maps. @@ -15,7 +15,7 @@ tools: description: "DIPY is the paragon 3D/4D+ imaging library in Python." homepage: https://dipy.org doi: "10.1016/j.neuroimage.2014.04.074" - - "Scilpy": + - "scilpy": description: "The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox." homepage: "https://github.com/scilus/scilpy.git" @@ -68,12 +68,12 @@ output: description: Json file containing tracking parameters. pattern: "*.{json}" - - include: + - includes: type: file description: Nifti probability map for tracking of ending the streamline and including it in the output. pattern: "*.{nii,nii.gz}" - - exclude: + - excludes: type: file description: Nifti probability map for tracking of ending the streamline and excluding it in the output. pattern: "*.{nii,nii.gz}" @@ -83,6 +83,16 @@ output: description: Nifti seeding mask for tracking. pattern: "*.{nii,nii.gz}" + - mqc: + type: file + description: PNG file containing quality control image for tracking. Made for use in MultiQC report. + pattern: "*pft_tracking_mqc.png" + + - global_mqc: + type: file + description: JSON file containing basic measurements of whole tractogram. Made for use in MultiQC report. + pattern: "*pft_tracking_stats.json" + - versions: type: file description: File containing software versions diff --git a/modules/nf-neuro/tracking/pfttracking/tests/main.nf.test b/modules/nf-neuro/tracking/pfttracking/tests/main.nf.test new file mode 100644 index 0000000..5558ca1 --- /dev/null +++ b/modules/nf-neuro/tracking/pfttracking/tests/main.nf.test @@ -0,0 +1,119 @@ +nextflow_process { + + name "Test Process TRACKING_PFTTRACKING" + script "../main.nf" + process "TRACKING_PFTTRACKING" + + tag "modules" + tag "modules_nfcore" + tag "tracking" + tag "tracking/pfttracking" + + 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( [ "tracking.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("tracking - pfttracking - wm") { + config "./nextflow_wm.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}/map_wm.nii.gz"), + file("\${test_data_directory}/map_gm.nii.gz"), + file("\${test_data_directory}/map_csf.nii.gz"), + file("\${test_data_directory}/fodf.nii.gz"), + [] + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.trk.get(0).get(1)).name, + process.out.config, + process.out.includes, + process.out.excludes, + process.out.seeding, + process.out.versions).match() } + ) + } + } + + test("tracking - pfttracking - fa") { + config "./nextflow_fa.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}/map_wm.nii.gz"), + file("\${test_data_directory}/map_gm.nii.gz"), + file("\${test_data_directory}/map_csf.nii.gz"), + file("\${test_data_directory}/fodf.nii.gz"), + file("\${test_data_directory}/fa.nii.gz") + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.trk.get(0).get(1)).name, + process.out.config, + process.out.includes, + process.out.excludes, + process.out.seeding, + process.out.versions).match() } + ) + } + } + + test("tracking - pfttracking - interface") { + config "./nextflow_interface.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}/map_wm.nii.gz"), + file("\${test_data_directory}/map_gm.nii.gz"), + file("\${test_data_directory}/map_csf.nii.gz"), + file("\${test_data_directory}/fodf.nii.gz"), + [] + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.trk.get(0).get(1)).name, + process.out.config, + process.out.includes, + process.out.excludes, + process.out.versions, + process.out.seeding).match() } + ) + } + } +} diff --git a/modules/nf-neuro/tracking/pfttracking/tests/main.nf.test.snap b/modules/nf-neuro/tracking/pfttracking/tests/main.nf.test.snap new file mode 100644 index 0000000..290bbb8 --- /dev/null +++ b/modules/nf-neuro/tracking/pfttracking/tests/main.nf.test.snap @@ -0,0 +1,149 @@ +{ + "tracking - pfttracking - fa": { + "content": [ + "test__pft_tracking.trk", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__pft_tracking_config.json:md5,d8a7d811f0c9bec571e8e583b0aecb94" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__map_include.nii.gz:md5,b974759b87b8d920195d93f86219f2ee" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__map_exclude.nii.gz:md5,2876363d0394ea01534738650d78f441" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__pft_seeding_mask.nii.gz:md5,fd41bd41399bd7234721715e6ecca018" + ] + ], + [ + "versions.yml:md5,2ae3399d07ef00610deae7988357dd55" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T19:53:53.332859167" + }, + "tracking - pfttracking - interface": { + "content": [ + "test__pft_tracking.trk", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__pft_tracking_config.json:md5,7e326ef48dfda3d63e288f3f49d29847" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__map_include.nii.gz:md5,b974759b87b8d920195d93f86219f2ee" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__map_exclude.nii.gz:md5,2876363d0394ea01534738650d78f441" + ] + ], + [ + "versions.yml:md5,2ae3399d07ef00610deae7988357dd55" + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__pft_seeding_mask.nii.gz:md5,4c9bb24b937fa8ee77923877f2ee6fd1" + ] + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T19:54:04.257171663" + }, + "tracking - pfttracking - wm": { + "content": [ + "test__pft_tracking.trk", + [ + [ + { + "id": "test", + "single_end": false + }, + "test__pft_tracking_config.json:md5,bedf3ac8834d78389b3b58809f0bb750" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__map_include.nii.gz:md5,b974759b87b8d920195d93f86219f2ee" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__map_exclude.nii.gz:md5,2876363d0394ea01534738650d78f441" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test__pft_seeding_mask.nii.gz:md5,4710e63db72f7dda7c10e46346bf98d3" + ] + ], + [ + "versions.yml:md5,2ae3399d07ef00610deae7988357dd55" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-24T19:53:38.784232647" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/tracking/pfttracking/tests/nextflow_fa.config b/modules/nf-neuro/tracking/pfttracking/tests/nextflow_fa.config new file mode 100644 index 0000000..352d151 --- /dev/null +++ b/modules/nf-neuro/tracking/pfttracking/tests/nextflow_fa.config @@ -0,0 +1,23 @@ +process { + withName: "TRACKING_PFTTRACKING" { + ext.pft_fa_seeding_mask_threshold = 0.1 + ext.pft_random_seed = 1234 + ext.pft_compress_streamlines = true + ext.pft_compress_value = 0.2 + ext.pft_algo = "prob" + ext.pft_seeding = "nt" + ext.pft_nbr_seeds = 100 + ext.pft_step = 0.5 + ext.pft_seeding_mask_type = "fa" + ext.pft_theta = 20 + ext.pft_sfthres = 0.1 + ext.pft_sfthres_init = 0.5 + ext.pft_min_len = 5 + ext.pft_max_len = 40 + ext.pft_particles = 5 + ext.pft_back = 2 + ext.pft_front = 1 + ext.basis = "descoteaux07" + ext.run_qc = true + } +} diff --git a/modules/nf-neuro/tracking/pfttracking/tests/nextflow_interface.config b/modules/nf-neuro/tracking/pfttracking/tests/nextflow_interface.config new file mode 100644 index 0000000..2a8b705 --- /dev/null +++ b/modules/nf-neuro/tracking/pfttracking/tests/nextflow_interface.config @@ -0,0 +1,22 @@ +process { + withName: "TRACKING_PFTTRACKING" { + ext.pft_random_seed = 1234 + ext.pft_compress_streamlines = true + ext.pft_compress_value = 0.2 + ext.pft_algo = "prob" + ext.pft_seeding = "nt" + ext.pft_nbr_seeds = 100 + ext.pft_step = 0.5 + ext.pft_seeding_mask_type = "interface" + ext.pft_theta = 20 + ext.pft_sfthres = 0.1 + ext.pft_sfthres_init = 0.5 + ext.pft_min_len = 5 + ext.pft_max_len = 40 + ext.pft_particles = 5 + ext.pft_back = 2 + ext.pft_front = 1 + ext.basis = "descoteaux07" + ext.run_qc = true + } +} diff --git a/modules/nf-neuro/tracking/pfttracking/tests/nextflow_wm.config b/modules/nf-neuro/tracking/pfttracking/tests/nextflow_wm.config new file mode 100644 index 0000000..1792688 --- /dev/null +++ b/modules/nf-neuro/tracking/pfttracking/tests/nextflow_wm.config @@ -0,0 +1,22 @@ +process { + withName: "TRACKING_PFTTRACKING" { + ext.pft_random_seed = 1234 + ext.pft_compress_streamlines = true + ext.pft_compress_value = 0.2 + ext.pft_algo = "prob" + ext.pft_seeding = "nt" + ext.pft_nbr_seeds = 100 + ext.pft_step = 0.5 + ext.pft_seeding_mask_type = "wm" + ext.pft_theta = 20 + ext.pft_sfthres = 0.1 + ext.pft_sfthres_init = 0.5 + ext.pft_min_len = 5 + ext.pft_max_len = 40 + ext.pft_particles = 5 + ext.pft_back = 2 + ext.pft_front = 1 + ext.basis = "descoteaux07" + ext.run_qc = true + } +} diff --git a/modules/nf-neuro/tracking/pfttracking/tests/tags.yml b/modules/nf-neuro/tracking/pfttracking/tests/tags.yml new file mode 100644 index 0000000..d4bdf83 --- /dev/null +++ b/modules/nf-neuro/tracking/pfttracking/tests/tags.yml @@ -0,0 +1,2 @@ +tracking/pfttracking: + - "modules/nf-neuro/tracking/pfttracking/**" diff --git a/modules/nf-neuro/utils/extractb0/environment.yml b/modules/nf-neuro/utils/extractb0/environment.yml new file mode 100644 index 0000000..fe2a686 --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: utils_extractb0 diff --git a/modules/nf-scil/utils/extractb0/main.nf b/modules/nf-neuro/utils/extractb0/main.nf old mode 100755 new mode 100644 similarity index 73% rename from modules/nf-scil/utils/extractb0/main.nf rename to modules/nf-neuro/utils/extractb0/main.nf index 51a2b44..ce8ebab --- a/modules/nf-scil/utils/extractb0/main.nf +++ b/modules/nf-neuro/utils/extractb0/main.nf @@ -1,12 +1,10 @@ - - process UTILS_EXTRACTB0 { tag "$meta.id" label 'process_single' container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" + 'https://scil.usherbrooke.ca/containers/scilus_2.0.2.sif': + 'scilus/scilus:2.0.2' }" input: tuple val(meta), path(dwi), path(bval), path(bvec) @@ -21,19 +19,19 @@ process UTILS_EXTRACTB0 { script: def prefix = task.ext.prefix ?: "${meta.id}" def extraction_strategy = task.ext.b0_extraction_strategy ? "--$task.ext.b0_extraction_strategy" : "--mean" - def b0_threshold = task.ext.b0_threshold ? "--b0_thr $task.ext.b0_threshold" : "" + def b0_threshold = task.ext.b0_threshold ? "--b0_threshold $task.ext.b0_threshold" : "" def output_series = task.ext.output_series ? "" : "--single-image" """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 - scil_extract_b0.py $dwi $bval $bvec ${prefix}_b0.nii.gz \ - $output_series $extraction_strategy $b0_threshold --force_b0_threshold + scil_dwi_extract_b0.py $dwi $bval $bvec ${prefix}_b0.nii.gz \ + $output_series $extraction_strategy $b0_threshold --skip_b0_check cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: 2.0.1 END_VERSIONS """ @@ -42,13 +40,13 @@ process UTILS_EXTRACTB0 { def prefix = task.ext.prefix ?: "${meta.id}" """ - scil_extract_b0.py - h + scil_dwi_extract_b0.py - h touch ${prefix}_b0.nii.gz cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: 1.6.0 + scilpy: 2.0.1 END_VERSIONS """ } diff --git a/modules/nf-scil/utils/extractb0/meta.yml b/modules/nf-neuro/utils/extractb0/meta.yml old mode 100755 new mode 100644 similarity index 96% rename from modules/nf-scil/utils/extractb0/meta.yml rename to modules/nf-neuro/utils/extractb0/meta.yml index dc40fb9..cbb5a39 --- a/modules/nf-scil/utils/extractb0/meta.yml +++ b/modules/nf-neuro/utils/extractb0/meta.yml @@ -1,5 +1,5 @@ --- -# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-scil/main/modules/meta-schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json name: "utils_extractb0" description: Extract a b0 volume from a DWI image. keywords: diff --git a/modules/nf-neuro/utils/extractb0/tests/main.nf.test b/modules/nf-neuro/utils/extractb0/tests/main.nf.test new file mode 100644 index 0000000..9f6fae8 --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/tests/main.nf.test @@ -0,0 +1,164 @@ +nextflow_process { + + name "Test Process UTILS_EXTRACTB0" + script "../main.nf" + process "UTILS_EXTRACTB0" + + tag "modules" + tag "modules_nfcore" + tag "utils" + tag "utils/extractb0" + + 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( [ "topup_eddy_light.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("utils - extractb0") { + config "./nextflow.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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true) + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("utils - extractb0 - all4d") { + config "./nextflow_all4d.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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true) + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("utils - extractb0 - allseries") { + config "./nextflow_allseries.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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true) + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("utils - extractb0 - clusterfirst") { + config "./nextflow_clusterfirst.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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true) + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("utils - extractb0 - clustermean") { + config "./nextflow_clustermean.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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true) + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("utils - extractb0 - mean") { + config "./nextflow_mean.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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), + file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true) + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-neuro/utils/extractb0/tests/main.nf.test.snap b/modules/nf-neuro/utils/extractb0/tests/main.nf.test.snap new file mode 100644 index 0000000..356cc98 --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/tests/main.nf.test.snap @@ -0,0 +1,212 @@ +{ + "utils - extractb0 - mean": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,91288914a80e7866ef8b08a363d617f9" + ] + ], + "1": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ], + "b0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,91288914a80e7866ef8b08a363d617f9" + ] + ], + "versions": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-09T00:14:29.554229" + }, + "utils - extractb0 - all4d": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,2066111f2b964735a168d6c653ba1a8b" + ] + ], + "1": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ], + "b0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,2066111f2b964735a168d6c653ba1a8b" + ] + ], + "versions": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-09T00:13:58.708201" + }, + "utils - extractb0": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,91288914a80e7866ef8b08a363d617f9" + ] + ], + "1": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ], + "b0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,91288914a80e7866ef8b08a363d617f9" + ] + ], + "versions": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-09T00:13:51.838198" + }, + "utils - extractb0 - clustermean": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,feb9010e5b72fc6f0c814f9dd54cb710" + ] + ], + "1": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ], + "b0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,feb9010e5b72fc6f0c814f9dd54cb710" + ] + ], + "versions": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-09T00:14:22.350869" + }, + "utils - extractb0 - clusterfirst": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,27e4aa847d60b2154d8865d19ba3e06e" + ] + ], + "1": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ], + "b0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,27e4aa847d60b2154d8865d19ba3e06e" + ] + ], + "versions": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-09T00:14:14.511257" + }, + "utils - extractb0 - allseries": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,2066111f2b964735a168d6c653ba1a8b" + ] + ], + "1": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ], + "b0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_b0.nii.gz:md5,2066111f2b964735a168d6c653ba1a8b" + ] + ], + "versions": [ + "versions.yml:md5,36a507648bd7506126303551eff1e6d0" + ] + } + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-05-09T00:14:07.253309" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/utils/extractb0/tests/nextflow.config b/modules/nf-neuro/utils/extractb0/tests/nextflow.config new file mode 100644 index 0000000..0f6f148 --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/tests/nextflow.config @@ -0,0 +1,6 @@ +process { + withName: "UTILS_EXTRACTB0" { + ext.b0_threshold = 50 + ext.output_single_volume = true + } +} diff --git a/modules/nf-neuro/utils/extractb0/tests/nextflow_all4d.config b/modules/nf-neuro/utils/extractb0/tests/nextflow_all4d.config new file mode 100644 index 0000000..8e9c38a --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/tests/nextflow_all4d.config @@ -0,0 +1,5 @@ +process { + withName: "UTILS_EXTRACTB0" { + ext.b0_extraction_strategy = "all" + } +} diff --git a/modules/nf-neuro/utils/extractb0/tests/nextflow_allseries.config b/modules/nf-neuro/utils/extractb0/tests/nextflow_allseries.config new file mode 100644 index 0000000..19c88a4 --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/tests/nextflow_allseries.config @@ -0,0 +1,6 @@ +process { + withName: "UTILS_EXTRACTB0" { + ext.b0_extraction_strategy = "all" + ext.output_single_volume = false + } +} diff --git a/modules/nf-neuro/utils/extractb0/tests/nextflow_clusterfirst.config b/modules/nf-neuro/utils/extractb0/tests/nextflow_clusterfirst.config new file mode 100644 index 0000000..6f1798c --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/tests/nextflow_clusterfirst.config @@ -0,0 +1,5 @@ +process { + withName: "UTILS_EXTRACTB0" { + ext.b0_extraction_strategy = "cluster-first" + } +} diff --git a/modules/nf-neuro/utils/extractb0/tests/nextflow_clustermean.config b/modules/nf-neuro/utils/extractb0/tests/nextflow_clustermean.config new file mode 100644 index 0000000..0d84879 --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/tests/nextflow_clustermean.config @@ -0,0 +1,5 @@ +process { + withName: "UTILS_EXTRACTB0" { + ext.b0_extraction_strategy = "cluster-mean" + } +} diff --git a/modules/nf-neuro/utils/extractb0/tests/nextflow_mean.config b/modules/nf-neuro/utils/extractb0/tests/nextflow_mean.config new file mode 100644 index 0000000..e6bb4e1 --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/tests/nextflow_mean.config @@ -0,0 +1,5 @@ +process { + withName: "UTILS_EXTRACTB0" { + ext.b0_extraction_strategy = "mean" + } +} diff --git a/modules/nf-neuro/utils/extractb0/tests/tags.yml b/modules/nf-neuro/utils/extractb0/tests/tags.yml new file mode 100644 index 0000000..76ede92 --- /dev/null +++ b/modules/nf-neuro/utils/extractb0/tests/tags.yml @@ -0,0 +1,2 @@ +utils/extractb0: + - "modules/nf-neuro/utils/extractb0/**" diff --git a/modules/nf-scil/betcrop/antsbet/main.nf b/modules/nf-scil/betcrop/antsbet/main.nf deleted file mode 100644 index e15cf70..0000000 --- a/modules/nf-scil/betcrop/antsbet/main.nf +++ /dev/null @@ -1,62 +0,0 @@ - -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_1.6.0.sif': - 'scilus/scilus:1.6.0' }" - - input: - tuple val(meta), path(t1), path(template), path(tissues_probabilities) - - 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 args = task.ext.args ?: '' - def prefix = task.ext.prefix ?: "${meta.id}" - - """ - 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 -e $template -o bet/ -m $tissues_probabilities -u 0 - scil_image_math.py convert bet/BrainExtractionMask.nii.gz ${prefix}__t1_bet_mask.nii.gz --data_type uint8 - mrcalc $t1 ${prefix}__t1_bet_mask.nii.gz -mult ${prefix}__t1_bet.nii.gz -nthreads $task.cpus - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 1.6.0 - mrtrix: \$(mrcalc -version 2>&1 | sed -n 's/== mrcalc \\([0-9.]\\+\\).*/\\1/p') - ants: 2.4.3 - END_VERSIONS - """ - - stub: - def args = task.ext.args ?: '' - def prefix = task.ext.prefix ?: "${meta.id}" - - """ - antsBrainExtraction.sh -h - scil_image_math.py -h - mrcalc -h - - touch ${prefix}__t1_bet.nii.gz - touch ${prefix}__t1_bet_mask.nii.gz - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 1.6.0 - mrtrix: \$(mrcalc -version 2>&1 | sed -n 's/== mrcalc \\([0-9.]\\+\\).*/\\1/p') - ants: 2.4.3 - END_VERSIONS - """ -} diff --git a/modules/nf-scil/io/readbids/main.nf b/modules/nf-scil/io/readbids/main.nf deleted file mode 100644 index e8e1349..0000000 --- a/modules/nf-scil/io/readbids/main.nf +++ /dev/null @@ -1,53 +0,0 @@ -process IO_READBIDS { - label 'process_single' - - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" - - input: - tuple path(bids_folder), path(fsfolder), path(bidsignore) - - output: - path("tractoflow_bids_struct.json"), emit: bids - path "versions.yml" , emit: versions - - - when: - task.ext.when == null || task.ext.when - - script: - def args = task.ext.args ?: '' - def fsfolder = task.ext.fs_folder ? "--fs " + task.ext.fs_folder : '' - def bidsignore = task.ext.bidsignore ? "--bids_ignore " + task.ext.bidsignore : '' - def readout = task.ext.readout ? "--readout " + task.ext.readout : "" - def clean_flag = task.ext.clean_bids ? '--clean ' : '' - - """ - scil_validate_bids.py $bids_folder tractoflow_bids_struct.json\ - $readout \ - $clean_flag\ - $fsfolder\ - $bidsignore\ - -v - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 1.6.0 - END_VERSIONS - """ - - - stub: - def args = task.ext.args ?: '' - """ - scil_validate_bids.py -h - - touch tractoflow_bids_struct.json - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 1.6.0 - END_VERSIONS - """ -} diff --git a/modules/nf-scil/preproc/eddy/environment.yml b/modules/nf-scil/preproc/eddy/environment.yml deleted file mode 100644 index d6b8e56..0000000 --- a/modules/nf-scil/preproc/eddy/environment.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: "preproc_eddy" -channels: - - Docker - - Apptainer -dependencies: - - "FSL" - - "scilpy" diff --git a/modules/nf-scil/preproc/eddy/tests/main.nf.test b/modules/nf-scil/preproc/eddy/tests/main.nf.test deleted file mode 100644 index 5d3af06..0000000 --- a/modules/nf-scil/preproc/eddy/tests/main.nf.test +++ /dev/null @@ -1,61 +0,0 @@ -nextflow_process { - - name "Test Process PREPROC_EDDY" - script "../main.nf" - process "PREPROC_EDDY" - - tag "modules" - tag "modules_nfcore" - tag "preproc" - tag "preproc/eddy" - - tag "subworkflows" - tag "subworkflows/load_test_data" - - config "./nextflow.config" - - setup { - run("LOAD_TEST_DATA", alias: "LOAD_DATA") { - script "../../../../../subworkflows/nf-scil/load_test_data/main.nf" - process { - """ - input[0] = Channel.from( [ "topup_eddy_light.zip" ] ) - input[1] = "test.load-test-data" - """ - } - } - } - - test("eddy") { - 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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-PA_dwi.nii.gz", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-PA_dwi.bval", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-PA_dwi.bvec", checkIfExists: true), - file("\${test_data_directory}/sub-01__corrected_b0s.nii.gz", checkIfExists: true), - file("\${test_data_directory}/topup_results_fieldcoef.nii.gz", checkIfExists: true), - file("\${test_data_directory}/topup_results_movpar.txt", checkIfExists: true)]} - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot( - file(process.out.dwi_corrected.get(0).get(1)).name, - process.out.bval_corrected, - file(process.out.bvec_corrected.get(0).get(1)).name, - process.out.b0_mask, - ).match() } - ) - } - } -} diff --git a/modules/nf-scil/preproc/eddy/tests/main.nf.test.snap b/modules/nf-scil/preproc/eddy/tests/main.nf.test.snap deleted file mode 100644 index 986380d..0000000 --- a/modules/nf-scil/preproc/eddy/tests/main.nf.test.snap +++ /dev/null @@ -1,31 +0,0 @@ -{ - "eddy": { - "content": [ - "test__dwi_corrected.nii.gz", - [ - [ - { - "id": "test", - "single_end": false - }, - "test__bval_eddy:md5,4c61c53078316c31b4d5daf446a3d6ac" - ] - ], - "test__dwi_eddy_corrected.bvec", - [ - [ - { - "id": "test", - "single_end": false - }, - "test__b0_bet_mask.nii.gz:md5,fc1f94e2a5e2cf5197d7fdc28a83ca28" - ] - ] - ], - "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" - }, - "timestamp": "2024-04-29T16:11:13.561798" - } -} \ No newline at end of file diff --git a/modules/nf-scil/preproc/eddy/tests/tags.yml b/modules/nf-scil/preproc/eddy/tests/tags.yml deleted file mode 100644 index bb53229..0000000 --- a/modules/nf-scil/preproc/eddy/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -preproc/eddy: - - "modules/nf-scil/preproc/eddy/**" diff --git a/modules/nf-scil/preproc/topup/environment.yml b/modules/nf-scil/preproc/topup/environment.yml deleted file mode 100644 index c98ff70..0000000 --- a/modules/nf-scil/preproc/topup/environment.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: "preproc_topup" -channels: - - Docker - - Apptainer -dependencies: - - "FSL" - - "scilpy" diff --git a/modules/nf-scil/preproc/topup/main.nf b/modules/nf-scil/preproc/topup/main.nf deleted file mode 100644 index 24857a2..0000000 --- a/modules/nf-scil/preproc/topup/main.nf +++ /dev/null @@ -1,100 +0,0 @@ -process PREPROC_TOPUP { - tag "$meta.id" - label 'process_single' - - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - "https://scil.usherbrooke.ca/containers/scilus_2.0.0.sif": - "scilus/scilus:2.0.0"}" - - input: - tuple val(meta), path(dwi), path(bval), path(bvec), path(b0), path(rev_dwi), path(rev_bval), path(rev_bvec), path(rev_b0) - val(config_topup) - - output: - tuple val(meta), path("*__corrected_b0s.nii.gz"), emit: topup_corrected_b0s - tuple val(meta), path("*_fieldcoef.nii.gz"), emit: topup_fieldcoef - tuple val(meta), path("*_movpar.txt"), emit: topup_movpart - tuple val(meta), path("*__rev_b0_warped.nii.gz"), emit: rev_b0_warped - tuple val(meta), path("*__rev_b0_mean.nii.gz"), emit: rev_b0_mean - tuple val(meta), path("*__b0_mean.nii.gz"), emit: b0_mean - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def prefix = task.ext.prefix ?: "${meta.id}" - - def prefix_topup = task.ext.prefix_topup ? task.ext.prefix_topup : "" - def config_topup = config_topup ?: task.ext.default_config_topup - def encoding = task.ext.encoding ? task.ext.encoding : "" - def readout = task.ext.readout ? task.ext.readout : "" - def b0_thr_extract_b0 = task.ext.b0_thr_extract_b0 ? task.ext.b0_thr_extract_b0 : "" - - """ - export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 - export OMP_NUM_THREADS=1 - export OPENBLAS_NUM_THREADS=1 - export ANTS_RANDOM_SEED=7468 - - if [[ -f "$b0" ]]; - then - scil_volume_math.py concatenate $b0 $b0 ${prefix}__concatenated_b0.nii.gz - scil_volume_math.py mean ${prefix}__concatenated_b0.nii.gz ${prefix}__b0_mean.nii.gz - else - scil_dwi_extract_b0.py $dwi $bval $bvec ${prefix}__b0_mean.nii.gz --mean --b0_threshold $b0_thr_extract_b0 --skip_b0_check - fi - - if [[ -f "$rev_b0" ]]; - then - scil_volume_math.py concatenate $rev_b0 $rev_b0 ${prefix}__concatenated_rev_b0.nii.gz - scil_volume_math.py mean ${prefix}__concatenated_rev_b0.nii.gz ${prefix}__rev_b0_mean.nii.gz - else - scil_dwi_extract_b0.py $rev_dwi $rev_bval $rev_bvec ${prefix}__rev_b0_mean.nii.gz --mean --b0_threshold $b0_thr_extract_b0 --skip_b0_check - fi - - antsRegistrationSyNQuick.sh -d 3 -f ${prefix}__b0_mean.nii.gz -m ${prefix}__rev_b0_mean.nii.gz -o output -t r -e 1 - mv outputWarped.nii.gz ${prefix}__rev_b0_warped.nii.gz - scil_dwi_prepare_topup_command.py ${prefix}__b0_mean.nii.gz ${prefix}__rev_b0_warped.nii.gz\ - --config $config_topup\ - --encoding_direction $encoding\ - --readout $readout --out_prefix $prefix_topup\ - --out_script - sh topup.sh - cp corrected_b0s.nii.gz ${prefix}__corrected_b0s.nii.gz - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 2.0.0 - antsRegistration: 2.4.3 - fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') - - END_VERSIONS - """ - - stub: - def args = task.ext.args ?: '' - def prefix = task.ext.prefix ?: "${meta.id}" - - """ - scil_volume_math.py -h - scil_dwi_extract_b0.py -h - antsRegistrationSyNQuick.sh -h - scil_dwi_prepare_topup_command.py -h - - touch ${prefix}__corrected_b0s.nii.gz - touch ${prefix}__rev_b0_warped.nii.gz - touch ${prefix}__rev_b0_mean.nii.gz - touch ${prefix}__b0_mean.nii.gz - touch ${prefix_topup}_fieldcoef.nii.gz - touch ${prefix_topup}_movpar.txt - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 2.0.0 - antsRegistration: 2.4.3 - fsl: \$(flirt -version 2>&1 | sed -n 's/FLIRT version \\([0-9.]\\+\\)/\\1/p') - - END_VERSIONS - """ -} diff --git a/modules/nf-scil/preproc/topup/tests/main.nf.test b/modules/nf-scil/preproc/topup/tests/main.nf.test deleted file mode 100644 index 632d403..0000000 --- a/modules/nf-scil/preproc/topup/tests/main.nf.test +++ /dev/null @@ -1,62 +0,0 @@ -nextflow_process { - - name "Test Process PREPROC_TOPUP" - script "../main.nf" - process "PREPROC_TOPUP" - - tag "modules" - tag "modules_nfcore" - tag "preproc" - tag "preproc/topup" - - tag "subworkflows" - tag "subworkflows/load_test_data" - - config "./nextflow.config" - - setup { - run("LOAD_TEST_DATA", alias: "LOAD_DATA") { - script "../../../../../subworkflows/nf-scil/load_test_data/main.nf" - process { - """ - input[0] = Channel.from( [ "topup_eddy_light.zip" ] ) - input[1] = "test.load-test-data" - """ - } - } - } - - test("topup") { - 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}/sub-01_dir-AP_dwi.nii.gz", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-AP_dwi.bval", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-AP_sbref.nii.gz", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-PA_dwi.nii.gz", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-PA_dwi.bval", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-PA_dwi.bvec", checkIfExists: true), - file("\${test_data_directory}/sub-01_dir-PA_sbref.nii.gz", checkIfExists: true)]} - input[1] = [] - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot( - file(process.out.topup_corrected_b0s.get(0).get(1)).name, - file(process.out.topup_fieldcoef.get(0).get(1)).name, - file(process.out.topup_movpart.get(0).get(1)).name, - file(process.out.rev_b0_warped.get(0).get(1)).name, - process.out.rev_b0_mean, - process.out.b0_mean).match() } - ) - } - } -} diff --git a/modules/nf-scil/preproc/topup/tests/main.nf.test.snap b/modules/nf-scil/preproc/topup/tests/main.nf.test.snap deleted file mode 100644 index 3c88ff1..0000000 --- a/modules/nf-scil/preproc/topup/tests/main.nf.test.snap +++ /dev/null @@ -1,33 +0,0 @@ -{ - "topup": { - "content": [ - "test__corrected_b0s.nii.gz", - "topup_results_fieldcoef.nii.gz", - "topup_results_movpar.txt", - "test__rev_b0_warped.nii.gz", - [ - [ - { - "id": "test", - "single_end": false - }, - "test__rev_b0_mean.nii.gz:md5,a0c63d7a8a234d85eeb63045ae2a8f60" - ] - ], - [ - [ - { - "id": "test", - "single_end": false - }, - "test__b0_mean.nii.gz:md5,f50e652bcdd1d45f7a4c592df7eb851a" - ] - ] - ], - "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" - }, - "timestamp": "2024-04-29T15:40:07.558114" - } -} \ No newline at end of file diff --git a/modules/nf-scil/preproc/topup/tests/tags.yml b/modules/nf-scil/preproc/topup/tests/tags.yml deleted file mode 100644 index a2777dd..0000000 --- a/modules/nf-scil/preproc/topup/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -preproc/topup: - - "modules/nf-scil/preproc/topup/**" diff --git a/modules/nf-scil/reconst/fodf/main.nf b/modules/nf-scil/reconst/fodf/main.nf deleted file mode 100755 index b33c5d3..0000000 --- a/modules/nf-scil/reconst/fodf/main.nf +++ /dev/null @@ -1,123 +0,0 @@ - -process RECONST_FODF { - tag "$meta.id" - label 'process_single' - - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" - - input: - tuple val(meta), path(dwi), path(bval), path(bvec), path(mask), path(fa), path(md), path(frf) - - output: - tuple val(meta), path("*fodf.nii.gz") , emit: fodf - tuple val(meta), path("*peaks.nii.gz") , emit: peaks, optional: true - tuple val(meta), path("*peak_indices.nii.gz") , emit: peak_indices, optional: true - tuple val(meta), path("*afd_max.nii.gz") , emit: afd_max, optional: true - tuple val(meta), path("*afd_total.nii.gz") , emit: afd_total, optional: true - tuple val(meta), path("*afd_sum.nii.gz") , emit: afd_sum, optional: true - tuple val(meta), path("*nufo.nii.gz") , emit: nufo, optional: true - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def prefix = task.ext.prefix ?: "${meta.id}" - - def dwi_shell_tolerance = task.ext.dwi_shell_tolerance ? "--tolerance " + task.ext.dwi_shell_tolerance : "" - def fodf_shells = task.ext.fodf_shells ?: "\$(cut -d ' ' --output-delimiter=\$'\\n' -f 1- $bval | awk -F' ' '{v=int(\$1)}{if(v>=$task.ext.min_fodf_shell_value|| v<=$task.ext.b0_thr_extract_b0)print v}' | uniq)" - def sh_order = task.ext.sh_order ? "--sh_order " + task.ext.sh_order : "" - def sh_basis = task.ext.sh_basis ? "--sh_basis " + task.ext.sh_basis : "" - def fa_threshold = task.ext.fa_threshold ? "--fa_t " + task.ext.fa_threshold : "" - def md_threshold = task.ext.md_threshold ? "--md_t " + task.ext.md_threshold : "" - def relative_threshold = task.ext.relative_threshold ? "--rt " + task.ext.relative_threshold : "" - def fodf_metrics_a_factor = task.ext.fodf_metrics_a_factor ? task.ext.fodf_metrics_a_factor : 2.0 - def processes = task.ext.processes ? "--processes " + task.ext.processes : "" - def set_mask = mask ? "--mask $mask" : "" - def absolute_peaks = task.ext.absolute_peaks ? "--abs_peaks_and_values" : "" - - if ( task.ext.peaks ) peaks = "--peaks ${prefix}__peaks.nii.gz" else peaks = "" - if ( task.ext.peak_indices ) peak_indices = "--peak_indices ${prefix}__peak_indices.nii.gz" else peak_indices = "" - if ( task.ext.afd_max ) afd_max = "--afd_max ${prefix}__afd_max.nii.gz" else afd_max = "" - if ( task.ext.afd_total ) afd_total = "--afd_total ${prefix}__afd_total.nii.gz" else afd_total = "" - if ( task.ext.afd_sum ) afd_sum = "--afd_sum ${prefix}__afd_sum.nii.gz" else afd_sum = "" - if ( task.ext.nufo ) nufo = "--nufo ${prefix}__nufo.nii.gz" else nufo = "" - - def run_fodf_metrics = [ - task.ext.peaks, task.ext.peak_indices, task.ext.afd_max, - task.ext.afd_sum, task.ext.afd_total, task.ext.nufo - ].any() - - """ - export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 - export OMP_NUM_THREADS=1 - export OPENBLAS_NUM_THREADS=1 - - scil_extract_dwi_shell.py $dwi $bval $bvec $fodf_shells \ - dwi_fodf_shells.nii.gz bval_fodf_shells bvec_fodf_shells \ - $dwi_shell_tolerance -f - - scil_compute_ssst_fodf.py dwi_fodf_shells.nii.gz bval_fodf_shells bvec_fodf_shells $frf ${prefix}__fodf.nii.gz \ - $sh_order $sh_basis --force_b0_threshold \ - $set_mask $processes - - if $run_fodf_metrics - then - - scil_compute_fodf_max_in_ventricles.py ${prefix}__fodf.nii.gz $fa $md \ - --max_value_output ventricles_fodf_max_value.txt $sh_basis \ - $fa_threshold $md_threshold --mask_output ventricles_mask.nii.gz -f - - echo "Maximal peak value in ventricle in file : \$(cat ventricles_fodf_max_value.txt)" - - a_factor=$fodf_metrics_a_factor - v_max=\$(sed -E 's/([+-]?[0-9.]+)[eE]\\+?(-?)([0-9]+)/(\\1*10^\\2\\3)/g' <<<"\$(cat ventricles_fodf_max_value.txt)") - - echo "Maximal peak value in ventricles : \${v_max}" - - a_threshold=\$(echo "scale=10; \${a_factor} * \${v_max}" | bc) - if (( \$(echo "\${a_threshold} <= 0" | bc -l) )); then - a_threshold=1E-10 - fi - - echo "Computing fodf metrics with absolute threshold : \${a_threshold}" - - scil_compute_fodf_metrics.py ${prefix}__fodf.nii.gz \ - $set_mask $sh_basis $absolute_peaks \ - $peaks $peak_indices \ - $afd_max $afd_total \ - $afd_sum $nufo \ - $relative_threshold --not_all --at \${a_threshold} - fi - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 1.6.0 - END_VERSIONS - """ - - stub: - def prefix = task.ext.prefix ?: "${meta.id}" - - """ - scil_extract_dwi_shell.py -h - scil_compute_ssst_fodf.py -h - scil_compute_fodf_max_in_ventricles.py -h - scil_compute_fodf_metrics.py -h - - touch ${prefix}__fodf.nii.gz - touch ${prefix}__peaks.nii.gz - touch ${prefix}__peak_indices.nii.gz - touch ${prefix}__afd_max.nii.gz - touch ${prefix}__afd_total.nii.gz - touch ${prefix}__afd_sum.nii.gz - touch ${prefix}__nufo.nii.gz - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 1.6.0 - END_VERSIONS - """ -} diff --git a/modules/nf-scil/reconst/fodf/meta.yml b/modules/nf-scil/reconst/fodf/meta.yml deleted file mode 100755 index a8bbe1f..0000000 --- a/modules/nf-scil/reconst/fodf/meta.yml +++ /dev/null @@ -1,103 +0,0 @@ ---- -name: "reconst_fodf" -description: Perform FODF reconstruction and compute FODF metrics from a dwi volume for a selected number of shells. -keywords: - - FODF - - Local Model - - Metrics -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 ]` - - - dwi: - type: file - description: Nifti DWI volume to reconstruct FODF from. - pattern: "*.{nii,nii.gz}" - - - bval: - type: file - description: B-values in FSL format. - pattern: "*.bval" - - - bvec: - type: file - description: B-vectors in FSL format. - pattern: "*.bvec" - - - mask: - type: file - description: B0 mask. - pattern: "*.{nii,nii.gz}" - - - fa: - type: file - description: FA map. - pattern: "*.{nii,nii.gz}" - - - md: - type: file - description: MD map. - pattern: "*.{nii,nii.gz}" - - - frf: - type: file - description: Fiber Response Function (FRF). - pattern: "*.txt" - -output: - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. `[ id:'test', single_end:false ]` - - - fodf: - type: file - description: FODF map. - pattern: "*fodf.nii.gz" - - - peaks: - type: file - description: Peaks file. - pattern: "*peaks.nii.gz" - - - peak_indices: - type: file - description: Peak indices file. - pattern: "*peak_indices.nii.gz" - - - afd_max: - type: file - description: Maximum Apparent Fiber Density (AFDmax) map. - pattern: "*afd_max.nii.gz" - - - afd_total: - type: file - description: Total Apparent Fiber Density (AFDtotal) map. - pattern: "*afd_total.nii.gz" - - - afd_sum: - type: file - description: Sum of all Apparent Fiber Density (Afdsum) map. - pattern: "*afd_sum.nii.gz" - - - nufo: - type: file - description: Number of Fiber Orientation (NuFO) map. - pattern: "*nufo.nii.gz" - - - versions: - type: file - description: File containing software versions - pattern: "versions.yml" - -authors: - - "@gagnonanthony" diff --git a/modules/nf-scil/reconst/frf/main.nf b/modules/nf-scil/reconst/frf/main.nf deleted file mode 100755 index e274376..0000000 --- a/modules/nf-scil/reconst/frf/main.nf +++ /dev/null @@ -1,63 +0,0 @@ - - -process RECONST_FRF { - tag "$meta.id" - label 'process_single' - - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" - - input: - tuple val(meta), path(dwi), path(bval), path(bvec), path(mask) - - output: - tuple val(meta), path("*__frf.txt") , emit: frf - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def prefix = task.ext.prefix ?: "${meta.id}" - def fa = task.ext.fa ? "--fa " + task.ext.fa : "" - def fa_min = task.ext.fa_min ? "--min_fa " + task.ext.fa_min : "" - def nvox_min = task.ext.nvox_min ? "--min_nvox " + task.ext.nvox_min : "" - def roi_radius = task.ext.roi_radius ? "--roi_radii " + task.ext.roi_radius : "" - def fix_frf = task.ext.manual_frf ? task.ext.manual_frf : "" - def set_mask = mask ? "--mask $mask" : "" - - """ - export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 - export OMP_NUM_THREADS=1 - export OPENBLAS_NUM_THREADS=1 - - scil_compute_ssst_frf.py $dwi $bval $bvec ${prefix}__frf.txt \ - $set_mask $fa $fa_min $nvox_min $roi_radius --force_b0_threshold - - if ( "$task.ext.set_frf" = true ); then - scil_set_response_function.py ${prefix}__frf.txt "${fix_frf}" \ - ${prefix}__frf.txt -f - fi - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 1.6.0 - END_VERSIONS - """ - - stub: - def prefix = task.ext.prefix ?: "${meta.id}" - - """ - scil_compute_ssst_frf.py -h - scil_set_response_function.py -h - - touch ${prefix}__frf.txt - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - scilpy: 1.6.0 - END_VERSIONS - """ -} diff --git a/modules/nf-scil/register/anattodwi/main.nf b/modules/nf-scil/register/anattodwi/main.nf deleted file mode 100644 index e96c5f9..0000000 --- a/modules/nf-scil/register/anattodwi/main.nf +++ /dev/null @@ -1,79 +0,0 @@ - - -process REGISTER_ANATTODWI { - tag "$meta.id" - label 'process_medium' - - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" - - input: - tuple val(meta), path(t1), path(b0), path(metric) - - output: - tuple val(meta), path("*0GenericAffine.mat"), path("*1InverseWarp.nii.gz") , emit: transfo_trk - tuple val(meta), path("*1Warp.nii.gz"), path("*0GenericAffine.mat") , emit: transfo_image - tuple val(meta), path("*t1_warped.nii.gz") , emit: t1_warped - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def prefix = task.ext.prefix ?: "${meta.id}" - - """ - export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus - export OMP_NUM_THREADS=1 - export OPENBLAS_NUM_THREADS=1 - export ANTS_RANDOM_SEED=1234 - - antsRegistration --dimensionality 3 --float 0\ - --output [output,outputWarped.nii.gz,outputInverseWarped.nii.gz]\ - --interpolation Linear --use-histogram-matching 0\ - --winsorize-image-intensities [0.005,0.995]\ - --initial-moving-transform [$b0,$t1,1]\ - --transform Rigid['0.2']\ - --metric MI[$b0,$t1,1,32,Regular,0.25]\ - --convergence [500x250x125x50,1e-6,10] --shrink-factors 8x4x2x1\ - --smoothing-sigmas 3x2x1x0\ - --transform Affine['0.2']\ - --metric MI[$b0,$t1,1,32,Regular,0.25]\ - --convergence [500x250x125x50,1e-6,10] --shrink-factors 8x4x2x1\ - --smoothing-sigmas 3x2x1x0\ - --transform SyN[0.1,3,0]\ - --metric MI[$b0,$t1,1,32]\ - --metric CC[$metric,$t1,1,4]\ - --convergence [50x25x10,1e-6,10] --shrink-factors 4x2x1\ - --smoothing-sigmas 3x2x1 - - mv outputWarped.nii.gz ${prefix}__t1_warped.nii.gz - mv output0GenericAffine.mat ${prefix}__output0GenericAffine.mat - mv output1InverseWarp.nii.gz ${prefix}__output1InverseWarp.nii.gz - mv output1Warp.nii.gz ${prefix}__output1Warp.nii.gz - - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - ants: 2.4.3 - END_VERSIONS - """ - - stub: - def prefix = task.ext.prefix ?: "${meta.id}" - - """ - antsRegistration -h - - touch ${prefix}__t1_warped.nii.gz - touch ${prefix}__output0GenericAffine.mat - touch ${prefix}__output1InverseWarp.nii.gz - touch ${prefix}__output1Warp.nii.gz - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - ants: 2.4.3 - END_VERSIONS - """ -} diff --git a/modules/nf-scil/registration/ants/main.nf b/modules/nf-scil/registration/ants/main.nf deleted file mode 100644 index 091876c..0000000 --- a/modules/nf-scil/registration/ants/main.nf +++ /dev/null @@ -1,88 +0,0 @@ - -process REGISTRATION_ANTS { - tag "$meta.id" - label 'process_medium' - - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" - - input: - tuple val(meta), path(fixedimage), path(movingimage), path(mask) - - output: - tuple val(meta), path("*_warped.nii.gz") , emit: image - tuple val(meta), path("*__output{0Warp.nii.gz,1GenericAffine.mat}") , emit: transfo_image - tuple val(meta), path("*__output{0,1}Inverse{Warp.nii.gz,Affine.mat}") , emit: transfo_trk - 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 ants = task.ext.quick ? "antsRegistrationSyNQuick.sh " : "antsRegistrationSyN.sh " - def dimension = task.ext.dimension ? "-d " + task.ext.dimension : "-d 3" - def transform = task.ext.transform ? task.ext.transform : "s" - def seed = task.ext.random_seed ? " -e " + task.ext.random_seed : "-e 1234" - - if ( task.ext.threads ) args += "-n " + task.ext.threads - if ( task.ext.initial_transform ) args += " -i " + task.ext.initial_transform - if ( task.ext.histogram_bins ) args += " -r " + task.ext.histogram_bins - if ( task.ext.spline_distance ) args += " -s " + task.ext.spline_distance - if ( task.ext.gradient_step ) args += " -g " + task.ext.gradient_step - if ( task.ext.mask ) args += " -x $mask" - if ( task.ext.type ) args += " -p " + task.ext.type - if ( task.ext.histogram_matching ) args += " -j " + task.ext.histogram_matching - if ( task.ext.repro_mode ) args += " -y " + task.ext.repro_mode - if ( task.ext.collapse_output ) args += " -z " + task.ext.collapse_output - - """ - export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus - export OMP_NUM_THREADS=1 - export OPENBLAS_NUM_THREADS=1 - - $ants $dimension -f $fixedimage -m $movingimage -o output -t $transform $args $seed - - mv outputWarped.nii.gz ${prefix}__warped.nii.gz - mv output0GenericAffine.mat ${prefix}__output1GenericAffine.mat - - if [ $transform != "t" ] && [ $transform != "r" ] && [ $transform != "a" ]; - then - mv output1InverseWarp.nii.gz ${prefix}__output1InverseWarp.nii.gz - mv output1Warp.nii.gz ${prefix}__output0Warp.nii.gz - fi - - antsApplyTransforms -d 3 -i $fixedimage -r $movingimage -o Linear[output.mat]\ - -t [${prefix}__output1GenericAffine.mat,1] - - mv output.mat ${prefix}__output0InverseAffine.mat - - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - ants: 2.4.3 - END_VERSIONS - """ - - stub: - def args = task.ext.args ?: '' - def prefix = task.ext.prefix ?: "${meta.id}" - - """ - antsRegistrationSyNQuick.sh -h - antsApplyTransforms -h - - touch ${prefix}__t1_warped.nii.gz - touch ${prefix}__output1GenericAffine.mat - touch ${prefix}__output0InverseAffine.mat - touch ${prefix}__output1InverseWarp.nii.gz - touch ${prefix}__output0Warp.nii.gz - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - ants: 2.4.3 - END_VERSIONS - """ -} diff --git a/modules/nf-scil/registration/antsapplytransforms/main.nf b/modules/nf-scil/registration/antsapplytransforms/main.nf deleted file mode 100644 index 1a44c6a..0000000 --- a/modules/nf-scil/registration/antsapplytransforms/main.nf +++ /dev/null @@ -1,69 +0,0 @@ -process REGISTRATION_ANTSAPPLYTRANSFORMS { - tag "$meta.id" - label 'process_low' - - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'scil.usherbrooke.ca/containers/scilus_1.6.0.sif': - 'scilus/scilus:1.6.0' }" - - input: - tuple val(meta), path(image), path(reference), path(transform) - - output: - tuple val(meta), path("*__warped.nii.gz"), emit: warpedimage - 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 suffix = task.ext.first_suffix ? "${task.ext.first_suffix}_warped" : "warped" - - def dimensionality = task.ext.dimensionality ? "-d " + task.ext.dimensionality : "" - def image_type = task.ext.image_type ? "-e " + task.ext.image_type : "" - def interpolation = task.ext.interpolation ? "-n " + task.ext.interpolation : "" - def output_dtype = task.ext.output_dtype ? "-u " + task.ext.output_dtype : "" - def default_val = task.ext.default_val ? "-f " + task.ext.default_val : "" - - """ - export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus - export OMP_NUM_THREADS=1 - export OPENBLAS_NUM_THREADS=1 - - antsApplyTransforms $dimensionality\ - -i $image\ - -r $reference\ - -o ${prefix}__${suffix}.nii.gz\ - $interpolation\ - -t $transform\ - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - ants: 2.4.3 - 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}_warped" : "warped" - - def dimensionality = task.ext.dimensionality ? "-d " + task.ext.dimensionality : "" - def image_type = task.ext.image_type ? "-e " + task.ext.image_type : "" - def interpolation = task.ext.interpolation ? "-n " + task.ext.interpolation : "" - def output_dtype = task.ext.output_dtype ? "-u " + task.ext.output_dtype : "" - def default_val = task.ext.default_val ? "-f " + task.ext.default_val : "" - - """ - antsApplyTransforms -h - - touch ${prefix}__${suffix}.nii.gz - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - ants: 2.4.3 - END_VERSIONS - """ -} diff --git a/modules/nf-scil/segmentation/fastseg/environment.yml b/modules/nf-scil/segmentation/fastseg/environment.yml deleted file mode 100644 index 65f6f5e..0000000 --- a/modules/nf-scil/segmentation/fastseg/environment.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: "segmentation_fastseg" -channels: - - Docker - - Apptainer -dependencies: - - "FSL" - - "Scilpy" diff --git a/modules/nf-scil/segmentation/fastseg/tests/main.nf.test b/modules/nf-scil/segmentation/fastseg/tests/main.nf.test deleted file mode 100644 index ffdc8d5..0000000 --- a/modules/nf-scil/segmentation/fastseg/tests/main.nf.test +++ /dev/null @@ -1,35 +0,0 @@ -nextflow_process { - - name "Test Process SEGMENTATION_FASTSEG" - script "../main.nf" - process "SEGMENTATION_FASTSEG" - config "./nextflow.config" - - tag "modules" - tag "modules_nfcore" - tag "segmentation" - tag "segmentation/fastseg" - - test("segmentation - fastseg") { - - when { - process { - """ - input[0] = Channel.fromList([ - tuple([ id:'test', single_end:false ], // meta map - file(params.test_data['segmentation']['fastseg']['image'], checkIfExists: true)) - ]) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - -} diff --git a/modules/nf-scil/segmentation/fastseg/tests/tags.yml b/modules/nf-scil/segmentation/fastseg/tests/tags.yml deleted file mode 100644 index 5da4c7a..0000000 --- a/modules/nf-scil/segmentation/fastseg/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -segmentation/fastseg: - - "modules/nf-scil/segmentation/fastseg/**" diff --git a/nextflow.config b/nextflow.config index c1c4c33..9f8e1cc 100644 --- a/nextflow.config +++ b/nextflow.config @@ -9,151 +9,139 @@ // Global default params, used in configs params { - //*BIDS options**// - clean_input_bids = false + //*Input + Output options (non-file)**// + clean_bids = false + b0_thr_extract_b0 = 10 + dwi_shell_tolerance = 20 - //**Global options**// - dwi_b0_extract_threshold = 10 - dwi_shell_extract_tolerance = 20 + //*DWI Processing options**// + bet_prelim_f = 0.16 + dilate_b0_mask_prelim_brain_extraction = 5 - //**SH fitting**// - dwi_fit_signal_sh = true //FIXME - dwi_signal_sh_fit_order = 6 //FIXME - dwi_signal_sh_fit_basis = "descoteaux07" //FIXME - dwi_signal_sh_fit_shell = null //FIXME - - //**Preliminary DWI brain extraction**// - dwi_mask_prelim_bet_dilation_radius = 5 - dwi_mask_prelim_bet_f_threshold = 0.16 //**Denoise dwi (dwidenoise in Mrtrix3)**// - dwi_filter_noise = true //FIXME - dwi_noise_filter_patch_size = 7 + run_dwi_denoising = true + extent = 7 //**GIBBS CORRECTION (mrdegibbs in Mrtrix3)**// - dwi_filter_gibbs = false //FIXME + run_gibbs_correction = false //**Topup**// - dwi_filter_susceptibility = true //FIXME - dwi_susceptibility_filter_config_file = "b02b0.cnf" - dwi_susceptibility_filter_output_prefix = "topup_results" + run_topup = true + encoding_direction = "y" + readout = 0.062 + config_topup = "b02b0.cnf" + prefix_topup = "topup_results" //**Eddy**// - dwi_filter_eddy_and_motion = true //FIXME - dwi_motion_and_eddy_filter_command = "eddy_cpu" - dwi_motion_and_eddy_filter_bet_f_threshold = 0.16 - dwi_motion_and_eddy_filter_restore_slices = true - - //**N4 bias correction**// - dwi_bias_correction_bspline_knots_per_voxel = 0.25 - dwi_bias_correction_shrink_factor = 4 + run_eddy = true + eddy_cmd = "eddy_cpu" + bet_topup_before_eddy_f = 0.16 + use_slice_drop_correction = true + bet_dwi_final_f = 0.16 - //**Final DWI BET**// - dwi_mask_final_bet_f_threshold = 0.16 + //**Normalize DWI**// + fa_mask_threshold = 0.4 //**Denoise T1**// - t1_filter_noise = true //FIXME - - //* T1 BET**// - t1_bet_template_t1 = "/human-data/mni_152_sym_09c/t1/t1_template.nii.gz" - t1_bet_template_probability_map = "/human-data/mni_152_sym_09c/t1/t1_brain_probability_map.nii.gz" + run_t1_denoising = true + template_t1 = "/human-data/mni_152_sym_09c/t1" + //**Resample DWI**// + run_resample_dwi = true + dwi_resolution = 1 + dwi_interpolation = "lin" //**Resample T1**// - t1_resample_spatial = true //FIXME - t1_spatial_resample_resolution = 1 - t1_spatial_resample_interpolation = "lin" - - //**Normalize DWI**// - dwi_intensities_normalize_fa_mask_threshold = 0.4 //FIXME - - //**Resample DWI**// - dwi_resample_spatial = true //FIXME - dwi_spatial_resample_resolution = 1 - dwi_spatial_resample_interpolation = "lin" + run_resample_t1 = true + t1_resolution = 1 + t1_interpolation = "lin" //**Extract DTI shells using this value as maximum**// - dwi_dti_fit_shell_max_bvalue = 1200 //FIXME - dwi_dti_fit_shells = null //FIXME + max_dti_shell_value = 1200 + dti_shells = null //**Extract fODF shells using this value as minimum**// - dwi_fodf_fit_shell_min_bvalue = 700 - dwi_fodf_fit_shells = null - - //**Segment tissues**// - t1_tissue_segment_n_classes = 3 //FIXME + min_fodf_shell_value = 700 + fodf_shells = null //**Compute fiber response function (frf)**// - dwi_fodf_fit_frf_max_fa_threshold = 0.7 - dwi_fodf_fit_frf_min_fa_threshold = 0.5 - dwi_fodf_fit_frf_min_n_voxels = 300 - dwi_fodf_fit_frf_roi_radius = 20 - dwi_fodf_fit_force_frf = null + fa = 0.7 + min_fa = 0.5 + min_nvox = 300 + roi_radius = 20 + manual_frf = null //**Mean fiber response function (frf)**// - dwi_fodf_fit_use_average_frf = false //FIXME + mean_frf = false //**Compute fODF metrics**// - dwi_fodf_fit_order = 8 - dwi_fodf_fit_basis = "descoteaux07" - dwi_fodf_fit_peaks_absolute_factor = 2.0 - dwi_fodf_fit_peaks_relative_threshold = 0.1 - dwi_fodf_fit_peaks_ventricle_max_fa = 0.1 - dwi_fodf_fit_peaks_ventricle_min_md = 0.003 + sh_order = 8 + basis = "descoteaux07" + fodf_metrics_a_factor = 2.0 + relative_threshold = 0.1 + max_fa_in_ventricle = 0.1 + min_md_in_ventricle = 0.003 + + //**SH fitting**// + sh_fitting = true + sh_fitting_order = 6 + sh_fitting_basis = "descoteaux07" + sh_fitting_shells = null //**PFT tracking**// - fodf_fit_pft = true - fodf_pft_fit_random_seed = 0 - fodf_pft_fit_algorithm = "prob" - fodf_pft_fit_step_size = 0.5 - fodf_pft_fit_theta_max_deviation = 20 - fodf_pft_fit_streamline_min_length = 20 - fodf_pft_fit_streamline_max_length = 200 - fodf_pft_fit_seeding_type = "wm" - fodf_pft_fit_seeding_strategy = "npv" - fodf_pft_fit_seeding_n_seeds = 10 - fodf_pft_fit_seeding_fa_mask_threshold = 0.1 - fodf_pft_fit_filter_n_particles = 15 - fodf_pft_fit_filter_backward_step_size = 2 - fodf_pft_fit_filter_forward_step_size = 1 - fodf_pft_fit_sf_threshold = 0.1 - fodf_pft_fit_sf_initial_threshold = 0.5 - fodf_pft_fit_compress_tractogram = true - fodf_pft_fit_compress_max_displacement = 0.2 - - //**Local tracking**// - fodf_fit_local = true - fodf_local_fit_processor = "cpu" //FIXME - fodf_local_fit_gpu_batch_size = 10000 //FIXME - fodf_local_fit_random_seed = 0 - fodf_local_fit_algorithm = "prob" - fodf_local_fit_step_size = 0.5 - fodf_local_fit_theta_max_deviation = 20 - fodf_local_fit_streamline_min_length = 20 - fodf_local_fit_streamline_max_length = 200 - fodf_local_fit_seeding_type = "wm" - fodf_local_fit_seeding_strategy = "npv" - fodf_local_fit_seeding_n_seeds = 10 - fodf_local_fit_seeding_fa_threshold = 0.1 - fodf_local_fit_tracking_mask_type = "wm" - fodf_local_fit_tracking_mask_fa_threshold = 0.1 - fodf_local_fit_sf_threshold = 0.1 - fodf_local_fit_sf_initial_threshold = 0.5 //FIXME - fodf_local_fit_compress_tractogram = true - fodf_local_fit_compress_max_displacement = 0.2 + run_pft_tracking = true + pft_seeding_mask_type = "wm" + pft_fa_seeding_mask_threshold = 0.1 + pft_algo = "prob" + pft_seeding = "npv" + pft_nbr_seeds = 10 + pft_step = 0.5 + pft_theta = 20 + pft_min_len = 20 + pft_max_len = 200 + pft_compress_value = 0.2 + pft_random_seed = 0 + pft_sfthres = 0.1 + pft_sfthres_init = 0.5 + pft_particles = 15 + pft_back = 2.0 + pft_front = 1.0 + + //**Local tracking (LT)**// + run_local_tracking = true + local_seeding_mask_type = "wm" + local_fa_seeding_mask_threshold = 0.1 + local_tracking_mask_type = "wm" + local_fa_tracking_mask_threshold = 0.1 + local_algo = "prob" + local_seeding = "npv" + local_nbr_seeds = 10 + local_step = 0.5 + local_theta = 20 + local_min_len = 20 + local_max_len = 200 + local_compress_value = 0.2 + local_random_seed = 0 + local_batch_size_gpu = 10000 //FIXME : this should be computed + local_tracking_gpu = false + local_sfthres = 0.1 + local_sfthres_init = 0.5 //**Processes Maximum CPU allocation**// - allocation_cpu_max_bet = 4 //FIXME - allocation_cpu_max_dwi_filter_noise = 4 //FIXME - allocation_cpu_max_dwi_filter_motion_and_eddy = 4 //FIXME - allocation_cpu_max_dwi_fit_fodf = 4 //FIXME - allocation_cpu_max_fodf_fit_local = 4 //FIXME - allocation_cpu_max_t1_filter_noise = 4 //FIXME - allocation_cpu_max_t1_registration = 4 //FIXME + processes_denoise_dwi = 4 + processes_eddy = 4 + processes_fodf = 4 + processes_local_tracking = 4 + processes_denoise_t1 = 4 + processes_brain_extraction_t1 = 4 + processes_registration = 4 // Input options input = null - bids = null + fsbids = null + bidsignore = null + bids_config = null // MultiQC options multiqc_config = null @@ -169,23 +157,22 @@ params { email_on_fail = null plaintext_email = false monochrome_logs = false - hook_url = null help = false help_full = false show_hidden = false version = false pipelines_testdata_base_path = 'https://raw.githubusercontent.com/nf-core/test-datasets/' trace_report_suffix = new java.util.Date().format( 'yyyy-MM-dd_HH-mm-ss')// Config options - config_profile_name = null - config_profile_description = null + config_profile_name = null + config_profile_description = null - custom_config_version = 'master' - custom_config_base = "https://raw.githubusercontent.com/nf-core/configs/${params.custom_config_version}" - config_profile_contact = null - config_profile_url = null + custom_config_version = 'master' + custom_config_base = "https://raw.githubusercontent.com/nf-core/configs/${params.custom_config_version}" + config_profile_contact = null + config_profile_url = null // Schema validation default options - validate_params = true + validate_params = true } // Load base.config by default for all pipelines @@ -293,7 +280,7 @@ profiles { resourceLimits = [ memory: 8.GB, cpus : 4, - time : 1.h + time : 4.h ] } } @@ -311,13 +298,11 @@ includeConfig !System.getenv('NXF_OFFLINE') && params.custom_config_base ? "${pa // Set default registry for Apptainer, Docker, Podman, Charliecloud and Singularity independent of -profile // Will not be used unless Apptainer / Docker / Podman / Charliecloud / Singularity are enabled // Set to your registry if you have a mirror of containers -apptainer.registry = 'quay.io' -docker.registry = 'quay.io' -podman.registry = 'quay.io' -singularity.registry = 'quay.io' -charliecloud.registry = 'quay.io' - - +apptainer.registry = 'docker.io' +docker.registry = 'docker.io' +podman.registry = 'docker.io' +singularity.registry = 'docker.io' +charliecloud.registry = 'docker.io' // Export these variables to prevent local Python/R libraries from conflicting with those in the container // The JULIA depot path has been adjusted to a fixed path `/usr/local/share/julia` that needs to be used for packages in the container. @@ -362,16 +347,31 @@ dag { manifest { name = 'scilus/nf-tractoflow' - author = """@AlexVCaron""" // The author field is deprecated from Nextflow version 24.10.0, use contributors instead + author = """@AlexVCaron""" contributors = [ - // TODO nf-core: Update the field with the details of the contributors to your pipeline. New with Nextflow version 24.10.0 [ name: '@AlexVCaron', - affiliation: '', + affiliation: 'Sherbrooke Connectivity Imaging Lab (SCIL) - Université de Sherbrooke', + email: 'alex.valcourt.caron@usherbrooke.ca', + github: 'https://github.com/AlexVCaron', + contribution: ['author', 'maintainer', 'contributor'], // List of contribution types ('author', 'maintainer' or 'contributor') + orcid: '0000-0002-1206-6395' + ], + [ + name: '@arnaudbore', + affiliation: 'Sherbrooke Connectivity Imaging Lab (SCIL) - Université de Sherbrooke', + email: 'arnaud.bore@usherbrooke.ca', + github: 'https://github.com/arnaudbore', + contribution: ['author', 'maintainer', 'contributor'], + orcid: '0000-0002-4822-1211' + ], + [ + name: '@GuillaumeTh', + affiliation: 'Université de Montréal affiliated research center (CRCHUM)', email: '', - github: '', - contribution: [], // List of contribution types ('author', 'maintainer' or 'contributor') - orcid: '' + github: 'https://github.com/guillaumeth', + contribution: ['author'], + orcid: '0000-0002-8440-1009' ], ] homePage = 'https://github.com/scilus/nf-tractoflow' @@ -393,9 +393,35 @@ validation { monochromeLogs = params.monochrome_logs help { enabled = true - command = "nextflow run scilus/nf-tractoflow -profile --input samplesheet.csv --outdir " + command = "nextflow run scilus/nf-tractoflow -profile <--input|--bids samplesheet.csv> --outdir " fullParameter = "help_full" showHiddenParameter = "show_hidden" + + beforeText = """ + -\033[2m------------------------------------------------------\033[0m- + \033[0;32m _.--'"'.\033[0m + \033[0;34m ___ ___ __ __ \033[0;32m( ( ( )\033[0m + \033[0;34m |\\ | |__ __ |\\ | |__ | | |__) / \\ \033[0;33m(o)_ ) )\033[0m + \033[0;34m | \\| | | \\| |___ |__| | \\ \\__/ \033[0;32m (o)_.'\033[0m + \033[0;32m )/\033[0m + \033[0;35m scilus/nf-tractoflow ${manifest.version}\033[0m + -\033[2m------------------------------------------------------\033[0m- + """ + afterText = """${manifest.doi ? "\n* The pipeline\n" : ""}${manifest.doi.tokenize(",").collect { " https://doi.org/${it.trim().replace('https://doi.org/','')}"}.join("\n")}${manifest.doi ? "\n" : ""} + * The nf-neuro project + https://scilus.github.io/nf-neuro + + * The nf-core framework + https://doi.org/10.1038/s41587-020-0439-x + + * Software dependencies + https://github.com/scilus/nf-tractoflow/blob/master/CITATIONS.md + """ + } + + summary { + beforeText = validation.help.beforeText + afterText = validation.help.afterText } } diff --git a/nextflow_schema.json b/nextflow_schema.json index eac7fc0..56204e6 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -10,70 +10,89 @@ "type": "object", "fa_icon": "fas fa-terminal", "description": "Define where the pipeline should find input data and save output data.", - "help_text": "Sepcify input samplesheet and output directory", - "anyOf": [ - { - "required": ["input", "outdir"] - }, - { - "required": ["bids", "outdir"] - } - ], + "help_text": "Specify input samplesheet and output directory", + "required": ["input", "outdir"], "properties": { "input": { "mimetype": "text/csv", - "description": "Path to comma-separated file containing information about the samples in the experiment.", - "help_text": "List of subjects to process. See `assets/samplesheet_raw.csv`.", - "fa_icon": "fas fa-file-csv", + "description": "Either a path to a BIDS database or to comma-separated file containing information about the input subjects files.", + "help_text": "BIDS database or csv file containing a list of subjects to process. See `assets/samplesheet.csv`.", + "fa_icon": "fas fa-folder-open", "exists": true, "type": "string", "format": "file-path", - "schema": "assets/schema_raw_input.json" + "schema": "assets/schema_input.json" }, - "bids": { - "mimetype": "text/csv", - "description": "Path to comma-separated file containing information about the bids databases used in the experiment.", - "help_text": "List of BIDS databases to load. See `assets/samplesheet_bids.csv`.", - "fa_icon": "fas fa-file-csv", - "exists": true, + "bids_config": { "type": "string", + "description": "BIDS configuration file in json pre-generated by TractoFlow(Read_BIDS process). This option must be used only if --bids did not work due to an issue in the BIDS folder.", + "fa_icon": "fas fa-file", + "help_text": "BIDS configuration file in json pre-generated by TractoFlow(Read_BIDS process). This option must be used only if --bids did not work due to an issue in the BIDS folder.", + "pattern": "^\\S+\\.json$", "format": "file-path", - "schema": "assets/schema_bids_input.json" + "mimetype": "text/json" }, - "dwi_shell_extract_tolerance": { - "type": "integer", - "default": 20, - "description": "Consider all b-values up to +-tolerance as belonging to the same shell.", - "minimum": 0 + "fsbids": { + "type": "string", + "description": "Freesurfer BIDS derivative to use to load segmentations and parcellations.", + "format": "directory-path", + "help_text": "Freesurfer BIDS derivative to use to load segmentations and parcellations.", + "fa_icon": "fas fa-folder-open", + "mimetype": "text/plain" }, - "dwi_b0_extract_threshold": { + "bidsignore": { + "type": "string", + "description": "If you want to ignore some subjects or some files, you can provide an extra bidsignore file. Check: https://github.com/bids-standard/bids-validator#bidsignore", + "fa_icon": "fas fa-file", + "help_text": "If you want to ignore some subjects or some files, you can provide an extra bidsignore file. Check: https://github.com/bids-standard/bids-validator#bidsignore", + "pattern": "^\\S+\\.bidsignore$", + "format": "file-path", + "mimetype": "text/plain" + }, + "clean_bids": { + "type": "boolean", + "description": "If set, it will remove all the participants that are missing any information.", + "fa_icon": "fas fa-wrench", + "help_text": "If set, it will remove all the participants that are missing any information." + }, + "b0_thr_extract_b0": { "type": "integer", "default": 10, "description": "Consider all b-values below threshold as b=0 images.", "minimum": 0, - "maximum": 100 + "maximum": 100, + "fa_icon": "fas fa-wrench", + "help_text": "Consider all b-values below threshold as b=0 images." }, - "clean_input_bids": { - "type": "boolean", - "description": "Remove all participants that are missing any information when reading input BIDS databases." + "dwi_shell_tolerance": { + "type": "integer", + "default": 20, + "description": "Consider all b-values up to +-tolerance as belonging to the same shell.", + "minimum": 0, + "fa_icon": "fas fa-wrench", + "help_text": "Consider all b-values up to +-tolerance as belonging to the same shell." }, "outdir": { "type": "string", "format": "directory-path", "description": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure.", - "fa_icon": "fas fa-folder-open" + "fa_icon": "fas fa-folder-open", + "help_text": "The output directory where the results will be saved. You have to use absolute paths to storage on Cloud infrastructure." }, "email": { "type": "string", "description": "Email address for completion summary.", "fa_icon": "fas fa-envelope", "help_text": "Set this parameter to your e-mail address to get a summary e-mail with details of the run sent to you when the workflow exits. If set in your user config file (`~/.nextflow/config`) then you don't need to specify this on the command line for every run.", - "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$" + "pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$", + "hidden": true }, "multiqc_title": { "type": "string", "description": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", - "fa_icon": "fas fa-file-signature" + "fa_icon": "fas fa-file-signature", + "help_text": "MultiQC report title. Printed as page header, used for filename if not otherwise specified.", + "hidden": true } } }, @@ -82,98 +101,109 @@ "type": "object", "description": "All parameters related to DWI preprocessing.", "properties": { - "dwi_mask_prelim_bet_dilation_radius": { - "type": "integer", - "default": 5, - "description": "Dilation radius applied to the preliminary mask to ensure whole brain coverage.", - "minimum": 1 - }, - "dwi_mask_prelim_bet_f_threshold": { + "bet_prelim_f": { "type": "number", "default": 0.16, "description": "Fractional Intensity threshold for preliminary brain masking.", + "help_text": "Fractional Intensity threshold for preliminary brain masking.", "minimum": -1, "maximum": 1 }, - "dwi_filter_noise": { + "dilate_b0_mask_prelim_brain_extraction": { + "type": "integer", + "default": 5, + "description": "Dilation radius applied to the preliminary mask to ensure whole brain coverage.", + "help_text": "Dilation radius applied to the preliminary mask to ensure whole brain coverage.", + "minimum": 1 + }, + "run_dwi_denoising": { "type": "boolean", "default": true, - "description": "Run background noise filtering on the DWI (Mrtrix3 dwidenoise)." + "description": "Run dwi denoising.", + "help_text": "Run dwi denoising." }, - "dwi_noise_filter_patch_size": { + "extent": { "type": "integer", "default": 7, - "description": "Size of the noise filtering kernel. Should at least as large as the cubic root of the number of independent directions in the dwi image.", + "description": "Denoising block size. Rule: extent^3 >= number of directions.", + "help_text": "Denoising block size. Rule: extent^3 >= number of directions.", "minimum": 2 }, - "dwi_filter_gibbs": { + "run_gibbs_correction": { "type": "boolean", - "description": "Run Gibbs Ringing artifact filtering on the DWI (Mrtrix3 mrdegibbs)." + "description": "RRun Gibbs correction.", + "help_text": "RRun Gibbs correction." }, - "dwi_filter_susceptibility": { + "run_topup": { "type": "boolean", "default": true, - "description": "Run Susceptibility Distortions Filtering on the DWI (FSL Topup)." + "description": "Run Topup.", + "help_text": "Run Topup." }, - "dwi_susceptibility_filter_config_file": { + "encoding_direction": { + "type": "string", + "description": "Encoding direction of the DWI [x, y, z]", + "help_text": "Encoding direction of the DWI [x, y, z]", + "default": "y" + }, + "readout": { + "type": "number", + "description": "Readout time", + "help_text": "Readout time", + "default": 0.062 + }, + "config_topup": { "type": "string", "default": "b02b0.cnf", - "description": "Configuration file to use for Topup. Can either be a full path to a file or the name of a configuration file in Topup resources." + "description": "Configuration file to use for Topup. Can either be a full path to a file or the name of a configuration file in Topup resources.", + "hidden": true }, - "dwi_susceptibility_filter_output_prefix": { + "prefix_topup": { "type": "string", "default": "topup_results", - "description": "Output prefix to use for susceptibility filtering output filenames." + "description": "Output prefix to use for susceptibility filtering output filenames.", + "hidden": true }, - "dwi_filter_eddy_and_motion": { + "run_eddy": { "type": "boolean", "default": true, - "description": "Filter eddy current and attenuate patient motion on the DWI (FSL Eddy)." + "description": "Run Eddy.", + "help_text": "Run Eddy." }, - "dwi_motion_and_eddy_filter_command": { + "eddy_cmd": { "type": "string", "default": "eddy_cpu", "description": "Command to use to run Eddy.", - "enum": [ - "eddy_cpu", - "eddy_cuda10.2" - ] + "help_text": "Command to use to run Eddy.", + "enum": ["eddy_cpu", "eddy_cuda10.2"] }, - "dwi_motion_and_eddy_filter_bet_f_threshold": { + "bet_topup_before_eddy_f": { "type": "number", "default": 0.16, - "description": "Fractional Intensity threshold for intermediate brain masking before Motion and Eddy filtering.", + "description": "Fractional Intensity threshold for intermediate brain masking on Topup corrected b0 images.", + "help_text": "Fractional Intensity threshold for intermediate brain masking on Topup corrected b0 images.", "minimum": -1, "maximum": 1 }, - "dwi_motion_and_eddy_filter_restore_slices": { + "use_slice_drop_correction": { "type": "boolean", "default": true, - "description": "Regenerate slices lost due to signal drop." + "description": "If set, will use the slice drop correction option from Eddy.", + "help_text": "If set, will use the slice drop correction option from Eddy." }, - "dwi_mask_final_bet_f_threshold": { + "bet_dwi_final_f": { "type": "number", "default": 0.16, "description": "Fractional Intensity threshold for final brain masking.", + "help_text": "Fractional Intensity threshold for final brain masking.", "minimum": -1, "maximum": 1 }, - "dwi_bias_correction_bspline_knots_per_voxel": { - "type": "number", - "default": 0.25, - "description": "Number of B-Spline knots per voxel for bias correction.", - "minimum": 0 - }, - "dwi_bias_correction_shrink_factor": { - "type": "integer", - "default": 4, - "description": "Shrink factor for bias correction.", - "minimum": 1 - }, - "dwi_intensities_normalize_fa_mask_threshold": { + "fa_mask_threshold": { "type": "number", "default": 0.4, - "description": "Maximum FA value for a voxel to be considered for intensity normalisation.", + "description": "FA threshold to compute WM mask for normalization.", + "help_text": "FA threshold to compute WM mask for normalization.", "minimum": 0, "maximum": 1 } @@ -184,30 +214,18 @@ "type": "object", "description": "All parameters related to T1 preprocessing.", "properties": { - "t1_filter_noise": { + "run_t1_denoising": { "type": "boolean", "default": true, - "description": "Run background noise filtering on the T1 (Dipy NL-Means)" - }, - "t1_tissue_segment_n_classes": { - "type": "integer", - "default": 3, - "description": "Number of tissue classes to estimate on the T1.", - "minimum": 1 + "description": "Run T1 denoising.", + "help_text": "Run T1 denoising." }, - "t1_bet_template_t1": { + "template_t1": { "type": "string", - "default": "/human-data/mni_152_sym_09c/t1/t1_template.nii.gz", - "description": "Path to the template T1 for antsBrainExtraction.", - "format": "file-path", - "exists": true - }, - "t1_bet_template_probability_map": { - "type": "string", - "default": "/human-data/mni_152_sym_09c/t1/t1_brain_probability_map.nii.gz", - "description": "Path to the template brain probability map for antsBrainExtraction.", - "format": "file-path", - "exists": true + "default": "/human-data/mni_152_sym_09c/t1", + "description": "Path to the template for antsBrainExtraction. The folder must contain t1_template.nii.gz and t1_brain_probability_map.nii.gz. The default path is the human_data folder in the singularity container", + "help_text": "Path to the template for antsBrainExtraction. The folder must contain t1_template.nii.gz and t1_brain_probability_map.nii.gz. The default path is the human_data folder in the singularity container", + "format": "directory-path" } } }, @@ -216,49 +234,45 @@ "type": "object", "description": "Resampling options for DWI and anatomical volumes.", "properties": { - "dwi_resample_spatial": { + "run_resample_dwi": { "type": "boolean", "default": true, - "description": "Resample DWI volumes to a finer isotropic spatial resolution before processing." + "description": "Run resample DWI.", + "help_text": "Run resample DWI." }, - "dwi_spatial_resample_resolution": { + "dwi_resolution": { "type": "number", "default": 1, - "description": "Isotropic spatial resolution for DWI resampling.", + "description": "DWI resolution.", + "help_text": "DWI resolution.", "minimum": 0 }, - "dwi_spatial_resample_interpolation": { + "dwi_interpolation": { "type": "string", "default": "lin", - "description": "Interpolation method to use for DWI resampling.", - "enum": [ - "nn", - "lin", - "quad", - "cubic" - ] + "description": "DWI interpolation method.", + "help_text": "DWI interpolation method", + "enum": ["nn", "lin", "quad", "cubic"] }, - "t1_resample_spatial": { + "run_resample_t1": { "type": "boolean", "default": true, - "description": "Resample T1 volumes to a finer isotropic spatial resolution before processing." + "description": "Run resample T1.", + "help_text": "Run resample T1." }, - "t1_spatial_resample_resolution": { + "t1_resolution": { "type": "number", "default": 1, - "description": "Isotropic spatial resolution for T1 resampling.", + "description": "T1 resolution.", + "help_text": "T1 resolution.", "minimum": 0 }, - "t1_spatial_resample_interpolation": { + "t1_interpolation": { "type": "string", "default": "lin", - "description": "Interpolation method to use for T1 resampling.", - "enum": [ - "nn", - "lin", - "quad", - "cubic" - ] + "description": "T1 interpolation method.", + "help_text": "T1 interpolation method.", + "enum": ["nn", "lin", "quad", "cubic"] } } }, @@ -267,123 +281,137 @@ "type": "object", "description": "All parameters related to DWI processing (SH, DTI, fODF).", "properties": { - "dwi_fit_signal_sh": { - "type": "boolean", - "default": true, - "description": "Compute a Spherical Harmonics fit on the DWI, and output the SH coefficients in a Nifti file." - }, - "dwi_signal_sh_fit_basis": { - "type": "string", - "default": "descoteaux07", - "description": "Spherical harmonics basis used for the signal fit.", - "enum": [ - "descoteaux07", - "tournier07" - ] - }, - "dwi_signal_sh_fit_order": { - "type": "integer", - "default": 6, - "description": "Spherical harmonics order for the signal fit. Must be an even number.", - "minimum": 2 - }, - "dwi_signal_sh_fit_shell": { - "type": "integer", - "description": "Shell to select for the signal fit. The b0 images will be extracted and concatenated to it for fitting." - }, - "dwi_dti_fit_shell_max_bvalue": { + "max_dti_shell_value": { "type": "integer", "default": 1200, - "description": "Maximum b-value considered for the DTI fit.", + "description": "Maximum shell threshold to be consider as a DTI shell. This is the default behavior to select DTI shells.", + "help_text": "Maximum shell threshold to be consider as a DTI shell. This is the default behavior to select DTI shells.", "minimum": 0 }, - "dwi_dti_fit_shells": { + "dti_shells": { "type": "string", - "description": "Shells to use for the DTI fit. List them through command line using a whitespace separated list of b-values. Overseeds other arguments setting shells." + "description": "Shells selected to compute the DTI metrics (generally b <= 1200). Please write them between quotes e.g. (--dti_shells \"0 300 1000\"). If selected, it will overwrite max_dti_shell_value.", + "help_text": "Shells selected to compute the DTI metrics (generally b <= 1200). Please write them between quotes e.g. (--dti_shells \"0 300 1000\"). If selected, it will overwrite max_dti_shell_value." }, - "dwi_fodf_fit_shell_min_bvalue": { + "min_fodf_shell_value": { "type": "integer", "default": 700, - "description": "Minimum b-value considered for fODF fit (b0 shell will be preserved).", + "description": "Minimum shell threshold to be consider as a fODF shell. This is the default behaviour to select fODF shells.", + "help_text": "Minimum shell threshold to be consider as a fODF shell. This is the default behaviour to select fODF shells.", "minimum": 0 }, - "dwi_fodf_fit_shells": { + "fodf_shells": { "type": "string", - "description": "Shells to use for the fODF fit. List them through command line using a whitespace separated list of b-values. Overseeds other arguments setting shells." + "description": "Shells selected to compute the fODF metrics (generally b >= 700). Please write them between quotes e.g. (--fodf_shells \"0 1000 2000\"). If selected, it will overwrite min_fodf_shell_value.", + "help_text": "Shells selected to compute the fODF metrics (generally b >= 700). Please write them between quotes e.g. (--fodf_shells \"0 1000 2000\"). If selected, it will overwrite min_fodf_shell_value." }, - "dwi_fodf_fit_frf_max_fa_threshold": { + "fa": { "type": "number", "default": 0.7, - "description": "Maximum FA threshold to compute the fiber response function. Value decreases over time until the minimum threshold is reached.", + "description": "Initial FA threshold to compute the frf.", + "help_text": "Initial FA threshold to compute the frf.", "minimum": 0, "maximum": 1 }, - "dwi_fodf_fit_frf_min_fa_threshold": { + "min_fa": { "type": "number", "default": 0.5, - "description": "Minimum FA threshold to compute the fiber response function. An error is raised if not enough voxels are found after reaching this threshold.", + "description": "Minimum FA threshold to compute the frf.", + "help_text": "Minimum FA threshold to compute the frf.", "minimum": 0, "maximum": 1 }, - "dwi_fodf_fit_frf_min_n_voxels": { + "min_nvox": { "type": "integer", "default": 300, - "description": "Minimum acceptable number of voxels to compute a fiber response function. An error is raised if not enough voxels are found.", + "description": "Minimum number of voxels to compute the frf.", + "help_text": "Minimum number of voxels to compute the frf.", "minimum": 1 }, - "dwi_fodf_fit_frf_roi_radius": { + "roi_radius": { "type": "integer", "default": 20, - "description": "Isotropic radius of the Region Of Interest to use to compute the fiber response function." + "description": "Region of interest radius to compute the frf.", + "help_text": "Region of interest radius to compute the frf." }, - "dwi_fodf_fit_force_frf": { + "manual_frf": { "type": "string", - "description": "Force a fiber response function. Supply as 3 values (mean b0, parallel diffusivity, perpendicular diffusivity). List it through command line using a whitespace separated list." + "description": "Force a fiber response function. Supply as 3 values : --manual_frf \"mean_b0 parallel_diff perpendicular_diff\".", + "help_text": "Force a fiber response function. Supply as 3 values : --manual_frf \"mean_b0 parallel_diff perpendicular_diff\"." }, - "dwi_fodf_fit_use_average_frf": { + "mean_frf": { "type": "boolean", - "description": "Average the fiber response function over inputs. USE ONLY IF ALL OF SUBJECTS COME FROM THE SAME SCANNER AND ADHERE TO THE SAME ACQUISITION PROTOCOL." + "description": "Average the frf of all subjects. USE ONLY IF ALL OF SUBJECTS COME FROM THE SAME SCANNER AND ADHERE TO THE SAME ACQUISITION PROTOCOL.", + "help_text": "Average the frf of all subjects. USE ONLY IF ALL OF SUBJECTS COME FROM THE SAME SCANNER AND ADHERE TO THE SAME ACQUISITION PROTOCOL." }, - "dwi_fodf_fit_order": { + "sh_order": { "type": "integer", "default": 8, - "description": "Spherical harmonics order for the fODF fit. Must be an even number.", + "description": "Spherical harmonics order, must be even. Rules : sh_order=8 for 45 directions, sh_order=6 for 28 directions.", + "help_text": "Spherical harmonics order, must be even. Rules : sh_order=8 for 45 directions, sh_order=6 for 28 directions.", "minimum": 2 }, - "dwi_fodf_fit_basis": { + "basis": { "type": "string", "default": "descoteaux07", - "description": "Spherical harmonics basis used for the signal fit.", - "enum": [ - "descoteaux07", - "tournier07" - ] + "description": "fODF basis.", + "help_text": "fODF basis.", + "enum": ["descoteaux07", "tournier07"] }, - "dwi_fodf_fit_peaks_absolute_factor": { + "fodf_metrics_a_factor": { "type": "number", "default": 2, - "description": "Multiplicative factor to threshold fODF peaks by amplitude, compared to values obtained in ventricles.", + "description": "Multiplicative factor for AFD max in ventricles.", + "help_text": "Multiplicative factor for AFD max in ventricles.", "minimum": 0 }, - "dwi_fodf_fit_peaks_relative_threshold": { + "relative_threshold": { "type": "number", "default": 0.1, - "description": "Relative fODF peak amplitude threshold, a a ratio of the largest local peak.", + "description": "Relative threshold on fODF amplitude in ]0,1].", + "help_text": "Relative threshold on fODF amplitude in ]0,1].", "minimum": 0, "maximum": 1 }, - "dwi_fodf_fit_peaks_ventricle_max_fa": { + "max_fa_in_ventricle": { "type": "number", "default": 0.1, - "description": "Maximum FA threshold for a voxel to be considered inside ventricules.", + "description": "Maximal threshold of FA to be considered as ventricles voxels.", + "help_text": "Maximal threshold of FA to be considered as ventricles voxels.", "minimum": 0, "maximum": 1 }, - "dwi_fodf_fit_peaks_ventricle_min_md": { + "min_md_in_ventricle": { "type": "number", "default": 0.003, - "description": "Minimum MD threshold for a voxel to be considered inside ventricules.", + "description": "Minimal threshold of MD in mm2/s to be considered as ventricles voxels.", + "help_text": "Minimal threshold of MD in mm2/s to be considered as ventricles voxels.", "minimum": 0 + }, + "sh_fitting": { + "type": "boolean", + "default": true, + "description": "Compute a Spherical Harmonics fit on the DWI, and output the SH coefficients in a Nifti file.", + "help_text": "Compute a Spherical Harmonics fit on the DWI, and output the SH coefficients in a Nifti file." + }, + "sh_fitting_basis": { + "type": "string", + "default": "descoteaux07", + "description": "SH basis used for the optional SH fitting.", + "help_text": "SH basis used for the optional SH fitting.", + "enum": ["descoteaux07", "tournier07"] + }, + "sh_fitting_order": { + "type": "integer", + "default": 6, + "description": "SH order used for the optional SH fitting, must be an even number. Rules : sh_order=8 for 45 directions, sh_order=6 for 28 directions", + "help_text": "SH order used for the optional SH fitting, must be an even number. Rules : sh_order=8 for 45 directions, sh_order=6 for 28 directions", + "minimum": 2 + }, + "sh_fitting_shells": { + "type": "string", + "description": "Shells selected to compute the SH fitting. Please write them between quotes e.g. (--sh_fitting_shells \"0 1000\"). NOTE: SH fitting works only on single shell. You must include the b0 shell as well.", + "help_text": "Shells selected to compute the SH fitting. Please write them between quotes e.g. (--sh_fitting_shells \"0 1000\"). NOTE: SH fitting works only on single shell. You must include the b0 shell as well." } } }, @@ -392,122 +420,124 @@ "type": "object", "description": "All parameters related to particle filtering tractography on fODF fields.", "properties": { - "fodf_fit_pft": { + "run_pft_tracking": { "type": "boolean", "default": true, - "description": "Run Particle Filtering Tractography (PFT)." + "description": "Run Particle Filtering Tractography (PFT).", + "help_text": "Run Particle Filtering Tractography (PFT)." }, - "fodf_pft_fit_random_seed": { - "type": "integer", - "default": 0, - "description": "List of seed numbers for the random number generator. List them through command line using a whitespace separated list of seeds.", - "minimum": 0 + "pft_seeding_mask_type": { + "type": "string", + "default": "wm", + "description": "[PFT] Seeding mask type.", + "help_text": "[PFT] Seeding mask type.", + "enum": ["wm", "interface", "fa"] }, - "fodf_pft_fit_algorithm": { + "pft_fa_seeding_mask_threshold": { + "type": "number", + "default": 0.1, + "description": "[PFT] FA threshold for FA seeding mask.", + "help_text": "[PFT] FA threshold for FA seeding mask.", + "minimum": 0, + "maximum": 1 + }, + "pft_algo": { "type": "string", "default": "prob", - "description": "Tractography algorithm type.", - "enum": [ - "prob", - "det" - ] + "description": "[PFT] Tracking algorithm.", + "help_text": "[PFT] Tracking algorithm.", + "enum": ["prob", "det"] }, - "fodf_pft_fit_step_size": { + "pft_seeding": { + "type": "string", + "default": "npv", + "description": "[PFT] Seeding type.", + "help_text": "[PFT] Seeding type.", + "enum": ["npv", "nt"] + }, + "pft_nbr_seeds": { + "type": "integer", + "default": 10, + "description": "[PFT] Number of seeds related to the seeding type param.", + "help_text": "[PFT] Number of seeds related to the seeding type param.", + "minimum": 1 + }, + "pft_step": { "type": "number", "default": 0.5, - "description": "Length between steps in mm.", + "description": "[PFT] Step size.", + "help_text": "[PFT] Step size.", "minimum": 0 }, - "fodf_pft_fit_theta_max_deviation": { + "pft_theta": { "type": "number", "default": 20, - "description": "Maximum angular deviation between steps in degrees.", + "description": "[PFT] Maximum angle between 2 steps.", + "help_text": "[PFT] Maximum angle between 2 steps.", "minimum": 0, "maximum": 360 }, - "fodf_pft_fit_streamline_min_length": { + "pft_sfthres": { + "type": "number", + "default": 0.1, + "description": "Threshold on spherical functions computed from fODF.", + "minimum": 0, + "hidden": true + }, + "pft_sfthres_init": { + "type": "number", + "default": 0.5, + "description": "Initial threshold on spherical functions computed from fODF.", + "minimum": 0, + "hidden": true + }, + "pft_min_len": { "type": "number", "default": 20, - "description": "Minimum streamline length in mm.", + "description": "[PFT] Minimum length.", + "help_text": "[PFT] Minimum length.", "minimum": 0 }, - "fodf_pft_fit_streamline_max_length": { + "pft_max_len": { "type": "number", "default": 200, - "description": "Maximum streamline length in mm.", + "description": "[PFT] Maximum length .", + "help_text": "[PFT] Maximum length .", "minimum": 0 }, - "fodf_pft_fit_seeding_type": { - "type": "string", - "default": "wm", - "description": "Type of seeding mask to compute.", - "enum": [ - "wm", - "interface", - "fa" - ] - }, - "fodf_pft_fit_seeding_strategy": { - "type": "string", - "default": "npv", - "description": "Number of seeds initiated per voxel (npv) or globally (nt).", - "enum": [ - "npv", - "nt" - ] - }, - "fodf_pft_fit_seeding_n_seeds": { - "type": "integer", - "default": 10, - "description": "Number of seeds to use, given the seeding strategy.", - "minimum": 1 - }, - "fodf_pft_fit_seeding_fa_mask_threshold": { - "type": "number", - "default": 0.1, - "description": "Minimum FA threshold when seeding in FA.", - "minimum": 0, - "maximum": 1 - }, - "fodf_pft_fit_filter_n_particles": { + "pft_particles": { "type": "integer", "default": 15, "description": "Number of particles propagated in case of premature termination.", - "minimum": 1 + "minimum": 1, + "hidden": true }, - "fodf_pft_fit_filter_backward_step_size": { + "pft_back": { "type": "number", "default": 2, "description": "Length of the backtracking steps of the particle filter in mm.", - "minimum": 0 + "minimum": 0, + "hidden": true }, - "fodf_pft_fit_filter_forward_step_size": { + "pft_front": { "type": "number", "default": 1, "description": "Length of the forward tracking steps of the particle filter in mm.", - "minimum": 0 - }, - "fodf_pft_fit_sf_threshold": { - "type": "number", - "default": 0.1, - "description": "Threshold on spherical functions computed from fODF.", - "minimum": 0 + "minimum": 0, + "hidden": true }, - "fodf_pft_fit_sf_initial_threshold": { + "pft_compress_value": { "type": "number", - "default": 0.5, - "description": "Initial threshold on spherical functions computed from fODF.", + "default": 0.2, + "description": "[PFT] Compression error threshold.", + "help_text": "[PFT] Compression error threshold.", "minimum": 0 }, - "fodf_pft_fit_compress_tractogram": { - "type": "boolean", - "default": true, - "description": "Compress streamlines to save space." - }, - "fodf_pft_fit_compress_max_displacement": { - "type": "number", - "default": 0.2, - "description": "Maximum point displacement allowed when compressing.", + "pft_random_seed": { + "type": "integer", + "default": 0, + "description": "[PFT] List of random seed numbers for the random number generator. Please write them as list separated using comma WITHOUT SPACE e.g. (--pft_random_seed 0,1,2)", + "help_text": "[PFT] List of random seed numbers for the random number generator. Please write them as list separated using comma WITHOUT SPACE e.g. (--pft_random_seed 0,1,2)", "minimum": 0 } } @@ -517,136 +547,131 @@ "type": "object", "description": "All parameters related to local tractography on fODF fields.", "properties": { - "fodf_fit_local": { + "run_local_tracking": { "type": "boolean", "default": true, - "description": "Run local tractography." - }, - "fodf_local_fit_processor": { - "type": "string", - "default": "cpu", - "description": "Type of processor to use, either CPU or GPU (using OpenCL).", - "enum": [ - "cpu", - "gpu" - ] - }, - "fodf_local_fit_gpu_batch_size": { - "type": "integer", - "default": 10000, - "description": "Approximate size of GPU batches (number of streamlines to track in parallel).", - "minimum": 1 + "description": "Run Local Tracking.", + "help_text": "Run Local Tracking." }, - "fodf_local_fit_random_seed": { - "type": "integer", - "default": 0, - "description": "List of seed numbers for the random number generator. List them through command line using a whitespace separated list of seeds.", - "minimum": 0 - }, - "fodf_local_fit_algorithm": { + "local_seeding_mask_type": { "type": "string", - "default": "prob", - "description": "Tractography algorithm type.", - "enum": [ - "prob", - "det" - ] - }, - "fodf_local_fit_step_size": { - "type": "number", - "default": 0.5, - "description": "Length between tracking steps in mm.", - "minimum": 0 + "default": "wm", + "description": "[LOCAL] seeding mask type.", + "help_text": "[LOCAL] seeding mask type.", + "enum": ["wm", "fa", "interface"] }, - "fodf_local_fit_theta_max_deviation": { + "local_fa_seeding_mask_threshold": { "type": "number", - "default": 20, - "description": "Maximum angular deviation between steps in degrees.", + "default": 0.1, + "description": "[LOCAL] FA threshold for FA seeding mask.", + "help_text": "[LOCAL] FA threshold for FA seeding mask.", "minimum": 0, - "maximum": 360 + "maximum": 1 }, - "fodf_local_fit_streamline_min_length": { - "type": "number", - "default": 20, - "description": "Minimum streamline length in mm.", - "minimum": 0 + "local_tracking_mask_type": { + "type": "string", + "description": "[LOCAL] tracking mask type.", + "help_text": "[LOCAL] tracking mask type.", + "default": "wm", + "enum": ["wm", "fa"] }, - "fodf_local_fit_streamline_max_length": { + "local_fa_tracking_mask_threshold": { "type": "number", - "default": 200, - "description": "Maximum streamline length in mm.", - "minimum": 0 + "description": "[LOCAL] FA threshold for FA tracking mask.", + "help_text": "[LOCAL] FA threshold for FA tracking mask.", + "default": 0.1, + "minimum": 0, + "maximum": 1 }, - "fodf_local_fit_seeding_type": { + "local_algo": { "type": "string", - "default": "wm", - "description": "Type of seeding mask to compute.", - "enum": [ - "wm", - "fa", - "interface" - ] + "default": "prob", + "description": "[LOCAL] Tracking algorithm.", + "help_text": "[LOCAL] Tracking algorithm.", + "enum": ["prob", "det"] }, - "fodf_local_fit_seeding_strategy": { + "local_seeding": { "type": "string", "default": "npv", - "description": "Number of seeds initiated per voxel (npv) or globally (nt).", - "enum": [ - "npv", - "nt" - ] + "description": "[LOCAL] Seeding type.", + "help_text": "[LOCAL] Seeding type.", + "enum": ["npv", "nt"] }, - "fodf_local_fit_seeding_n_seeds": { + "local_nbr_seeds": { "type": "integer", "default": 10, - "description": "Number of seeds to use, given the seeding strategy.", + "description": "[LOCAL] Number of seeds related to the seeding type param.", + "help_text": "[LOCAL] Number of seeds related to the seeding type param.", "minimum": 1 }, - "fodf_local_fit_seeding_fa_threshold": { + "local_step": { "type": "number", - "default": 0.1, - "description": "Minimum FA threshold when seeding in FA.", - "minimum": 0, - "maximum": 1 - }, - "fodf_local_fit_tracking_mask_type": { - "type": "string", - "description": "Type of tracking mask to compute.", - "default": "wm", - "enum": [ - "wm", - "fa" - ] + "default": 0.5, + "description": "[LOCAL] Step size.", + "help_text": "[LOCAL] Step size.", + "minimum": 0 }, - "fodf_local_fit_tracking_mask_fa_threshold": { + "local_theta": { "type": "number", - "description": "Minimum FA threshold when tracking in FA.", - "default": 0.1, + "default": 20, + "description": "[LOCAL] Maximum angle between 2 steps.", + "help_text": "[LOCAL] Maximum angle between 2 steps.", "minimum": 0, - "maximum": 1 + "maximum": 360 }, - "fodf_local_fit_sf_threshold": { + "local_sfthres": { "type": "number", "default": 0.1, "description": "Threshold on spherical functions computed from fODF.", - "minimum": 0 + "minimum": 0, + "hidden": true }, - "fodf_local_fit_sf_initial_threshold": { + "local_sfthres_init": { "type": "number", "default": 0.5, "description": "Initial threshold on spherical functions computed from fODF.", + "minimum": 0, + "hidden": true + }, + "local_min_len": { + "type": "number", + "default": 20, + "description": "[LOCAL] Minimum length.", + "help_text": "[LOCAL] Minimum length.", "minimum": 0 }, - "fodf_local_fit_compress_tractogram": { - "type": "boolean", - "default": true, - "description": "Compress streamlines to save space." + "local_max_len": { + "type": "number", + "default": 200, + "description": "[LOCAL] Maximum length.", + "help_text": "[LOCAL] Maximum length.", + "minimum": 0 }, - "fodf_local_fit_compress_max_displacement": { + "local_compress_value": { "type": "number", "default": 0.2, - "description": "Maximum point displacement allowed when compressing.", + "description": "[LOCAL] Compression error threshold.", + "help_text": "[LOCAL] Compression error threshold.", + "minimum": 0 + }, + "local_random_seed": { + "type": "integer", + "default": 0, + "description": "[LOCAL] List of random seed numbers for the random number generator. Please write them as list separated using comma WITHOUT SPACE e.g. (--local_random_seed 0,1,2).", + "help_text": "[LOCAL] List of random seed numbers for the random number generator. Please write them as list separated using comma WITHOUT SPACE e.g. (--local_random_seed 0,1,2).", "minimum": 0 + }, + "local_batch_size_gpu": { + "type": "integer", + "default": 10000, + "description": "[LOCAL-GPU] Approximate size of GPU batches (number of streamlines to track in parallel).", + "help_text": "[LOCAL-GPU] Approximate size of GPU batches (number of streamlines to track in parallel).", + "minimum": 1 + }, + "local_tracking_gpu": { + "type": "boolean", + "description": "Use GPU tracking", + "hidden": true } } }, @@ -748,13 +773,6 @@ "fa_icon": "fas fa-palette", "hidden": true }, - "hook_url": { - "type": "string", - "description": "Incoming hook URL for messaging service", - "fa_icon": "fas fa-people-group", - "help_text": "Incoming hook URL for messaging service. Currently, MS Teams and Slack are supported.", - "hidden": true - }, "multiqc_config": { "type": "string", "format": "file-path", @@ -771,7 +789,8 @@ "multiqc_methods_description": { "type": "string", "description": "Custom MultiQC yaml file containing HTML including a methods description.", - "fa_icon": "fas fa-cog" + "fa_icon": "fas fa-cog", + "hidden": true }, "validate_params": { "type": "boolean", @@ -794,72 +813,160 @@ "hidden": true } } + }, + "subworkflows_hidden_options": { + "properties": { + "preproc_dwi_run_denoising": { + "type": "boolean", + "hidden": true + }, + "preproc_dwi_run_degibbs": { + "type": "boolean", + "hidden": true + }, + "topup_eddy_run_topup": { + "type": "boolean", + "hidden": true + }, + "topup_eddy_run_eddy": { + "type": "boolean", + "hidden": true + }, + "preproc_dwi_run_N4": { + "type": "boolean", + "hidden": true + }, + "preproc_t1_run_denoising": { + "type": "boolean", + "hidden": true + }, + "preproc_t1_run_N4": { + "type": "boolean", + "hidden": true + }, + "preproc_t1_run_synthbet": { + "type": "boolean", + "hidden": true + }, + "preproc_t1_run_ants_bet": { + "type": "boolean", + "hidden": true + }, + "preproc_t1_run_crop": { + "type": "boolean", + "hidden": true + }, + "preproc_dwi_run_resampling": { + "type": "boolean", + "hidden": true + }, + "preproc_t1_run_resampling": { + "type": "boolean", + "hidden": true + }, + "frf_average_from_data": { + "type": "boolean", + "hidden": true + }, + "run_pft": { + "type": "boolean", + "hidden": true + } + } } }, "allOf": [ { "$ref": "#/$defs/input_output_options" }, + { + "$ref": "#/$defs/dwi_preprocessing_options" + }, + { + "$ref": "#/$defs/t1_preprocessing_options" + }, + { + "$ref": "#/$defs/resampling_options" + }, + { + "$ref": "#/$defs/dwi_processing_options" + }, + { + "$ref": "#/$defs/particle_filtering_tractography_options" + }, + { + "$ref": "#/$defs/local_tractography_options" + }, { "$ref": "#/$defs/institutional_config_options" }, { - "$ref": "#/$definitions/generic_options" + "$ref": "#/$defs/generic_options" + }, + { + "$ref": "#/$defs/dwi_preprocessing_options" }, { - "$ref": "#/definitions/dwi_preprocessing_options" + "$ref": "#/$defs/t1_preprocessing_options" }, { - "$ref": "#/definitions/t1_preprocessing_options" + "$ref": "#/$defs/resampling_options" }, { - "$ref": "#/definitions/resampling_options" + "$ref": "#/$defs/dwi_processing_options" }, { - "$ref": "#/definitions/dwi_processing_options" + "$ref": "#/$defs/particle_filtering_tractography_options" }, { - "$ref": "#/definitions/particle_filtering_tractography_options" + "$ref": "#/$defs/local_tractography_options" }, { - "$ref": "#/definitions/local_tractography_options" + "$ref": "#/$defs/subworkflows_hidden_options" } ], "properties": { - "allocation_cpu_max_bet": { + "processes_denoise_dwi": { "type": "integer", "default": 4, - "description": "Number of processes for T1 brain extraction." + "description": "Number of processes for DWI denoising.", + "help_text": "Number of processes for DWI denoising." }, - "allocation_cpu_max_dwi_filter_noise": { + "processes_eddy": { "type": "integer", "default": 4, - "description": "Number of processes for DWI denoising." + "description": "Number of processes for Eddy and motion filtering.", + "help_text": "Number of processes for Eddy and motion filtering." }, - "allocation_cpu_max_dwi_filter_motion_and_eddy": { + "processes_fodf": { "type": "integer", "default": 4, - "description": "Number of processes for Eddy and motion filtering." + "description": "Number of processes for fODF fitting.", + "help_text": "Number of processes for fODF fitting." }, - "allocation_cpu_max_dwi_fit_fodf": { + "processes_local_tracking": { "type": "integer", "default": 4, - "description": "Number of processes for fODF fitting." + "description": "Number of processes for local tractography.", + "help_text": "Number of processes for local tractography." }, - "allocation_cpu_max_fodf_fit_local": { + "processes_denoise_t1": { "type": "integer", "default": 4, - "description": "Number of processes for local tractography." + "description": "Number of processes for T1 denoising", + "help_text": "Number of processes for T1 denoising" }, - "allocation_cpu_max_t1_filter_noise": { + "processes_brain_extraction_t1": { "type": "integer", "default": 4, - "description": "Number of processes for T1 denoising" + "description": "Number of processes for T1 brain extraction.", + "help_text": "Number of processes for T1 brain extraction." }, - "allocation_cpu_max_t1_registration": { + "processes_registration": { "type": "integer", "default": 4, - "description": "Number of processes for registration task" + "description": "Number of processes for registration task", + "help_text": "Number of processes for registration task" } } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..aba2578 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,26 @@ +{ + "name": "nf-tractoflow", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "prettier": "^3.5.3" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3f4b13c --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "prettier": "^3.5.3" + } +} diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index 3fcd09a..76c8a0a 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -22,8 +22,8 @@ "@id": "./", "@type": "Dataset", "creativeWorkStatus": "InProgress", - "datePublished": "2025-03-03T21:18:22+00:00", - "description": "# scilus/nf-tractoflow\n\n[![GitHub Actions CI Status](https://github.com/scilus/nf-tractoflow/actions/workflows/ci.yml/badge.svg)](https://github.com/scilus/nf-tractoflow/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/scilus/nf-tractoflow/actions/workflows/linting.yml/badge.svg)](https://github.com/scilus/nf-tractoflow/actions/workflows/linting.yml)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX)\n[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com)\n\n[![Nextflow](https://img.shields.io/badge/nextflow%20DSL2-%E2%89%A524.04.2-23aa62.svg)](https://www.nextflow.io/)\n[![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/)\n[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/)\n[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/)\n[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/scilus/nf-tractoflow)\n\n## Introduction\n\n**scilus/nf-tractoflow** is a bioinformatics pipeline that ...\n\n\n\n\n2. Present QC for raw reads ([`MultiQC`](http://multiqc.info/))\n\n## Usage\n\n> [!NOTE]\n> If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/usage/installation) on how to set-up Nextflow. Make sure to [test your setup](https://nf-co.re/docs/usage/introduction#how-to-run-a-pipeline) with `-profile test` before running the workflow on actual data.\n\n\n\nNow, you can run the pipeline using:\n\n\n\n```bash\nnextflow run scilus/nf-tractoflow \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \n```\n\n> [!WARNING]\n> Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files).\n\n## Credits\n\nscilus/nf-tractoflow was originally written by @AlexVCaron.\n\nWe thank the following people for their extensive assistance in the development of this pipeline:\n\n\n\n## Contributions and Support\n\nIf you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md).\n\n## Citations\n\n\n\n\n\n\nAn extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file.\n\nThis pipeline uses code and infrastructure developed and maintained by the [nf-core](https://nf-co.re) community, reused here under the [MIT license](https://github.com/nf-core/tools/blob/main/LICENSE).\n\n> **The nf-core framework for community-curated bioinformatics pipelines.**\n>\n> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen.\n>\n> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x).\n", + "datePublished": "2025-03-17T15:41:05+00:00", + "description": "# scilus/nf-tractoflow\n\n[![GitHub Actions CI Status](https://github.com/scilus/nf-tractoflow/actions/workflows/ci.yml/badge.svg)](https://github.com/scilus/nf-tractoflow/actions/workflows/ci.yml)\n[![GitHub Actions Linting Status](https://github.com/scilus/nf-tractoflow/actions/workflows/linting.yml/badge.svg)](https://github.com/scilus/nf-tractoflow/actions/workflows/linting.yml)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX)\n[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com)\n\n[![Nextflow](https://img.shields.io/badge/nextflow%20DSL2-%E2%89%A524.04.2-23aa62.svg)](https://www.nextflow.io/)\n[![run with conda](http://img.shields.io/badge/run%20with-conda-3EB049?labelColor=000000&logo=anaconda)](https://docs.conda.io/en/latest/)\n[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/)\n[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/)\n[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/scilus/nf-tractoflow)\n\n## Introduction\n\n**scilus/nf-tractoflow** is a medical imaging pipeline that processes diffusion MRI images of human brains and reconstructs white matter pathways using tractography.\n\n\n\n\n1. Read Inputs\n2. Preprocess DWI\n3. Preprocess T1\n4. Segment T1\n5. Reconstruct diffusion profiles\n6. Run tractography\n\n## Usage\n\n> [!NOTE]\n> If you are new to Nextflow and nf-core, please refer to [this page](https://nf-co.re/docs/usage/installation) on how to set-up Nextflow. Make sure to [test your setup](https://nf-co.re/docs/usage/introduction#how-to-run-a-pipeline) with `-profile test` before running the workflow on actual data.\n\n\n\nNow, you can run the pipeline using:\n\n\n\n```bash\nnextflow run scilus/nf-tractoflow \\\n -profile \\\n <--input|--bids> samplesheet.csv \\\n --outdir \n```\n\n> [!WARNING]\n> Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files).\n\n## Credits\n\nscilus/nf-tractoflow was originally written by @AlexVCaron.\n\nWe thank the following people for their extensive assistance in the development of this pipeline:\n\n\n\n## Contributions and Support\n\nIf you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md).\n\n## Citations\n\n\n\n\n\n\nAn extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file.\n\nThis pipeline uses code and infrastructure developed and maintained by the [nf-core](https://nf-co.re) community, reused here under the [MIT license](https://github.com/nf-core/tools/blob/main/LICENSE).\n\n> **The nf-core framework for community-curated bioinformatics pipelines.**\n>\n> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen.\n>\n> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x).\n", "hasPart": [ { "@id": "main.nf" @@ -37,9 +37,15 @@ { "@id": "docs/" }, + { + "@id": "docs/images/" + }, { "@id": "modules/" }, + { + "@id": "modules/local/" + }, { "@id": "modules/nf-core/" }, @@ -93,7 +99,7 @@ }, "mentions": [ { - "@id": "#ece4bfa1-3155-45e6-873a-27e68110d28b" + "@id": "#41dcf70d-9188-41da-abfa-1af5c63aa58e" } ], "name": "scilus/nf-tractoflow" @@ -126,7 +132,7 @@ } ], "dateCreated": "", - "dateModified": "2025-03-03T16:18:22Z", + "dateModified": "2025-03-17T11:41:05Z", "dct:conformsTo": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE/", "keywords": [ "nf-core", @@ -165,11 +171,11 @@ "version": "!>=24.04.2" }, { - "@id": "#ece4bfa1-3155-45e6-873a-27e68110d28b", + "@id": "#41dcf70d-9188-41da-abfa-1af5c63aa58e", "@type": "TestSuite", "instance": [ { - "@id": "#af6dc317-60d5-4a7f-9149-9379f36f3f90" + "@id": "#037e9227-20c7-4ec0-8418-de943387e5f8" } ], "mainEntity": { @@ -178,7 +184,7 @@ "name": "Test suite for scilus/nf-tractoflow" }, { - "@id": "#af6dc317-60d5-4a7f-9149-9379f36f3f90", + "@id": "#037e9227-20c7-4ec0-8418-de943387e5f8", "@type": "TestInstance", "name": "GitHub Actions workflow for testing scilus/nf-tractoflow", "resource": "repos/scilus/nf-tractoflow/actions/workflows/ci.yml", @@ -210,11 +216,21 @@ "@type": "Dataset", "description": "Markdown files for documenting the pipeline" }, + { + "@id": "docs/images/", + "@type": "Dataset", + "description": "Images for the documentation files" + }, { "@id": "modules/", "@type": "Dataset", "description": "Modules used by the pipeline" }, + { + "@id": "modules/local/", + "@type": "Dataset", + "description": "Pipeline-specific modules" + }, { "@id": "modules/nf-core/", "@type": "Dataset", diff --git a/subworkflows/local/utils_nfcore_nf-tractoflow_pipeline/main.nf b/subworkflows/local/utils_nfcore_nf-tractoflow_pipeline/main.nf index 0e21626..10d8577 100644 --- a/subworkflows/local/utils_nfcore_nf-tractoflow_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_nf-tractoflow_pipeline/main.nf @@ -13,13 +13,10 @@ include { paramsSummaryMap } from 'plugin/nf-schema' include { samplesheetToList } from 'plugin/nf-schema' include { completionEmail } from '../../nf-core/utils_nfcore_pipeline' include { completionSummary } from '../../nf-core/utils_nfcore_pipeline' -include { imNotification } from '../../nf-core/utils_nfcore_pipeline' include { UTILS_NFCORE_PIPELINE } from '../../nf-core/utils_nfcore_pipeline' include { UTILS_NEXTFLOW_PIPELINE } from '../../nf-core/utils_nextflow_pipeline' - -include { IO_SAFECASTINPUTS } from '../../../modules/local/io/safecastinputs/main' -include { IO_READBIDS } from '../../../modules/nf-scil/io/readbids/main' - +include { IO_BIDS } from '../../nf-neuro/io_bids/main' +include { IO_SAFECASTINPUTS } from '../../../modules/local/io/safecastinputs' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SUBWORKFLOW TO INITIALISE PIPELINE @@ -39,6 +36,7 @@ workflow PIPELINE_INITIALISATION { main: ch_versions = Channel.empty() + ch_samplesheet = Channel.empty() // // Print version and exit if required and dump pipeline parameters to JSON file @@ -70,69 +68,71 @@ workflow PIPELINE_INITIALISATION { // // Create channel from input file provided through params.input // - ch_input_sheets = Channel - .fromSamplesheet("input") - .map{ validateInputSamplesheet(it) } - .branch { - bids: it.size() == 4 - raw: true + if (params.input) { + // + // params.input is either a BIDS compliant directory or a samplesheet + // - if directory, we assume it is BIDS + // - everything else is a samplesheet + // + if (file(params.input).isDirectory()) { + IO_BIDS( + Channel.fromPath(params.input), + Channel.value(params.fsbids ?: []), + Channel.value(params.bidsignore ?: []) + ) + ch_samplesheet = IO_BIDS.out + ch_samplesheet.b0 = Channel.empty() + ch_samplesheet.lesion = Channel.empty() } - - IO_READBIDS ( ch_input_sheets.bids.map{ it[1..-1] } ) - ch_versions = ch_versions.mix(IO_READBIDS.out.versions) - - /// - /// Unpack BIDS into subjects - /// - ch_samplesheet = IO_READBIDS.out.bids.map{ - jsonSlurper = new JsonSlurper() - data = jsonSlurper.parseText(it.getText()) - for (item in data){ - sid = "sub-" + item.subject - - if (item.session) - { - sid += "_ses-" + item.session - } - - if (item.run) - { - sid += "_run-" + item.run - } - - for (key in item.keySet()) - { - if(item[key] == 'todo') - { - error "Error ~ Please look at your tractoflow_bids_struct.json " + - "in Read_BIDS folder.\nPlease fix todo fields and give " + - "this file in input using --bids_config option instead of " + - "using --bids." + else { + ch_input_sheets = Channel + .fromList(samplesheetToList(params.input, "${projectDir}/assets/schema_input.json")) + .map{ + meta, dwi, bval, bvec, sbref, rev_dwi, rev_bval, rev_bvec, rev_sbref, t1, wmparc, aparc_aseg, lesion -> + return [ + meta, + dwi, + bval, + bvec, + sbref ?: [], + rev_dwi ?: [], + rev_bval ?: [], + rev_bvec ?: [], + rev_sbref ?: [], + t1, + wmparc ?: [], + aparc_aseg ?: [], + lesion ?: [] + ] } - else if (item[key] == 'error_readout') - { - error "Error ~ Please look at your tractoflow_bids_struct.json " + - "in Read_BIDS folder.\nPlease fix error_readout fields. "+ - "This error indicate that readout time looks wrong.\n"+ - "Please correct the value or remove the subject in the json and " + - "give the updated file in input using --bids_config option instead of " + - "using --bids." + .map{ samplesheet -> + validateInputSamplesheet(samplesheet) } - } - return [[id: sid], - file(item.dwi), file(item.bval), file(item.bvec), file(item.topup), - file(item.rev_dwi), file(item.rev_bval), file(item.rev_bvec), file(item.rev_topup), - file(item.t1), file(item.wmparc), file(item.aparc_aseg)] + IO_SAFECASTINPUTS(ch_input_sheets) + ch_samplesheet = IO_SAFECASTINPUTS.out.safe_inputs + .multiMap{ meta, dwi, bval, bvec, sbref, rev_dwi, rev_bval, rev_bvec, rev_sbref, t1, wmparc, aparc_aseg, lesion -> + t1: [meta, t1] + wmparc: [meta, wmparc] + aparc_aseg: [meta, aparc_aseg] + dwi_bval_bvec: [meta, dwi, bval, bvec] + b0: [meta, sbref] + rev_dwi_bval_bvec: [meta, rev_dwi, rev_bval, rev_bvec] + rev_b0: [meta, rev_sbref] + lesion: [meta, lesion] + } } } - ch_samplesheet = ch_samplesheet.mix(ch_input_sheets.raw) - - IO_SAFECASTINPUTS( ch_samplesheet ) - emit: - samplesheet = IO_SAFECASTINPUTS.out.safe_inputs + t1 = ch_samplesheet.t1 + wmparc = ch_samplesheet.wmparc + aparc_aseg = ch_samplesheet.aparc_aseg + dwi_bval_bvec = ch_samplesheet.dwi_bval_bvec + b0 = ch_samplesheet.b0 + rev_dwi_bval_bvec = ch_samplesheet.rev_dwi_bval_bvec + rev_b0 = ch_samplesheet.rev_b0 + lesion = ch_samplesheet.lesion versions = ch_versions } @@ -150,7 +150,6 @@ workflow PIPELINE_COMPLETION { plaintext_email // boolean: Send plain-text email instead of HTML outdir // path: Path to output directory where results will be published monochrome_logs // boolean: Disable ANSI colour codes in log output - hook_url // string: hook URL for notifications multiqc_report // string: Path to MultiQC report main: @@ -174,9 +173,6 @@ workflow PIPELINE_COMPLETION { } completionSummary(monochrome_logs) - if (hook_url) { - imNotification(summary_params, hook_url) - } } workflow.onError { @@ -196,6 +192,7 @@ workflow PIPELINE_COMPLETION { def validateInputSamplesheet(input) { return input } + // // Generate methods description for MultiQC // diff --git a/subworkflows/nf-neuro/anatomical_segmentation/main.nf b/subworkflows/nf-neuro/anatomical_segmentation/main.nf new file mode 100644 index 0000000..7290dca --- /dev/null +++ b/subworkflows/nf-neuro/anatomical_segmentation/main.nf @@ -0,0 +1,98 @@ +// ** Importing modules from nf-neuro ** // +include { SEGMENTATION_FASTSEG } from '../../../modules/nf-neuro/segmentation/fastseg/main' +include { SEGMENTATION_FREESURFERSEG } from '../../../modules/nf-neuro/segmentation/freesurferseg/main' +include { SEGMENTATION_SYNTHSEG } from '../../../modules/nf-neuro/segmentation/synthseg/main' + +params.run_synthseg = false + +workflow ANATOMICAL_SEGMENTATION { + + // ** Two input channels for the segmentation processes since they are using ** // + // ** different image files. Supplying an empty channel for the one that isn't ** // + // ** relevant will make the workflow run the appropriate module. ** // + take: + ch_image // channel: [ val(meta), [ image ] ] + ch_freesurferseg // channel: [ val(meta), [ aparc_aseg, wmparc ] ], optional + ch_lesion // channel: [ val(meta), [ lesion ] ], optional + ch_fs_license // channel: [ val[meta], [ fs_license ] ], optional + + main: + + ch_versions = Channel.empty() + + if ( params.run_synthseg ) { + // ** Freesurfer synthseg segmentation ** // + SEGMENTATION_SYNTHSEG ( + ch_image + .join(ch_lesion, remainder: true) + .map{ it[0..1] + [it[2] ?: []] } + .combine(ch_fs_license) + ) + ch_versions = ch_versions.mix(SEGMENTATION_SYNTHSEG.out.versions.first()) + + // ** Setting outputs ** // + wm_mask = SEGMENTATION_SYNTHSEG.out.wm_mask + gm_mask = SEGMENTATION_SYNTHSEG.out.gm_mask + csf_mask = SEGMENTATION_SYNTHSEG.out.csf_mask + wm_map = SEGMENTATION_SYNTHSEG.out.wm_map + gm_map = SEGMENTATION_SYNTHSEG.out.gm_map + csf_map = SEGMENTATION_SYNTHSEG.out.csf_map + seg = SEGMENTATION_SYNTHSEG.out.seg + aparc_aseg = SEGMENTATION_SYNTHSEG.out.aparc_aseg + resample = SEGMENTATION_SYNTHSEG.out.resample + volume = SEGMENTATION_SYNTHSEG.out.volume + qc_score = SEGMENTATION_SYNTHSEG.out.qc_score + } + + else { + // ** FSL fast segmentation ** // + SEGMENTATION_FASTSEG ( + ch_image + .join(ch_lesion, remainder: true) + .map{ it[0..1] + [it[2] ?: []] } + ) + ch_versions = ch_versions.mix(SEGMENTATION_FASTSEG.out.versions.first()) + + // ** Setting outputs ** // + wm_mask = SEGMENTATION_FASTSEG.out.wm_mask + gm_mask = SEGMENTATION_FASTSEG.out.gm_mask + csf_mask = SEGMENTATION_FASTSEG.out.csf_mask + wm_map = SEGMENTATION_FASTSEG.out.wm_map + gm_map = SEGMENTATION_FASTSEG.out.gm_map + csf_map = SEGMENTATION_FASTSEG.out.csf_map + seg = Channel.empty() + aparc_aseg = Channel.empty() + resample = Channel.empty() + volume = Channel.empty() + qc_score = Channel.empty() + + + // ** Freesurfer segmentation ** // + SEGMENTATION_FREESURFERSEG ( + ch_freesurferseg + .join(ch_lesion, remainder: true) + .map{ it[0..2] + [it[3] ?: []] } + ) + ch_versions = ch_versions.mix(SEGMENTATION_FREESURFERSEG.out.versions.first()) + + // ** Setting outputs ** // + wm_mask = wm_mask.mix( SEGMENTATION_FREESURFERSEG.out.wm_mask ) + gm_mask = gm_mask.mix( SEGMENTATION_FREESURFERSEG.out.gm_mask ) + csf_mask = csf_mask.mix( SEGMENTATION_FREESURFERSEG.out.csf_mask ) + } + + emit: + wm_mask = wm_mask // channel: [ val(meta), [ wm_mask ] ] + gm_mask = gm_mask // channel: [ val(meta), [ gm_mask ] ] + csf_mask = csf_mask // channel: [ val(meta), [ csf_mask ] ] + wm_map = wm_map // channel: [ val(meta), [ wm_map ] ] + gm_map = gm_map // channel: [ val(meta), [ gm_map ] ] + csf_map = csf_map // channel: [ val(meta), [ csf_map ] ] + seg = seg // channel: [ val(meta), [ seg ] ] + aparc_aseg = aparc_aseg // channel: [ val(meta), [ aparc_aseg ] ] + resample = resample // channel: [ val(meta), [ resample ] ] + volume = volume // channel: [ val(meta), [ volume ] ] + qc_score = qc_score // channel: [ val(meta), [ qc_score ] ] + + versions = ch_versions // channel: [ versions.yml ] +} diff --git a/subworkflows/nf-scil/anatomical_segmentation/meta.yml b/subworkflows/nf-neuro/anatomical_segmentation/meta.yml similarity index 50% rename from subworkflows/nf-scil/anatomical_segmentation/meta.yml rename to subworkflows/nf-neuro/anatomical_segmentation/meta.yml index a1f29a1..bb3fc1a 100644 --- a/subworkflows/nf-scil/anatomical_segmentation/meta.yml +++ b/subworkflows/nf-neuro/anatomical_segmentation/meta.yml @@ -1,12 +1,14 @@ name: "anatomical_segmentation" description: | Subworkflow performing anatomical segmentation to produce WM, GM and CSF maps/masks. It handles - two type of input channels, either an anatomical image (most likely T1) or freesurfer parcellations + two type of input channels, either an anatomical image (most likely T1) and/or freesurfer parcellations files. Depending on which channel is provided, the subworkflow will perform either of the following: - 1) Channel with an anatomical image will result in using FSL's fast segmentation producing WM, GM, + 1) Channel with an anatomical image with run_synthseg parameter will result in using Freesurfer synthseg producing WM, GM and CSF masks/maps. - 2) Channel with FreeSurfer's parcellations files will result in using Scilpy's tools to produce - WM, GM, and CSF masks (probability maps are not produced with this option). + 2) Channel with an anatomical image will result in using FSL's fast segmentation producing WM, GM, + and CSF masks/maps. + 3) Channel with FreeSurfer's parcellations files will result in using Scilpy's tools to produce + WM, GM, and CSF masks. Probability maps will be produced by FSLFast. Typical next steps after this subworkflow would be to combine the resulting masks/maps with fODF data to perform TRACKING. keywords: @@ -17,6 +19,7 @@ keywords: components: - segmentation/fastseg - segmentation/freesurferseg + - segmentation/synthseg input: - ch_image: type: file @@ -33,6 +36,17 @@ input: parcellations into masks. Structure: [ val(meta), path(aparc_aseg), path(wm_parc) ] pattern: "*.mgz" + - ch_lesion: + type: file + description: | + The input channel containing a lesion mask file to correct the white matter mask. The lesion mask must be a binary mask. + Structure: [ val(meta), path(lesion) ] + pattern: "*{nii.nii.gz}" + - ch_fs_license: + type: file + description: | + The input channel containing the FreeSurfer license.To get one, go to https://surfer.nmr.mgh.harvard.edu/registration.html. Optional. If you have already set your license as prescribed by Freesurfer (copied to a .license file in your $FREESURFER_HOME), this is not required. + Structure: [ val(meta). path(fs_license) ] output: - wm_mask: type: file @@ -55,27 +69,54 @@ output: segmentation method and inputs provided. Structure: [ val(meta), path(csf_mask) ] pattern: "*.{nii,nii.gz}" - - wm_maps: + - wm_map: type: file description: | - Channel containing WM probability maps. Will be only outputted if FSL's fast segmentation - is used and not with FreeSurfer's parcellation files. - Structure: [ val(meta), path(wm_maps) ] + Channel containing WM probability maps. + Structure: [ val(meta), path(wm_map) ] pattern: "*.{nii,nii.gz}" - - gm_maps: + - gm_map: type: file description: | - Channel containing GM probability maps. Will be only outputted if FSL's fast segmentation - is used and not with FreeSurfer's parcellation files. - Structure: [ val(meta), path(gm_maps) ] + Channel containing GM probability maps. + Structure: [ val(meta), path(gm_map) ] pattern: "*.{nii,nii.gz}" - - csf_maps: + - csf_map: type: file description: | - Channel containing CSF probability maps. Will be only outputted if FSL's fast segmentation - is used and not with FreeSurfer's parcellation files. - Structure: [ val(meta), path(csf_maps) ] + Channel containing CSF probability maps. + Structure: [ val(meta), path(csf_map) ] pattern: "*.{nii,nii.gz}" + - seg: + type: file + description: | + Channel containing the optional nifti segmentation volume from synthseg. + Structure: [ val(meta), path(seg) ] + pattern: "*.{nii,nii.gz}" + - aparc_aseg: + type: file + description: | + Channel containing the optional nifti cortical parcellation and segmentation volume from synthseg. + Structure: [ val(meta), path(aparc_aseg) ] + pattern: "*.{nii,nii.gz}" + - resample: + type: file + description: | + Channel containing the optional resampled images at 1mm. + Structure: [ val(meta), path(resample) ] + pattern: "*.{nii.nii.gz}" + - volume: + type: file + description: | + Channel containing the optional Output CSV file with volumes for all structures and subjects. + Structure: [ val(meta), path(volume) ] + pattern: "*.csv" + - qc_score: + type: file + description: | + Channel containing the optional output CSV file with qc scores for all subjects. + Structure: [ val(meta), path(qc_score) ] + pattern: "*.csv" - versions: type: file description: | diff --git a/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test b/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test new file mode 100644 index 0000000..0a05a32 --- /dev/null +++ b/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test @@ -0,0 +1,177 @@ +nextflow_workflow { + + name "Test Subworkflow ANATOMICAL_SEGMENTATION" + script "../main.nf" + workflow "ANATOMICAL_SEGMENTATION" + + tag "subworkflows" + tag "subworkflows_nfcore" + tag "subworkflows/anatomical_segmentation" + + tag "segmentation" + tag "segmentation/fastseg" + tag "segmentation/freesurferseg" + tag "segmentation/synthseg" + + tag "load_test_data" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "T1w.zip", "freesurfer_nifti.zip", "freesurfer.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("anatomical_segmentation - fslfast") { + config "./nextflow.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + freesurfer_nifti: it.simpleName == "freesurfer_nifti" + freesurfer: it.simpleName == "freesurfer" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz", checkIfExists: true) + ] + } + input[1] = Channel.empty() + input[2] = Channel.empty() + input[3] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success}, + { assert snapshot(workflow.out).match()} + ) + } + } + + test("anatomical_segmentation - freesurferseg") { + config "./nextflow.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + freesurfer_nifti: it.simpleName == "freesurfer_nifti" + freesurfer: it.simpleName == "freesurfer" + } + input[0] = Channel.empty() + input[1] = ch_split_test_data.freesurfer_nifti.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/aparc+aseg.nii.gz", checkIfExists: true), + file("\${test_data_directory}/wmparc.nii.gz", checkIfExists: true) + ] + } + input[2] = Channel.empty() + input[3] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + } + + test("anatomical_segmentation - both") { + config "./nextflow.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + freesurfer_nifti: it.simpleName == "freesurfer_nifti" + freesurfer: it.simpleName == "freesurfer" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz", checkIfExists: true) + ] + } + input[1] = ch_split_test_data.freesurfer_nifti.map{ + test_data_directory -> [ + [ id:'test' ], // meta map + file("\${test_data_directory}/aparc+aseg.nii.gz", checkIfExists: true), + file("\${test_data_directory}/wmparc.nii.gz", checkIfExists: true) + ] + } + input[2] = Channel.empty() + input[3] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + } + + test("anatomical_segmentation - synthseg") { + config "./nextflow_synthseg.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + freesurfer_nifti: it.simpleName == "freesurfer_nifti" + freesurfer: it.simpleName == "freesurfer" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz", checkIfExists: true) + ] + } + input[1] = Channel.empty() + input[2] = Channel.empty() + input[3] = ch_split_test_data.freesurfer.map{ + test_data_directory -> [ + file("\${test_data_directory}/license.txt", checkIfExists: true) + ] + } + """ + } + } + + then { + assertAll( + { assert workflow.success}, + { assert snapshot( + file(workflow.out.wm_mask.get(0).get(1)).name, + file(workflow.out.gm_mask.get(0).get(1)).name, + file(workflow.out.csf_mask.get(0).get(1)).name, + file(workflow.out.wm_map.get(0).get(1)).name, + file(workflow.out.gm_map.get(0).get(1)).name, + file(workflow.out.csf_map.get(0).get(1)).name, + file(workflow.out.seg.get(0).get(1)).name, + workflow.out.versions + ).match()} + ) + } + } +} diff --git a/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test.snap b/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test.snap new file mode 100644 index 0000000..36d84b9 --- /dev/null +++ b/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test.snap @@ -0,0 +1,464 @@ +{ + "anatomical_segmentation - freesurferseg": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__mask_wm.nii.gz:md5,0ed4a06defc512b1b898d6e0c3ef678a" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test__mask_gm.nii.gz:md5,a363d6207419b08346a7c05c6525430e" + ] + ], + "10": [ + + ], + "11": [ + "versions.yml:md5,474a51e2162c95ec73b13b01c89eff7f" + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test__mask_csf.nii.gz:md5,3dbcb3eefb507bedab91fb80ed636f36" + ] + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + + ], + "6": [ + + ], + "7": [ + + ], + "8": [ + + ], + "9": [ + + ], + "aparc_aseg": [ + + ], + "csf_map": [ + + ], + "csf_mask": [ + [ + { + "id": "test", + "single_end": false + }, + "test__mask_csf.nii.gz:md5,3dbcb3eefb507bedab91fb80ed636f36" + ] + ], + "gm_map": [ + + ], + "gm_mask": [ + [ + { + "id": "test", + "single_end": false + }, + "test__mask_gm.nii.gz:md5,a363d6207419b08346a7c05c6525430e" + ] + ], + "qc_score": [ + + ], + "resample": [ + + ], + "seg": [ + + ], + "versions": [ + "versions.yml:md5,474a51e2162c95ec73b13b01c89eff7f" + ], + "volume": [ + + ], + "wm_map": [ + + ], + "wm_mask": [ + [ + { + "id": "test", + "single_end": false + }, + "test__mask_wm.nii.gz:md5,0ed4a06defc512b1b898d6e0c3ef678a" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-11-22T18:07:13.719673472" + }, + "anatomical_segmentation - synthseg": { + "content": [ + "test__mask_wm.nii.gz", + "test__mask_gm.nii.gz", + "test__mask_csf.nii.gz", + "test__map_wm.nii.gz", + "test__map_gm.nii.gz", + "test__map_csf.nii.gz", + "test__seg.nii.gz", + [ + "versions.yml:md5,95e31db072c7bb2505128bd08beaecf4" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-30T16:59:25.733839079" + }, + "anatomical_segmentation - fslfast": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__mask_wm.nii.gz:md5,98d75f5c0ddd2b7f2bfff8fbf1877e34" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test__mask_gm.nii.gz:md5,a840f08c5b90cb351d3272d8cab6f00c" + ] + ], + "10": [ + + ], + "11": [ + "versions.yml:md5,f098240b7d7f6df9a2a43215c5828669" + ], + "2": [ + [ + { + "id": "test" + }, + "test__mask_csf.nii.gz:md5,f22037987a38e5132ccbb2887ee71842" + ] + ], + "3": [ + [ + { + "id": "test" + }, + "test__map_wm.nii.gz:md5,c7d666c66baea2132d70be38017c6901" + ] + ], + "4": [ + [ + { + "id": "test" + }, + "test__map_gm.nii.gz:md5,6dbc993e43f6efcdd6a3a097df2b4574" + ] + ], + "5": [ + [ + { + "id": "test" + }, + "test__map_csf.nii.gz:md5,dcc224f10f31ee306491ebbdcfbe5bfe" + ] + ], + "6": [ + + ], + "7": [ + + ], + "8": [ + + ], + "9": [ + + ], + "aparc_aseg": [ + + ], + "csf_map": [ + [ + { + "id": "test" + }, + "test__map_csf.nii.gz:md5,dcc224f10f31ee306491ebbdcfbe5bfe" + ] + ], + "csf_mask": [ + [ + { + "id": "test" + }, + "test__mask_csf.nii.gz:md5,f22037987a38e5132ccbb2887ee71842" + ] + ], + "gm_map": [ + [ + { + "id": "test" + }, + "test__map_gm.nii.gz:md5,6dbc993e43f6efcdd6a3a097df2b4574" + ] + ], + "gm_mask": [ + [ + { + "id": "test" + }, + "test__mask_gm.nii.gz:md5,a840f08c5b90cb351d3272d8cab6f00c" + ] + ], + "qc_score": [ + + ], + "resample": [ + + ], + "seg": [ + + ], + "versions": [ + "versions.yml:md5,f098240b7d7f6df9a2a43215c5828669" + ], + "volume": [ + + ], + "wm_map": [ + [ + { + "id": "test" + }, + "test__map_wm.nii.gz:md5,c7d666c66baea2132d70be38017c6901" + ] + ], + "wm_mask": [ + [ + { + "id": "test" + }, + "test__mask_wm.nii.gz:md5,98d75f5c0ddd2b7f2bfff8fbf1877e34" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-10-12T03:17:48.731576565" + }, + "anatomical_segmentation - both": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__mask_wm.nii.gz:md5,0ed4a06defc512b1b898d6e0c3ef678a" + ], + [ + { + "id": "test" + }, + "test__mask_wm.nii.gz:md5,98d75f5c0ddd2b7f2bfff8fbf1877e34" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test__mask_gm.nii.gz:md5,a363d6207419b08346a7c05c6525430e" + ], + [ + { + "id": "test" + }, + "test__mask_gm.nii.gz:md5,a840f08c5b90cb351d3272d8cab6f00c" + ] + ], + "10": [ + + ], + "11": [ + "versions.yml:md5,474a51e2162c95ec73b13b01c89eff7f", + "versions.yml:md5,f098240b7d7f6df9a2a43215c5828669" + ], + "2": [ + [ + { + "id": "test" + }, + "test__mask_csf.nii.gz:md5,3dbcb3eefb507bedab91fb80ed636f36" + ], + [ + { + "id": "test" + }, + "test__mask_csf.nii.gz:md5,f22037987a38e5132ccbb2887ee71842" + ] + ], + "3": [ + [ + { + "id": "test" + }, + "test__map_wm.nii.gz:md5,c7d666c66baea2132d70be38017c6901" + ] + ], + "4": [ + [ + { + "id": "test" + }, + "test__map_gm.nii.gz:md5,6dbc993e43f6efcdd6a3a097df2b4574" + ] + ], + "5": [ + [ + { + "id": "test" + }, + "test__map_csf.nii.gz:md5,dcc224f10f31ee306491ebbdcfbe5bfe" + ] + ], + "6": [ + + ], + "7": [ + + ], + "8": [ + + ], + "9": [ + + ], + "aparc_aseg": [ + + ], + "csf_map": [ + [ + { + "id": "test" + }, + "test__map_csf.nii.gz:md5,dcc224f10f31ee306491ebbdcfbe5bfe" + ] + ], + "csf_mask": [ + [ + { + "id": "test" + }, + "test__mask_csf.nii.gz:md5,3dbcb3eefb507bedab91fb80ed636f36" + ], + [ + { + "id": "test" + }, + "test__mask_csf.nii.gz:md5,f22037987a38e5132ccbb2887ee71842" + ] + ], + "gm_map": [ + [ + { + "id": "test" + }, + "test__map_gm.nii.gz:md5,6dbc993e43f6efcdd6a3a097df2b4574" + ] + ], + "gm_mask": [ + [ + { + "id": "test" + }, + "test__mask_gm.nii.gz:md5,a363d6207419b08346a7c05c6525430e" + ], + [ + { + "id": "test" + }, + "test__mask_gm.nii.gz:md5,a840f08c5b90cb351d3272d8cab6f00c" + ] + ], + "qc_score": [ + + ], + "resample": [ + + ], + "seg": [ + + ], + "versions": [ + "versions.yml:md5,474a51e2162c95ec73b13b01c89eff7f", + "versions.yml:md5,f098240b7d7f6df9a2a43215c5828669" + ], + "volume": [ + + ], + "wm_map": [ + [ + { + "id": "test" + }, + "test__map_wm.nii.gz:md5,c7d666c66baea2132d70be38017c6901" + ] + ], + "wm_mask": [ + [ + { + "id": "test" + }, + "test__mask_wm.nii.gz:md5,0ed4a06defc512b1b898d6e0c3ef678a" + ], + [ + { + "id": "test" + }, + "test__mask_wm.nii.gz:md5,98d75f5c0ddd2b7f2bfff8fbf1877e34" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-11-22T18:09:33.134141155" + } +} \ No newline at end of file diff --git a/modules/nf-scil/segmentation/fastseg/tests/nextflow.config b/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow.config similarity index 100% rename from modules/nf-scil/segmentation/fastseg/tests/nextflow.config rename to subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow.config diff --git a/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow_synthseg.config b/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow_synthseg.config new file mode 100644 index 0000000..49ea1e1 --- /dev/null +++ b/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow_synthseg.config @@ -0,0 +1,9 @@ +process { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + withName: "SEGMENTATION_SYNTHSEG" { + memory = "16G" + ext.fast = true + } +} + +params.run_synthseg = true diff --git a/subworkflows/nf-neuro/anatomical_segmentation/tests/tags.yml b/subworkflows/nf-neuro/anatomical_segmentation/tests/tags.yml new file mode 100644 index 0000000..7444409 --- /dev/null +++ b/subworkflows/nf-neuro/anatomical_segmentation/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/anatomical_segmentation: + - subworkflows/nf-neuro/anatomical_segmentation/** diff --git a/subworkflows/nf-neuro/io_bids/main.nf b/subworkflows/nf-neuro/io_bids/main.nf new file mode 100644 index 0000000..c2dddc0 --- /dev/null +++ b/subworkflows/nf-neuro/io_bids/main.nf @@ -0,0 +1,96 @@ +include { IO_READBIDS } from '../../../modules/nf-neuro/io/readbids/main.nf' + +workflow IO_BIDS { + + take: + bids_folder // channel: [ path(bids_folder) ] + fs_folder // channel: [ path(fs_folder) ] + bidsignore // channel: [ path(bids_ignore) ] + + main: + + ch_versions = Channel.empty() + + // ** Sanity check to ensure channels are single-item ** // + bids_folder.collect().map { folders -> + if (folders.size() > 1) { + error "ERROR: You must supply only a single BIDS folder." + } + } + + // ** Fetching the BIDS data as a json file ** // + IO_READBIDS ( + bids_folder, + fs_folder, + bidsignore + ) + ch_versions = ch_versions.mix(IO_READBIDS.out.versions) + + // ** Converting the json file into channels. ** // + ch_files = IO_READBIDS.out.bidsstructure + .flatMap{ layout -> + def json = new groovy.json.JsonSlurper().parseText(layout.getText()) + return json.collect { item -> + // ** Collecting the subject's ID ** // + def sid = "sub-" + item.subject + + // ** Collecting the session's ID if present ** // + def ses = item.session ? "_ses-" + item.session : "" + + // ** Collecting the run's ID if present ** // + def run = item.run ? "run-" + item.run : "" + + // ** Collecting TotalReadoutTime, PhaseEncodingDirection ** // + def dwi_tr = item.TotalReadoutTime ?: "" + def dwi_phase = item.DWIPhaseEncodingDirection ?: "" + def dwi_revphase = item.rev_DWIPhaseEncodingDirection ?: "" + + // ** Validating there is not missing data ** // + item.each { _key, value -> + if (value == "todo") { + error "ERROR ~ $sid contains missing files, please" + + "check the BIDS layout. You can validate your" + + "structure using the bids-validator tool:" + + "https://bids-standard.github.io/bids-validator/" + + "or the docker version: " + + "https://hub.docker.com/r/bids/validator" + } + } + + // ** Collecting the files ** // + return [ + [id: sid, session: ses, run: run, dwi_tr: dwi_tr, + dwi_phase: dwi_phase, dwi_revphase: dwi_revphase], + item.t1 ? file(item.t1) : [], + item.wmparc ? file(item.wmparc) : [], + item.aparc_aseg ? file(item.aparc_aseg) : [], + item.dwi ? file(item.dwi) : [], + item.bval ? file(item.bval) : [], + item.bvec ? file(item.bvec) : [], + item.rev_dwi ? file(item.rev_dwi) : [], + item.rev_bval ? file(item.rev_bval) : [], + item.rev_bvec ? file(item.rev_bvec) : [], + item.rev_topup ? file(item.rev_topup) : [], + ] + } + } + .multiMap{ meta, t1, wmparc, aparc_aseg, dwi, bval, bvec, rev_dwi, rev_bval, rev_bvec, rev_b0 -> + t1: [meta, t1] + wmparc: [meta, wmparc] + aparc_aseg: [meta, aparc_aseg] + dwi_bval_bvec: [meta, dwi, bval, bvec] + rev_dwi_bval_bvec: [meta, rev_dwi, rev_bval, rev_bvec] + rev_b0: [meta, rev_b0] + } + + emit: + ch_t1 = ch_files.t1 // channel: [ [meta], file(t1) ] + ch_wmparc = ch_files.wmparc // channel: [ [meta], file(wmparc) ] + ch_aparc_aseg = ch_files.aparc_aseg // channel: [ [meta], file(aparc_aseg) ] + ch_dwi_bval_bvec = ch_files.dwi_bval_bvec // channel: [ [meta], file(dwi), file(bval), file(bvec) ] + ch_rev_dwi_bval_bvec = ch_files.rev_dwi_bval_bvec // channel: [ [meta], file(rev_dwi), file(rev_bval), file(rev_bvec) ] + ch_rev_b0 = ch_files.rev_b0 // channel: [ [meta], file(fieldmap) ] + + versions = ch_versions // channel: [ versions.yml ] +} + diff --git a/subworkflows/nf-neuro/io_bids/meta.yml b/subworkflows/nf-neuro/io_bids/meta.yml new file mode 100644 index 0000000..6caf7b1 --- /dev/null +++ b/subworkflows/nf-neuro/io_bids/meta.yml @@ -0,0 +1,101 @@ +name: "io_bids" +description: | + Subworkflow loading files from a BIDS directory. It is used in conjunction + with the IO_READBIDS module which uses the scilpy CLI script to parse the + BIDS directory and fetch the metadata. + ------------- Current supported files/metadata ------------- + Files: + - T1w + - White matter parcellation from FreeSurfer. + - Grey matter parcellation from FreeSurfer. + - Diffusion weighted images (dwi) + - B-values (bval) + - B-vectors (bvec) + - Reverse encoded diffusion weighted images (rev_dwi) + - B-values for the rev_dwi (rev_bval) + - B-vectors for the rev_dwi (rev_bvec) + - Reverse b0 image (rev_b0) + Metadata (within the meta): + - Subject ID (id) + - Session ID (ses) + - Run ID (run) + - DWI Total Readout Time (dwi_tr) + - DWI Phase Encoding Direction (dwi_phase) + - Reverse DWI Phase Encoding Direction (dwi_revphase) + + **Note**: This subworkflow is meant to be an example of how to use the + IO_READBIDS module. It only supports a single BIDS folder as an input. + It can be modified to fit the user's needs. + +keywords: + - IO + - BIDS + - Files + +components: + - io/readbids + +input: + - bids_folder: + type: directory + description: | + Path to the BIDS directory. (You must supply only a single BIDS directory) + Structure: [ path(bids_folder) ] + - fs_folder: + type: directory + description: | + Path to the FreeSurfer directory. + Structure: [ path(fs_folder) ] + - bidsignore: + type: file + description: | + Path to the .bidsignore file. + Structure: [ path(bidsignore) ] + +output: + - ch_t1: + type: file + description: | + Channel containing all T1w files + Structure: [ val(meta), path(t1) ] + pattern: "*.nii.gz" + - ch_wmparc: + type: file + description: | + Channel containing all FreeSurfer white matter parcellation files. + Structure: [ val(meta), path(wmparc) ] + pattern: "*.mgz" + - ch_aparc_aseg: + type: file + description: | + Channel containing all FreeSurfer grey matter parcellation files. + Structure: [ val(meta), path(aparc_aseg) ] + pattern: "*.mgz" + - ch_dwi_bval_bvec: + type: file + description: | + Channel containing all diffusion weighted images, b-values and b-vectors. + Structure: [ val(meta), path(dwi), path(bval), path(bvec) ] + pattern: "*.{nii.gz,bval,bvec}" + - ch_rev_dwi_bval_bvec: + type: file + description: | + Channel containing all reverse encoded diffusion weighted images, b-values and b-vectors. + Structure: [ val(meta), path(rev_dwi), path(rev_bval), path(rev_bvec) ] + pattern: "*.{nii.gz,bval,bvec}" + - ch_rev_b0: + type: file + description: | + Channel containing all reverse b0 images. + Structure: [ val(meta), path(rev_b0) ] + pattern: "*.nii.gz" + - versions: + type: file + description: | + File containing software versions + Structure: [ path(versions.yml) ] + pattern: "versions.yml" +authors: + - "@gagnonanthony" +maintainers: + - "@gagnonanthony" diff --git a/subworkflows/nf-neuro/io_bids/tests/main.nf.test b/subworkflows/nf-neuro/io_bids/tests/main.nf.test new file mode 100644 index 0000000..14ba3bf --- /dev/null +++ b/subworkflows/nf-neuro/io_bids/tests/main.nf.test @@ -0,0 +1,106 @@ +nextflow_workflow { + + name "Test Subworkflow IO_BIDS" + script "../main.nf" + workflow "IO_BIDS" + + config "./nextflow.config" + + tag "subworkflows" + tag "subworkflows_nfcore" + tag "subworkflows/io_bids" + + tag "io" + tag "io/readbids" + + tag "load_test_data" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "bids.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("Read BIDS directory - without .bidsignore") { + + when { + workflow { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + file("\${test_data_directory}/i_bids", checkIfExists: true) + ] + } + input[1] = Channel.empty() + input[2] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + } + + test("Read BIDS directory - with .bidsignore") { + + when { + workflow { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + file("\${test_data_directory}/i_bids", checkIfExists: true) + ] + } + input[1] = Channel.empty() + input[2] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + file("\${test_data_directory}/i_bids/.bidsignore") + ] + } + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + } + + test("Read BIDS directory - with 2 folder - invalid") { + + when { + workflow { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + file("\${test_data_directory}/i_bids", checkIfExists: true), + file("\${test_data_directory}/i_bids", checkIfExists: true) + ] + } + input[1] = Channel.empty() + input[2] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.failed }, + { assert workflow.stdout.contains("ERROR: You must supply only a single BIDS folder.") } + ) + } + } +} diff --git a/subworkflows/nf-neuro/io_bids/tests/main.nf.test.snap b/subworkflows/nf-neuro/io_bids/tests/main.nf.test.snap new file mode 100644 index 0000000..96c2463 --- /dev/null +++ b/subworkflows/nf-neuro/io_bids/tests/main.nf.test.snap @@ -0,0 +1,108 @@ +{ + "Read BIDS directory - with .bidsignore": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + + ], + "6": [ + + ], + "ch_aparc_aseg": [ + + ], + "ch_dwi_bval_bvec": [ + + ], + "ch_rev_b0": [ + + ], + "ch_rev_dwi_bval_bvec": [ + + ], + "ch_t1": [ + + ], + "ch_wmparc": [ + + ], + "versions": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-14T08:08:38.357417" + }, + "Read BIDS directory - without .bidsignore": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + + ], + "6": [ + + ], + "ch_aparc_aseg": [ + + ], + "ch_dwi_bval_bvec": [ + + ], + "ch_rev_b0": [ + + ], + "ch_rev_dwi_bval_bvec": [ + + ], + "ch_t1": [ + + ], + "ch_wmparc": [ + + ], + "versions": [ + + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-14T08:08:01.011554" + } +} \ No newline at end of file diff --git a/subworkflows/nf-neuro/io_bids/tests/nextflow.config b/subworkflows/nf-neuro/io_bids/tests/nextflow.config new file mode 100644 index 0000000..7a40ebe --- /dev/null +++ b/subworkflows/nf-neuro/io_bids/tests/nextflow.config @@ -0,0 +1,7 @@ +process { + withName: "IO_READBIDS" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.readout = 0.062 + ext.clean_bids = true + } +} diff --git a/subworkflows/nf-neuro/io_bids/tests/tags.yml b/subworkflows/nf-neuro/io_bids/tests/tags.yml new file mode 100644 index 0000000..597feb2 --- /dev/null +++ b/subworkflows/nf-neuro/io_bids/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/io_bids: + - subworkflows/nf-neuro/io_bids/** diff --git a/subworkflows/nf-neuro/preproc_dwi/main.nf b/subworkflows/nf-neuro/preproc_dwi/main.nf new file mode 100644 index 0000000..28c46dc --- /dev/null +++ b/subworkflows/nf-neuro/preproc_dwi/main.nf @@ -0,0 +1,184 @@ +include { DENOISING_MPPCA as DENOISE_DWI } from '../../../modules/nf-neuro/denoising/mppca/main' +include { DENOISING_MPPCA as DENOISE_REVDWI } from '../../../modules/nf-neuro/denoising/mppca/main' +include { PREPROC_GIBBS as PREPROC_GIBBS_DWI } from '../../../modules/nf-neuro/preproc/gibbs/main' +include { PREPROC_GIBBS as PREPROC_GIBBS_REVDWI } from '../../../modules/nf-neuro/preproc/gibbs/main' +include { BETCROP_FSLBETCROP } from '../../../modules/nf-neuro/betcrop/fslbetcrop/main' +include { IMAGE_CROPVOLUME } from '../../../modules/nf-neuro/image/cropvolume/main' +include { PREPROC_N4 as N4_DWI } from '../../../modules/nf-neuro/preproc/n4/main' +include { PREPROC_NORMALIZE as NORMALIZE_DWI } from '../../../modules/nf-neuro/preproc/normalize/main' +include { IMAGE_RESAMPLE as RESAMPLE_DWI } from '../../../modules/nf-neuro/image/resample/main' +include { IMAGE_RESAMPLE as RESAMPLE_MASK } from '../../../modules/nf-neuro/image/resample/main' +include { UTILS_EXTRACTB0 as EXTRACTB0_RESAMPLE } from '../../../modules/nf-neuro/utils/extractb0/main' +include { UTILS_EXTRACTB0 as EXTRACTB0_TOPUP } from '../../../modules/nf-neuro/utils/extractb0/main' +include { TOPUP_EDDY } from '../topup_eddy/main' + + +workflow PREPROC_DWI { + + take: + ch_dwi // channel: [ val(meta), dwi, bval, bvec ] + ch_rev_dwi // channel: [ val(meta), rev-dwi, bval, bvec ], optional + ch_b0 // Channel: [ val(meta), b0 ], optional + ch_rev_b0 // channel: [ val(meta), rev-b0 ], optional + ch_config_topup // channel: [ 'topup.cnf' ], optional + + main: + + ch_versions = Channel.empty() + 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 ] + } + + // Need to append "rev" to the ID, to ensure output filenames + // are different from the DWI and prevent file collisions + // - "cache: meta" is used to save the "real" metadata with valid ID for + // join operations, so it can be recovered after execution + ch_rev_dwi_bvalbvec = ch_rev_dwi + .multiMap { meta, dwi, bval, bvec -> + rev_dwi: [ [id: "${meta.id}_rev", cache: meta], dwi ] + rev_bvs_files: [ meta, bval, bvec ] + } + + ch_denoise_dwi = ch_dwi_bvalbvec.dwi + .map{ it + [[]] } + + DENOISE_DWI ( ch_denoise_dwi ) + ch_versions = ch_versions.mix(DENOISE_DWI.out.versions.first()) + + // ** Denoise REV-DWI ** // + + ch_denoise_rev_dwi = ch_rev_dwi_bvalbvec.rev_dwi + .map{ it + [[]] } + + DENOISE_REVDWI ( ch_denoise_rev_dwi ) + ch_versions = ch_versions.mix(DENOISE_REVDWI.out.versions.first()) + + ch_dwi = DENOISE_DWI.out.image + .join(ch_dwi_bvalbvec.bvs_files) + // Recover the "real" ID from "meta[cache]" (see above), to join with the bval/bvec + ch_rev_dwi = DENOISE_REVDWI.out.image + .map{ meta, dwi -> [ meta.cache, dwi ] } + .join(ch_rev_dwi_bvalbvec.rev_bvs_files) + } // No else, we just use the input DWI + + if (params.preproc_dwi_run_degibbs) { + ch_dwi_bvalbvec = ch_dwi + .multiMap { meta, dwi, bval, bvec -> + dwi: [ meta, dwi ] + bvs_files: [ meta, bval, bvec ] + } + + ch_rev_dwi_bvalbvec = ch_rev_dwi + .multiMap { meta, dwi, bval, bvec -> + rev_dwi: [ [id: "${meta.id}_rev", cache: meta], dwi ] + rev_bvs_files: [ meta, bval, bvec ] + } + + PREPROC_GIBBS_DWI(ch_dwi_bvalbvec.dwi) + ch_versions = ch_versions.mix(PREPROC_GIBBS_DWI.out.versions.first()) + + // Need to append "rev" to the ID, to ensure output filenames + // are different from the DWI and prevent file collisions + // - "cache: meta" is used to save the "real" metadata with valid ID for + // join operations, so it can be recovered after execution + PREPROC_GIBBS_REVDWI(ch_rev_dwi_bvalbvec.rev_dwi) + ch_versions = ch_versions.mix(PREPROC_GIBBS_REVDWI.out.versions.first()) + + ch_dwi = PREPROC_GIBBS_DWI.out.dwi + .join(ch_dwi_bvalbvec.bvs_files) + // Recover the "real" ID from "meta[cache]" (see above), to join with the bval/bvec + ch_rev_dwi = PREPROC_GIBBS_REVDWI.out.dwi + .map{ meta, dwi -> [ meta.cache, dwi ] } + .join(ch_rev_dwi_bvalbvec.rev_bvs_files) + + } // No else, we just use the input DWI + + // ** Eddy Topup ** // + TOPUP_EDDY ( ch_dwi, ch_b0, ch_rev_dwi, ch_rev_b0, ch_config_topup ) + ch_versions = ch_versions.mix(TOPUP_EDDY.out.versions.first()) + ch_multiqc_files = ch_multiqc_files.mix(TOPUP_EDDY.out.mqc) + + // ** Bet-crop DWI ** // + ch_betcrop_dwi = TOPUP_EDDY.out.dwi + .join(TOPUP_EDDY.out.bval) + .join(TOPUP_EDDY.out.bvec) + + BETCROP_FSLBETCROP ( ch_betcrop_dwi ) + ch_versions = ch_versions.mix(BETCROP_FSLBETCROP.out.versions.first()) + + // ** Crop b0 ** // + ch_crop_b0 = TOPUP_EDDY.out.b0 + .join(BETCROP_FSLBETCROP.out.bbox) + + IMAGE_CROPVOLUME ( ch_crop_b0 ) + ch_versions = ch_versions.mix(IMAGE_CROPVOLUME.out.versions.first()) + + ch_dwi_preproc = BETCROP_FSLBETCROP.out.image + ch_dwi_n4 = Channel.empty() + if (params.preproc_dwi_run_N4) { + // ** N4 DWI ** // + ch_N4 = ch_dwi_preproc + .join(IMAGE_CROPVOLUME.out.image) + .join(BETCROP_FSLBETCROP.out.mask) + + N4_DWI ( ch_N4 ) + ch_versions = ch_versions.mix(N4_DWI.out.versions.first()) + + ch_dwi_preproc = N4_DWI.out.image + ch_dwi_n4 = N4_DWI.out.image + } + + // ** Normalize DWI ** // + ch_normalize = ch_dwi_preproc + .join(TOPUP_EDDY.out.bval) + .join(TOPUP_EDDY.out.bvec) + .join(BETCROP_FSLBETCROP.out.mask) + + NORMALIZE_DWI ( ch_normalize ) + ch_versions = ch_versions.mix(NORMALIZE_DWI.out.versions.first()) + + ch_dwi_preproc = NORMALIZE_DWI.out.dwi + if (params.preproc_dwi_run_resampling) { + // ** Resample DWI ** // + ch_resample_dwi = NORMALIZE_DWI.out.dwi + .map{ it + [[]] } + + RESAMPLE_DWI ( ch_resample_dwi ) + ch_versions = ch_versions.mix(RESAMPLE_DWI.out.versions.first()) + + ch_dwi_preproc = RESAMPLE_DWI.out.image + } + + // ** Extract b0 ** // + ch_dwi_extract_b0 = ch_dwi_preproc + .join(TOPUP_EDDY.out.bval) + .join(TOPUP_EDDY.out.bvec) + + EXTRACTB0_RESAMPLE { ch_dwi_extract_b0 } + ch_versions = ch_versions.mix(EXTRACTB0_RESAMPLE.out.versions.first()) + + // ** Resample mask ** // + ch_resample_mask = BETCROP_FSLBETCROP.out.mask + .map{ it + [[]] } + + RESAMPLE_MASK ( ch_resample_mask ) + ch_versions = ch_versions.mix(RESAMPLE_MASK.out.versions.first()) + + emit: + dwi = ch_dwi_preproc // channel: [ val(meta), dwi-preproc ] + bval = TOPUP_EDDY.out.bval // channel: [ val(meta), bval-corrected ] + bvec = TOPUP_EDDY.out.bvec // channel: [ val(meta), bvec-corrected ] + b0 = EXTRACTB0_RESAMPLE.out.b0 // channel: [ val(meta), b0-preproc ] + b0_mask = RESAMPLE_MASK.out.image // channel: [ val(meta), b0-mask ] + dwi_bounding_box = BETCROP_FSLBETCROP.out.bbox // channel: [ val(meta), dwi-bounding-box ] + dwi_topup_eddy = TOPUP_EDDY.out.dwi // channel: [ val(meta), dwi-after-topup-eddy ] + dwi_n4 = ch_dwi_n4 // channel: [ val(meta), dwi-after-n4 ] + mqc = ch_multiqc_files // channel: [ val(meta), mqc ] + versions = ch_versions // channel: [ versions.yml ] +} diff --git a/subworkflows/nf-scil/preproc_dwi/meta.yml b/subworkflows/nf-neuro/preproc_dwi/meta.yml similarity index 96% rename from subworkflows/nf-scil/preproc_dwi/meta.yml rename to subworkflows/nf-neuro/preproc_dwi/meta.yml index 705b64e..bc64b7c 100644 --- a/subworkflows/nf-scil/preproc_dwi/meta.yml +++ b/subworkflows/nf-neuro/preproc_dwi/meta.yml @@ -45,13 +45,14 @@ keywords: - Normalization - Resampling components: - - betcrop/cropvolume + - image/cropvolume - betcrop/fslbetcrop - denoising/mppca - image/resample - preproc/n4 - preproc/normalize - utils/extractb0 + - preproc/gibbs - topup_eddy input: - ch_dwi: @@ -133,6 +134,12 @@ output: Channel containing DWI after denoised, corrected for susceptibility and/or eddy currents and motion, cropped and normalized (N4). Structure: [ val(meta), path(dwi) ] pattern: "*_normalized.{nii,nii.gz}" + - mqc: + type: file + description: | + Channel containing the quality control images for MultiQC. + Structure: [ val(meta), path(mqc) ] + pattern: "*_mqc.{gif,png}" - versions: type: file description: | diff --git a/subworkflows/nf-scil/preproc_dwi/tests/main.nf.test b/subworkflows/nf-neuro/preproc_dwi/tests/main.nf.test similarity index 91% rename from subworkflows/nf-scil/preproc_dwi/tests/main.nf.test rename to subworkflows/nf-neuro/preproc_dwi/tests/main.nf.test index dcfe9a6..e3b4414 100644 --- a/subworkflows/nf-scil/preproc_dwi/tests/main.nf.test +++ b/subworkflows/nf-neuro/preproc_dwi/tests/main.nf.test @@ -9,9 +9,10 @@ nextflow_workflow { tag "subworkflows_nfcore" tag "subworkflows/preproc_dwi" tag "subworkflows/topup_eddy" - tag "betcrop/cropvolume" + tag "image/cropvolume" tag "betcrop/fslbetcrop" tag "denoising/mppca" + tag "preproc/gibbs" tag "image/resample" tag "preproc/n4" tag "preproc/normalize" @@ -44,14 +45,14 @@ nextflow_workflow { file("\${test_data_directory}/sub-01_dir-AP_dwi.bval"), file("\${test_data_directory}/sub-01_dir-AP_dwi.bvec") ]} - input[1] = [] - input[2] = [] + input[1] = Channel.from( [] ) + input[2] = Channel.from( [] ) input[3] = LOAD_DATA.out.test_data_directory.map{ test_data_directory -> [ [ id:'test', single_end:false ], file("\${test_data_directory}/sub-01_dir-PA_sbref.nii.gz") ]} - input[4] = [] + input[4] = Channel.from( [] ) """ } } @@ -67,7 +68,7 @@ nextflow_workflow { file(workflow.out.bvec.get(0).get(1)).name, file(workflow.out.dwi_bounding_box.get(0).get(1)).name, file(workflow.out.dwi_n4.get(0).get(1)).name, - file(workflow.out.dwi_resample.get(0).get(1)).name, + file(workflow.out.dwi.get(0).get(1)).name, file(workflow.out.dwi_topup_eddy.get(0).get(1)).name).match() } ) } @@ -92,9 +93,9 @@ nextflow_workflow { file("\${test_data_directory}/sub-01_dir-PA_dwi.bval"), file("\${test_data_directory}/sub-01_dir-PA_dwi.bvec") ]} - input[2] = [] - input[3] = [] - input[4] = [] + input[2] = Channel.from( [] ) + input[3] = Channel.from( [] ) + input[4] = Channel.from( [] ) """ } } @@ -110,7 +111,7 @@ nextflow_workflow { file(workflow.out.bvec.get(0).get(1)).name, file(workflow.out.dwi_bounding_box.get(0).get(1)).name, file(workflow.out.dwi_n4.get(0).get(1)).name, - file(workflow.out.dwi_resample.get(0).get(1)).name, + file(workflow.out.dwi.get(0).get(1)).name, file(workflow.out.dwi_topup_eddy.get(0).get(1)).name).match() } ) } @@ -146,7 +147,7 @@ nextflow_workflow { [ id:'test', single_end:false ], file("\${test_data_directory}/sub-01_dir-PA_sbref.nii.gz") ]} - input[4] = [] + input[4] = Channel.from( [] ) """ } } @@ -162,7 +163,7 @@ nextflow_workflow { file(workflow.out.bvec.get(0).get(1)).name, file(workflow.out.dwi_bounding_box.get(0).get(1)).name, file(workflow.out.dwi_n4.get(0).get(1)).name, - file(workflow.out.dwi_resample.get(0).get(1)).name, + file(workflow.out.dwi.get(0).get(1)).name, file(workflow.out.dwi_topup_eddy.get(0).get(1)).name).match() } ) } diff --git a/subworkflows/nf-neuro/preproc_dwi/tests/main.nf.test.snap b/subworkflows/nf-neuro/preproc_dwi/tests/main.nf.test.snap new file mode 100644 index 0000000..6788609 --- /dev/null +++ b/subworkflows/nf-neuro/preproc_dwi/tests/main.nf.test.snap @@ -0,0 +1,117 @@ +{ + "preproc_dwi_rev_dwi_nob0": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_eddy_corrected.bval:md5,8192e97b08e8032382397bb844b31892" + ] + ], + [ + "versions.yml:md5,0280006ceecc575513278292734ffb27", + "versions.yml:md5,0b8c908e52917b0b706fc9d1b4d6cd24", + "versions.yml:md5,10ec368dc91791041043fddf6ea3d5c9", + "versions.yml:md5,4d9548c7486e8dbb65af71f143ed331c", + "versions.yml:md5,571265e710ca29198e69be22c0f970d5", + "versions.yml:md5,662ea558da42564a0f6140473132bcb4", + "versions.yml:md5,7ddf2e98f59b19c9b933670550f26ad7", + "versions.yml:md5,9ed0cfbbfa39c46a53dbe911a68cbc88", + "versions.yml:md5,be7d5bfaf3bf493f62efa98e274cc01d", + "versions.yml:md5,c15ba5efd24564dba4710b6da8c4b791", + "versions.yml:md5,ea5858879452a59bb355228ae7f38111", + "versions.yml:md5,f041502e22449973d84ac1c618e8ebf9" + ], + "test_b0.nii.gz", + "test_resampled.nii.gz", + "test__dwi_eddy_corrected.bvec", + "test__image_boundingBox.pkl", + "test__image_n4.nii.gz", + "test_resampled.nii.gz", + "test__dwi_corrected.nii.gz" + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-28T08:50:35.321464" + }, + "preproc_dwi_rev_b0": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_eddy_corrected.bval:md5,8192e97b08e8032382397bb844b31892" + ] + ], + [ + "versions.yml:md5,0280006ceecc575513278292734ffb27", + "versions.yml:md5,0b8c908e52917b0b706fc9d1b4d6cd24", + "versions.yml:md5,10ec368dc91791041043fddf6ea3d5c9", + "versions.yml:md5,571265e710ca29198e69be22c0f970d5", + "versions.yml:md5,7ddf2e98f59b19c9b933670550f26ad7", + "versions.yml:md5,9ed0cfbbfa39c46a53dbe911a68cbc88", + "versions.yml:md5,be7d5bfaf3bf493f62efa98e274cc01d", + "versions.yml:md5,c15ba5efd24564dba4710b6da8c4b791", + "versions.yml:md5,ea5858879452a59bb355228ae7f38111", + "versions.yml:md5,f041502e22449973d84ac1c618e8ebf9" + ], + "test_b0.nii.gz", + "test_resampled.nii.gz", + "test__dwi_eddy_corrected.bvec", + "test__image_boundingBox.pkl", + "test__image_n4.nii.gz", + "test_resampled.nii.gz", + "test__dwi_corrected.nii.gz" + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-28T08:47:25.557731" + }, + "preproc_dwi_all_options": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test__dwi_eddy_corrected.bval:md5,8192e97b08e8032382397bb844b31892" + ] + ], + [ + "versions.yml:md5,0280006ceecc575513278292734ffb27", + "versions.yml:md5,0b8c908e52917b0b706fc9d1b4d6cd24", + "versions.yml:md5,10ec368dc91791041043fddf6ea3d5c9", + "versions.yml:md5,4d9548c7486e8dbb65af71f143ed331c", + "versions.yml:md5,571265e710ca29198e69be22c0f970d5", + "versions.yml:md5,662ea558da42564a0f6140473132bcb4", + "versions.yml:md5,7ddf2e98f59b19c9b933670550f26ad7", + "versions.yml:md5,9ed0cfbbfa39c46a53dbe911a68cbc88", + "versions.yml:md5,be7d5bfaf3bf493f62efa98e274cc01d", + "versions.yml:md5,c15ba5efd24564dba4710b6da8c4b791", + "versions.yml:md5,ea5858879452a59bb355228ae7f38111", + "versions.yml:md5,f041502e22449973d84ac1c618e8ebf9" + ], + "test_b0.nii.gz", + "test_resampled.nii.gz", + "test__dwi_eddy_corrected.bvec", + "test__image_boundingBox.pkl", + "test__image_n4.nii.gz", + "test_resampled.nii.gz", + "test__dwi_corrected.nii.gz" + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-28T08:53:44.242823" + } +} \ No newline at end of file diff --git a/subworkflows/nf-scil/preproc_dwi/tests/nextflow.config b/subworkflows/nf-neuro/preproc_dwi/tests/nextflow.config similarity index 83% rename from subworkflows/nf-scil/preproc_dwi/tests/nextflow.config rename to subworkflows/nf-neuro/preproc_dwi/tests/nextflow.config index 4bf578f..38325eb 100644 --- a/subworkflows/nf-scil/preproc_dwi/tests/nextflow.config +++ b/subworkflows/nf-neuro/preproc_dwi/tests/nextflow.config @@ -41,7 +41,7 @@ process { ext.voxel_size = 1 ext.interp = "lin" } - withName: "BETCROP_CROPVOLUME" { + withName: "IMAGE_CROPVOLUME" { ext.output_bbox = false } @@ -50,3 +50,10 @@ process { ext.interp = "nn" } } + +params.topup_eddy_run_topup = true +params.topup_eddy_run_eddy = true +params.preproc_dwi_run_denoising = true +params.preproc_dwi_run_degibbs = true +params.preproc_dwi_run_N4 = true +params.preproc_dwi_run_resampling = true diff --git a/subworkflows/nf-neuro/preproc_dwi/tests/tags.yml b/subworkflows/nf-neuro/preproc_dwi/tests/tags.yml new file mode 100644 index 0000000..55b99e3 --- /dev/null +++ b/subworkflows/nf-neuro/preproc_dwi/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/preproc_dwi: + - subworkflows/nf-neuro/preproc_dwi/** 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-scil/preproc_t1/meta.yml b/subworkflows/nf-neuro/preproc_t1/meta.yml similarity index 86% rename from subworkflows/nf-scil/preproc_t1/meta.yml rename to subworkflows/nf-neuro/preproc_t1/meta.yml index 35baefa..e736187 100644 --- a/subworkflows/nf-scil/preproc_t1/meta.yml +++ b/subworkflows/nf-neuro/preproc_t1/meta.yml @@ -8,7 +8,8 @@ description: | 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. + 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, @@ -21,9 +22,10 @@ description: | 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 (bet, ANTs). + 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. + 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 @@ -42,7 +44,8 @@ components: - preproc/n4 - image/resample - betcrop/antsbet - - betcrop/cropvolume + - betcrop/synthbet + - image/cropvolume input: - ch_image: @@ -54,13 +57,14 @@ input: - ch_template: type: file description: | - The input channel containing the anatomical template to perform BET. + 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, with intensity range 1 (definitely brain) to 0 (definitely background). + 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: @@ -81,6 +85,12 @@ input: 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 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-scil/preproc_t1/tests/nextflow.config b/subworkflows/nf-neuro/preproc_t1/tests/nextflow.config similarity index 51% rename from subworkflows/nf-scil/preproc_t1/tests/nextflow.config rename to subworkflows/nf-neuro/preproc_t1/tests/nextflow.config index 0cf9bdf..3a82000 100644 --- a/subworkflows/nf-scil/preproc_t1/tests/nextflow.config +++ b/subworkflows/nf-neuro/preproc_t1/tests/nextflow.config @@ -4,8 +4,15 @@ process { ext.voxel_size = 1 ext.interp = "lin" } - withName: "BETCROP_CROPVOLUME_T1" { + 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/** diff --git a/subworkflows/nf-neuro/registration/main.nf b/subworkflows/nf-neuro/registration/main.nf new file mode 100644 index 0000000..e04e0b7 --- /dev/null +++ b/subworkflows/nf-neuro/registration/main.nf @@ -0,0 +1,143 @@ +include { REGISTRATION_ANATTODWI } from '../../../modules/nf-neuro/registration/anattodwi/main' +include { REGISTRATION_ANTS } from '../../../modules/nf-neuro/registration/ants/main' +include { REGISTRATION_EASYREG } from '../../../modules/nf-neuro/registration/easyreg/main' +include { REGISTRATION_SYNTHREGISTRATION } from '../../../modules/nf-neuro/registration/synthregistration/main' + +params.run_easyreg = false +params.run_synthmorph = false + +workflow REGISTRATION { + + // The subworkflow requires at least ch_image and ch_ref as inputs to + // properly perform the registration. Supplying a ch_metric will select + // the REGISTRATION_ANATTODWI module meanwhile NOT supplying a ch_metric + // will select the REGISTRATION_ANTS (SyN or SyNQuick) module. Alternatively, + // NOT supplying ch_metric and activating alternative module flag with select + // REGISTRATION_EASYREG or REGISTRATION_SYNTHMORPH + + take: + ch_image // channel: [ val(meta), image ] + ch_ref // channel: [ val(meta), reference ] + ch_metric // channel: [ val(meta), metric ], optional + ch_mask // channel: [ val(meta), mask ], optional + ch_segmentation // channel: [ val(meta), segmentation ], optional + ch_ref_segmentation // channel: [ val(meta), ref-segmentation ], optional + + main: + + ch_versions = Channel.empty() + + if ( params.run_easyreg ) { + // ** Registration using Easyreg ** // + // Result : [ meta, reference, image | [], ref-segmentation | [], segmentation | [] ] + // Steps : + // - join [ meta, reference, image | null ] + // - join [ meta, reference, image | null, ref-segmentation | null ] + // - join [ meta, reference, image | null, ref-segmentation | null, segmentation | null ] + // - map [ meta, reference, image | [], ref-segmentation | [], segmentation | [] ] + ch_register = ch_ref + .join(ch_image, remainder: true) + .join(ch_ref_segmentation, remainder: true) + .join(ch_segmentation, remainder: true) + .map{ it[0..1] + [it[2] ?: [], it[3] ?: [], it[4] ?: []] } + + REGISTRATION_EASYREG ( ch_register ) + ch_versions = ch_versions.mix(REGISTRATION_EASYREG.out.versions.first()) + + // ** Set compulsory outputs ** // + image_warped = REGISTRATION_EASYREG.out.flo_reg + transfo_image = REGISTRATION_EASYREG.out.fwd_field + transfo_trk = REGISTRATION_EASYREG.out.bak_field + ref_warped = REGISTRATION_EASYREG.out.ref_reg + + // ** Set optional outputs. ** // + // If segmentations are not provided as inputs, + // easyreg will outputs synthseg segmentations + out_segmentation = ch_segmentation.mix( REGISTRATION_EASYREG.out.flo_seg ) + out_ref_segmentation = ch_ref_segmentation.mix( REGISTRATION_EASYREG.out.ref_seg ) + } + else if ( params.run_synthmorph ) { + // ** Registration using synthmorph ** // + ch_register = ch_image + .join(ch_ref) + + REGISTRATION_SYNTHREGISTRATION ( ch_register ) + ch_versions = ch_versions.mix(REGISTRATION_SYNTHREGISTRATION.out.versions.first()) + + // ** Set outputs ** // + image_warped = REGISTRATION_SYNTHREGISTRATION.out.warped_image + transfo_image = REGISTRATION_SYNTHREGISTRATION.out.warp + .join(REGISTRATION_SYNTHREGISTRATION.out.affine) // FIXME : this is .lta, should be .mat, but we need a custom container for that + transfo_trk = Channel.empty() // FIXME : this transformation should be available + ref_warped = Channel.empty() + out_segmentation = Channel.empty() + out_ref_segmentation = Channel.empty() + } + else { + // ** Classic registration using antsRegistration ** // + // Result : [ meta, image, reference, metric | [] ] + // Steps : + // - join [ meta, image, ref ] + // - join [ meta, image, ref, metric | null ] + // - map [ meta, image, ref, metric | [] ] + // Branches : + // - anat_to_dwi : has a metric at index 3 + // - ants_syn : doesn't have a metric at index 3 ( [] or null ) + ch_register = ch_image + .join(ch_ref) + .join(ch_metric, remainder: true) + .map{ it[0..2] + [it[3] ?: []] } + .branch{ + anat_to_dwi : it[3] + ants_syn: true + } + + // ** Registration using ANAT TO DWI ** // + REGISTRATION_ANATTODWI ( ch_register.anat_to_dwi ) + ch_versions = ch_versions.mix(REGISTRATION_ANATTODWI.out.versions.first()) + + // ** Set compulsory outputs ** // + image_warped = REGISTRATION_ANATTODWI.out.t1_warped + transfo_image = REGISTRATION_ANATTODWI.out.warp + .join(REGISTRATION_ANATTODWI.out.affine) + transfo_trk = REGISTRATION_ANATTODWI.out.affine + .join(REGISTRATION_ANATTODWI.out.inverse_warp) + + // ** Registration using ANTS SYN SCRIPTS ** // + // Registration using antsRegistrationSyN.sh or antsRegistrationSyNQuick.sh, has + // to be defined in the config file or else the default (SyN) will be used. + // Result : [ meta, image, mask | [] ] + // Steps : + // - join [ meta, image, metric | [], mask | null ] + // - map [ meta, image, mask | [] ] + ch_register = ch_register.ants_syn + .join(ch_mask, remainder: true) + .map{ it[0..2] + [it[4] ?: []] } + + REGISTRATION_ANTS ( ch_register ) + ch_versions = ch_versions.mix(REGISTRATION_ANTS.out.versions.first()) + + // ** Set compulsory outputs ** // + image_warped = image_warped.mix(REGISTRATION_ANTS.out.image) + transfo_image = REGISTRATION_ANTS.out.warp + .join(REGISTRATION_ANTS.out.affine) + .mix(transfo_image) + transfo_trk = REGISTRATION_ANTS.out.inverse_affine + .join(REGISTRATION_ANTS.out.inverse_warp) + .mix(transfo_trk) + + // **et optional outputs **// + ref_warped = Channel.empty() + out_segmentation = Channel.empty() + out_ref_segmentation = Channel.empty() + } + + emit: + image_warped = image_warped // channel: [ val(meta), image ] ] + ref_warped = ref_warped // channel: [ val(meta), ref ] + transfo_image = transfo_image // channel: [ val(meta), [ warp ], [ ] ] + transfo_trk = transfo_trk // channel: [ val(meta), [ ], [ inverse-warp ] ] + segmentation = out_segmentation // channel: [ val(meta), segmentation ] + ref_segmentation = out_ref_segmentation // channel: [ val(meta), ref-segmentation ] + versions = ch_versions // channel: [ versions.yml ] +} diff --git a/subworkflows/nf-scil/registration/meta.yml b/subworkflows/nf-neuro/registration/meta.yml similarity index 66% rename from subworkflows/nf-scil/registration/meta.yml rename to subworkflows/nf-neuro/registration/meta.yml index d6fc95f..55e059e 100644 --- a/subworkflows/nf-scil/registration/meta.yml +++ b/subworkflows/nf-neuro/registration/meta.yml @@ -2,11 +2,12 @@ name: "registration" description: | Subworkflow to perform registration between a moving and a fixed image (e.g. T1 -> DWI). It requires as input at least a moving (ch_image) and a reference (ch_ref) image to properly - perform registration. Two modes are available: + perform registration. Three modes are available: 1) if a metric file is supplied (ch_metric), the subworkflow will use the REGISTER_ANATTODWI module calling AntsRegistration, with the metric as additional target. 2) if NO metric file is supplied, the subworkflow will use the REGISTRATION_ANTS module calling antsRegistrationSyN.sh or antsRegistrationSyNQuick.sh. + 3) alternatively, if an alternative model parameter is activated, the subworkflow will use the specified module This subworkflow outputs transformation files that can be used with the ANTSAPPLYTRANSFORMS module to warp any new image. Simply provide your moving image, reference image, and transformations files to the module to register a new image in the current space. Similar steps can be used to register @@ -17,8 +18,10 @@ keywords: - DWI - Anatomical components: - - register/anattodwi + - registration/anattodwi - registration/ants + - registration/easyreg + - registration/synthregistration input: - ch_image: type: file @@ -50,6 +53,20 @@ input: REGISTRATION_ANTS, see the description section above for more details. Structure: [ val(meta), path(mask) ] pattern: "*.{nii,nii.gz}" + - ch_segmentation: + type: file + description: | + The input channel containing the the SynthSeg v2 (non-robust) segmentation + parcellation of the moving (floating in Easyreg naming convention) image. + If it does not exist, Easyreg will create it. If it already exists (e.g., from a previous EasyReg run), + then EasyReg will read it from disk (which is faster than segmenting). + pattern: "*.{nii,nii.gz}" + - ch_ref_segmentation: + type: file + description: | + The input channel containing the SynthSeg v2 (non-robust) segmentation + parcellation of the fixed (reference in Easyreg naming convention) image. + If it does not exist, Easyreg will create it. If it already exists (e.g., from a previous EasyReg run), + then EasyReg will read it from disk (which is faster than segmenting). + pattern: "*.{nii,nii.gz}" output: - image_warped: type: file @@ -73,6 +90,22 @@ output: be used to register tractograms or bundle in the subject's anatomical space. Structure: [ val(meta), [ path(inverseAffine), path(inverseWarp) ] pattern: "*.{nii,nii.gz,mat}" + - ref_warped: + type: file + description: | + Channel containing warped reference image. Typically, this would be the warped DWI in T1 space. + Structure: [ val(meta), path(ref) ] + pattern: "*.{nii,nii.gz}" + - out_segmentation: + type: file + description: | + Channel containing the file with the SynthSeg v2 (non-robust) segmentation + parcellation of the moving (floating in Easyreg naming convention) image. + pattern: "*.{nii,nii.gz}" + - out_ref_segmentation: + type: file + description: | + Channel containing the file with the SynthSeg v2 (non-robust) segmentation + parcellation of the fixed (reference in Easyreg naming convention) image. + pattern: "*.{nii,nii.gz}" - versions: type: file description: | diff --git a/subworkflows/nf-neuro/registration/tests/main.nf.test b/subworkflows/nf-neuro/registration/tests/main.nf.test new file mode 100644 index 0000000..9340881 --- /dev/null +++ b/subworkflows/nf-neuro/registration/tests/main.nf.test @@ -0,0 +1,202 @@ +nextflow_workflow { + + name "Test Subworkflow REGISTRATION" + script "../main.nf" + workflow "REGISTRATION" + + tag "subworkflows" + tag "subworkflows_nfcore" + tag "subworkflows/registration" + + tag "registration" + tag "registration/anattodwi" + tag "registration" + tag "registration/ants" + tag "registration/easyreg" + tag "registration/synthregistration" + + tag "load_test_data" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "T1w.zip", "b0.zip", "dti.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("registration - antsRegistration") { + config "./nextflow.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + dti: it.simpleName == "dti" + } + 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.b0.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + input[2] = ch_split_test_data.dti.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/fa.nii.gz") + ] + } + input[3] = Channel.empty() + input[4] = Channel.empty() + input[5] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success}, + { assert snapshot(workflow.out).match()} + ) + } + } + + test("registration - SyNQuick") { + config "./nextflow.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + dti: it.simpleName == "dti" + } + 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.b0.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], + file("\${test_data_directory}/b0.nii.gz") + ]} + input[2] = Channel.empty() + input[3] = Channel.empty() + input[4] = Channel.empty() + input[5] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success}, + { assert snapshot(workflow.out).match()} + ) + } + } + + test("registration - easyreg") { + config "./nextflow_easyreg.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + dti: it.simpleName == "dti" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz") + ] + } + input[1] = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + input[2] = Channel.empty() + input[3] = Channel.empty() + input[4] = Channel.empty() + input[5] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success}, + { assert snapshot( + file(workflow.out.image_warped.get(0).get(1)).name, + file(workflow.out.ref_warped.get(0).get(1)).name, + file(workflow.out.transfo_image.get(0).get(1)).name, + file(workflow.out.transfo_trk.get(0).get(1)).name, + niftiMD5SUM(workflow.out.segmentation.get(0).get(1), 6), + file(workflow.out.ref_segmentation.get(0).get(1)).name, + workflow.out.versions + ).match()} + ) + } + } + test("registration - synthregistration") { + config "./nextflow_synthregistration.config" + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + t1w: it.simpleName == "T1w" + b0: it.simpleName == "b0" + dti: it.simpleName == "dti" + } + input[0] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz") + ] + } + input[1] = ch_split_test_data.b0.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/b0.nii.gz") + ] + } + input[2] = Channel.empty() + input[3] = Channel.empty() + input[4] = Channel.empty() + input[5] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success}, + { assert snapshot( + file(workflow.out.image_warped.get(0).get(1)).name, + file(workflow.out.transfo_image.get(0).get(1)).name, + file(workflow.out.transfo_image.get(0).get(2)).name, + workflow.out.versions + ).match()} + ) + } + } +} diff --git a/subworkflows/nf-neuro/registration/tests/main.nf.test.snap b/subworkflows/nf-neuro/registration/tests/main.nf.test.snap new file mode 100644 index 0000000..120d228 --- /dev/null +++ b/subworkflows/nf-neuro/registration/tests/main.nf.test.snap @@ -0,0 +1,221 @@ +{ + "registration - synthregistration": { + "content": [ + "test__output_warped.nii.gz", + "test__deform_warp.nii.gz", + "test__affine_warp.lta", + [ + "versions.yml:md5,8a5fc770044eef3575fe68a3f7792976" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.4" + }, + "timestamp": "2025-02-04T21:05:42.689055803" + }, + "registration - easyreg": { + "content": [ + "test_floating_registered.nii.gz", + "test_reference_registered.nii.gz", + "test_forward_field.nii.gz", + "test_backward_field.nii.gz", + "test_floating_segmentation.nii.gz:md5:header,74ed7e917b9fde7b2aaba7f512cc1edf,data,e1ed38ea961180d3e4c03a56c1c7bc4c", + "test_reference_segmentation.nii.gz", + [ + "versions.yml:md5,3f38c911476c605480c02e484fcf6e9f" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-03-06T16:52:24.923573494" + }, + "registration - antsRegistration": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__t1_warped.nii.gz:md5,9c34f424900adea263e39ed1f37a793f" + ] + ], + "1": [ + + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output1Warp.nii.gz:md5,e0995852e251bee5521572a4495a0bd6", + "test__output0GenericAffine.mat:md5,6eef07f9004fb96e57ae0404ffef174c" + ] + ], + "3": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output0GenericAffine.mat:md5,6eef07f9004fb96e57ae0404ffef174c", + "test__output1InverseWarp.nii.gz:md5,28694d2b41f5209ab8f1acbb219014fe" + ] + ], + "4": [ + + ], + "5": [ + + ], + "6": [ + "versions.yml:md5,04d2203fd83e0422e103384bee7a1696" + ], + "image_warped": [ + [ + { + "id": "test", + "single_end": false + }, + "test__t1_warped.nii.gz:md5,9c34f424900adea263e39ed1f37a793f" + ] + ], + "ref_segmentation": [ + + ], + "ref_warped": [ + + ], + "segmentation": [ + + ], + "transfo_image": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output1Warp.nii.gz:md5,e0995852e251bee5521572a4495a0bd6", + "test__output0GenericAffine.mat:md5,6eef07f9004fb96e57ae0404ffef174c" + ] + ], + "transfo_trk": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output0GenericAffine.mat:md5,6eef07f9004fb96e57ae0404ffef174c", + "test__output1InverseWarp.nii.gz:md5,28694d2b41f5209ab8f1acbb219014fe" + ] + ], + "versions": [ + "versions.yml:md5,04d2203fd83e0422e103384bee7a1696" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.4" + }, + "timestamp": "2025-02-04T20:22:29.587468307" + }, + "registration - SyNQuick": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test__warped.nii.gz:md5,f7e262c9c75c651a9057d3082c2a69ad" + ] + ], + "1": [ + + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output0Warp.nii.gz:md5,964cf7e60171214164f280452f1ae6e0", + "test__output1GenericAffine.mat:md5,bfd589d2b237a20bbeef0656ecd64e14" + ] + ], + "3": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output0InverseAffine.mat:md5,235417a8c7eee8ab7bde320b9e48d2f1", + "test__output1InverseWarp.nii.gz:md5,fac9af1885f5e1cce39fcf06fca93dfc" + ] + ], + "4": [ + + ], + "5": [ + + ], + "6": [ + "versions.yml:md5,b39c35bdc0e79c6a3595616e01e37a72" + ], + "image_warped": [ + [ + { + "id": "test", + "single_end": false + }, + "test__warped.nii.gz:md5,f7e262c9c75c651a9057d3082c2a69ad" + ] + ], + "ref_segmentation": [ + + ], + "ref_warped": [ + + ], + "segmentation": [ + + ], + "transfo_image": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output0Warp.nii.gz:md5,964cf7e60171214164f280452f1ae6e0", + "test__output1GenericAffine.mat:md5,bfd589d2b237a20bbeef0656ecd64e14" + ] + ], + "transfo_trk": [ + [ + { + "id": "test", + "single_end": false + }, + "test__output0InverseAffine.mat:md5,235417a8c7eee8ab7bde320b9e48d2f1", + "test__output1InverseWarp.nii.gz:md5,fac9af1885f5e1cce39fcf06fca93dfc" + ] + ], + "versions": [ + "versions.yml:md5,b39c35bdc0e79c6a3595616e01e37a72" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.3" + }, + "timestamp": "2025-02-24T07:57:27.223538265" + } +} diff --git a/subworkflows/nf-scil/registration/tests/nextflow.config b/subworkflows/nf-neuro/registration/tests/nextflow.config similarity index 90% rename from subworkflows/nf-scil/registration/tests/nextflow.config rename to subworkflows/nf-neuro/registration/tests/nextflow.config index 2109599..8f8c6c2 100644 --- a/subworkflows/nf-scil/registration/tests/nextflow.config +++ b/subworkflows/nf-neuro/registration/tests/nextflow.config @@ -1,5 +1,5 @@ process { - withName: "REGISTER_ANATTODWI" { + withName: "REGISTRATION_ANATTODWI" { ext.cpus = 1 publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } } diff --git a/subworkflows/nf-neuro/registration/tests/nextflow_easyreg.config b/subworkflows/nf-neuro/registration/tests/nextflow_easyreg.config new file mode 100644 index 0000000..b96d6d8 --- /dev/null +++ b/subworkflows/nf-neuro/registration/tests/nextflow_easyreg.config @@ -0,0 +1,9 @@ +process { + withName: "REGISTRATION_EASYREG" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + memory = '14G' + ext.field = true + } +} + +params.run_easyreg = true diff --git a/subworkflows/nf-neuro/registration/tests/nextflow_synthregistration.config b/subworkflows/nf-neuro/registration/tests/nextflow_synthregistration.config new file mode 100644 index 0000000..d14b9be --- /dev/null +++ b/subworkflows/nf-neuro/registration/tests/nextflow_synthregistration.config @@ -0,0 +1,12 @@ +process { + withName: "REGISTRATION_SYNTHREGISTRATION" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + memory = '40G' + ext.affine = "affine" + ext.warp = "deform" + ext.lambda = 0.9 + ext.steps = 9 + } +} + +params.run_synthmorph = true diff --git a/subworkflows/nf-neuro/registration/tests/tags.yml b/subworkflows/nf-neuro/registration/tests/tags.yml new file mode 100644 index 0000000..2fc450c --- /dev/null +++ b/subworkflows/nf-neuro/registration/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/registration: + - subworkflows/nf-neuro/registration/** diff --git a/subworkflows/nf-neuro/topup_eddy/main.nf b/subworkflows/nf-neuro/topup_eddy/main.nf new file mode 100644 index 0000000..8185831 --- /dev/null +++ b/subworkflows/nf-neuro/topup_eddy/main.nf @@ -0,0 +1,122 @@ +// ** Importing modules from nf-neuro ** // +include { PREPROC_TOPUP } from '../../../modules/nf-neuro/preproc/topup/main' +include { PREPROC_EDDY } from '../../../modules/nf-neuro/preproc/eddy/main' +include { UTILS_EXTRACTB0 } from '../../../modules/nf-neuro/utils/extractb0/main' +include { BETCROP_FSLBETCROP } from '../../../modules/nf-neuro/betcrop/fslbetcrop/main' + +workflow TOPUP_EDDY { + + // ** The subworkflow will optionally run topup if a reverse b0 or reverse DWI is provided. ** // + // ** In both cases, it will perform EDDY and also extract a b0 from the corrected DWI image. ** // + + take: + ch_dwi // channel: [ val(meta), dwi, bval, bvec ] + ch_b0 // channel: [ val(meta), b0 ], optional + ch_rev_dwi // channel: [ val(meta), rev-dwi, rev-bval, rev-bvec ], optional + ch_rev_b0 // channel: [ val(meta), rev-b0 ], optional + ch_config_topup // channel: [ 'topup.cnf' ], optional + + main: + ch_versions = Channel.empty() + ch_multiqc_files = Channel.empty() + + ch_topup_fieldcoeff = Channel.empty() + ch_topup_movpart = Channel.empty() + ch_b0_corrected = Channel.empty() + if (params.topup_eddy_run_topup) { + // ** Create channel for TOPUP ** // + // Result : [ meta, dwi, bval, bvec, b0 | [], rev-dwi | [], rev-bval | [], rev-bvec | [], rev-b0 | [] ] + // Steps : + // - join [ meta, dwi, bval, bvec, b0 | null ] + // - map [ meta, dwi, bval, bvec, b0 | [] ] + // - join [ meta, dwi, bval, bvec, b0 | [] ] + [ rev-dwi, rev-bval, rev-bvec ] | [ null ] + // - map [ meta, dwi, bval, bvec, b0 | [], rev-dwi | [], rev-bval | [], rev-bvec | [] ] + // - join [ meta, dwi, bval, bvec, b0 | [], rev-dwi | [], rev-bval | [], rev-bvec | [], rev-b0 | null ] + // - map [ meta, dwi, bval, bvec, b0 | [], rev-dwi | [], rev-bval | [], rev-bvec | [], rev-b0 | [] ] + // + // Finally, to create ch_topup, filter ensures DWI comes with either a rev-dwi (index 5) or a rev-b0 (index 8) + ch_topup = ch_dwi + .join(ch_b0, remainder: true) + .map{ it[0..3] + [it[4] ?: []] } + .join(ch_rev_dwi, remainder: true) + .map{ it[5] ? it : it[0..4] + [[], [], []] } + .join(ch_rev_b0, remainder: true) + .map{ it[0..7] + [it[8] ?: []] } + .filter{ it[5] || it[8] } + + // ** RUN TOPUP ** // + PREPROC_TOPUP ( ch_topup, ch_config_topup ) + ch_versions = ch_versions.mix(PREPROC_TOPUP.out.versions.first()) + ch_multiqc_files = ch_multiqc_files.mix(PREPROC_TOPUP.out.mqc) + + ch_topup_fieldcoeff = PREPROC_TOPUP.out.topup_fieldcoef + ch_topup_movpart = PREPROC_TOPUP.out.topup_movpart + ch_b0_corrected = PREPROC_TOPUP.out.topup_corrected_b0s + } + + + if (params.topup_eddy_run_eddy) { + // ** Create channel for EDDY ** // + // Result : [ meta, dwi, bval, bvec, rev-dwi | [], rev-bval | [], rev-bvec | [], b0 | [], coeffs | [], movpar | [] ] + // Steps : + // - join [ meta, dwi, bval, bvec ] + [ rev-dwi, rev-bval, rev-bvec ] | [ null ] + // - map [ meta, dwi, bval, bvec, rev-dwi | [], rev-bval | [], rev-bvec | [] ] + // - join [ meta, dwi, bval, bvec, rev-dwi | [], rev-bval | [], rev-bvec | [], b0 | null ] + // - map [ meta, dwi, bval, bvec, rev-dwi | [], rev-bval | [], rev-bvec | [], b0 | [] ] + // - join [ meta, dwi, bval, bvec, rev-dwi | [], rev-bval | [], rev-bvec | [], b0 | [], coeffs | null ] + // - map [ meta, dwi, bval, bvec, rev-dwi | [], rev-bval | [], rev-bvec | [], b0 | [], coeffs | [] ] + // - join [ meta, dwi, bval, bvec, rev-dwi | [], rev-bval | [], rev-bvec | [], b0 | [], coeffs | [], movpar | null ] + // - map [ meta, dwi, bval, bvec, rev-dwi | [], rev-bval | [], rev-bvec | [], b0 | [], coeffs | [], movpar | [] ] + ch_eddy_input = ch_dwi + .join(ch_rev_dwi, remainder: true) + .map{ it[0..3] + [it[4] ? it[4..-1] : [], [], []] } + .join(ch_b0_corrected, remainder: true) + .map{ it[0..6] + [it[7] ?: []] } + .join(ch_topup_fieldcoeff, remainder: true) + .map{ it[0..7] + [it[8] ?: []] } + .join(ch_topup_movpart, remainder: true) + .map{ it[0..8] + [it[9] ?: []] } + + // ** RUN EDDY **// + PREPROC_EDDY ( ch_eddy_input ) + ch_versions = ch_versions.mix(PREPROC_EDDY.out.versions.first()) + ch_multiqc_files = ch_multiqc_files.mix(PREPROC_EDDY.out.dwi_eddy_mqc) + ch_multiqc_files = ch_multiqc_files.mix(PREPROC_EDDY.out.rev_dwi_eddy_mqc) + + ch_dwi_extract_b0 = PREPROC_EDDY.out.dwi_corrected + .join(PREPROC_EDDY.out.bval_corrected) + .join(PREPROC_EDDY.out.bvec_corrected) + + UTILS_EXTRACTB0 { ch_dwi_extract_b0 } + ch_versions = ch_versions.mix(UTILS_EXTRACTB0.out.versions.first()) + + ch_b0_corrected = UTILS_EXTRACTB0.out.b0 + ch_dwi = PREPROC_EDDY.out.dwi_corrected + .join(PREPROC_EDDY.out.bval_corrected) + .join(PREPROC_EDDY.out.bvec_corrected) + ch_b0_mask = PREPROC_EDDY.out.b0_mask + } + else { + // Compute bet mask on b0, since Eddy did not do it + BETCROP_FSLBETCROP(ch_b0_corrected.map{ it + [[], []] }) + ch_versions = ch_versions.mix(BETCROP_FSLBETCROP.out.versions.first()) + + ch_b0_mask = BETCROP_FSLBETCROP.out.mask + } + + ch_output_dwi = ch_dwi + .multiMap{ meta, dwi, bval, bvec -> + dwi: [meta, dwi] + bval: [meta, bval] + bvec: [meta, bvec] + } + + emit: + dwi = ch_output_dwi.dwi // channel: [ val(meta), dwi-corrected ] + bval = ch_output_dwi.bval // channel: [ val(meta), bval-corrected ] + bvec = ch_output_dwi.bvec // channel: [ val(meta), bvec-corrected ] + b0 = ch_b0_corrected // channel: [ val(meta), b0-corrected ] + b0_mask = ch_b0_mask // channel: [ val(meta), b0-mask ] + mqc = ch_multiqc_files // channel: [ val(meta), mqc ] + versions = ch_versions // channel: [ versions.yml ] +} diff --git a/subworkflows/nf-scil/topup_eddy/meta.yml b/subworkflows/nf-neuro/topup_eddy/meta.yml similarity index 92% rename from subworkflows/nf-scil/topup_eddy/meta.yml rename to subworkflows/nf-neuro/topup_eddy/meta.yml index 43ffc19..54cec07 100644 --- a/subworkflows/nf-scil/topup_eddy/meta.yml +++ b/subworkflows/nf-neuro/topup_eddy/meta.yml @@ -21,6 +21,7 @@ components: - preproc/topup - preproc/eddy - utils/extractb0 + - betcrop/fslbetcrop input: - meta: type: map @@ -105,6 +106,13 @@ output: description: Nifti volume - Mask for b0 corrected pattern: "*__b0_bet_mask.nii.gz" + - mqc: + type: file + description: | + Channel containing the quality control images for MultiQC. + Structure: [ val(meta), path(mqc) ] + pattern: "*_mqc.{gif,png}" + - versions: type: file description: File containing software versions diff --git a/subworkflows/nf-scil/topup_eddy/tests/main.nf.test b/subworkflows/nf-neuro/topup_eddy/tests/main.nf.test similarity index 95% rename from subworkflows/nf-scil/topup_eddy/tests/main.nf.test rename to subworkflows/nf-neuro/topup_eddy/tests/main.nf.test index 02ba90f..a26260c 100644 --- a/subworkflows/nf-scil/topup_eddy/tests/main.nf.test +++ b/subworkflows/nf-neuro/topup_eddy/tests/main.nf.test @@ -13,6 +13,7 @@ nextflow_workflow { tag "preproc/topup" tag "preproc/eddy" tag "utils/extractb0" + tag "betcrop/fslbetcrop" tag "load_test_data" @@ -45,9 +46,9 @@ nextflow_workflow { [ id:'test', single_end:false ], file("\${test_data_directory}/sub-01_dir-AP_sbref.nii.gz") ]} - input[2] = [] - input[3] = [] - input[4] = [] + input[2] = Channel.from( [] ) + input[3] = Channel.from( [] ) + input[4] = Channel.from( [] ) """ } } @@ -94,7 +95,7 @@ nextflow_workflow { [ id:'test', single_end:false ], file("\${test_data_directory}/sub-01_dir-PA_sbref.nii.gz") ]} - input[4] = [] + input[4] = Channel.from( [] ) """ } } @@ -141,7 +142,7 @@ nextflow_workflow { [ id:'test', single_end:false ], file("\${test_data_directory}/sub-01_dir-PA_sbref.nii.gz") ]} - input[4] = [] + input[4] = Channel.from( [] ) """ } } diff --git a/subworkflows/nf-scil/topup_eddy/tests/main.nf.test.snap b/subworkflows/nf-neuro/topup_eddy/tests/main.nf.test.snap similarity index 57% rename from subworkflows/nf-scil/topup_eddy/tests/main.nf.test.snap rename to subworkflows/nf-neuro/topup_eddy/tests/main.nf.test.snap index 668cb34..17b90f2 100644 --- a/subworkflows/nf-scil/topup_eddy/tests/main.nf.test.snap +++ b/subworkflows/nf-neuro/topup_eddy/tests/main.nf.test.snap @@ -7,7 +7,7 @@ "id": "test", "single_end": false }, - "test__b0_bet_mask.nii.gz:md5,fc1f94e2a5e2cf5197d7fdc28a83ca28" + "test__b0_bet_mask.nii.gz:md5,1b45aae94d5bb093dca6ca4a71a8bf97" ] ], [ @@ -16,23 +16,22 @@ "id": "test", "single_end": false }, - "test__bval_eddy:md5,8192e97b08e8032382397bb844b31892" + "test__dwi_eddy_corrected.bval:md5,8192e97b08e8032382397bb844b31892" ] ], [ - "versions.yml:md5,1210f8705c10f136e83e5f07c0d77b70", - "versions.yml:md5,3e57e2f1a94bbabf9be75f50b4cbcdb0", - "versions.yml:md5,f4dfd5f1c68f28be5dc9abe67eb673e4" + "versions.yml:md5,16c27f9c0f1fd6e8f290fdd07827c2df", + "versions.yml:md5,dc9478b4d3cb93d93799e493aefe5220" ], "test__dwi_corrected.nii.gz", "test__dwi_eddy_corrected.bvec", "test_b0.nii.gz" ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.10.4" }, - "timestamp": "2024-04-29T16:45:05.559117" + "timestamp": "2025-02-23T19:09:04.311390187" }, "eddy": { "content": [ @@ -42,7 +41,7 @@ "id": "test", "single_end": false }, - "test__b0_bet_mask.nii.gz:md5,3574cc8f29da429a0f610f2572c4886c" + "test__b0_bet_mask.nii.gz:md5,1b45aae94d5bb093dca6ca4a71a8bf97" ] ], [ @@ -51,22 +50,22 @@ "id": "test", "single_end": false }, - "test__bval_eddy:md5,8192e97b08e8032382397bb844b31892" + "test__dwi_eddy_corrected.bval:md5,8192e97b08e8032382397bb844b31892" ] ], [ - "versions.yml:md5,1210f8705c10f136e83e5f07c0d77b70", - "versions.yml:md5,f4dfd5f1c68f28be5dc9abe67eb673e4" + "versions.yml:md5,16c27f9c0f1fd6e8f290fdd07827c2df", + "versions.yml:md5,dc9478b4d3cb93d93799e493aefe5220" ], "test__dwi_corrected.nii.gz", "test__dwi_eddy_corrected.bvec", "test_b0.nii.gz" ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.10.4" }, - "timestamp": "2024-04-29T16:43:47.157381" + "timestamp": "2025-02-23T19:08:20.174366099" }, "topup_eddy_rev_dwi": { "content": [ @@ -76,7 +75,7 @@ "id": "test", "single_end": false }, - "test__b0_bet_mask.nii.gz:md5,fc1f94e2a5e2cf5197d7fdc28a83ca28" + "test__b0_bet_mask.nii.gz:md5,1b45aae94d5bb093dca6ca4a71a8bf97" ] ], [ @@ -85,22 +84,21 @@ "id": "test", "single_end": false }, - "test__bval_eddy:md5,4c61c53078316c31b4d5daf446a3d6ac" + "test__dwi_eddy_corrected.bval:md5,8192e97b08e8032382397bb844b31892" ] ], [ - "versions.yml:md5,1210f8705c10f136e83e5f07c0d77b70", - "versions.yml:md5,3e57e2f1a94bbabf9be75f50b4cbcdb0", - "versions.yml:md5,f4dfd5f1c68f28be5dc9abe67eb673e4" + "versions.yml:md5,16c27f9c0f1fd6e8f290fdd07827c2df", + "versions.yml:md5,dc9478b4d3cb93d93799e493aefe5220" ], "test__dwi_corrected.nii.gz", "test__dwi_eddy_corrected.bvec", "test_b0.nii.gz" ], "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" + "nf-test": "0.9.0", + "nextflow": "24.10.4" }, - "timestamp": "2024-04-29T16:46:43.753231" + "timestamp": "2025-02-23T19:09:48.530536539" } } \ No newline at end of file diff --git a/subworkflows/nf-scil/topup_eddy/tests/nextflow.config b/subworkflows/nf-neuro/topup_eddy/tests/nextflow.config similarity index 91% rename from subworkflows/nf-scil/topup_eddy/tests/nextflow.config rename to subworkflows/nf-neuro/topup_eddy/tests/nextflow.config index 5c966be..bd7cf96 100644 --- a/subworkflows/nf-scil/topup_eddy/tests/nextflow.config +++ b/subworkflows/nf-neuro/topup_eddy/tests/nextflow.config @@ -25,3 +25,6 @@ process { ext.output_single_volume = true } } + +params.topup_eddy_run_topup = true +params.topup_eddy_run_eddy = true diff --git a/subworkflows/nf-neuro/topup_eddy/tests/tags.yml b/subworkflows/nf-neuro/topup_eddy/tests/tags.yml new file mode 100644 index 0000000..6641d31 --- /dev/null +++ b/subworkflows/nf-neuro/topup_eddy/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/topup_eddy: + - subworkflows/nf-neuro/topup_eddy/** diff --git a/subworkflows/nf-neuro/tractoflow/main.nf b/subworkflows/nf-neuro/tractoflow/main.nf new file mode 100644 index 0000000..460f433 --- /dev/null +++ b/subworkflows/nf-neuro/tractoflow/main.nf @@ -0,0 +1,324 @@ + +// PREPROCESSING +include { PREPROC_DWI } from '../preproc_dwi/main' +include { PREPROC_T1 } from '../preproc_t1/main' +include { REGISTRATION as T1_REGISTRATION } from '../registration/main' +include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_WMPARC } from '../../../modules/nf-neuro/registration/antsapplytransforms/main' +include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_APARC_ASEG } from '../../../modules/nf-neuro/registration/antsapplytransforms/main' +include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_LESION_MASK } from '../../../modules/nf-neuro/registration/antsapplytransforms/main' +include { ANATOMICAL_SEGMENTATION } from '../anatomical_segmentation/main' + +// RECONSTRUCTION +include { RECONST_FRF } from '../../../modules/nf-neuro/reconst/frf/main' +include { RECONST_MEANFRF } from '../../../modules/nf-neuro/reconst/meanfrf/main' +include { RECONST_DTIMETRICS } from '../../../modules/nf-neuro/reconst/dtimetrics/main' +include { RECONST_FODF } from '../../../modules/nf-neuro/reconst/fodf/main' + +// TRACKING +include { TRACKING_PFTTRACKING } from '../../../modules/nf-neuro/tracking/pfttracking/main' +include { TRACKING_LOCALTRACKING } from '../../../modules/nf-neuro/tracking/localtracking/main' + + +// ** UTILITY FUNCTIONS ** // + +def group_frf ( label, ch_frf ) { + return ch_frf + .map{ _meta, frf -> frf } + .flatten() + .map{ frf_list -> [label, frf_list] } +} + + +workflow TRACTOFLOW { + take: + ch_dwi // channel : [required] meta, dwi, bval, bvec + ch_t1 // channel : [required] meta, t1 + ch_sbref // channel : [optional] meta, sbref + ch_rev_dwi // channel : [optional] meta, rev_dwi, rev_bval, rev_bvec + ch_rev_sbref // channel : [optional] meta, rev_sbref + ch_wmparc // channel : [optional] meta, wmparc + ch_aparc_aseg // channel : [optional] meta, aparc_aseg + ch_topup_config // channel : [optional] topup_config + ch_bet_template // channel : [optional] meta, bet_template + ch_bet_probability // channel : [optional] meta, bet_probability + ch_lesion_mask // channel : [optional] meta, lesion_mask + main: + + ch_versions = Channel.empty() + + /* PREPROCESSING */ + + // + // SUBWORKFLOW: Run PREPROC_DWI + // + PREPROC_DWI( + ch_dwi, + ch_rev_dwi, + ch_sbref, + ch_rev_sbref, + ch_topup_config + ) + ch_versions = ch_versions.mix(PREPROC_DWI.out.versions.first()) + + // + // SUBWORKFLOW: Run PREPROC_T1 + // + PREPROC_T1( + ch_t1, + ch_bet_template, + ch_bet_probability, + Channel.empty(), + Channel.empty(), + Channel.empty(), + Channel.empty() + ) + ch_versions = ch_versions.mix(PREPROC_T1.out.versions.first()) + + /* RECONSTRUCTION - PART I - doesn't need anatomy */ + + // + // MODULE: Run RECONST/DTIMETRICS + // + ch_dti_metrics = PREPROC_DWI.out.dwi + .join(PREPROC_DWI.out.bval) + .join(PREPROC_DWI.out.bvec) + .join(PREPROC_DWI.out.b0_mask) + + RECONST_DTIMETRICS( ch_dti_metrics ) + ch_versions = ch_versions.mix(RECONST_DTIMETRICS.out.versions.first()) + + + // + // SUBWORKFLOW: Run REGISTRATION + // + T1_REGISTRATION( + PREPROC_T1.out.t1_final, + PREPROC_DWI.out.b0, + RECONST_DTIMETRICS.out.fa, + Channel.empty(), + Channel.empty(), + Channel.empty() + ) + ch_versions = ch_versions.mix(T1_REGISTRATION.out.versions.first()) + + /* SEGMENTATION */ + + // + // MODULE: Run REGISTRATION_ANTSAPPLYTRANSFORMS (TRANSFORM_WMPARC) + // + TRANSFORM_WMPARC( + ch_wmparc + .join(PREPROC_DWI.out.b0) + .join(T1_REGISTRATION.out.transfo_image) + ) + ch_versions = ch_versions.mix(TRANSFORM_WMPARC.out.versions.first()) + + // + // MODULE: Run REGISTRATION_ANTSAPPLYTRANSFORMS (TRANSFORM_APARC_ASEG) + // + TRANSFORM_APARC_ASEG( + ch_aparc_aseg + .join(PREPROC_DWI.out.b0) + .join(T1_REGISTRATION.out.transfo_image) + ) + ch_versions = ch_versions.mix(TRANSFORM_APARC_ASEG.out.versions.first()) + + // + // Module: Run REGISTRATION_ANTSAPPLYTRANSFORMS (TRANSFORM_LESION_MASK) + TRANSFORM_LESION_MASK( + ch_lesion_mask + .join(PREPROC_DWI.out.b0) + .join(T1_REGISTRATION.out.transfo_image) + ) + ch_versions = ch_versions.mix(TRANSFORM_LESION_MASK.out.versions.first()) + + // + // SUBWORKFLOW: Run ANATOMICAL_SEGMENTATION + // + ANATOMICAL_SEGMENTATION( + T1_REGISTRATION.out.image_warped, + TRANSFORM_WMPARC.out.warped_image + .join(TRANSFORM_APARC_ASEG.out.warped_image), + TRANSFORM_LESION_MASK.out.warped_image, + Channel.empty() + ) + ch_versions = ch_versions.mix(ANATOMICAL_SEGMENTATION.out.versions.first()) + + /* RECONSTRUCTION - PART II - needs anatomy */ + + // + // MODULE: Run RECONST/FRF + // + ch_reconst_frf = PREPROC_DWI.out.dwi + .join(PREPROC_DWI.out.bval) + .join(PREPROC_DWI.out.bvec) + .join(PREPROC_DWI.out.b0_mask) + .join(ANATOMICAL_SEGMENTATION.out.wm_mask) + .join(ANATOMICAL_SEGMENTATION.out.gm_mask) + .join(ANATOMICAL_SEGMENTATION.out.csf_mask) + + RECONST_FRF( ch_reconst_frf ) + ch_versions = ch_versions.mix(RECONST_FRF.out.versions.first()) + + /* Run fiber response averaging over subjects */ + ch_single_frf = RECONST_FRF.out.frf + .map{ it + [[], []] } + + ch_fiber_response = RECONST_FRF.out.wm_frf + .join(RECONST_FRF.out.gm_frf) + .join(RECONST_FRF.out.csf_frf) + .mix(ch_single_frf) + + if ( params.frf_average_from_data ) { + ch_single_frf = group_frf("ssst", RECONST_FRF.out.frf) + + ch_wm_frf = group_frf("wm", RECONST_FRF.out.wm_frf) + ch_gm_frf = group_frf("gm", RECONST_FRF.out.gm_frf) + ch_csf_frf = group_frf("csf", RECONST_FRF.out.csf_frf) + + ch_meanfrf = ch_single_frf + .mix(ch_wm_frf) + .mix(ch_gm_frf) + .mix(ch_csf_frf) + + RECONST_MEANFRF( ch_meanfrf ) + ch_versions = ch_versions.mix(RECONST_MEANFRF.out.versions.first()) + + ch_meanfrf = RECONST_MEANFRF.out.meanfrf + .map{ ["frf"] + it } + .branch{ + ssst: it[1] == "ssst" + wm: it[1] == "wm" + gm: it[1] == "gm" + csf: it[1] == "csf" + } + + ch_fiber_response = ch_meanfrf.wm.map{ [it[0], it[2]] } + .join(ch_meanfrf.gm.map{ [it[0], it[2]] }) + .join(ch_meanfrf.csf.map{ [it[0], it[2]] }) + .map{ it[1..-1] } + .mix(ch_meanfrf.ssst.map{ [it[1], [], []] }) + .combine(RECONST_FRF.out.map{ it[0] }) + } + + // + // MODULE: Run RECONST/FODF + // + ch_reconst_fodf = PREPROC_DWI.out.dwi + .join(PREPROC_DWI.out.bval) + .join(PREPROC_DWI.out.bvec) + .join(PREPROC_DWI.out.b0_mask) + .join(RECONST_DTIMETRICS.out.fa) + .join(RECONST_DTIMETRICS.out.md) + .join(ch_fiber_response) + RECONST_FODF( ch_reconst_fodf ) + ch_versions = ch_versions.mix(RECONST_FODF.out.versions.first()) + + // + // MODULE: Run TRACKING/PFTTRACKING + // + ch_pft_tracking = Channel.empty() + if ( params.run_pft ) { + ch_input_pft_tracking = ANATOMICAL_SEGMENTATION.out.wm_mask + .join(ANATOMICAL_SEGMENTATION.out.gm_mask) + .join(ANATOMICAL_SEGMENTATION.out.csf_mask) + .join(RECONST_FODF.out.fodf) + .join(RECONST_DTIMETRICS.out.fa) + TRACKING_PFTTRACKING( ch_input_pft_tracking ) + ch_versions = ch_versions.mix(TRACKING_PFTTRACKING.out.versions.first()) + + ch_pft_tracking = TRACKING_PFTTRACKING.out.trk + .join(TRACKING_PFTTRACKING.out.config) + .join(TRACKING_PFTTRACKING.out.includes) + .join(TRACKING_PFTTRACKING.out.excludes) + .join(TRACKING_PFTTRACKING.out.seeding) + } + + // + // MODULE: Run TRACKING/LOCALTRACKING + // + ch_local_tracking = Channel.empty() + if ( params.run_local_tracking ) { + ch_input_local_tracking = ANATOMICAL_SEGMENTATION.out.wm_mask + .join(RECONST_FODF.out.fodf) + .join(RECONST_DTIMETRICS.out.fa) + TRACKING_LOCALTRACKING( ch_input_local_tracking ) + ch_versions = ch_versions.mix(TRACKING_LOCALTRACKING.out.versions.first()) + + ch_local_tracking = TRACKING_LOCALTRACKING.out.trk + .join(TRACKING_LOCALTRACKING.out.config) + .join(TRACKING_LOCALTRACKING.out.seedmask) + .join(TRACKING_LOCALTRACKING.out.trackmask) + } + + emit: + + // IN DIFFUSION SPACE + dwi = PREPROC_DWI.out.dwi + .join(PREPROC_DWI.out.bval) + .join(PREPROC_DWI.out.bvec) + t1 = T1_REGISTRATION.out.image_warped + wm_mask = ANATOMICAL_SEGMENTATION.out.wm_mask + gm_mask = ANATOMICAL_SEGMENTATION.out.gm_mask + csf_mask = ANATOMICAL_SEGMENTATION.out.csf_mask + wm_map = ANATOMICAL_SEGMENTATION.out.wm_map + gm_map = ANATOMICAL_SEGMENTATION.out.gm_map + csf_map = ANATOMICAL_SEGMENTATION.out.csf_map + aparc_aseg = TRANSFORM_APARC_ASEG.out.warped_image + wmparc = TRANSFORM_WMPARC.out.warped_image + + // REGISTRATION + anatomical_to_diffusion = T1_REGISTRATION.out.transfo_image + diffusion_to_anatomical = T1_REGISTRATION.out.transfo_trk + + // IN ANATOMICAL SPACE + t1_native = PREPROC_T1.out.t1_final + + // DTI + dti_tensor = RECONST_DTIMETRICS.out.tensor + dti_md = RECONST_DTIMETRICS.out.md + dti_rd = RECONST_DTIMETRICS.out.rd + dti_ad = RECONST_DTIMETRICS.out.ad + dti_fa = RECONST_DTIMETRICS.out.fa + dti_rgb = RECONST_DTIMETRICS.out.rgb + dti_peaks = RECONST_DTIMETRICS.out.evecs_v1 + dti_evecs = RECONST_DTIMETRICS.out.evecs + dti_evals = RECONST_DTIMETRICS.out.evals + dti_residual = RECONST_DTIMETRICS.out.residual + dti_ga = RECONST_DTIMETRICS.out.ga + dti_mode = RECONST_DTIMETRICS.out.mode + dti_norm = RECONST_DTIMETRICS.out.norm + + // FODF + fiber_response = ch_fiber_response + fodf = RECONST_FODF.out.fodf + wm_fodf = RECONST_FODF.out.wm_fodf + gm_fodf = RECONST_FODF.out.gm_fodf + csf_fodf = RECONST_FODF.out.csf_fodf + fodf_rgb = RECONST_FODF.out.vf_rgb + fodf_peaks = RECONST_FODF.out.peaks + afd_max = RECONST_FODF.out.afd_max + afd_total = RECONST_FODF.out.afd_total + afd_sum = RECONST_FODF.out.afd_sum + nufo = RECONST_FODF.out.nufo + volume_fraction = RECONST_FODF.out.vf + + // TRACKING + pft_tractogram = ch_pft_tracking.map{ [it[0], it[1]] } + pft_config = ch_pft_tracking.map{ [it[0], it[2]] } + pft_map_include = ch_pft_tracking.map{ [it[0], it[3]] } + pft_map_exclude = ch_pft_tracking.map{ [it[0], it[4]] } + pft_seeding_mask = ch_pft_tracking.map{ [it[0], it[5]] } + local_tractogram = ch_local_tracking.map{ [it[0], it[1]] } + local_config = ch_local_tracking.map{ [it[0], it[2]] } + local_seeding_mask = ch_local_tracking.map{ [it[0], it[3]] } + local_tracking_mask = ch_local_tracking.map{ [it[0], it[4]] } + + // QC + nonphysical_voxels = RECONST_DTIMETRICS.out.nonphysical + pulsation_in_dwi = RECONST_DTIMETRICS.out.pulsation_std_dwi + pulsation_in_b0 = RECONST_DTIMETRICS.out.pulsation_std_b0 + dti_residual_stats = RECONST_DTIMETRICS.out.residual_residuals_stats + + versions = ch_versions // channel: [ path(versions.yml) ] +} diff --git a/subworkflows/nf-neuro/tractoflow/meta.yml b/subworkflows/nf-neuro/tractoflow/meta.yml new file mode 100644 index 0000000..a98d660 --- /dev/null +++ b/subworkflows/nf-neuro/tractoflow/meta.yml @@ -0,0 +1,353 @@ +name: "tractoflow" +description: | + This subworkflow implements the TractoFlow [1] pipeline. It can process raw diffusion and T1 weighted image to reduce + acquisition biases, align anatomical and diffusion space, compute DTI and fODF metrics and generate whole brain + tractograms. + ---------- Configuration ---------- + - nextflow.config : contains an example configuration for the subworkflow. + └── modules.config : contains the bindings to the modules parameters and some defaults making it Tractoflow. + + i.e: nextflow.config import modules.config + -------------- Steps -------------- + PREPROCESS DWI (preproc_dwi, nf-neuro) + Preprocess the DWI image including brain extraction, MP-PCA denoising, eddy current and motion correction, N4 bias + correction, normalization and resampling. + PREPROCESS T1 (preproc_t1, nf-neuro) + Preprocess the T1 image including brain extraction, NL-Means denoising, bias field correction and resampling. + T1 REGISTRATION (anatomical_registration, nf-neuro) + Register the T1 image to the DWI image, using the b0 and the FA map as target for the diffusion space. + SEGMENTATION (anatomical_segmentation, nf-neuro) + Segment the T1 image into white matter, gray matter and CSF, in diffusion space. + DTI FITTING (dipy) + Fit the diffusion tensor model on the preprocessed DWI image and extract relevant metrics. + FRF ESTIMATION (scilpy) + Estimate the Fiber Response Function (FRF) from the preprocessed DWI image. + FODF FITTING (dipy) + Fit the Fiber Orientation Distribution Function (fODF), using Single or Multi Shell, Single or Multi Tissues models, + on the preprocessed DWI image and extract relevant metrics. + PFT TRACKING (dipy) + Perform Particle Filtering Tractography (PFT) on the FODF to generate whole brain tractograms. + LOCAL TRACKING (dipy on CPU, scilpy on GPU) + Perform Local Tracking on the FODF to generate whole brain tractograms. + + [1] https://tractoflow-documentation.readthedocs.io +keywords: + - diffusion + - MRI + - end-to-end + - tractography + - preprocessing + - fodf + - dti +components: + - anatomical_segmentation + - preproc_dwi + - preproc_t1 + - registration + - reconst/dtimetrics + - reconst/frf + - reconst/fodf + - reconst/meanfrf + - registration/antsapplytransforms + - tracking/localtracking + - tracking/pfttracking +input: + - ch_dwi: + type: file + description: | + The input channel containing the DWI file, B-values and B-vectors in FSL format files. + Structure: [ val(meta), path(dwi), path(bval), path(bvec) ] + - ch_t1: + type: file + description: | + The input channel containing the anatomical T1 weighted image. + Structure: [ val(meta), path(t1) ] + - ch_sbref: + type: file + description: | + (Optional) The input channel containing the single-band b0 reference for the DWI. + Structure: [ val(meta), path(rev_b0) ] + - ch_rev_dwi: + type: file + description: | + (Optional) The input channel containing the reverse DWI file, B-values and B-vectors in FSL format files. + Structure: [ val(meta), path(rev_dwi), path(bval), path(bvec) ] + - ch_rev_sbref: + type: file + description: | + (Optional) The input channel containing the reverse b0 file. + Structure: [ val(meta), path(rev_b0) ] + - ch_aparc_aseg: + type: file + description: | + (Optional) The input channel containing freesurfer brain segmentation and gray matter parcellation (aparc+aseg). + Must be supplied with ch_wm_parc. When supplied, those are used to generate tissues masks and probability maps. + Structure: [ val(meta), path(aparc_aseg) ] + - ch_wm_parc: + type: file + description: | + (Optional) The input channel containing freesurfer white matter parcellations (wmparc). Must be supplied with + ch_aparc_aseg. When supplied, those are used to generate tissues masks and probability maps. + Structure: [ val(meta), path(wmparc) ] + - ch_topup_config: + type: file + description: | + (Optional) The input channel containing the config file for Topup. This input is optional. + See https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide#Configuration_files. + Structure: [ path(config_file) ] + - ch_bet_template: + type: file + description: | + (Optional) The input channel containing the anatomical template for antsBET. + Structure: [ val(meta), path(bet_template) ] + - ch_bet_probability_map: + type: file + description: | + (Optional) The input channel containing the brain probability mask for antsBET, + with intensity range 1 (definitely brain) to 0 (definitely background). + Structure: [ val(meta), path(probability_map) ] + - ch_lesion_mask: + type: file + description: | + (Optional) The input channel containing the lesion mask for segmentation. + Structure: [ val(meta), path(lesion_mask) ] +output: + - dwi: + type: file + description: | + Preprocessed DWI image. + Structure: [ val(meta), path(dwi), path(bval), path(bvec) ] + - t1: + type: file + description: | + T1 image warped to the DWI space. + Structure: [ val(meta), path(t1) ] + - wm_mask: + type: file + description: | + White matter mask. + Structure: [ val(meta), path(wm_mask) ] + - gm_mask: + type: file + description: | + Gray matter mask. + Structure: [ val(meta), path(gm_mask) ] + - csf_mask: + type: file + description: | + Cerebrospinal fluid mask. + Structure: [ val(meta), path(csf_mask) ] + - wm_map: + type: file + description: | + White matter probability map. + Structure: [ val(meta), path(wm_map) ] + - gm_map: + type: file + description: | + Gray matter probability map. + Structure: [ val(meta), path(gm_map) ] + - csf_map: + type: file + description: | + Cerebrospinal fluid probability map. + Structure: [ val(meta), path(csf_map) ] + - aparc_aseg: + type: file + description: | + (Optional) Freesurfer brain segmentation and gray matter parcellation (aparc+aseg) in diffusion space. Only + available if ch_aparc_aseg is provided in inputs. + Structure: [ val(meta), path(aparc_aseg) ] + - wmparc: + type: file + description: | + (Optional) Freesurfer white matter parcellations (wmparc) in diffusion space. Only available if ch_wm_parc + is provided in inputs. + Structure: [ val(meta), path(wmparc) ] + - anatomical_to_diffusion: + type: file + description: | + Transformation matrix from the anatomical space to the diffusion space. + Structure: [ val(meta), [path(warp), path(affine)] ] + - diffusion_to_anatomical: + type: file + description: | + Transformation matrix from the diffusion space to the anatomical space. + Structure: [ val(meta), [path(affine), path(warp)] ] + - t1_native: + type: file + description: | + Preprocessed T1 in anatomical space. + Structure: [ val(meta), path(t1) ] + - dti_tensor: + type: file + description: | + 4-D Diffusion tensor image, with 6 components in the last dimensions, ordered by FSL convention + (row-major : Dxx, Dxy, Dxz, Dyy, Dyz, Dzz). + Structure: [ val(meta), path(dti_tensor) ] + - dti_md: + type: file + description: | + Mean diffusivity map. + Structure: [ val(meta), path(dti_md) ] + - dti_rd: + type: file + description: | + Radial diffusivity map. + Structure: [ val(meta), path(dti_rd) ] + - dti_ad: + type: file + description: | + Axial diffusivity map. + Structure: [ val(meta), path(dti_ad) ] + - dti_fa: + type: file + description: | + Fractional anisotropy map. + Structure: [ val(meta), path(dti_fa) ] + - dti_rgb: + type: file + description: | + RGB map of the diffusion tensor. + Structure: [ val(meta), path(dti_rgb) ] + - dti_peaks: + type: file + description: | + Principal direction of the diffusion tensor. + Structure: [ val(meta), path(dti_peaks) ] + - dti_evecs: + type: file + description: | + Eigenvectors of the diffusion tensor, ordered by eigenvalue. + Structure: [ val(meta), path(dti_evecs) ] + - dti_evals: + type: file + description: | + Eigenvalues of the diffusion tensor. + Structure: [ val(meta), path(dti_evals) ] + - dti_residual: + type: file + description: | + Residuals of the diffusion tensor fitting. + Structure: [ val(meta), path(dti_residual) ] + - dti_ga: + type: file + description: | + Generalized anisotropy map. + Structure: [ val(meta), path(dti_ga) ] + - dti_mode: + type: file + description: | + Mode of the diffusion tensor. + Structure: [ val(meta), path(dti_mode) ] + - dti_norm: + type: file + description: | + Norm of the diffusion tensor. + Structure: [ val(meta), path(dti_norm) ] + - fiber_response: + type: file + description: | + Fiber Response Function (FRF) estimated from the DWI image. + If using Single Tissue + Structure: [ val(meta), path(fiber_response) ] + If using Multi Tissues + Structure: [ val(meta), path(wm_fiber_response), path(gm_fiber_response), path(csf_fiber_response) ] + - fodf: + type: file + description: | + Fiber Orientation Distribution Function (fODF) estimated from the DWI image. + If using Single Tissue + Structure: [ val(meta), path(fodf) ] + If using Multi Tissues + Structure: [ val(meta), path(wm_fodf), path(gm_fodf), path(csf_fodf) ] + - fodf_rgb: + type: file + description: | + RGB map of the fODF, normalized by volume fraction of WM. + Structure: [ val(meta), path(fodf_rgb) ] + - fodf_peaks: + type: file + description: | + Peaks of the fODF. + Structure: [ val(meta), path(fodf_peaks) ] + - afd_max: + type: file + description: | + Maximum Apparent Fiber Density (AFD) map. + Structure: [ val(meta), path(afd_max) ] + - afd_total: + type: file + description: | + Total Apparent Fiber Density (AFD) map. + Structure: [ val(meta), path(afd_total) ] + - afd_sum: + type: file + description: | + Sum of Apparent Fiber Density (AFD) map. + Structure: [ val(meta), path(afd_sum) ] + - nufo: + type: file + description: | + Number of Unique Fibers Orientations (NUFO) map. + Structure: [ val(meta), path(nufo) ] + - volume_fraction: + type: file + description: | + Tissues volume fraction map. + Structure: [ val(meta), path(volume_fraction) ] + - pft_tractogram: + type: file + description: | + Whole brain tractogram generated with Particle Filtering Tractography (PFT). + Structure: [ val(meta), path(pft_tractogram) ] + - pft_config: + type: file + description: | + Configuration file used for Particle Filtering Tractography (PFT). + Structure: [ val(meta), path(pft_config) ] + - pft_map_include: + type: file + description: | + Include map used for Particle Filtering Tractography (PFT). + Structure: [ val(meta), path(pft_map_include) ] + - pft_map_exclude: + type: file + description: | + Exclude map used for Particle Filtering Tractography (PFT). + Structure: [ val(meta), path(pft_map_exclude) ] + - pft_seeding_mask: + type: file + description: | + Seeding mask used for Particle Filtering Tractography (PFT). + Structure: [val(meta), path(pft_seeding_mask)] + - local_tractogram: + type: file + description: | + Whole brain tractogram generated with Local Tracking. + Structure: [ val(meta), path(local_tractogram) ] + - local_config: + type: file + description: | + Configuration file used for Local Tracking. + Structure: [ val(meta), path(local_config) ] + - local_seeding_mask: + type: file + description: | + Seeding mask used for Local Tracking. + Structure: [ val(meta), path(local_seeding_mask) ] + - local_tracking_mask: + type: file + description: | + Tracking mask used for Local Tracking. + Structure: [ val(meta), path(local_tracking_mask) ] + - versions: + type: file + description: | + File containing software versions + Structure: [ path(versions.yml) ] + pattern: "versions.yml" +authors: + - "@AlexVCaron" +maintainers: + - "@AlexVCaron" diff --git a/subworkflows/nf-neuro/tractoflow/modules.config b/subworkflows/nf-neuro/tractoflow/modules.config new file mode 100644 index 0000000..311bf74 --- /dev/null +++ b/subworkflows/nf-neuro/tractoflow/modules.config @@ -0,0 +1,208 @@ + +process { + + /* SUBWORKFLOWS CONFIGURATION */ + + withName: "TRACTOFLOW:PREPROC_DWI:DENOISE_DWI" { + ext.extent = params.dwi_denoise_patch_size + } + + withName: "TRACTOFLOW:PREPROC_DWI:DENOISE_REVDWI" { + ext.extent = params.dwi_denoise_patch_size + } + + withName: "TRACTOFLOW:PREPROC_DWI:TOPUP_EDDY:UTILS_EXTRACTB0" { + ext.b0_extraction_strategy = "mean" + } + + withName: "TRACTOFLOW:PREPROC_DWI:TOPUP_EDDY:PREPROC_TOPUP" { + ext.prefix_topup = "topup_results" + ext.default_config_topup = params.dwi_topup_config_file + ext.encoding = "y" //FIXME : this is subject bound, pass through meta ? + ext.readout = 0.062 //FIXME : this is subject bound, pass through meta ? + ext.b0_thr_extract_b0 = params.b0_max_threshold + } + + withName: "TRACTOFLOW:PREPROC_DWI:TOPUP_EDDY:PREPROC_EDDY" { + ext.prefix_topup = "topup_results" + ext.slice_drop_flag = params.dwi_eddy_restore_slices + ext.bet_topup_before_eddy_f = 0.16 + ext.eddy_cmd = params.dwi_eddy_executable + ext.dilate_b0_mask_prelim_brain_extraction = 5 + ext.bet_prelim_f = 0.16 + ext.b0_thr_extract_b0 = params.b0_max_threshold + ext.encoding = "y" //FIXME : this is subject bound, pass through meta ? + ext.readout = 0.062 //FIXME : this is subject bound, pass through meta ? + } + + withName: "TRACTOFLOW:PREPROC_DWI:BETCROP_FSLBETCROP" { + ext.bet_f = 0.16 + ext.b0_thr = params.b0_max_threshold + ext.crop = true + ext.dilate = false + } + + withName: "TRACTOFLOW:PREPROC_DWI:N4_DWI" { + ext.bspline_knot_per_voxel = params.dwi_n4_density + ext.shrink_factor = params.dwi_n4_subsampling + } + + withName: "TRACTOFLOW:PREPROC_DWI:RESAMPLE_DWI" { + ext.voxel_size = params.dwi_resample_resolution_mm_iso + ext.interp = params.dwi_resample_interpolation + ext.first_suffix = "dwi" + } + + withName: "TRACTOFLOW:PREPROC_DWI:IMAGE_CROPVOLUME" { + ext.output_bbox = false + } + + withName: "TRACTOFLOW:PREPROC_DWI:RESAMPLE_MASK" { + ext.voxel_size = params.dwi_resample_resolution_mm_iso + ext.interp = "nn" + ext.first_suffix = "dwi_mask" + } + + withName: "TRACTOFLOW:PREPROC_T1:IMAGE_CROPVOLUME_T1" { + ext.output_bbox = true + ext.first_suffix = "t1" + } + + withName: "TRACTOFLOW:PREPROC_T1:IMAGE_RESAMPLE" { + ext.voxel_size = params.t1_resample_resolution_mm_iso + ext.interp = params.t1_resample_interpolation + ext.first_suffix = "t1" + } + + //withName: "TRACTOFLOW:T1_REGISTRATION:REGISTER_ANATTODWI" { + // Nothing to do ! + //} + + //withName: "TRACTOFLOW:ANATOMICAL_SEGMENTATION:SEGMENTATION_FASTSEG" { + // Nothing to do ! + //} + + //withName: "TRACTOFLOW:ANATOMICAL_SEGMENTATION:SEGMENTATION_FREESURFERSEG" { + // Nothing to do ! + //} + + /* MODULES CONFIGURATION */ + + withName: "TRACTOFLOW:TRANSFORM_WMPARC" { + ext.dimensionality = 3 + ext.image_type = 0 + ext.interpolation = "NearestNeighbor" + ext.output_dtype = "uchar" + ext.default_val = 0 + } + + withName: "TRACTOFLOW:TRANSFORM_APARC_ASEG" { + ext.dimensionality = 3 + ext.image_type = 0 + ext.interpolation = "MultiLabel" + ext.output_dtype = "short" + ext.default_val = 0 + } + + withName: "TRACTOFLOW:TRANSFORM_LESION_MASK" { + ext.dimensionality = 3 + ext.image_type = 0 + ext.interpolation = "NearestNeighbor" + ext.output_dtype = "uchar" + ext.default_val = 0 + } + + withName: "TRACTOFLOW:RECONST_FRF" { + ext.fa = params.frf_fa_max_threshold + ext.fa_min = params.frf_fa_min_threshold + ext.nvox_min = params.frf_min_n_voxels + ext.roi_radius = params.frf_roi_radius + ext.set_frf = params.frf_value_to_force ? true : false + ext.manual_frf = params.frf_value_to_force + } + + //withName: "RECONST_MEANFRF" { + // Nothing to do ! + //} + + withName: "TRACTOFLOW:RECONST_DTIMETRICS" { + ext.ad = true + ext.evecs = true + ext.evals = true + ext.fa = true + ext.ga = true + ext.rgb = true + ext.md = true + ext.mode = true + ext.norm = true + ext.rd = true + ext.tensor = true + ext.nonphysical = true + ext.pulsation = true + ext.residual = true + ext.max_dti_shell_value = params.dti_max_bvalue + ext.dti_shells = params.dti_shells_to_fit + } + + withName: "TRACTOFLOW:RECONST_FODF" { + ext.b0_thr_extract_b0 = params.b0_max_threshold + ext.bvalue_tolerance = params.bvalue_tolerance + ext.min_fodf_shell_value = params.fodf_min_bvalue + ext.fodf_shells = params.fodf_shells_to_fit + ext.sh_order = params.fodf_sh_order + ext.sh_basis = params.fodf_sh_basis + ext.fa_threshold = params.fodf_peaks_ventricle_max_fa + ext.md_threshold = params.fodf_peaks_ventricle_min_md + ext.relative_threshold = params.fodf_peaks_relative_threshold + ext.fodf_metrics_a_factor = params.fodf_peaks_absolute_factor + ext.absolute_peaks = true + ext.peaks = true + ext.peak_indices = true + ext.afd_max = true + ext.afd_total = true + ext.afd_sum = true + ext.nufo = true + } + + withName: "TRACTOFLOW:TRACKING_PFTTRACKING" { + ext.pft_seeding_mask_type = params.pft_seeding_type + ext.pft_fa_seeding_mask_threshold = params.pft_fa_min_threshold + ext.pft_seeding = params.pft_seeding_strategy + ext.pft_nbr_seeds = params.pft_number_of_seeds + ext.pft_algo = params.pft_algorithm + ext.pft_step = params.pft_step_mm + ext.pft_theta = params.pft_theta_max_deviation + ext.pft_sfthres = 0.1 + ext.pft_sfthres_init = 0.5 + ext.pft_min_len = params.pft_min_streamline_length + ext.pft_max_len = params.pft_max_streamline_length + ext.pft_particles = params.pft_number_of_particles + ext.pft_back = params.pft_backward_step_mm + ext.pft_front = params.pft_forward_step_mm + ext.pft_random_seed = params.pft_random_seed + ext.pft_compress_streamlines = params.pft_compression_step_mm > 0 + ext.pft_compress_value = params.pft_compression_step_mm + ext.basis = params.fodf_sh_basis + } + + withName: "TRACTOFLOW:TRACKING_LOCALTRACKING" { + ext.enable_gpu = params.lt_processor == "gpu" + ext.gpu_batch_size = params.lt_processor == "gpu" ? params.lt_gpu_batch_size : null + ext.local_tracking_mask_type = params.lt_tracking_type + ext.local_fa_tracking_mask_threshold = params.lt_fa_min_threshold_for_tracking + ext.local_seeding_mask_type = params.lt_seeding_type + ext.local_fa_seeding_mask_threshold = params.lt_fa_min_threshold_for_seeding + ext.local_seeding = params.lt_seeding_strategy + ext.local_nbr_seeds = params.lt_number_of_seeds + ext.local_algo = params.lt_algorithm + ext.local_step = params.lt_step_mm + ext.local_theta = params.lt_theta_max_deviation + ext.local_sfthres = 0.1 + ext.local_min_len = params.lt_min_streamline_length + ext.local_max_len = params.lt_max_streamline_length + ext.local_random_seed = params.lt_random_seed + ext.local_compress_streamlines = params.lt_compression_step_mm > 0 + ext.local_compress_value = params.lt_compression_step_mm + ext.basis = params.fodf_sh_basis + } +} diff --git a/subworkflows/nf-neuro/tractoflow/nextflow.config b/subworkflows/nf-neuro/tractoflow/nextflow.config new file mode 100644 index 0000000..2575856 --- /dev/null +++ b/subworkflows/nf-neuro/tractoflow/nextflow.config @@ -0,0 +1,112 @@ +params { + //**Global options**// + b0_max_threshold = 10 + bvalue_tolerance = 20 + + //**SH fitting**// + dwi_signal_sh_fit = true + dwi_signal_sh_fit_order = 6 + dwi_signal_sh_fit_basis = "descoteaux07" + dwi_signal_sh_fit_shell = null + + //**Denoise dwi (dwidenoise in Mrtrix3)**// + preproc_dwi_run_denoising = true + dwi_denoise_patch_size = 7 + + //**GIBBS CORRECTION (mrdegibbs in Mrtrix3)**// + preproc_dwi_run_degibbs = false + + //**Topup**// + topup_eddy_run_topup = true + dwi_topup_config_file = "b02b0.cnf" + + //**Eddy**// + topup_eddy_run_eddy = true + dwi_eddy_executable = "eddy_cpu" + dwi_eddy_restore_slices = true + + //**Denoise T1**// + preproc_t1_run_denoising = true + preproc_t1_run_N4 = true + preproc_t1_run_synthbet = false + preproc_t1_run_ants_bet = true + preproc_t1_run_crop = true + + //**Resample T1**// + preproc_t1_run_resampling = true + t1_resample_resolution_mm_iso = 1 + t1_resample_interpolation = "lin" + + //**Normalize DWI**// + preproc_dwi_run_N4 = true + dwi_n4_density = 1 + dwi_n4_subsampling = 2 + + //**Resample DWI**// + preproc_dwi_run_resampling = true + dwi_resample_resolution_mm_iso = 1 + dwi_resample_interpolation = "lin" + + //**Extract DTI shells using this value as maximum**// + dti_max_bvalue = 1200 + dti_shells_to_fit = null + + //**Extract fODF shells using this value as minimum**// + fodf_min_bvalue = 700 + fodf_shells_to_fit = null + + //**Compute fiber response function (frf)**// + frf_fa_max_threshold = 0.7 + frf_fa_min_threshold = 0.5 + frf_min_n_voxels = 300 + frf_roi_radius = 20 + frf_value_to_force = null + + //**Mean fiber response function (frf)**// + frf_average_from_data = false + + //**Compute fODF metrics**// + fodf_sh_order = 8 + fodf_sh_basis = "descoteaux07" + fodf_peaks_absolute_factor = 2.0 + fodf_peaks_relative_threshold = 0.1 + fodf_peaks_ventricle_max_fa = 0.1 + fodf_peaks_ventricle_min_md = 0.003 + + //**PFT tracking**// + run_pft = true + pft_random_seed = 0 + pft_algorithm = "prob" + pft_step_mm = 0.5 + pft_theta_max_deviation = 20 + pft_min_streamline_length = 20 + pft_max_streamline_length = 200 + pft_seeding_type = "wm" + pft_seeding_strategy = "npv" + pft_number_of_seeds = 10 + pft_fa_min_threshold = 0.1 + pft_number_of_particles = 15 + pft_backward_step_mm = 2 + pft_forward_step_mm = 1 + pft_compression_step_mm = 0.2 + + //**Local tracking (LT)**// + run_local_tracking = true + lt_processor = "cpu" + lt_gpu_batch_size = 10000 //FIXME : this should be computed + lt_random_seed = 0 + lt_algorithm = "prob" + lt_step_mm = 0.5 + lt_theta_max_deviation = 20 + lt_min_streamline_length = 20 + lt_max_streamline_length = 200 + lt_seeding_type = "wm" + lt_seeding_strategy = "npv" + lt_number_of_seeds = 10 + lt_fa_min_threshold_for_seeding = 0.1 + lt_tracking_type = "wm" + lt_fa_min_threshold_for_tracking = 0.1 + lt_compression_step_mm = 0.2 +} + +includeConfig "modules.config" diff --git a/subworkflows/nf-neuro/tractoflow/tests/main.nf.test b/subworkflows/nf-neuro/tractoflow/tests/main.nf.test new file mode 100644 index 0000000..ec7585f --- /dev/null +++ b/subworkflows/nf-neuro/tractoflow/tests/main.nf.test @@ -0,0 +1,159 @@ + +nextflow_workflow { + + name "Test Subworkflow TRACTOFLOW" + script "../main.nf" + config "../nextflow.config" + workflow "TRACTOFLOW" + + tag "subworkflows" + tag "subworkflows_nfcore" + tag "subworkflows/tractoflow" + + tag "load_test_data" + tag "subworkflows/anatomical_segmentation" + tag "subworkflows/preproc_dwi" + tag "subworkflows/preproc_t1" + tag "subworkflows/registration" + + tag "reconst/dtimetrics" + tag "reconst/frf" + tag "reconst/fodf" + tag "reconst/meanfrf" + tag "registration/antsapplytransforms" + tag "tracking/localtracking" + tag "tracking/pfttracking" + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "antsbet.zip", "raw_DWIss1000-dir32.zip", "raw_T1w.zip" ] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("tractoflow - no rev") { + when { + params { + pft_seeding_strategy = "nt" + pft_number_of_seeds = 4000 + pft_number_of_particles = 4 + lt_seeding_strategy = "nt" + lt_number_of_seeds = 4000 + } + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + dwi: it.simpleName == "raw_DWIss1000-dir32" + t1w: it.simpleName == "raw_T1w" + template: it.simpleName == "antsbet" + } + 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") + ]} + input[1] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w.nii.gz") + ]} + input[2] = Channel.empty() + input[3] = Channel.empty() + input[4] = Channel.empty() + input[5] = Channel.empty() + input[6] = Channel.empty() + input[7] = Channel.empty() + input[8] = ch_split_test_data.template.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/t1_template.nii.gz") + ]} + input[9] = ch_split_test_data.template.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/t1_brain_probability_map.nii.gz") + ]} + input[10] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success}, + { + assert snapshot( + // IN DIFFUSION SPACE + file(workflow.out.dwi.get(0).get(1)).name, // dwi + workflow.out.dwi.get(0).get(2), // bval + file(workflow.out.dwi.get(0).get(3)).name, // bvec + file(workflow.out.t1.get(0).get(1)).name, // t1 + file(workflow.out.wm_mask.get(0).get(1)).name, // wm_mask + file(workflow.out.gm_mask.get(0).get(1)).name, // gm_mask + file(workflow.out.csf_mask.get(0).get(1)).name, // csf_mask + file(workflow.out.wm_map.get(0).get(1)).name, // wm_map + file(workflow.out.gm_map.get(0).get(1)).name, // gm_map + file(workflow.out.csf_map.get(0).get(1)).name, // csf_map + // REGISTRATION + // anatomical_to_diffusion + file(workflow.out.anatomical_to_diffusion.get(0).get(1)).name, // warp (or affine) + workflow.out.anatomical_to_diffusion.get(0).get(2) + ? file(workflow.out.anatomical_to_diffusion.get(0).get(2)).name : "", // affine + // diffusion_to_anatomical + file(workflow.out.diffusion_to_anatomical.get(0).get(1)).name, // affine (or warp) + workflow.out.diffusion_to_anatomical.get(0).get(2) + ? file(workflow.out.diffusion_to_anatomical.get(0).get(2)).name : "", // warp + // IN ANATOMICAL SPACE + file(workflow.out.t1_native.get(0).get(1)).name, // t1_native + // DTI + file(workflow.out.dti_tensor.get(0).get(1)).name, // dti_tensor + file(workflow.out.dti_md.get(0).get(1)).name, // dti_md + file(workflow.out.dti_rd.get(0).get(1)).name, // dti_rd + file(workflow.out.dti_ad.get(0).get(1)).name, // dti_ad + file(workflow.out.dti_fa.get(0).get(1)).name, // dti_fa + file(workflow.out.dti_rgb.get(0).get(1)).name, // dti_rgb + file(workflow.out.dti_peaks.get(0).get(1)).name, // dti_peaks + file(workflow.out.dti_evecs.get(0).get(1)).name, // dti_evecs + file(workflow.out.dti_evals.get(0).get(1)).name, // dti_evals + file(workflow.out.dti_residual.get(0).get(1)).name, // dti_residual + file(workflow.out.dti_ga.get(0).get(1)).name, // dti_ga + file(workflow.out.dti_mode.get(0).get(1)).name, // dti_mode + file(workflow.out.dti_norm.get(0).get(1)).name, // dti_norm + // FODF + file(workflow.out.fiber_response.get(0).get(1)).name, // fiber_response + file(workflow.out.fodf.get(0).get(1)).name, // fodf + file(workflow.out.fodf_peaks.get(0).get(1)).name, // fodf_peaks + file(workflow.out.afd_max.get(0).get(1)).name, // afd_max + file(workflow.out.afd_total.get(0).get(1)).name, // afd_total + file(workflow.out.afd_sum.get(0).get(1)).name, // afd_sum + file(workflow.out.nufo.get(0).get(1)).name, // nufo + // TRACKING + file(workflow.out.pft_tractogram.get(0).get(1)).name, // pft_tractogram + file(workflow.out.pft_config.get(0).get(1)).name, // pft_config + file(workflow.out.pft_map_include.get(0).get(1)).name, // pft_map_include + file(workflow.out.pft_map_exclude.get(0).get(1)).name, // pft_map_exclude + file(workflow.out.pft_seeding_mask.get(0).get(1)).name, // pft_seeding_mask + file(workflow.out.local_tractogram.get(0).get(1)).name, // local_tractogram + file(workflow.out.local_config.get(0).get(1)).name, // local_config + file(workflow.out.local_seeding_mask.get(0).get(1)).name, // local_seeding_mask + file(workflow.out.local_tracking_mask.get(0).get(1)).name, // local_tracking_mask + // QC + file(workflow.out.nonphysical_voxels.get(0).get(1)).name, // nonphysical_voxels + file(workflow.out.pulsation_in_dwi.get(0).get(1)).name, // pulsation_in_dwi + file(workflow.out.pulsation_in_b0.get(0).get(1)).name, // pulsation_in_b0 + file(workflow.out.dti_residual_stats.get(0).get(1)).name, // dti_residual_stats + workflow.out.versions // versions + ).match() + } + ) + } + } +} diff --git a/subworkflows/nf-neuro/tractoflow/tests/main.nf.test.snap b/subworkflows/nf-neuro/tractoflow/tests/main.nf.test.snap new file mode 100644 index 0000000..bf8f298 --- /dev/null +++ b/subworkflows/nf-neuro/tractoflow/tests/main.nf.test.snap @@ -0,0 +1,70 @@ +{ + "tractoflow - no rev": { + "content": [ + "test_dwi_resampled.nii.gz", + "test__dwi_eddy_corrected.bval:md5,7995daabbd74fcf7c365a39a779f67e9", + "test__dwi_eddy_corrected.bvec", + "test__t1_warped.nii.gz", + "test__mask_wm.nii.gz", + "test__mask_gm.nii.gz", + "test__mask_csf.nii.gz", + "test__map_wm.nii.gz", + "test__map_gm.nii.gz", + "test__map_csf.nii.gz", + "test__output1Warp.nii.gz", + "test__output0GenericAffine.mat", + "test__output0GenericAffine.mat", + "test__output1InverseWarp.nii.gz", + "test_t1_cropped.nii.gz", + "test__tensor.nii.gz", + "test__md.nii.gz", + "test__rd.nii.gz", + "test__ad.nii.gz", + "test__fa.nii.gz", + "test__rgb.nii.gz", + "test__evecs_v1.nii.gz", + "test__evecs.nii.gz", + "test__evals.nii.gz", + "test__residual.nii.gz", + "test__ga.nii.gz", + "test__mode.nii.gz", + "test__norm.nii.gz", + "test__frf.txt", + "test__fodf.nii.gz", + "test__peaks.nii.gz", + "test__afd_max.nii.gz", + "test__afd_total.nii.gz", + "test__afd_sum.nii.gz", + "test__nufo.nii.gz", + "test__pft_tracking.trk", + "test__pft_tracking_config.json", + "test__map_include.nii.gz", + "test__map_exclude.nii.gz", + "test__pft_seeding_mask.nii.gz", + "test__local_tracking.trk", + "test__local_tracking_config.json", + "test__local_seeding_mask.nii.gz", + "test__local_tracking_mask.nii.gz", + "test__nonphysical.nii.gz", + "test__pulsation_std_dwi.nii.gz", + "test__pulsation_std_b0.nii.gz", + "test__residual_residuals_stats.png", + [ + "versions.yml:md5,29eb4791c0e509ddbfa054f7d864e119", + "versions.yml:md5,329641acd94a1eb6e65ef73b738b9165", + "versions.yml:md5,4a7c801d46bade11523003b6bd06f8ed", + "versions.yml:md5,5076076435a50493640c9175de39f57f", + "versions.yml:md5,647b63809e635c82552cf53178bf73be", + "versions.yml:md5,9ec2ceb761ab4faf9c19e46d7aa201c9", + "versions.yml:md5,aaab00d1fa7d070cc7ee67fd768416b8", + "versions.yml:md5,da10c87dbb2ad07aa8bbf89a0c172a60", + "versions.yml:md5,e092829f98d53bda1802d1cda0cb017f" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.10.4" + }, + "timestamp": "2025-03-07T16:57:23.114019628" + } +} diff --git a/subworkflows/nf-neuro/tractoflow/tests/tags.yml b/subworkflows/nf-neuro/tractoflow/tests/tags.yml new file mode 100644 index 0000000..240dbdb --- /dev/null +++ b/subworkflows/nf-neuro/tractoflow/tests/tags.yml @@ -0,0 +1,2 @@ +subworkflows/tractoflow: + - subworkflows/nf-neuro/tractoflow/** diff --git a/subworkflows/nf-scil/anatomical_segmentation/main.nf b/subworkflows/nf-scil/anatomical_segmentation/main.nf deleted file mode 100644 index 29e8c46..0000000 --- a/subworkflows/nf-scil/anatomical_segmentation/main.nf +++ /dev/null @@ -1,55 +0,0 @@ -// ** Importing modules from nf-scil ** // -include { SEGMENTATION_FASTSEG } from '../../../modules/nf-scil/segmentation/fastseg/main' -include { SEGMENTATION_FREESURFERSEG } from '../../../modules/nf-scil/segmentation/freesurferseg/main' - -workflow ANATOMICAL_SEGMENTATION { - - // ** Two input channels for the segmentation processes since they are using ** // - // ** different image files. Supplying an empty channel for the one that isn't ** // - // ** relevant will make the workflow run the appropriate module. ** // - take: - ch_image // channel: [ val(meta), [ image ] ] - ch_freesurferseg // channel: [ val(meta), [ aparc_aseg, wmparc ] ] - - main: - - ch_versions = Channel.empty() - - // ** FSL fast segmentation ** // - SEGMENTATION_FASTSEG ( ch_image ) - ch_versions = ch_versions.mix(SEGMENTATION_FASTSEG.out.versions.first()) - - // ** Setting outputs ** // - wm_mask = SEGMENTATION_FASTSEG.out.wm_mask - gm_mask = SEGMENTATION_FASTSEG.out.gm_mask - csf_mask = SEGMENTATION_FASTSEG.out.csf_mask - wm_map = SEGMENTATION_FASTSEG.out.wm_map - gm_map = SEGMENTATION_FASTSEG.out.gm_map - csf_map = SEGMENTATION_FASTSEG.out.csf_map - - // ** Freesurfer segmentation ** // - SEGMENTATION_FREESURFERSEG ( ch_freesurferseg ) - ch_versions = ch_versions.mix(SEGMENTATION_FREESURFERSEG.out.versions.first()) - - // ** Setting outputs ** // - fs_wm_mask = SEGMENTATION_FREESURFERSEG.out.wm_mask - fs_gm_mask = SEGMENTATION_FREESURFERSEG.out.gm_mask - fs_csf_mask = SEGMENTATION_FREESURFERSEG.out.csf_mask - wm_map = Channel.empty() - gm_map = Channel.empty() - csf_map = Channel.empty() - - emit: - wm_mask = wm_mask // channel: [ val(meta), [ wm_mask ] ] - gm_mask = gm_mask // channel: [ val(meta), [ gm_mask ] ] - csf_mask = csf_mask // channel: [ val(meta), [ csf_mask ] ] - wm_map = wm_map // channel: [ val(meta), [ wm_map ] ] - gm_map = gm_map // channel: [ val(meta), [ gm_map ] ] - csf_map = csf_map // channel: [ val(meta), [ csf_map ] ] - fs_wm_mask = fs_wm_mask // channel: [ val(meta), [ wm_mask ] ] - fs_gm_mask = fs_gm_mask // channel: [ val(meta), [ gm_mask ] ] - fs_csf_mask = fs_csf_mask // channel: [ val(meta), [ csf_mask ] ] - - versions = ch_versions // channel: [ versions.yml ] -} - diff --git a/subworkflows/nf-scil/anatomical_segmentation/tests/main.nf.test b/subworkflows/nf-scil/anatomical_segmentation/tests/main.nf.test deleted file mode 100644 index 5724af0..0000000 --- a/subworkflows/nf-scil/anatomical_segmentation/tests/main.nf.test +++ /dev/null @@ -1,59 +0,0 @@ -nextflow_workflow { - - name "Test Subworkflow ANATOMICAL_SEGMENTATION" - script "../main.nf" - workflow "ANATOMICAL_SEGMENTATION" - config "./nextflow.config" - - tag "subworkflows" - tag "subworkflows_nfcore" - tag "subworkflows/anatomical_segmentation" - - tag "segmentation" - tag "segmentation/fastseg" - tag "segmentation/freesurferseg" - - test("anatomical_segmentation - fslfast") { - - when { - workflow { - """ - input[0] = [ [ id:'test', single_end:false ], // meta map - file(params.test_data['segmentation']['fastseg']['image'], checkIfExists: true) - ] - input[1] = [] - """ - } - } - - then { - assertAll( - { assert workflow.success}, - { assert snapshot(workflow.out).match()} - ) - } - } - - test("anatomical_segmentation - freesurferseg") { - - when { - workflow { - """ - input[0] = [] - input[1] = [ - [ id:'test', single_end:false ], // meta map - file(params.test_data['segmentation']['freesurferseg']['aparc_aseg'], checkIfExists: true), - file(params.test_data['segmentation']['freesurferseg']['wmparc'], checkIfExists: true) - ] - """ - } - } - - then { - assertAll( - { assert workflow.success }, - { assert snapshot(workflow.out).match() } - ) - } - } -} diff --git a/subworkflows/nf-scil/anatomical_segmentation/tests/tags.yml b/subworkflows/nf-scil/anatomical_segmentation/tests/tags.yml deleted file mode 100644 index abaa3e6..0000000 --- a/subworkflows/nf-scil/anatomical_segmentation/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -subworkflows/anatomical_segmentation: - - subworkflows/nf-scil/anatomical_segmentation/** diff --git a/subworkflows/nf-scil/preproc_dwi/main.nf b/subworkflows/nf-scil/preproc_dwi/main.nf deleted file mode 100644 index 58bfb5d..0000000 --- a/subworkflows/nf-scil/preproc_dwi/main.nf +++ /dev/null @@ -1,123 +0,0 @@ -include { DENOISING_MPPCA as DENOISE_DWI } from '../../../modules/nf-scil/denoising/mppca/main' -include { DENOISING_MPPCA as DENOISE_REVDWI } from '../../../modules/nf-scil/denoising/mppca/main' -include { BETCROP_FSLBETCROP } from '../../../modules/nf-scil/betcrop/fslbetcrop/main' -include { BETCROP_CROPVOLUME } from '../../../modules/nf-scil/betcrop/cropvolume/main' -include { PREPROC_N4 as N4_DWI } from '../../../modules/nf-scil/preproc/n4/main' -include { PREPROC_NORMALIZE as NORMALIZE_DWI } from '../../../modules/nf-scil/preproc/normalize/main' -include { IMAGE_RESAMPLE as RESAMPLE_DWI } from '../../../modules/nf-scil/image/resample/main' -include { IMAGE_RESAMPLE as RESAMPLE_MASK } from '../../../modules/nf-scil/image/resample/main' -include { UTILS_EXTRACTB0 as EXTRACTB0_RESAMPLE } from '../../../modules/nf-scil/utils/extractb0/main' -include { UTILS_EXTRACTB0 as EXTRACTB0_TOPUP } from '../../../modules/nf-scil/utils/extractb0/main' -include { TOPUP_EDDY } from '../topup_eddy/main' - - -workflow PREPROC_DWI { - - take: - ch_dwi // channel: [ val(meta), [ dwi, bval, bvec ] ] - ch_rev_dwi // channel: [ val(meta), [ rev_dwi, bval, bvec ] ], optional - ch_b0 // Channel: [ val(meta), [ b0 ] ], optional - ch_rev_b0 // channel: [ val(meta), [ reverse b0 ] ], optional - ch_config_topup // channel: [ 'config_topup' ], optional - - main: - - ch_versions = Channel.empty() - - ch_denoise_dwi = ch_dwi - .multiMap { meta, dwi, bval, bvec -> - dwi: [ meta, dwi ] - bvs_files: [ meta, bval, bvec ] - } - - // ** Denoised DWI ** // - DENOISE_DWI ( ch_denoise_dwi.dwi ) - ch_versions = ch_versions.mix(DENOISE_DWI.out.versions.first()) - - ch_denoise_rev_dwi = ch_rev_dwi - .filter{ it[1] } - .multiMap { meta, dwi, bval, bvec -> - rev_dwi: [ [id: "${meta.id}_rev", cache: meta], dwi ] - rev_bvs_files: [ meta, bval, bvec ] - } - // ** Denoised reverse DWI ** // - DENOISE_REVDWI ( ch_denoise_rev_dwi.rev_dwi ) - ch_versions = ch_versions.mix(DENOISE_REVDWI.out.versions.first()) - - ch_topup_eddy_rev_dwi = DENOISE_REVDWI.out.image - .map{ meta, dwi -> [ meta.cache, dwi ] } - .join(ch_denoise_rev_dwi.rev_bvs_files) - - // ** Eddy Topup ** // - ch_topup_eddy_dwi = DENOISE_DWI.out.image.join(ch_denoise_dwi.bvs_files) - - ch_extract_b0 = ch_topup_eddy_dwi - .join(ch_b0, remainder: true) - .filter{ !it[4] } - .map{ it[0..3]} - - EXTRACTB0_TOPUP { ch_extract_b0 } - ch_versions = ch_versions.mix(EXTRACTB0_TOPUP.out.versions.first()) - - ch_b0 = EXTRACTB0_TOPUP.out.b0.mix(ch_b0) - - TOPUP_EDDY ( ch_topup_eddy_dwi, ch_b0, ch_topup_eddy_rev_dwi, ch_rev_b0, ch_config_topup ) - ch_versions = ch_versions.mix(TOPUP_EDDY.out.versions.first()) - - // ** Bet-crop DWI ** // - ch_betcrop_dwi = TOPUP_EDDY.out.dwi - .join(TOPUP_EDDY.out.bval) - .join(TOPUP_EDDY.out.bvec) - BETCROP_FSLBETCROP ( ch_betcrop_dwi ) - ch_versions = ch_versions.mix(BETCROP_FSLBETCROP.out.versions.first()) - - // ** Crop b0 ** // - ch_crop_b0 = TOPUP_EDDY.out.b0 - .join(BETCROP_FSLBETCROP.out.bbox) - BETCROP_CROPVOLUME ( ch_crop_b0 ) - ch_versions = ch_versions.mix(BETCROP_CROPVOLUME.out.versions.first()) - - // ** N4 DWI ** // - ch_N4 = BETCROP_FSLBETCROP.out.image - .join(BETCROP_CROPVOLUME.out.image) - .join(BETCROP_FSLBETCROP.out.mask) - N4_DWI ( ch_N4 ) - ch_versions = ch_versions.mix(N4_DWI.out.versions.first()) - - // ** Normalize DWI ** // - ch_normalize = N4_DWI.out.image - .join(BETCROP_FSLBETCROP.out.mask) - .join(TOPUP_EDDY.out.bval) - .join(TOPUP_EDDY.out.bvec) - NORMALIZE_DWI ( ch_normalize ) - ch_versions = ch_versions.mix(NORMALIZE_DWI.out.versions.first()) - - // ** Resample DWI ** // - ch_resample_dwi = NORMALIZE_DWI.out.dwi.map{ it + [[]] } - RESAMPLE_DWI ( ch_resample_dwi ) - ch_versions = ch_versions.mix(RESAMPLE_DWI.out.versions.first()) - - // ** Extract b0 ** // - ch_dwi_extract_b0 = RESAMPLE_DWI.out.image - .join(TOPUP_EDDY.out.bval) - .join(TOPUP_EDDY.out.bvec) - - EXTRACTB0_RESAMPLE { ch_dwi_extract_b0 } - ch_versions = ch_versions.mix(EXTRACTB0_RESAMPLE.out.versions.first()) - - // ** Resample mask ** // - ch_resample_mask = BETCROP_FSLBETCROP.out.mask.map{ it + [[]] } - RESAMPLE_MASK ( ch_resample_mask ) - ch_versions = ch_versions.mix(RESAMPLE_MASK.out.versions.first()) - - emit: - dwi_resample = RESAMPLE_DWI.out.image // channel: [ val(meta), [ dwi_resample ] ] - bval = TOPUP_EDDY.out.bval // channel: [ val(meta), [ bval_corrected ] ] - bvec = TOPUP_EDDY.out.bvec // channel: [ val(meta), [ bvec_corrected ] ] - b0 = EXTRACTB0_RESAMPLE.out.b0 // channel: [ val(meta), [ b0 ] ] - b0_mask = RESAMPLE_MASK.out.image // channel: [ val(meta), [ b0_mask ] ] - dwi_bounding_box = BETCROP_FSLBETCROP.out.bbox // channel: [ val(meta), [ dwi_bounding_box ] ] - dwi_topup_eddy = TOPUP_EDDY.out.dwi // channel: [ val(meta), [ dwi_topup_eddy ] ] - dwi_n4 = N4_DWI.out.image // channel: [ val(meta), [ dwi_n4 ] ] - versions = ch_versions // channel: [ versions.yml ] -} diff --git a/subworkflows/nf-scil/preproc_dwi/tests/main.nf.test.snap b/subworkflows/nf-scil/preproc_dwi/tests/main.nf.test.snap deleted file mode 100644 index c1b0fd4..0000000 --- a/subworkflows/nf-scil/preproc_dwi/tests/main.nf.test.snap +++ /dev/null @@ -1,114 +0,0 @@ -{ - "preproc_dwi_rev_dwi_nob0": { - "content": [ - [ - [ - { - "id": "test", - "single_end": false - }, - "test__bval_eddy:md5,4c61c53078316c31b4d5daf446a3d6ac" - ] - ], - [ - "versions.yml:md5,054ed5e3f19bcd691db58b11d47581f1", - "versions.yml:md5,1662ab8cb093ea6d16798a8fa82639e1", - "versions.yml:md5,4f7a89ba455d122508e2c3dd5553c8ca", - "versions.yml:md5,55e94706d9e485c8a8c64ebb565bb013", - "versions.yml:md5,5dce7a8c0b0d6b91ef7826f88c86370e", - "versions.yml:md5,962f76092c8795ce5e6d768304e2d230", - "versions.yml:md5,977c7cd0a14188f1dfeb0f69a819b79e", - "versions.yml:md5,b4c068383da080f871c90828b1de0082", - "versions.yml:md5,c7a4760b17ed27d9d4df828d3677acee", - "versions.yml:md5,ea1ce12c71e2655bfff08c00836416ec", - "versions.yml:md5,f8e51f2cdc7ca029d03c0c92b0f5cd73" - ], - "test_b0.nii.gz", - "test__resampled.nii.gz", - "test__dwi_eddy_corrected.bvec", - "test__image_boundingBox.pkl", - "test__image_n4.nii.gz", - "test__resampled.nii.gz", - "test__dwi_corrected.nii.gz" - ], - "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" - }, - "timestamp": "2024-04-29T22:22:16.327973" - }, - "preproc_dwi_rev_b0": { - "content": [ - [ - [ - { - "id": "test", - "single_end": false - }, - "test__bval_eddy:md5,8192e97b08e8032382397bb844b31892" - ] - ], - [ - "versions.yml:md5,054ed5e3f19bcd691db58b11d47581f1", - "versions.yml:md5,4f7a89ba455d122508e2c3dd5553c8ca", - "versions.yml:md5,55e94706d9e485c8a8c64ebb565bb013", - "versions.yml:md5,5dce7a8c0b0d6b91ef7826f88c86370e", - "versions.yml:md5,962f76092c8795ce5e6d768304e2d230", - "versions.yml:md5,977c7cd0a14188f1dfeb0f69a819b79e", - "versions.yml:md5,b4c068383da080f871c90828b1de0082", - "versions.yml:md5,c7a4760b17ed27d9d4df828d3677acee", - "versions.yml:md5,ea1ce12c71e2655bfff08c00836416ec", - "versions.yml:md5,f8e51f2cdc7ca029d03c0c92b0f5cd73" - ], - "test_b0.nii.gz", - "test__resampled.nii.gz", - "test__dwi_eddy_corrected.bvec", - "test__image_boundingBox.pkl", - "test__image_n4.nii.gz", - "test__resampled.nii.gz", - "test__dwi_corrected.nii.gz" - ], - "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" - }, - "timestamp": "2024-04-29T22:18:33.366866" - }, - "preproc_dwi_all_options": { - "content": [ - [ - [ - { - "id": "test", - "single_end": false - }, - "test__bval_eddy:md5,4c61c53078316c31b4d5daf446a3d6ac" - ] - ], - [ - "versions.yml:md5,1662ab8cb093ea6d16798a8fa82639e1", - "versions.yml:md5,4f7a89ba455d122508e2c3dd5553c8ca", - "versions.yml:md5,55e94706d9e485c8a8c64ebb565bb013", - "versions.yml:md5,5dce7a8c0b0d6b91ef7826f88c86370e", - "versions.yml:md5,962f76092c8795ce5e6d768304e2d230", - "versions.yml:md5,977c7cd0a14188f1dfeb0f69a819b79e", - "versions.yml:md5,b4c068383da080f871c90828b1de0082", - "versions.yml:md5,c7a4760b17ed27d9d4df828d3677acee", - "versions.yml:md5,ea1ce12c71e2655bfff08c00836416ec", - "versions.yml:md5,f8e51f2cdc7ca029d03c0c92b0f5cd73" - ], - "test_b0.nii.gz", - "test__resampled.nii.gz", - "test__dwi_eddy_corrected.bvec", - "test__image_boundingBox.pkl", - "test__image_n4.nii.gz", - "test__resampled.nii.gz", - "test__dwi_corrected.nii.gz" - ], - "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" - }, - "timestamp": "2024-04-29T22:26:23.082935" - } -} \ No newline at end of file diff --git a/subworkflows/nf-scil/preproc_dwi/tests/tags.yml b/subworkflows/nf-scil/preproc_dwi/tests/tags.yml deleted file mode 100644 index 9613a9d..0000000 --- a/subworkflows/nf-scil/preproc_dwi/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -subworkflows/preproc_dwi: - - subworkflows/nf-scil/preproc_dwi/** diff --git a/subworkflows/nf-scil/preproc_t1/main.nf b/subworkflows/nf-scil/preproc_t1/main.nf deleted file mode 100644 index f85cbc5..0000000 --- a/subworkflows/nf-scil/preproc_t1/main.nf +++ /dev/null @@ -1,80 +0,0 @@ -// ** Importing modules from nf-scil ** // -include { DENOISING_NLMEANS } from '../../../modules/nf-scil/denoising/nlmeans/main' -include { PREPROC_N4 } from '../../../modules/nf-scil/preproc/n4/main' -include { IMAGE_RESAMPLE } from '../../../modules/nf-scil/image/resample/main' -include { BETCROP_ANTSBET } from '../../../modules/nf-scil/betcrop/antsbet/main' -include { BETCROP_CROPVOLUME as BETCROP_CROPVOLUME_T1 } from '../../../modules/nf-scil/betcrop/cropvolume/main' -include { BETCROP_CROPVOLUME as BETCROP_CROPVOLUME_MASK } from '../../../modules/nf-scil/betcrop/cropvolume/main' - -workflow PREPROC_T1 { - - take: - ch_image // channel: [ val(meta), [ image ] ] - ch_template // channel: [ val(meta), [ template ] ] - ch_probability_map // channel: [ val(meta), [ probability_map ] ] - 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 - - main: - - ch_versions = Channel.empty() - - // ** Denoising ** // - 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()) - - // ** N4 correction ** // - ch_N4 = DENOISING_NLMEANS.out.image - .join(ch_ref_n4, remainder: true) - .map{ it[0..1] + [it[2] ?: []] } - .join(ch_mask_nlmeans, remainder: true) - .map{ it[0..2] + [it[3] ?: []] } - - PREPROC_N4 ( ch_N4 ) - ch_versions = ch_versions.mix(PREPROC_N4.out.versions.first()) - - // ** Resampling ** // - ch_resampling = PREPROC_N4.out.image - .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()) - - // ** Brain extraction ** // - ch_bet = IMAGE_RESAMPLE.out.image - .join(ch_template) - .join(ch_probability_map) - - BETCROP_ANTSBET ( ch_bet ) - ch_versions = ch_versions.mix(BETCROP_ANTSBET.out.versions.first()) - - // ** crop image ** // - ch_crop = BETCROP_ANTSBET.out.t1.map{it + [[]]} - BETCROP_CROPVOLUME_T1 ( ch_crop ) - ch_versions = ch_versions.mix(BETCROP_CROPVOLUME_T1.out.versions.first()) - - // ** crop mask ** // - ch_crop_mask = BETCROP_ANTSBET.out.mask - .join(BETCROP_CROPVOLUME_T1.out.bounding_box) - - BETCROP_CROPVOLUME_MASK ( ch_crop_mask ) - ch_versions = ch_versions.mix(BETCROP_CROPVOLUME_MASK.out.versions.first()) - - emit: - image_nlmeans = DENOISING_NLMEANS.out.image // channel: [ val(meta), [ image ] ] - image_N4 = PREPROC_N4.out.image // channel: [ val(meta), [ image ] ] - image_resample = IMAGE_RESAMPLE.out.image // channel: [ val(meta), [ image ] ] - image_bet = BETCROP_ANTSBET.out.t1 // channel: [ val(meta), [ t1 ] ] - mask_bet = BETCROP_ANTSBET.out.mask // channel: [ val(meta), [ mask ] ] - crop_box = BETCROP_CROPVOLUME_T1.out.bounding_box // channel: [ val(meta), [ bounding_box ] ] - mask_final = BETCROP_CROPVOLUME_MASK.out.image // channel: [ val(meta), [ mask ] ] - t1_final = BETCROP_CROPVOLUME_T1.out.image // channel: [ val(meta), [ image ] ] - versions = ch_versions // channel: [ versions.yml ] -} - diff --git a/subworkflows/nf-scil/preproc_t1/tests/main.nf.test b/subworkflows/nf-scil/preproc_t1/tests/main.nf.test deleted file mode 100644 index 3e74d2b..0000000 --- a/subworkflows/nf-scil/preproc_t1/tests/main.nf.test +++ /dev/null @@ -1,137 +0,0 @@ -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/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" , "others.zip" ] ) - input[1] = "test.load-test-data" - """ - } - } - } - - test("preproc_t1_classic") { - config "./nextflow.config" - when { - workflow { - """ - ch_split_test_data = LOAD_DATA.out.test_data_directory - .branch{ - antsbet: it.simpleName == "antsbet" - others: it.simpleName == "others" - } - input[0] = ch_split_test_data.antsbet.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - file("\${test_data_directory}/t1_unaligned.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.others.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - [] - ]} - input[4] = ch_split_test_data.others.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - [], - [] - ]} - input[5] = ch_split_test_data.others.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - [] - ]} - """ - } - } - - then { - assertAll( - { assert workflow.success}, - { assert snapshot(workflow.out).match()} - ) - } - } - - test("preproc_t1_option") { - config "./nextflow_2.config" - - when { - workflow { - """ - ch_split_test_data = LOAD_DATA.out.test_data_directory - .branch{ - antsbet: it.simpleName == "antsbet" - others: it.simpleName == "others" - } - input[0] = ch_split_test_data.antsbet.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - file("\${test_data_directory}/t1_unaligned.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.antsbet.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - file("\${test_data_directory}/t1_unaligned_brainmask.nii.gz") - ]} - input[4] = ch_split_test_data.others.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - [], - [] - ]} - input[5] = ch_split_test_data.others.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - file("\${test_data_directory}/fa.nii.gz") - ]} - """ - } - } - - then { - assertAll( - { assert workflow.success}, - { assert snapshot(workflow.out).match()} - ) - } - } -} diff --git a/subworkflows/nf-scil/preproc_t1/tests/main.nf.test.snap b/subworkflows/nf-scil/preproc_t1/tests/main.nf.test.snap deleted file mode 100644 index fc8050a..0000000 --- a/subworkflows/nf-scil/preproc_t1/tests/main.nf.test.snap +++ /dev/null @@ -1,344 +0,0 @@ -{ - "preproc_t1_option": { - "content": [ - { - "0": [ - [ - { - "id": "test", - "single_end": false - }, - "test_denoised.nii.gz:md5,ba910fdd1c9cc8f3d1c98b24dde522ef" - ] - ], - "1": [ - [ - { - "id": "test", - "single_end": false - }, - "test__image_n4.nii.gz:md5,f9b1e5305c8c176c8ef72168b8a33f2d" - ] - ], - "2": [ - [ - { - "id": "test", - "single_end": false - }, - "test__resampled.nii.gz:md5,ce7bf43f6baf3fb8928a34727d536a80" - ] - ], - "3": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_bet.nii.gz:md5,c303c87d5cc30323e628ba0a31a27f5c" - ] - ], - "4": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_bet_mask.nii.gz:md5,c74e5ac1afc877f2e4051d70b668427a" - ] - ], - "5": [ - [ - { - "id": "test", - "single_end": false - }, - "test_t1_cropped_bbox.pkl:md5,8cb3708ea72d010a57fc6e4ba93308d0" - ] - ], - "6": [ - [ - { - "id": "test", - "single_end": false - }, - "test_cropped.nii.gz:md5,3f711c4a82acf7d370a8f9849f319ae2" - ] - ], - "7": [ - [ - { - "id": "test", - "single_end": false - }, - "test_t1_cropped.nii.gz:md5,b334782a2670846937ce60d35dade2d4" - ] - ], - "8": [ - "versions.yml:md5,15044237a18a642d172b2bdb3abcad78", - "versions.yml:md5,37327d900785469fe8aba4e90c045be3", - "versions.yml:md5,6597a7973bba0ca9124596ffb1b85282", - "versions.yml:md5,792251134d89d9f82a5c75d6d323ae0f", - "versions.yml:md5,b2baa0cca8ea5661b95a16dd3e8b45ca", - "versions.yml:md5,c9425c1a9d1fc500e1d10f0997c0bae0" - ], - "crop_box": [ - [ - { - "id": "test", - "single_end": false - }, - "test_t1_cropped_bbox.pkl:md5,8cb3708ea72d010a57fc6e4ba93308d0" - ] - ], - "image_N4": [ - [ - { - "id": "test", - "single_end": false - }, - "test__image_n4.nii.gz:md5,f9b1e5305c8c176c8ef72168b8a33f2d" - ] - ], - "image_bet": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_bet.nii.gz:md5,c303c87d5cc30323e628ba0a31a27f5c" - ] - ], - "image_nlmeans": [ - [ - { - "id": "test", - "single_end": false - }, - "test_denoised.nii.gz:md5,ba910fdd1c9cc8f3d1c98b24dde522ef" - ] - ], - "image_resample": [ - [ - { - "id": "test", - "single_end": false - }, - "test__resampled.nii.gz:md5,ce7bf43f6baf3fb8928a34727d536a80" - ] - ], - "mask_bet": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_bet_mask.nii.gz:md5,c74e5ac1afc877f2e4051d70b668427a" - ] - ], - "mask_final": [ - [ - { - "id": "test", - "single_end": false - }, - "test_cropped.nii.gz:md5,3f711c4a82acf7d370a8f9849f319ae2" - ] - ], - "t1_final": [ - [ - { - "id": "test", - "single_end": false - }, - "test_t1_cropped.nii.gz:md5,b334782a2670846937ce60d35dade2d4" - ] - ], - "versions": [ - "versions.yml:md5,15044237a18a642d172b2bdb3abcad78", - "versions.yml:md5,37327d900785469fe8aba4e90c045be3", - "versions.yml:md5,6597a7973bba0ca9124596ffb1b85282", - "versions.yml:md5,792251134d89d9f82a5c75d6d323ae0f", - "versions.yml:md5,b2baa0cca8ea5661b95a16dd3e8b45ca", - "versions.yml:md5,c9425c1a9d1fc500e1d10f0997c0bae0" - ] - } - ], - "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" - }, - "timestamp": "2024-04-22T20:33:32.493594" - }, - "preproc_t1_classic": { - "content": [ - { - "0": [ - [ - { - "id": "test", - "single_end": false - }, - "test_denoised.nii.gz:md5,b6d25b37b1101fea5c77bbbc711757c3" - ] - ], - "1": [ - [ - { - "id": "test", - "single_end": false - }, - "test__image_n4.nii.gz:md5,ef0d9af46aa1e330c5d7679ead500596" - ] - ], - "2": [ - [ - { - "id": "test", - "single_end": false - }, - "test__resampled.nii.gz:md5,88bae6bfeea1734111621b437e09548d" - ] - ], - "3": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_bet.nii.gz:md5,016f12c75e3543c27987a91eb2160a19" - ] - ], - "4": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_bet_mask.nii.gz:md5,cf0cc416ca8b352d3cba58d9132ab0da" - ] - ], - "5": [ - [ - { - "id": "test", - "single_end": false - }, - "test_t1_cropped_bbox.pkl:md5,64485c09488a1dca9ae86826eaa4afb9" - ] - ], - "6": [ - [ - { - "id": "test", - "single_end": false - }, - "test_cropped.nii.gz:md5,aee59cb0bf2a4e14bfe116dc68be365b" - ] - ], - "7": [ - [ - { - "id": "test", - "single_end": false - }, - "test_t1_cropped.nii.gz:md5,f4bea1286a90760eecf348dd9bffd2e6" - ] - ], - "8": [ - "versions.yml:md5,15044237a18a642d172b2bdb3abcad78", - "versions.yml:md5,37327d900785469fe8aba4e90c045be3", - "versions.yml:md5,6597a7973bba0ca9124596ffb1b85282", - "versions.yml:md5,792251134d89d9f82a5c75d6d323ae0f", - "versions.yml:md5,b2baa0cca8ea5661b95a16dd3e8b45ca", - "versions.yml:md5,c9425c1a9d1fc500e1d10f0997c0bae0" - ], - "crop_box": [ - [ - { - "id": "test", - "single_end": false - }, - "test_t1_cropped_bbox.pkl:md5,64485c09488a1dca9ae86826eaa4afb9" - ] - ], - "image_N4": [ - [ - { - "id": "test", - "single_end": false - }, - "test__image_n4.nii.gz:md5,ef0d9af46aa1e330c5d7679ead500596" - ] - ], - "image_bet": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_bet.nii.gz:md5,016f12c75e3543c27987a91eb2160a19" - ] - ], - "image_nlmeans": [ - [ - { - "id": "test", - "single_end": false - }, - "test_denoised.nii.gz:md5,b6d25b37b1101fea5c77bbbc711757c3" - ] - ], - "image_resample": [ - [ - { - "id": "test", - "single_end": false - }, - "test__resampled.nii.gz:md5,88bae6bfeea1734111621b437e09548d" - ] - ], - "mask_bet": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_bet_mask.nii.gz:md5,cf0cc416ca8b352d3cba58d9132ab0da" - ] - ], - "mask_final": [ - [ - { - "id": "test", - "single_end": false - }, - "test_cropped.nii.gz:md5,aee59cb0bf2a4e14bfe116dc68be365b" - ] - ], - "t1_final": [ - [ - { - "id": "test", - "single_end": false - }, - "test_t1_cropped.nii.gz:md5,f4bea1286a90760eecf348dd9bffd2e6" - ] - ], - "versions": [ - "versions.yml:md5,15044237a18a642d172b2bdb3abcad78", - "versions.yml:md5,37327d900785469fe8aba4e90c045be3", - "versions.yml:md5,6597a7973bba0ca9124596ffb1b85282", - "versions.yml:md5,792251134d89d9f82a5c75d6d323ae0f", - "versions.yml:md5,b2baa0cca8ea5661b95a16dd3e8b45ca", - "versions.yml:md5,c9425c1a9d1fc500e1d10f0997c0bae0" - ] - } - ], - "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" - }, - "timestamp": "2024-04-22T20:08:50.082833" - } -} \ No newline at end of file diff --git a/subworkflows/nf-scil/preproc_t1/tests/nextflow_2.config b/subworkflows/nf-scil/preproc_t1/tests/nextflow_2.config deleted file mode 100644 index f4a3069..0000000 --- a/subworkflows/nf-scil/preproc_t1/tests/nextflow_2.config +++ /dev/null @@ -1,10 +0,0 @@ -process { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - withName: "IMAGE_RESAMPLE" { - ext.interp = "lin" - } - withName: "BETCROP_CROPVOLUME_T1" { - ext.output_bbox = true - ext.first_suffix = "t1" - } -} diff --git a/subworkflows/nf-scil/preproc_t1/tests/tags.yml b/subworkflows/nf-scil/preproc_t1/tests/tags.yml deleted file mode 100644 index 58362d3..0000000 --- a/subworkflows/nf-scil/preproc_t1/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -subworkflows/preproc_t1: - - subworkflows/nf-scil/preproc_t1/** diff --git a/subworkflows/nf-scil/registration/main.nf b/subworkflows/nf-scil/registration/main.nf deleted file mode 100644 index e1bcf15..0000000 --- a/subworkflows/nf-scil/registration/main.nf +++ /dev/null @@ -1,65 +0,0 @@ -include { REGISTER_ANATTODWI } from '../../../modules/nf-scil/register/anattodwi/main' -include { REGISTRATION_ANTS } from '../../../modules/nf-scil/registration/ants/main' - -workflow REGISTRATION { - - // ** The subworkflow requires at least ch_image and ch_ref as inputs to ** // - // ** properly perform the registration. Supplying a ch_metric will select ** // - // ** the REGISTER_ANATTODWI module meanwhile NOT supplying a ch_metric ** // - // ** will select the REGISTRATION_ANTS (SyN or SyNQuick) module. ** // - - take: - ch_image // channel: [ val(meta), [ image ] ] - ch_ref // channel: [ val(meta), [ ref ] ] - ch_metric // channel: [ val(meta), [ metric ] ], optional - ch_mask // channel: [ val(meta), [ mask ] ], optional - - main: - - ch_versions = Channel.empty() - - if ( ch_metric ) { - // ** Set up input channel ** // - ch_register = ch_image.combine(ch_ref, by: 0) - .combine(ch_metric, by: 0) - - // ** Registration using AntsRegistration ** // - REGISTER_ANATTODWI ( ch_register ) - ch_versions = ch_versions.mix(REGISTER_ANATTODWI.out.versions.first()) - - // ** Setting outputs ** // - image_warped = REGISTER_ANATTODWI.out.t1_warped - transfo_image = REGISTER_ANATTODWI.out.transfo_image - transfo_trk = REGISTER_ANATTODWI.out.transfo_trk - } - else { - // ** Set up input channel, input are inverted compared to REGISTER_ANATTODWI. ** // - if ( ch_mask ) { - ch_register = ch_ref.combine(ch_image, by: 0) - .combine(ch_mask, by: 0) - } - else { - ch_register = ch_ref.combine(ch_image, by: 0) - .map{ it + [[]] } - } - - // ** Registration using antsRegistrationSyN.sh or antsRegistrationSyNQuick.sh. ** // - // ** Has to be defined in the config file or else the default (SyN) will be ** // - // ** used. ** // - REGISTRATION_ANTS ( ch_register ) - ch_versions = ch_versions.mix(REGISTRATION_ANTS.out.versions.first()) - - // ** Setting outputs ** // - image_warped = REGISTRATION_ANTS.out.image - transfo_image = REGISTRATION_ANTS.out.transfo_image - transfo_trk = REGISTRATION_ANTS.out.transfo_trk - } - - emit: - image_warped = image_warped // channel: [ val(meta), [ image ] ] - transfo_image = transfo_image // channel: [ val(meta), [ warp, affine ] ] - transfo_trk = transfo_trk // channel: [ val(meta), [ inverseAffine, inverseWarp ] ] - - versions = ch_versions // channel: [ versions.yml ] -} - diff --git a/subworkflows/nf-scil/registration/tests/main.nf.test b/subworkflows/nf-scil/registration/tests/main.nf.test deleted file mode 100644 index 3c8c421..0000000 --- a/subworkflows/nf-scil/registration/tests/main.nf.test +++ /dev/null @@ -1,92 +0,0 @@ -nextflow_workflow { - - name "Test Subworkflow REGISTRATION" - script "../main.nf" - workflow "REGISTRATION" - config "./nextflow.config" - - tag "subworkflows" - tag "subworkflows_nfcore" - tag "subworkflows/registration" - - tag "register" - tag "register/anattodwi" - tag "registration" - tag "registration/ants" - - tag "load_test_data" - - setup { - run("LOAD_TEST_DATA", alias: "LOAD_DATA") { - script "../../load_test_data/main.nf" - process { - """ - input[0] = Channel.from( [ "processing.zip" ] ) - input[1] = "test.load-test-data" - """ - } - } - } - - test("registration - antsRegistration") { - - when { - workflow { - """ - input[0] = LOAD_DATA.out.test_data_directory.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - file("\${test_data_directory}/mni_masked_2x2x2.nii.gz") - ]} - input[1] = LOAD_DATA.out.test_data_directory.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - file("\${test_data_directory}/b0_mean.nii.gz") - ]} - input[2] = LOAD_DATA.out.test_data_directory.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - file("\${test_data_directory}/fa.nii.gz") - ]} - input[3] = [] - """ - } - } - - then { - assertAll( - { assert workflow.success}, - { assert snapshot(workflow.out).match()} - ) - } - } - - test("registration - SyNQuick") { - - when { - workflow { - """ - input[0] = LOAD_DATA.out.test_data_directory.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - file("\${test_data_directory}/mni_masked_2x2x2.nii.gz") - ]} - input[1] = LOAD_DATA.out.test_data_directory.map{ - test_data_directory -> [ - [ id:'test', single_end:false ], - file("\${test_data_directory}/b0_mean.nii.gz") - ]} - input[2] = [] - input[3] = [] - """ - } - } - - then { - assertAll( - { assert workflow.success}, - { assert snapshot(workflow.out).match()} - ) - } - } -} diff --git a/subworkflows/nf-scil/registration/tests/main.nf.test.snap b/subworkflows/nf-scil/registration/tests/main.nf.test.snap deleted file mode 100644 index c45e46e..0000000 --- a/subworkflows/nf-scil/registration/tests/main.nf.test.snap +++ /dev/null @@ -1,160 +0,0 @@ -{ - "registration - antsRegistration": { - "content": [ - { - "0": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_warped.nii.gz:md5,ff651c76f4ea493de0a06a202e9f9fb2" - ] - ], - "1": [ - [ - { - "id": "test", - "single_end": false - }, - "test__output1Warp.nii.gz:md5,f896e4fef21f26c84bf834afd7fb5d55", - "test__output0GenericAffine.mat:md5,043decee0d131e79a802e3dcc61c22f0" - ] - ], - "2": [ - [ - { - "id": "test", - "single_end": false - }, - "test__output0GenericAffine.mat:md5,043decee0d131e79a802e3dcc61c22f0", - "test__output1InverseWarp.nii.gz:md5,62d6287e60dd36ffd477e71061f23077" - ] - ], - "3": [ - "versions.yml:md5,2207e66bf22435748071842a500885f7" - ], - "image_warped": [ - [ - { - "id": "test", - "single_end": false - }, - "test__t1_warped.nii.gz:md5,ff651c76f4ea493de0a06a202e9f9fb2" - ] - ], - "transfo_image": [ - [ - { - "id": "test", - "single_end": false - }, - "test__output1Warp.nii.gz:md5,f896e4fef21f26c84bf834afd7fb5d55", - "test__output0GenericAffine.mat:md5,043decee0d131e79a802e3dcc61c22f0" - ] - ], - "transfo_trk": [ - [ - { - "id": "test", - "single_end": false - }, - "test__output0GenericAffine.mat:md5,043decee0d131e79a802e3dcc61c22f0", - "test__output1InverseWarp.nii.gz:md5,62d6287e60dd36ffd477e71061f23077" - ] - ], - "versions": [ - "versions.yml:md5,2207e66bf22435748071842a500885f7" - ] - } - ], - "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" - }, - "timestamp": "2024-04-12T10:46:36.252304" - }, - "registration - SyNQuick": { - "content": [ - { - "0": [ - [ - { - "id": "test", - "single_end": false - }, - "test__warped.nii.gz:md5,88442ab57066e1fb1e03f90d73cdfc42" - ] - ], - "1": [ - [ - { - "id": "test", - "single_end": false - }, - [ - "test__output0Warp.nii.gz:md5,61436a721b29633474255640b5e44f7f", - "test__output1GenericAffine.mat:md5,3946e4d33c15911a9a1d72374fdbe4df" - ] - ] - ], - "2": [ - [ - { - "id": "test", - "single_end": false - }, - [ - "test__output0InverseAffine.mat:md5,b30458b37b728d8ce97890b391ad9df9", - "test__output1InverseWarp.nii.gz:md5,278d1774561435ff0a3ec239872b74e5" - ] - ] - ], - "3": [ - "versions.yml:md5,96376b8bc28640eef565e7f1e258dbb2" - ], - "image_warped": [ - [ - { - "id": "test", - "single_end": false - }, - "test__warped.nii.gz:md5,88442ab57066e1fb1e03f90d73cdfc42" - ] - ], - "transfo_image": [ - [ - { - "id": "test", - "single_end": false - }, - [ - "test__output0Warp.nii.gz:md5,61436a721b29633474255640b5e44f7f", - "test__output1GenericAffine.mat:md5,3946e4d33c15911a9a1d72374fdbe4df" - ] - ] - ], - "transfo_trk": [ - [ - { - "id": "test", - "single_end": false - }, - [ - "test__output0InverseAffine.mat:md5,b30458b37b728d8ce97890b391ad9df9", - "test__output1InverseWarp.nii.gz:md5,278d1774561435ff0a3ec239872b74e5" - ] - ] - ], - "versions": [ - "versions.yml:md5,96376b8bc28640eef565e7f1e258dbb2" - ] - } - ], - "meta": { - "nf-test": "0.8.4", - "nextflow": "23.10.1" - }, - "timestamp": "2024-04-12T10:50:27.051312" - } -} \ No newline at end of file diff --git a/subworkflows/nf-scil/registration/tests/tags.yml b/subworkflows/nf-scil/registration/tests/tags.yml deleted file mode 100644 index 3e78c17..0000000 --- a/subworkflows/nf-scil/registration/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -subworkflows/registration: - - subworkflows/nf-scil/registration/** diff --git a/subworkflows/nf-scil/topup_eddy/main.nf b/subworkflows/nf-scil/topup_eddy/main.nf deleted file mode 100644 index e5935f1..0000000 --- a/subworkflows/nf-scil/topup_eddy/main.nf +++ /dev/null @@ -1,70 +0,0 @@ -// ** Importing modules from nf-scil ** // -include { PREPROC_TOPUP } from '../../../modules/nf-scil/preproc/topup/main' -include { PREPROC_EDDY } from '../../../modules/nf-scil/preproc/eddy/main' -include { UTILS_EXTRACTB0 } from '../../../modules/nf-scil/utils/extractb0/main' - -workflow TOPUP_EDDY { - - // ** The subworkflow will optionally run topup if a reverse b0 or reverse DWI is provided. ** // - // ** In both cases, it will perform EDDY and also extract a b0 from the corrected DWI image. ** // - - take: - ch_dwi // channel: [ val(meta), [ dwi, bval, bvec ] - ch_b0 // channel: [ val(meta), b0 ] - ch_rev_dwi // channel: [ val(meta), [ rev_dwi, rev_bval, rev_bvec ] - ch_rev_b0 // channel: [ val(meta), rev_b0 ] - ch_config_topup // channel - - main: - ch_versions = Channel.empty() - - // ** Create channel for TOPUP ** // - ch_topup = ch_dwi - .join(ch_b0, remainder: true) - .map{ it[0..3] + [it[4] ?: []] } - - ch_topup_reverse = ch_rev_dwi - .join(ch_rev_b0, remainder: true) - .filter{ it ? it[1] : false } - .map{ it[0..3] + [it[4] ?: []] } - - ch_topup = ch_topup - .join(ch_topup_reverse, remainder: true) - .branch{ - with_topup: it[5] || it[8] - } - - // ** RUN TOPUP ** // - PREPROC_TOPUP ( ch_topup.with_topup, ch_config_topup ) - ch_versions = ch_versions.mix(PREPROC_TOPUP.out.versions.first()) - - // ** Create channel for EDDY ** // - ch_eddy_input = ch_dwi - .join(ch_rev_dwi, remainder: true) - .map{ it[0..3] + [it[4] ? it[4..-1] : [], [], []] } - .join(PREPROC_TOPUP.out.topup_corrected_b0s, remainder: true) - .map{ it[0..6] + [it[7] ?: []] } - .join(PREPROC_TOPUP.out.topup_fieldcoef, remainder: true) - .map{ it[0..7] + [it[8] ?: []] } - .join(PREPROC_TOPUP.out.topup_movpart, remainder: true) - .map{ it[0..8] + [it[9] ?: []] } - - PREPROC_EDDY ( ch_eddy_input ) - ch_versions = ch_versions.mix(PREPROC_EDDY.out.versions.first()) - - ch_dwi_extract_b0 = PREPROC_EDDY.out.dwi_corrected - .join(PREPROC_EDDY.out.bval_corrected) - .join(PREPROC_EDDY.out.bvec_corrected) - - UTILS_EXTRACTB0 { ch_dwi_extract_b0 } - ch_versions = ch_versions.mix(UTILS_EXTRACTB0.out.versions.first()) - - - emit: - dwi = PREPROC_EDDY.out.dwi_corrected // channel: [ val(meta), [ dwi_corrected ] ] - bval = PREPROC_EDDY.out.bval_corrected // channel: [ val(meta), [ bval_corrected ] ] - bvec = PREPROC_EDDY.out.bvec_corrected // channel: [ val(meta), [ bvec_corrected ] ] - b0 = UTILS_EXTRACTB0.out.b0 // channel: [ val(meta), [ b0 ] ] - b0_mask = PREPROC_EDDY.out.b0_mask // channel: [ val(meta), [ b0_mask ] ] - versions = ch_versions // channel: [ versions.yml ] -} diff --git a/subworkflows/nf-scil/topup_eddy/tests/tags.yml b/subworkflows/nf-scil/topup_eddy/tests/tags.yml deleted file mode 100644 index 7ab9869..0000000 --- a/subworkflows/nf-scil/topup_eddy/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -subworkflows/topup_eddy: - - subworkflows/nf-scil/topup_eddy/** diff --git a/workflows/nf-tractoflow.nf b/workflows/nf-tractoflow.nf index bed0da2..1dff476 100644 --- a/workflows/nf-tractoflow.nf +++ b/workflows/nf-tractoflow.nf @@ -8,26 +8,8 @@ include { paramsSummaryMap } from 'plugin/nf-schema' include { paramsSummaryMultiqc } from '../subworkflows/nf-core/utils_nfcore_pipeline' include { softwareVersionsToYAML } from '../subworkflows/nf-core/utils_nfcore_pipeline' include { methodsDescriptionText } from '../subworkflows/local/utils_nfcore_nf-tractoflow_pipeline' - -// PREPROCESSING -include { PREPROC_DWI } from '../subworkflows/nf-scil/preproc_dwi/main' -include { PREPROC_T1 } from '../subworkflows/nf-scil/preproc_t1/main' -include { RECONST_DTIMETRICS as REGISTRATION_FA } from '../modules/nf-scil/reconst/dtimetrics/main' -include { REGISTRATION as T1_REGISTRATION } from '../subworkflows/nf-scil/registration/main' -include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_WMPARC } from '../modules/nf-scil/registration/antsapplytransforms/main' -include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_APARC_ASEG } from '../modules/nf-scil/registration/antsapplytransforms/main' -include { ANATOMICAL_SEGMENTATION } from '../subworkflows/nf-scil/anatomical_segmentation/main' - -// RECONSTRUCTION -include { RECONST_FRF } from '../modules/nf-scil/reconst/frf/main' -include { RECONST_MEANFRF } from '../modules/nf-scil/reconst/meanfrf/main' -include { RECONST_DTIMETRICS } from '../modules/nf-scil/reconst/dtimetrics/main' -include { RECONST_FODF } from '../modules/nf-scil/reconst/fodf/main' - -// TRACKING -include { TRACKING_PFTTRACKING } from '../modules/nf-scil/tracking/pfttracking/main' -include { TRACKING_LOCALTRACKING } from '../modules/nf-scil/tracking/localtracking/main' - +include { TRACTOFLOW as RUN } from '../subworkflows/nf-neuro/tractoflow' +include { RECONST_SHSIGNAL } from '../modules/nf-neuro/reconst/shsignal' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -36,178 +18,73 @@ include { TRACKING_LOCALTRACKING } from '../modules/nf-scil/tracking/localtracki */ workflow NF_TRACTOFLOW { - take: - ch_samplesheet // channel: samplesheet read in from --input + ch_t1 + ch_wmparc + ch_aparc_aseg + ch_dwi_bval_bvec + ch_b0 + ch_rev_dwi_bval_bvec + ch_rev_b0 + ch_lesion main: ch_versions = Channel.empty() ch_multiqc_files = Channel.empty() ch_topup_config = Channel.empty() ch_bet_template = Channel.empty() + ch_bet_probability = Channel.empty() /* Load topup config if provided */ - if ( params.dwi_susceptibility_filter_config_file ) { - if ( file(params.dwi_susceptibility_filter_config_file).exists()) { - ch_topup_config = Channel.fromPath(params.topup_config, checkIfExists: true) + if ( params.config_topup ) { + if ( file(params.config_topup).exists()) { + ch_topup_config = Channel.fromPath(params.config_topup, checkIfExists: true) } else { - ch_topup_config = Channel.from( params.dwi_susceptibility_filter_config_file ) + ch_topup_config = Channel.from( params.config_topup ) } } /* Load bet template */ - ch_bet_template = Channel.fromPath(params.t1_bet_template_t1, checkIfExists: true) - ch_bet_probability = Channel.fromPath(params.t1_bet_template_probability_map, checkIfExists: true) - - /* Unpack inputs */ - ch_inputs = ch_samplesheet - .multiMap{ meta, dwi, bval, bvec, sbref, rev_dwi, rev_bval, rev_bvec, rev_sbref, t1, wmparc, aparc_aseg -> - dwi: [meta, dwi, bval, bvec] - sbref: [meta, sbref] - rev_dwi: [meta, rev_dwi, rev_bval, rev_bvec] - rev_sbref: [meta, rev_sbref] - t1: [meta, t1] - wmparc: [meta, wmparc] - aparc_aseg: [meta, aparc_aseg] + if (params.template_t1) { + template_directory = file(params.template_t1) + if (template_directory.exists() && template_directory.isDirectory()){ + ch_bet_template = ch_t1.map{ it[0] } + .combine(Channel.fromPath(template_directory / "t1_template.nii.gz")) + ch_bet_probability = ch_t1.map{ it[0] } + .combine(Channel.fromPath(template_directory / "t1_brain_probability_map.nii.gz")) } + } - /* PREPROCESSING */ - - // - // SUBWORKFLOW: Run PREPROC_DWI - // - PREPROC_DWI( - ch_inputs.dwi, ch_inputs.rev_dwi, - ch_inputs.sbref, ch_inputs.rev_sbref, - ch_topup_config - ) - - // - // SUBWORKFLOW: Run PREPROC_T1 - // - ch_t1_meta = ch_inputs.t1.map{ it[0] } - PREPROC_T1( - ch_inputs.t1, - ch_t1_meta.combine(ch_bet_template), - ch_t1_meta.combine(ch_bet_probability), - Channel.empty(), - Channel.empty(), - Channel.empty() - ) - - // - // MODULE: Run RECONST/DTIMETRICS (REGISTRATION_FA) - // - ch_registration_fa = PREPROC_DWI.out.dwi_resample - .join(PREPROC_DWI.out.bval) - .join(PREPROC_DWI.out.bvec) - .join(PREPROC_DWI.out.b0_mask) - - REGISTRATION_FA( ch_registration_fa ) - - // - // SUBWORKFLOW: Run REGISTRATION - // - T1_REGISTRATION( - PREPROC_T1.out.t1_final, - PREPROC_DWI.out.b0, - REGISTRATION_FA.out.fa, - [] - ) - - /* SEGMENTATION */ - - // - // MODULE: Run REGISTRATION_ANTSAPPLYTRANSFORMS (TRANSFORM_WMPARC) - // - TRANSFORM_WMPARC( - ch_inputs.wmparc - .filter{ it[1] } - .join(PREPROC_DWI.out.b0) - .join(T1_REGISTRATION.out.transfo_image) - .map{ it[0..2] + [it[3..-1]] } - ) - - // - // MODULE: Run REGISTRATION_ANTSAPPLYTRANSFORMS (TRANSFORM_APARC_ASEG) - // - TRANSFORM_APARC_ASEG( - ch_inputs.aparc_aseg + RUN( + ch_dwi_bval_bvec, + ch_t1, + ch_b0 + .filter{ it[1] }, + ch_rev_dwi_bval_bvec + .filter{ it[1] }, + ch_rev_b0 + .filter{ it[1] }, + ch_wmparc + .filter{ it[1] }, + ch_aparc_aseg + .filter{ it[1] }, + ch_topup_config, + ch_bet_template, + ch_bet_probability, + ch_lesion .filter{ it[1] } - .join(PREPROC_DWI.out.b0) - .join(T1_REGISTRATION.out.transfo_image) - .map{ it[0..2] + [it[3..-1]] } - ) - - // - // SUBWORKFLOW: Run ANATOMICAL_SEGMENTATION - // - ANATOMICAL_SEGMENTATION( - T1_REGISTRATION.out.image_warped, - TRANSFORM_WMPARC.out.warpedimage - .join(TRANSFORM_APARC_ASEG.out.warpedimage) ) - - /* RECONSTRUCTION */ - - // - // MODULE: Run RECONST/DTIMETRICS - // - ch_dti_metrics = PREPROC_DWI.out.dwi_resample - .join(PREPROC_DWI.out.bval) - .join(PREPROC_DWI.out.bvec) - .join(PREPROC_DWI.out.b0_mask) - - RECONST_DTIMETRICS( ch_dti_metrics ) - - // - // MODULE: Run RECONST/FRF - // - ch_reconst_frf = PREPROC_DWI.out.dwi_resample - .join(PREPROC_DWI.out.bval) - .join(PREPROC_DWI.out.bvec) - .join(PREPROC_DWI.out.b0_mask) - - RECONST_FRF( ch_reconst_frf ) - - /* Run fiber response aeraging over subjects */ - ch_fiber_response = RECONST_FRF.out.frf - if ( params.dwi_fodf_fit_use_average_frf ) { - RECONST_MEANFRF( RECONST_FRF.out.frf.map{ it[1] }.flatten() ) - ch_fiber_response = RECONST_FRF.out.map{ it[0] } - .combine( RECONST_MEANFRF.out.meanfrf ) - } + ch_versions = ch_versions.mix(RUN.out.versions) // - // MODULE: Run RECONST/FODF + // Run RECONST/SH_METRICS // - ch_reconst_fodf = PREPROC_DWI.out.dwi_resample - .join(PREPROC_DWI.out.bval) - .join(PREPROC_DWI.out.bvec) - .join(PREPROC_DWI.out.b0_mask) - .join(RECONST_DTIMETRICS.out.fa) - .join(RECONST_DTIMETRICS.out.md) - .join(ch_fiber_response) - RECONST_FODF( ch_reconst_fodf ) - - // - // MODULE: Run TRACKING/PFTTRACKING - // - ch_pft_tracking = ANATOMICAL_SEGMENTATION.out.wm_mask - .join(ANATOMICAL_SEGMENTATION.out.gm_mask) - .join(ANATOMICAL_SEGMENTATION.out.csf_mask) - .join(RECONST_FODF.out.fodf) - .join(RECONST_DTIMETRICS.out.fa) - TRACKING_PFTTRACKING( ch_pft_tracking ) - - // - // MODULE: Run TRACKING/LOCALTRACKING - // - ch_local_tracking = ANATOMICAL_SEGMENTATION.out.wm_mask - .join(RECONST_FODF.out.fodf) - .join(RECONST_DTIMETRICS.out.fa) - TRACKING_LOCALTRACKING( ch_local_tracking ) + if (params.sh_fitting) + RECONST_SHSIGNAL( + RUN.out.dwi + .map{ it + [[]] } + ) // // Collate and save software versions