diff --git a/.circleci/ds005_fasttrack_outputs.txt b/.circleci/ds005_fasttrack_outputs.txt index 4785687aa..1b2052d68 100644 --- a/.circleci/ds005_fasttrack_outputs.txt +++ b/.circleci/ds005_fasttrack_outputs.txt @@ -23,16 +23,12 @@ sub-01/anat/sub-01_hemi-R_desc-reg_sphere.surf.gii sub-01/anat/sub-01_hemi-R_space-fsLR_desc-msmsulc_sphere.surf.gii sub-01/anat/sub-01_hemi-R_space-fsLR_desc-reg_sphere.surf.gii sub-01/func -sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-brain_mask.json -sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-brain_mask.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-confounds_timeseries.json sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-confounds_timeseries.tsv sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-coreg_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-coreg_boldref.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-hmc_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-hmc_boldref.nii.gz -sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-preproc_bold.json -sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-preproc_bold.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-01_from-boldref_to-T1w_mode-image_desc-coreg_xfm.json sub-01/func/sub-01_task-mixedgamblestask_run-01_from-boldref_to-T1w_mode-image_desc-coreg_xfm.txt sub-01/func/sub-01_task-mixedgamblestask_run-01_from-orig_to-boldref_mode-image_desc-hmc_xfm.json @@ -45,16 +41,12 @@ sub-01/func/sub-01_task-mixedgamblestask_run-01_hemi-R_space-fsaverage5_bold.fun sub-01/func/sub-01_task-mixedgamblestask_run-01_hemi-R_space-fsaverage5_bold.json sub-01/func/sub-01_task-mixedgamblestask_run-01_hemi-R_space-fsnative_bold.func.gii sub-01/func/sub-01_task-mixedgamblestask_run-01_hemi-R_space-fsnative_bold.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-brain_mask.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-brain_mask.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-confounds_timeseries.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-confounds_timeseries.tsv sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-coreg_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-coreg_boldref.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-hmc_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-hmc_boldref.nii.gz -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-T1w_mode-image_desc-coreg_xfm.json sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-T1w_mode-image_desc-coreg_xfm.txt sub-01/func/sub-01_task-mixedgamblestask_run-02_from-orig_to-boldref_mode-image_desc-hmc_xfm.json diff --git a/.circleci/ds005_outputs.txt b/.circleci/ds005_outputs.txt index 167d82ee9..1634090d5 100644 --- a/.circleci/ds005_outputs.txt +++ b/.circleci/ds005_outputs.txt @@ -43,16 +43,12 @@ sub-01/anat/sub-01_label-CSF_probseg.nii.gz sub-01/anat/sub-01_label-GM_probseg.nii.gz sub-01/anat/sub-01_label-WM_probseg.nii.gz sub-01/func -sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-brain_mask.json -sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-brain_mask.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-confounds_timeseries.json sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-confounds_timeseries.tsv sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-coreg_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-coreg_boldref.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-hmc_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-hmc_boldref.nii.gz -sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-preproc_bold.json -sub-01/func/sub-01_task-mixedgamblestask_run-01_desc-preproc_bold.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-01_from-boldref_to-T1w_mode-image_desc-coreg_xfm.json sub-01/func/sub-01_task-mixedgamblestask_run-01_from-boldref_to-T1w_mode-image_desc-coreg_xfm.txt sub-01/func/sub-01_task-mixedgamblestask_run-01_from-orig_to-boldref_mode-image_desc-hmc_xfm.json @@ -65,16 +61,12 @@ sub-01/func/sub-01_task-mixedgamblestask_run-01_hemi-R_space-fsaverage5_bold.fun sub-01/func/sub-01_task-mixedgamblestask_run-01_hemi-R_space-fsaverage5_bold.json sub-01/func/sub-01_task-mixedgamblestask_run-01_hemi-R_space-fsnative_bold.func.gii sub-01/func/sub-01_task-mixedgamblestask_run-01_hemi-R_space-fsnative_bold.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-brain_mask.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-brain_mask.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-confounds_timeseries.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-confounds_timeseries.tsv sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-coreg_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-coreg_boldref.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-hmc_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-hmc_boldref.nii.gz -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-T1w_mode-image_desc-coreg_xfm.json sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-T1w_mode-image_desc-coreg_xfm.txt sub-01/func/sub-01_task-mixedgamblestask_run-02_from-orig_to-boldref_mode-image_desc-hmc_xfm.json diff --git a/.circleci/ds005_partial_fasttrack_outputs.txt b/.circleci/ds005_partial_fasttrack_outputs.txt index 9a712a4a2..39533d137 100644 --- a/.circleci/ds005_partial_fasttrack_outputs.txt +++ b/.circleci/ds005_partial_fasttrack_outputs.txt @@ -51,16 +51,12 @@ sub-01/fmap/sub-01_run-02_fmapid-auto00000_desc-magnitude_fieldmap.nii.gz sub-01/fmap/sub-01_run-02_fmapid-auto00000_desc-preproc_fieldmap.json sub-01/fmap/sub-01_run-02_fmapid-auto00000_desc-preproc_fieldmap.nii.gz sub-01/func -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-brain_mask.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-brain_mask.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-confounds_timeseries.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-confounds_timeseries.tsv sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-coreg_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-coreg_boldref.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-hmc_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-hmc_boldref.nii.gz -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-T1w_mode-image_desc-coreg_xfm.json sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-T1w_mode-image_desc-coreg_xfm.txt sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-auto00000_mode-image_xfm.json diff --git a/.circleci/ds005_partial_outputs.txt b/.circleci/ds005_partial_outputs.txt index 40b563d52..80e9078ca 100644 --- a/.circleci/ds005_partial_outputs.txt +++ b/.circleci/ds005_partial_outputs.txt @@ -73,16 +73,12 @@ sub-01/fmap/sub-01_run-02_fmapid-auto00000_desc-magnitude_fieldmap.nii.gz sub-01/fmap/sub-01_run-02_fmapid-auto00000_desc-preproc_fieldmap.json sub-01/fmap/sub-01_run-02_fmapid-auto00000_desc-preproc_fieldmap.nii.gz sub-01/func -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-brain_mask.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-brain_mask.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-confounds_timeseries.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-confounds_timeseries.tsv sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-coreg_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-coreg_boldref.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-hmc_boldref.json sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-hmc_boldref.nii.gz -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.json -sub-01/func/sub-01_task-mixedgamblestask_run-02_desc-preproc_bold.nii.gz sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-auto00000_mode-image_xfm.json sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-auto00000_mode-image_xfm.txt sub-01/func/sub-01_task-mixedgamblestask_run-02_from-boldref_to-T1w_mode-image_desc-coreg_xfm.json diff --git a/.circleci/ds210_fasttrack_outputs.txt b/.circleci/ds210_fasttrack_outputs.txt index 700c31970..5030a97cd 100644 --- a/.circleci/ds210_fasttrack_outputs.txt +++ b/.circleci/ds210_fasttrack_outputs.txt @@ -31,8 +31,6 @@ sub-02/func/sub-02_task-cuedSGT_run-01_desc-coreg_boldref.json sub-02/func/sub-02_task-cuedSGT_run-01_desc-coreg_boldref.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_desc-hmc_boldref.json sub-02/func/sub-02_task-cuedSGT_run-01_desc-hmc_boldref.nii.gz -sub-02/func/sub-02_task-cuedSGT_run-01_desc-preproc_bold.json -sub-02/func/sub-02_task-cuedSGT_run-01_desc-preproc_bold.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_echo-1_desc-preproc_bold.json sub-02/func/sub-02_task-cuedSGT_run-01_echo-1_desc-preproc_bold.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_echo-2_desc-preproc_bold.json @@ -45,8 +43,6 @@ sub-02/func/sub-02_task-cuedSGT_run-01_from-boldref_to-T1w_mode-image_desc-coreg sub-02/func/sub-02_task-cuedSGT_run-01_from-boldref_to-T1w_mode-image_desc-coreg_xfm.txt sub-02/func/sub-02_task-cuedSGT_run-01_from-orig_to-boldref_mode-image_desc-hmc_xfm.json sub-02/func/sub-02_task-cuedSGT_run-01_from-orig_to-boldref_mode-image_desc-hmc_xfm.txt -sub-02/func/sub-02_task-cuedSGT_run-01_space-boldref_T2starmap.json -sub-02/func/sub-02_task-cuedSGT_run-01_space-boldref_T2starmap.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_space-MNI152NLin2009cAsym_boldref.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_space-MNI152NLin2009cAsym_desc-brain_mask.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_space-MNI152NLin2009cAsym_desc-preproc_bold.json diff --git a/.circleci/ds210_outputs.txt b/.circleci/ds210_outputs.txt index 3a880afc4..32f864cbf 100644 --- a/.circleci/ds210_outputs.txt +++ b/.circleci/ds210_outputs.txt @@ -41,8 +41,6 @@ sub-02/func/sub-02_task-cuedSGT_run-01_desc-coreg_boldref.json sub-02/func/sub-02_task-cuedSGT_run-01_desc-coreg_boldref.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_desc-hmc_boldref.json sub-02/func/sub-02_task-cuedSGT_run-01_desc-hmc_boldref.nii.gz -sub-02/func/sub-02_task-cuedSGT_run-01_desc-preproc_bold.json -sub-02/func/sub-02_task-cuedSGT_run-01_desc-preproc_bold.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_echo-1_desc-preproc_bold.json sub-02/func/sub-02_task-cuedSGT_run-01_echo-1_desc-preproc_bold.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_echo-2_desc-preproc_bold.json @@ -55,8 +53,6 @@ sub-02/func/sub-02_task-cuedSGT_run-01_from-boldref_to-T1w_mode-image_desc-coreg sub-02/func/sub-02_task-cuedSGT_run-01_from-boldref_to-T1w_mode-image_desc-coreg_xfm.txt sub-02/func/sub-02_task-cuedSGT_run-01_from-orig_to-boldref_mode-image_desc-hmc_xfm.json sub-02/func/sub-02_task-cuedSGT_run-01_from-orig_to-boldref_mode-image_desc-hmc_xfm.txt -sub-02/func/sub-02_task-cuedSGT_run-01_space-boldref_T2starmap.json -sub-02/func/sub-02_task-cuedSGT_run-01_space-boldref_T2starmap.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_space-MNI152NLin2009cAsym_boldref.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_space-MNI152NLin2009cAsym_desc-brain_mask.nii.gz sub-02/func/sub-02_task-cuedSGT_run-01_space-MNI152NLin2009cAsym_desc-preproc_bold.json diff --git a/.github/workflows/contrib.yml b/.github/workflows/contrib.yml index 8136dd966..dd3a565a4 100644 --- a/.github/workflows/contrib.yml +++ b/.github/workflows/contrib.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Display Python version diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 4dd37e8b9..04ec84b68 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -27,7 +27,7 @@ jobs: strategy: matrix: os: ['ubuntu-latest'] - python-version: ['3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12'] install: ['pip'] check: ['tests'] pip-flags: ['PRE_PIP_FLAGS'] @@ -45,7 +45,7 @@ jobs: - name: Install dependencies run: .maint/ci/install_dependencies.sh - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Display Python version diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index bfa3f83e9..8698df129 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -30,7 +30,7 @@ jobs: strategy: matrix: os: ['ubuntu-latest'] - python-version: ['3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12'] install: ['pip'] check: ['tests'] pip-flags: [''] @@ -48,7 +48,7 @@ jobs: - name: Install dependencies run: .maint/ci/install_dependencies.sh - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Display Python version diff --git a/.maint/CONTRIBUTORS.md b/.maint/CONTRIBUTORS.md index da6823b8c..bb3cf9f8f 100644 --- a/.maint/CONTRIBUTORS.md +++ b/.maint/CONTRIBUTORS.md @@ -32,6 +32,7 @@ Before every release, unlisted contributors will be invited again to add their n | Liem | Franz | | 0000-0003-0646-4810 | URPP Dynamics of Healthy Aging, University of Zurich | | Lurie | Daniel J. | | 0000-0001-8012-6399 | Department of Psychology, University of California, Berkeley | | Ma | Feilong | | 0000-0002-6838-3971 | Dartmouth College: Hanover, NH, United States | +| Madison | Thomas | | 0000-0003-3030-6580 | Department of Pediatrics, University of Minnesota, MN, USA | | Mentch | Jeff | | 0000-0002-7762-8678 | Speech & Hearing Bioscience & Technology Program, Harvard University | | Moodie | Craig A. | | 0000-0003-0867-1469 | Department of Psychology, Stanford University | | Naveau | Mikaël | | 0000-0001-6948-9068 | Cyceron, UMS 3408 (CNRS - UCBN), France | diff --git a/.zenodo.json b/.zenodo.json index 4f6a7ed83..d9b5542d9 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -8,12 +8,24 @@ "orcid": "0000-0002-1668-9629", "type": "Researcher" }, + { + "affiliation": "Department of Pediatrics, University of Minnesota, MN, USA", + "name": "Madison, Thomas", + "orcid": "0000-0003-3030-6580", + "type": "Researcher" + }, { "affiliation": "Florey Institute of Neuroscience and Mental Health", "name": "Smith, Robert E.", "orcid": "0000-0003-3636-4642", "type": "Researcher" }, + { + "affiliation": "Neurospin, CEA", + "name": "Papadopoulos, Dimitri", + "orcid": "0000-0002-1242-8990", + "type": "Researcher" + }, { "affiliation": "Centre for Modern Interdisciplinary Technologies, Nicolaus Copernicus University in Toruń", "name": "Finc, Karolina", @@ -26,18 +38,6 @@ "orcid": "0000-0002-6533-2909", "type": "Researcher" }, - { - "affiliation": "Neurospin, CEA", - "name": "Papadopoulos, Dimitri", - "orcid": "0000-0002-1242-8990", - "type": "Researcher" - }, - { - "affiliation": "Dartmouth College: Hanover, NH, United States", - "name": "Halchenko, Yaroslav O.", - "orcid": "0000-0003-3456-2493", - "type": "Researcher" - }, { "affiliation": "Department of Neuroscience, University of Pennsylvania, PA, USA", "name": "Tooley, Ursula A.", @@ -51,9 +51,9 @@ "type": "Researcher" }, { - "affiliation": "University of Texas at Austin", - "name": "de la Vega, Alejandro", - "orcid": "0000-0001-9062-3778", + "affiliation": "Dartmouth College: Hanover, NH, United States", + "name": "Halchenko, Yaroslav O.", + "orcid": "0000-0003-3456-2493", "type": "Researcher" }, { @@ -63,9 +63,9 @@ "type": "Researcher" }, { - "affiliation": "Montreal Neurological Institute, McGill University", - "name": "Urchs, Sebastian", - "orcid": "0000-0001-5504-8579", + "affiliation": "University of Texas at Austin", + "name": "de la Vega, Alejandro", + "orcid": "0000-0001-9062-3778", "type": "Researcher" }, { @@ -75,9 +75,15 @@ "type": "Researcher" }, { - "affiliation": "Charite Universitatsmedizin Berlin, Germany", - "name": "Waller, Lea", - "orcid": "0000-0002-3239-6957", + "affiliation": "Montreal Neurological Institute, McGill University", + "name": "Urchs, Sebastian", + "orcid": "0000-0001-5504-8579", + "type": "Researcher" + }, + { + "affiliation": "Machine Learning Team, National Institute of Mental Health, USA", + "name": "Nielson, Dylan M.", + "orcid": "0000-0003-4613-6643", "type": "Researcher" }, { @@ -86,6 +92,12 @@ "orcid": "0000-0002-2050-0614", "type": "Researcher" }, + { + "affiliation": "Charite Universitatsmedizin Berlin, Germany", + "name": "Waller, Lea", + "orcid": "0000-0002-3239-6957", + "type": "Researcher" + }, { "affiliation": "Speech & Hearing Bioscience & Technology Program, Harvard University", "name": "Mentch, Jeff", @@ -98,12 +110,6 @@ "orcid": "0000-0001-8012-6399", "type": "Researcher" }, - { - "affiliation": "Department of Psychology, Columbia University", - "name": "Jacoby, Nir", - "orcid": "0000-0001-7936-9991", - "type": "Researcher" - }, { "affiliation": "Computational Neuroimaging Lab, BioCruces Health Research Institute", "name": "Erramuzpe, Asier", @@ -194,6 +200,18 @@ "orcid": "0000-0002-1652-9297", "type": "Researcher" }, + { + "affiliation": "Department of Psychology, Columbia University", + "name": "Jacoby, Nir", + "orcid": "0000-0001-7936-9991", + "type": "Researcher" + }, + { + "affiliation": "Department of Psychology, University of Washington", + "name": "Kruper, John", + "orcid": "0000-0003-0081-391X", + "type": "Researcher" + }, { "affiliation": "URPP Dynamics of Healthy Aging, University of Zurich", "name": "Liem, Franz", @@ -301,9 +319,9 @@ "orcid": "0000-0002-1668-9629" }, { - "affiliation": "Montreal Neurological Institute, McGill University", - "name": "DuPre, Elizabeth", - "orcid": "0000-0003-1358-196X" + "affiliation": "Department of Psychology, Florida International University", + "name": "Salo, Taylor", + "orcid": "0000-0001-9813-3167" }, { "affiliation": "Neuroscience Program, University of Iowa", @@ -311,9 +329,9 @@ "orcid": "0000-0002-4892-2659" }, { - "affiliation": "Department of Psychology, Florida International University", - "name": "Salo, Taylor", - "orcid": "0000-0001-9813-3167" + "affiliation": "Montreal Neurological Institute, McGill University", + "name": "DuPre, Elizabeth", + "orcid": "0000-0003-1358-196X" }, { "affiliation": "Department of Psychology, Stanford University", @@ -330,11 +348,6 @@ "name": "Blair, Ross W.", "orcid": "0000-0003-3007-1056" }, - { - "affiliation": "Machine Learning Team, National Institute of Mental Health", - "name": "Nielson, Dylan M.", - "orcid": "0000-0003-4613-6643" - }, { "affiliation": "Department of Psychology, Stanford University", "name": "Poldrack, Russell A.", diff --git a/CHANGES.rst b/CHANGES.rst index 0dbaeb742..b67a2e7f3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -23.2.0 (To be determined) +23.2.0 (January 10, 2024) ========================= New feature release in the 23.2.x series. @@ -33,17 +33,27 @@ This release resolves a number of issues with fieldmaps inducing distortions during correction. Phase difference and direct fieldmaps are now masked correctly, preventing the overestimation of distortions outside the brain. Additionally, we now implement Jacobian weighting during unwarping, which corrects for compression -and expansion effects on signal intensity. +and expansion effects on signal intensity. To disable Jacobian weighting, use +``--ignore fmap-jacobian``. Finally, a new resampling method has been added, to better account for susceptibility distortion and motion in a single shot resampling to a volumetric target space. We anticipate extending this to surface targets in the future. +* FIX: Restore --ignore sbref functionality (#3180) +* FIX: Retrieve atlas ROIs at requested density (#3179) +* FIX: Keep minctracc executable in FreeSurfer installation (#3175) +* FIX: Exclude echo entity from optimally combined derivatives (#3166) +* FIX: Disable boldref-space outputs unless requested (#3159) +* FIX: Tag memory estimates in resamplers (#3150) * FIX: Final revisions for next branch (#3134) * FIX: Minor fixes to work with MSMSulc-enabled smriprep-next (#3098) * FIX: Connect EPI-to-fieldmap transform (#3099) * FIX: Use Py2-compatible version file template for fmriprep-docker (#3101) * FIX: Update connections to unwarp_wf, convert ITK transforms to text (#3077) +* ENH: Allow --ignore fmap-jacobian to disable Jacobian determinant modulation during fieldmap correction (#3186) +* ENH: Exclude non-steady-state volumes from confound correlation plot (#3171) +* ENH: Pass FLAIR images to anatomical workflow builder to include in boilerplate (#3146) * ENH: Restore carpetplot and other final adjustments (#3131) * ENH: Restore CIFTI-2 generation (#3129) * ENH: Restore resampling to surface GIFTIs (#3126) @@ -53,13 +63,25 @@ target space. We anticipate extending this to surface targets in the future. * ENH: Add MSMSulc (#3085) * ENH: Add reporting workflow for BOLD fit (#3082) * ENH: Generate anatomical derivatives useful for resampling (#3081) +* RF: Load reportlets interfaces from nireports rather than niworkflows (#3176, #3184) +* RF: Separate goodvoxels mask creation from fsLR resampling (#3170) * RF: Write out anatomical template derivatives (#3136) * RF: Update primary bold workflow to incorporate single shot resampling (#3114) * RF: Update derivative cache spec, calculate per-BOLD, reuse boldref2fmap (#3078) * RF: Split fMRIPrep into fit and derivatives workflows (#2913) +* RPT: Rename CSF/WM confounds in fMRIPlot (#3172) +* TST: Add smoke tests for full workflow and most branching flags (#3155) +* TST: Add smoke-tests for bold_fit_wf (#3152) * DOC: Fix documentation and description for init_bold_grayords_wf (#3051) +* DOC: Minor updates in outputs.rst (#3148) +* STY: Apply a couple refurb suggestions (#3151) * STY: Fix flake8 warnings (#3044) * STY: Apply pyupgrade suggestions (#3043) +* MNT: Restore mritotal subcommands to Dockerfile (#3149) +* MNT: Update smriprep to 0.13.1 (#3153) +* MNT: optimise size of PNG files (#3145) +* MNT: update vendored docs script ``github_link.py`` (#3144) +* MNT: Update tedana pin, test on Python 3.12 (#3141) * MNT: Bump environment (#3132) * MNT: Bump version requirements (#3107) * MNT: http:// → https:// (#3097) @@ -71,6 +93,7 @@ target space. We anticipate extending this to surface targets in the future. * MNT: update update_zenodo.py script (#3042) * MNT: Fix welcome message formatting and instructions (#3039) * MNT: Python 3.11 should be supported (#3038) +* CI: Bump actions/setup-python from 4 to 5 (#3181) * CI: Stop testing legacy layout (#3079) * CI: Improve tag detection for docker builds (#3066) * CI: Clean up pre-release builds (#3040) diff --git a/docker/files/freesurfer7.3.2-exclude.txt b/docker/files/freesurfer7.3.2-exclude.txt index 7bc9b5400..8d9a7ef3d 100644 --- a/docker/files/freesurfer7.3.2-exclude.txt +++ b/docker/files/freesurfer7.3.2-exclude.txt @@ -766,8 +766,6 @@ freesurfer/lib/tktools freesurfer/lib/vtk freesurfer/matlab freesurfer/mni-1.4 -freesurfer/mni/bin/autocrop -freesurfer/mni/bin/check_scale freesurfer/mni/bin/correct_field freesurfer/mni/bin/crispify freesurfer/mni/bin/dcm2mnc @@ -783,7 +781,6 @@ freesurfer/mni/bin/make_phantom freesurfer/mni/bin/make_template freesurfer/mni/bin/mincaverage freesurfer/mni/bin/mincbbox -freesurfer/mni/bin/mincblur freesurfer/mni/bin/minccalc freesurfer/mni/bin/mincchamfer freesurfer/mni/bin/mincconcat @@ -801,12 +798,10 @@ freesurfer/mni/bin/mincmakevector freesurfer/mni/bin/mincmath freesurfer/mni/bin/minc_modify_header freesurfer/mni/bin/mincpik -freesurfer/mni/bin/mincresample freesurfer/mni/bin/mincreshape freesurfer/mni/bin/mincstats freesurfer/mni/bin/minctoecat freesurfer/mni/bin/minctoraw -freesurfer/mni/bin/minctracc freesurfer/mni/bin/mincview freesurfer/mni/bin/mincwindow freesurfer/mni/bin/mnc2nii @@ -827,7 +822,6 @@ freesurfer/mni/bin/sharpen_volume freesurfer/mni/bin/spline_smooth freesurfer/mni/bin/transformtags freesurfer/mni/bin/upet2mnc -freesurfer/mni/bin/volume_cog freesurfer/mni/bin/volume_hist freesurfer/mni/bin/volume_stats freesurfer/mni/bin/voxeltoworld diff --git a/docs/_static/OHBM2017-poster.png b/docs/_static/OHBM2017-poster.png index ed13696da..d8431c2d2 100644 Binary files a/docs/_static/OHBM2017-poster.png and b/docs/_static/OHBM2017-poster.png differ diff --git a/docs/_static/OHBM2017-poster_thumb.png b/docs/_static/OHBM2017-poster_thumb.png index 952de2166..336f2e435 100644 Binary files a/docs/_static/OHBM2017-poster_thumb.png and b/docs/_static/OHBM2017-poster_thumb.png differ diff --git a/docs/_static/OHBM2018-poster.png b/docs/_static/OHBM2018-poster.png index ffce379d8..801298df5 100644 Binary files a/docs/_static/OHBM2018-poster.png and b/docs/_static/OHBM2018-poster.png differ diff --git a/docs/_static/OHBM2018-poster_thumb.png b/docs/_static/OHBM2018-poster_thumb.png index 1c9d84a2e..0f127e5b9 100644 Binary files a/docs/_static/OHBM2018-poster_thumb.png and b/docs/_static/OHBM2018-poster_thumb.png differ diff --git a/docs/_static/fmriprep-workflow-all.png b/docs/_static/fmriprep-workflow-all.png index 7553db3a4..0767bfe07 100644 Binary files a/docs/_static/fmriprep-workflow-all.png and b/docs/_static/fmriprep-workflow-all.png differ diff --git a/docs/outputs.rst b/docs/outputs.rst index 1dbfc4d86..b01c587ee 100644 --- a/docs/outputs.rst +++ b/docs/outputs.rst @@ -285,7 +285,7 @@ mid-thickness surface mesh:: func/ sub-_[specifiers]_space-T1w_desc-aparcaseg_dseg.nii.gz sub-_[specifiers]_space-T1w_desc-aseg_dseg.nii.gz - sub-_[specifiers]_space-_hemi-[LR]_bold.func.gii + sub-_[specifiers]_hemi-[LR]_space-_bold.func.gii Surface output spaces include ``fsnative`` (full density subject-specific mesh), ``fsaverage`` and the down-sampled meshes ``fsaverage6`` (41k vertices) and @@ -375,6 +375,8 @@ to perform more advanced denoising or alternative combination strategies. slice as reference). Alternatively, you could manually adjust the volume onsets (e.g. as mentioned in the example above from [0, 2, 4] to [1, 3, 5]) or the event onsets accordingly. + In contrast to volume onsets, event onsets need to be shifted *backward* by half a TR, + for example, from [5, 10, 15] to [4, 9, 14]. Further information on this issue is found at `this blog post (with thanks to Russell Poldrack and Jeanette Mumford) @@ -627,7 +629,7 @@ An example of these plots follows: .. figure:: _static/sub-405_ses-01_task-rest_run-01_desc-carpetplot_bold.svg The figure shows on top several confounds estimated for the BOLD series: - global signals ('GS', 'GSCSF', 'GSWM'), DVARS, + global signals ('GS', 'CSF', 'WM'), DVARS, and framewise-displacement ('FD'). At the bottom, a 'carpetplot' summarizing the BOLD series [Power2016]_. The carpet plot rows correspond to voxelwise time series, diff --git a/docs/sphinxext/github_link.py b/docs/sphinxext/github_link.py index f15ff9138..54b5c2f8d 100644 --- a/docs/sphinxext/github_link.py +++ b/docs/sphinxext/github_link.py @@ -1,24 +1,24 @@ """ -This script comes from scikit-learn: +This vendored script comes from scikit-learn: https://github.com/scikit-learn/scikit-learn/blob/master/doc/sphinxext/github_link.py """ -from operator import attrgetter import inspect -import subprocess import os +import subprocess import sys from functools import partial +from operator import attrgetter -REVISION_CMD = 'git rev-parse --short HEAD' +REVISION_CMD = "git rev-parse --short HEAD" def _get_git_revision(): try: revision = subprocess.check_output(REVISION_CMD.split()).strip() except (subprocess.CalledProcessError, OSError): - print('Failed to execute git to get revision') + print("Failed to execute git to get revision") return None - return revision.decode('utf-8') + return revision.decode("utf-8") def _linkcode_resolve(domain, info, package, url_fmt, revision): @@ -30,25 +30,26 @@ def _linkcode_resolve(domain, info, package, url_fmt, revision): >>> _linkcode_resolve('py', {'module': 'tty', ... 'fullname': 'setraw'}, ... package='tty', - ... url_fmt='https://hg.python.org/cpython/file/' + ... url_fmt='http://hg.python.org/cpython/file/' ... '{revision}/Lib/{package}/{path}#L{lineno}', ... revision='xxxx') - 'https://hg.python.org/cpython/file/xxxx/Lib/tty/tty.py#L18' + 'http://hg.python.org/cpython/file/xxxx/Lib/tty/tty.py#L18' """ if revision is None: return - if domain not in ('py', 'pyx'): + if domain not in ("py", "pyx"): return - if not info.get('module') or not info.get('fullname'): + if not info.get("module") or not info.get("fullname"): return - class_name = info['fullname'].split('.')[0] - if type(class_name) != str: - # Python 2 only - class_name = class_name.encode('utf-8') - module = __import__(info['module'], fromlist=[class_name]) - obj = attrgetter(info['fullname'])(module) + class_name = info["fullname"].split(".")[0] + module = __import__(info["module"], fromlist=[class_name]) + obj = attrgetter(info["fullname"])(module) + + # Unwrap the object to get the correct source + # file in case that is wrapped by a decorator + obj = inspect.unwrap(obj) try: fn = inspect.getsourcefile(obj) @@ -62,14 +63,12 @@ def _linkcode_resolve(domain, info, package, url_fmt, revision): if not fn: return - fn = os.path.relpath(fn, - start=os.path.dirname(__import__(package).__file__)) + fn = os.path.relpath(fn, start=os.path.dirname(__import__(package).__file__)) try: lineno = inspect.getsourcelines(obj)[1] except Exception: - lineno = '' - return url_fmt.format(revision=revision, package=package, - path=fn, lineno=lineno) + lineno = "" + return url_fmt.format(revision=revision, package=package, path=fn, lineno=lineno) def make_linkcode_resolve(package, url_fmt): @@ -84,5 +83,6 @@ def make_linkcode_resolve(package, url_fmt): '{path}#L{lineno}') """ revision = _get_git_revision() - return partial(_linkcode_resolve, revision=revision, package=package, - url_fmt=url_fmt) + return partial( + _linkcode_resolve, revision=revision, package=package, url_fmt=url_fmt + ) diff --git a/fmriprep/cli/parser.py b/fmriprep/cli/parser.py index be6cdff0d..2bedf16f3 100644 --- a/fmriprep/cli/parser.py +++ b/fmriprep/cli/parser.py @@ -285,7 +285,7 @@ def _slice_time_ref(value, parser): action="store", nargs="+", default=[], - choices=["fieldmaps", "slicetiming", "sbref", "t2w", "flair"], + choices=["fieldmaps", "slicetiming", "sbref", "t2w", "flair", "fmap-jacobian"], help="Ignore selected aspects of the input dataset to disable corresponding " "parts of the workflow (a space delimited list)", ) diff --git a/fmriprep/cli/run.py b/fmriprep/cli/run.py index ae1b3be62..891284463 100644 --- a/fmriprep/cli/run.py +++ b/fmriprep/cli/run.py @@ -202,7 +202,7 @@ def main(): _copy_any(dseg_tsv, str(config.execution.fmriprep_dir / "desc-aparcaseg_dseg.tsv")) errno = 0 finally: - from pkg_resources import resource_filename as pkgrf + from .. import data # Code Carbon if config.execution.track_carbon: @@ -218,7 +218,7 @@ def main(): config.execution.participant_label, config.execution.fmriprep_dir, config.execution.run_uuid, - config=pkgrf("fmriprep", "data/reports-spec.yml"), + config=data.load("reports-spec.yml"), packagename="fmriprep", ) write_derivative_description(config.execution.bids_dir, config.execution.fmriprep_dir) diff --git a/fmriprep/cli/workflow.py b/fmriprep/cli/workflow.py index 59a4f4972..6d754a93d 100644 --- a/fmriprep/cli/workflow.py +++ b/fmriprep/cli/workflow.py @@ -38,12 +38,11 @@ def build_workflow(config_file, retval): from niworkflows.utils.bids import collect_participants from niworkflows.utils.misc import check_valid_fs_license - from pkg_resources import resource_filename as pkgrf from fmriprep.reports.core import generate_reports from fmriprep.utils.bids import check_pipeline_version - from .. import config + from .. import config, data from ..utils.misc import check_deps from ..workflows.base import init_fmriprep_wf @@ -57,7 +56,7 @@ def build_workflow(config_file, retval): retval["workflow"] = None banner = [f"Running fMRIPrep version {version}"] - notice_path = Path(pkgrf("fmriprep", "data/NOTICE")) + notice_path = data.load.readable("NOTICE") if notice_path.exists(): banner[0] += "\n" banner += [f"License NOTICE {'#' * 50}"] @@ -91,7 +90,7 @@ def build_workflow(config_file, retval): config.execution.participant_label, config.execution.fmriprep_dir, config.execution.run_uuid, - config=pkgrf("fmriprep", "data/reports-spec.yml"), + config=data.load("reports-spec.yml"), packagename="fmriprep", ) return retval @@ -183,9 +182,9 @@ def build_boilerplate(config_file, workflow): from pathlib import Path from subprocess import CalledProcessError, TimeoutExpired, check_call - from pkg_resources import resource_filename as pkgrf + from .. import data - bib_text = Path(pkgrf("fmriprep", "data/boilerplate.bib")).read_text() + bib_text = data.load.readable("boilerplate.bib").read_text() citation_files["bib"].write_text( bib_text.replace("fMRIPrep ", f"fMRIPrep {config.environment.version}") ) diff --git a/fmriprep/interfaces/confounds.py b/fmriprep/interfaces/confounds.py index f3e632dbc..b59b11534 100644 --- a/fmriprep/interfaces/confounds.py +++ b/fmriprep/interfaces/confounds.py @@ -47,8 +47,8 @@ traits, ) from nipype.utils.filemanip import fname_presuffix +from nireports.reportlets.modality.func import fMRIPlot from niworkflows.utils.timeseries import _cifti_timeseries, _nifti_timeseries -from niworkflows.viz.plots import fMRIPlot LOGGER = logging.getLogger('nipype.interface') diff --git a/fmriprep/interfaces/reports.py b/fmriprep/interfaces/reports.py index 445a05aa5..5108e6236 100644 --- a/fmriprep/interfaces/reports.py +++ b/fmriprep/interfaces/reports.py @@ -39,7 +39,7 @@ isdefined, traits, ) -from niworkflows.interfaces.reportlets import base as nrb +from nireports.interfaces.reporting import base as nrb from smriprep.interfaces.freesurfer import ReconAll LOGGER = logging.getLogger('nipype.interface') diff --git a/fmriprep/interfaces/resampling.py b/fmriprep/interfaces/resampling.py index f997765ea..6111623cb 100644 --- a/fmriprep/interfaces/resampling.py +++ b/fmriprep/interfaces/resampling.py @@ -49,6 +49,7 @@ class ResampleSeriesInputSpec(TraitedSpec): "k-", desc="the phase-encoding direction corresponding to in_data", ) + jacobian = traits.Bool(mandatory=True, desc="Whether to apply Jacobian correction") num_threads = traits.Int(1, usedefault=True, desc="Number of threads to use for resampling") output_data_type = traits.Str("float32", usedefault=True, desc="Data type of output image") order = traits.Int(3, usedefault=True, desc="Order of interpolation (0=nearest, 3=cubic)") @@ -105,6 +106,7 @@ def _run_interface(self, runtime): transforms=transforms, fieldmap=fieldmap, pe_info=pe_info, + jacobian=self.inputs.jacobian, nthreads=self.inputs.num_threads, output_dtype=self.inputs.output_data_type, order=self.inputs.order, @@ -217,6 +219,7 @@ def resample_vol( data: np.ndarray, coordinates: np.ndarray, pe_info: tuple[int, float], + jacobian: bool, hmc_xfm: np.ndarray | None, fmap_hz: np.ndarray, output: np.dtype | np.ndarray | None = None, @@ -282,8 +285,6 @@ def resample_vol( vsm = fmap_hz * pe_info[1] coordinates[pe_info[0], ...] += vsm - jacobian = 1 + np.gradient(vsm, axis=pe_info[0]) - result = ndi.map_coordinates( data, coordinates, @@ -293,7 +294,10 @@ def resample_vol( cval=cval, prefilter=prefilter, ) - result *= jacobian + + if jacobian: + result *= 1 + np.gradient(vsm, axis=pe_info[0]) + return result @@ -301,6 +305,7 @@ async def resample_series_async( data: np.ndarray, coordinates: np.ndarray, pe_info: list[tuple[int, float]], + jacobian: bool, hmc_xfms: list[np.ndarray] | None, fmap_hz: np.ndarray, output_dtype: np.dtype | None = None, @@ -361,6 +366,7 @@ async def resample_series_async( data, coordinates, pe_info[0], + jacobian, hmc_xfms[0] if hmc_xfms else None, fmap_hz, output_dtype, @@ -384,6 +390,7 @@ async def resample_series_async( data=volume, coordinates=coordinates, pe_info=pe_info[volid], + jacobian=jacobian, hmc_xfm=hmc_xfms[volid] if hmc_xfms else None, fmap_hz=fmap_hz, output=out_array[..., volid], @@ -407,6 +414,7 @@ def resample_series( data: np.ndarray, coordinates: np.ndarray, pe_info: list[tuple[int, float]], + jacobian: bool, hmc_xfms: list[np.ndarray] | None, fmap_hz: np.ndarray, output_dtype: np.dtype | None = None, @@ -467,6 +475,7 @@ def resample_series( data=data, coordinates=coordinates, pe_info=pe_info, + jacobian=jacobian, hmc_xfms=hmc_xfms, fmap_hz=fmap_hz, output_dtype=output_dtype, @@ -485,6 +494,7 @@ def resample_image( transforms: nt.TransformChain, fieldmap: nb.Nifti1Image | None, pe_info: list[tuple[int, float]] | None, + jacobian: bool = True, nthreads: int = 1, output_dtype: np.dtype | str | None = 'f4', order: int = 3, @@ -566,6 +576,7 @@ def resample_image( data=source.get_fdata(dtype='f4'), coordinates=mapped_coordinates.T.reshape((3, *target.shape[:3])), pe_info=pe_info, + jacobian=jacobian, hmc_xfms=hmc_xfms, fmap_hz=fieldmap.get_fdata(dtype='f4'), output_dtype=output_dtype, diff --git a/fmriprep/interfaces/workbench.py b/fmriprep/interfaces/workbench.py index c78867098..f13fb5b67 100644 --- a/fmriprep/interfaces/workbench.py +++ b/fmriprep/interfaces/workbench.py @@ -279,7 +279,7 @@ class MetricResample(WBCommand, OpenMPCommandMixin): _cmd = "wb_command -metric-resample" def _format_arg(self, opt, spec, val): - if opt in ["current_area", "new_area"]: + if opt in ("current_area", "new_area"): if not self.inputs.area_surfs and not self.inputs.area_metrics: raise ValueError( "{} was set but neither area_surfs or" " area_metrics were set".format(opt) diff --git a/fmriprep/tests/test_config.py b/fmriprep/tests/test_config.py index 895598c3d..92b7f0703 100644 --- a/fmriprep/tests/test_config.py +++ b/fmriprep/tests/test_config.py @@ -27,10 +27,9 @@ import pytest from niworkflows.utils.spaces import format_reference -from pkg_resources import resource_filename as pkgrf from toml import loads -from .. import config +from .. import config, data def _reset_config(): @@ -59,8 +58,7 @@ def test_reset_config(): def test_config_spaces(): """Check that all necessary spaces are recorded in the config.""" - filename = Path(pkgrf('fmriprep', 'data/tests/config.toml')) - settings = loads(filename.read_text()) + settings = loads(data.load.readable("tests/config.toml").read_text()) for sectionname, configs in settings.items(): if sectionname != 'environment': section = getattr(config, sectionname) diff --git a/fmriprep/utils/bids.py b/fmriprep/utils/bids.py index dbf52f300..b93b92d0e 100644 --- a/fmriprep/utils/bids.py +++ b/fmriprep/utils/bids.py @@ -34,6 +34,7 @@ from bids.utils import listify from packaging.version import Version +from .. import config from ..data import load as load_data @@ -324,3 +325,15 @@ def _unique(inlist): return inlist return {k: _unique(v) for k, v in entities.items()} + + +def dismiss_echo(entities=None): + """Set entities to dismiss in a DerivativesDataSink.""" + if entities is None: + entities = [] + + echo_idx = config.execution.echo_idx + if echo_idx is None or len(listify(echo_idx)) > 2: + entities.append("echo") + + return entities diff --git a/fmriprep/workflows/base.py b/fmriprep/workflows/base.py index f815199fd..f5c5b5fd5 100644 --- a/fmriprep/workflows/base.py +++ b/fmriprep/workflows/base.py @@ -43,6 +43,7 @@ from .. import config from ..interfaces import DerivativesDataSink from ..interfaces.reports import AboutSummary, SubjectSummary +from ..utils.bids import dismiss_echo def init_fmriprep_wf(): @@ -296,7 +297,7 @@ def init_single_subject_wf(subject_id: str): base_directory=config.execution.fmriprep_dir, desc='summary', datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name='ds_report_summary', run_without_submitting=True, @@ -307,7 +308,7 @@ def init_single_subject_wf(subject_id: str): base_directory=config.execution.fmriprep_dir, desc='about', datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name='ds_report_about', run_without_submitting=True, @@ -328,6 +329,7 @@ def init_single_subject_wf(subject_id: str): msm_sulc=msm_sulc, t1w=subject_data['t1w'], t2w=subject_data['t2w'], + flair=subject_data['flair'], skull_strip_mode=config.workflow.skull_strip_t1w, skull_strip_template=Reference.from_string(config.workflow.skull_strip_template)[0], spaces=spaces, diff --git a/fmriprep/workflows/bold/apply.py b/fmriprep/workflows/bold/apply.py index 38f8a2db0..c5ff2fd8b 100644 --- a/fmriprep/workflows/bold/apply.py +++ b/fmriprep/workflows/bold/apply.py @@ -15,6 +15,8 @@ def init_bold_volumetric_resample_wf( *, metadata: dict, + mem_gb: dict[str, float], + jacobian: bool, fieldmap_id: str | None = None, omp_nthreads: int = 1, name: str = 'bold_volumetric_resample_wf', @@ -119,9 +121,14 @@ def init_bold_volumetric_resample_wf( gen_ref = pe.Node(GenerateSamplingReference(), name='gen_ref', mem_gb=0.3) - boldref2target = pe.Node(niu.Merge(2), name='boldref2target') - bold2target = pe.Node(niu.Merge(2), name='bold2target') - resample = pe.Node(ResampleSeries(), name="resample", n_procs=omp_nthreads) + boldref2target = pe.Node(niu.Merge(2), name='boldref2target', run_without_submitting=True) + bold2target = pe.Node(niu.Merge(2), name='bold2target', run_without_submitting=True) + resample = pe.Node( + ResampleSeries(jacobian=jacobian), + name="resample", + n_procs=omp_nthreads, + mem_gb=mem_gb['resampled'], + ) workflow.connect([ (inputnode, gen_ref, [ @@ -156,10 +163,14 @@ def init_bold_volumetric_resample_wf( name="distortion_params", run_without_submitting=True, ) - fmap2target = pe.Node(niu.Merge(2), name='fmap2target') - inverses = pe.Node(niu.Function(function=_gen_inverses), name='inverses') + fmap2target = pe.Node(niu.Merge(2), name='fmap2target', run_without_submitting=True) + inverses = pe.Node( + niu.Function(function=_gen_inverses), + name='inverses', + run_without_submitting=True, + ) - fmap_recon = pe.Node(ReconstructFieldmap(), name="fmap_recon") + fmap_recon = pe.Node(ReconstructFieldmap(), name="fmap_recon", mem_gb=1) workflow.connect([ (inputnode, fmap_select, [ diff --git a/fmriprep/workflows/bold/base.py b/fmriprep/workflows/bold/base.py index 14acfb8ab..c7289c6d0 100644 --- a/fmriprep/workflows/bold/base.py +++ b/fmriprep/workflows/bold/base.py @@ -31,14 +31,13 @@ """ import typing as ty -import nibabel as nb -import numpy as np from nipype.interfaces import utility as niu from nipype.pipeline import engine as pe from niworkflows.utils.connections import listify from ... import config from ...interfaces import DerivativesDataSink +from ...utils.bids import dismiss_echo from ...utils.misc import estimate_bold_mem_usage # BOLD workflows @@ -192,21 +191,6 @@ def init_bold_wf( mem_gb["largemem"], ) - functional_cache = {} - if config.execution.derivatives: - from fmriprep.utils.bids import collect_derivatives, extract_entities - - entities = extract_entities(bold_series) - - for deriv_dir in config.execution.derivatives: - functional_cache.update( - collect_derivatives( - derivatives_dir=deriv_dir, - entities=entities, - fieldmap_id=fieldmap_id, - ) - ) - workflow = Workflow(name=_get_wf_name(bold_file, "bold")) workflow.__postdesc__ = """\ All resamplings can be performed with *a single interpolation @@ -266,7 +250,7 @@ def init_bold_wf( bold_fit_wf = init_bold_fit_wf( bold_series=bold_series, - precomputed=functional_cache, + precomputed=precomputed, fieldmap_id=fieldmap_id, omp_nthreads=omp_nthreads, ) @@ -309,13 +293,6 @@ def init_bold_wf( fieldmap_id=fieldmap_id, omp_nthreads=omp_nthreads, ) - bold_anat_wf = init_bold_volumetric_resample_wf( - metadata=all_metadata[0], - fieldmap_id=fieldmap_id if not multiecho else None, - omp_nthreads=omp_nthreads, - name='bold_anat_wf', - ) - bold_anat_wf.inputs.inputnode.resolution = "native" workflow.connect([ (inputnode, bold_native_wf, [ @@ -323,13 +300,6 @@ def init_bold_wf( ("fmap_coeff", "inputnode.fmap_coeff"), ("fmap_id", "inputnode.fmap_id"), ]), - (inputnode, bold_anat_wf, [ - ("t1w_preproc", "inputnode.target_ref_file"), - ("t1w_mask", "inputnode.target_mask"), - ("fmap_ref", "inputnode.fmap_ref"), - ("fmap_coeff", "inputnode.fmap_coeff"), - ("fmap_id", "inputnode.fmap_id"), - ]), (bold_fit_wf, bold_native_wf, [ ("outputnode.coreg_boldref", "inputnode.boldref"), ("outputnode.bold_mask", "inputnode.bold_mask"), @@ -337,19 +307,10 @@ def init_bold_wf( ("outputnode.boldref2fmap_xfm", "inputnode.boldref2fmap_xfm"), ("outputnode.dummy_scans", "inputnode.dummy_scans"), ]), - (bold_fit_wf, bold_anat_wf, [ - ("outputnode.coreg_boldref", "inputnode.bold_ref_file"), - ("outputnode.boldref2fmap_xfm", "inputnode.boldref2fmap_xfm"), - ("outputnode.boldref2anat_xfm", "inputnode.boldref2anat_xfm"), - ]), - (bold_native_wf, bold_anat_wf, [ - ("outputnode.bold_minimal", "inputnode.bold_file"), - ("outputnode.motion_xfm", "inputnode.motion_xfm"), - ]), ]) # fmt:skip boldref_out = bool(nonstd_spaces.intersection(('func', 'run', 'bold', 'boldref', 'sbref'))) - boldref_out |= config.workflow.level == 'full' + boldref_out &= config.workflow.level == 'full' echos_out = multiecho and config.execution.me_output_echos if boldref_out or echos_out: @@ -381,7 +342,7 @@ def init_bold_wf( DerivativesDataSink( desc="t2scomp", datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name="ds_report_t2scomp", run_without_submitting=True, @@ -391,7 +352,7 @@ def init_bold_wf( DerivativesDataSink( desc="t2starhist", datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name="ds_report_t2star_hist", run_without_submitting=True, @@ -413,6 +374,36 @@ def init_bold_wf( if config.workflow.level == "resampling": return workflow + # Resample to anatomical space + bold_anat_wf = init_bold_volumetric_resample_wf( + metadata=all_metadata[0], + fieldmap_id=fieldmap_id if not multiecho else None, + omp_nthreads=omp_nthreads, + mem_gb=mem_gb, + jacobian='fmap-jacobian' not in config.workflow.ignore, + name='bold_anat_wf', + ) + bold_anat_wf.inputs.inputnode.resolution = "native" + + workflow.connect([ + (inputnode, bold_anat_wf, [ + ("t1w_preproc", "inputnode.target_ref_file"), + ("t1w_mask", "inputnode.target_mask"), + ("fmap_ref", "inputnode.fmap_ref"), + ("fmap_coeff", "inputnode.fmap_coeff"), + ("fmap_id", "inputnode.fmap_id"), + ]), + (bold_fit_wf, bold_anat_wf, [ + ("outputnode.coreg_boldref", "inputnode.bold_ref_file"), + ("outputnode.boldref2fmap_xfm", "inputnode.boldref2fmap_xfm"), + ("outputnode.boldref2anat_xfm", "inputnode.boldref2anat_xfm"), + ]), + (bold_native_wf, bold_anat_wf, [ + ("outputnode.bold_minimal", "inputnode.bold_file"), + ("outputnode.motion_xfm", "inputnode.motion_xfm"), + ]), + ]) # fmt:skip + # Full derivatives, including resampled BOLD series if nonstd_spaces.intersection(('anat', 'T1w')): ds_bold_t1_wf = init_ds_volumes_wf( @@ -446,6 +437,8 @@ def init_bold_wf( metadata=all_metadata[0], fieldmap_id=fieldmap_id if not multiecho else None, omp_nthreads=omp_nthreads, + mem_gb=mem_gb, + jacobian='fmap-jacobian' not in config.workflow.ignore, name='bold_std_wf', ) ds_bold_std_wf = init_ds_volumes_wf( @@ -519,31 +512,55 @@ def init_bold_wf( ]) # fmt:skip if config.workflow.cifti_output: - from .resampling import init_bold_fsLR_resampling_wf, init_bold_grayords_wf + from .resampling import ( + init_bold_fsLR_resampling_wf, + init_bold_grayords_wf, + init_goodvoxels_bold_mask_wf, + ) bold_MNI6_wf = init_bold_volumetric_resample_wf( metadata=all_metadata[0], fieldmap_id=fieldmap_id if not multiecho else None, omp_nthreads=omp_nthreads, + mem_gb=mem_gb, + jacobian='fmap-jacobian' not in config.workflow.ignore, name='bold_MNI6_wf', ) bold_fsLR_resampling_wf = init_bold_fsLR_resampling_wf( - estimate_goodvoxels=config.workflow.project_goodvoxels, grayord_density=config.workflow.cifti_output, omp_nthreads=omp_nthreads, mem_gb=mem_gb["resampled"], ) + if config.workflow.project_goodvoxels: + goodvoxels_bold_mask_wf = init_goodvoxels_bold_mask_wf(mem_gb["resampled"]) + + workflow.connect([ + (inputnode, goodvoxels_bold_mask_wf, [("anat_ribbon", "inputnode.anat_ribbon")]), + (bold_anat_wf, goodvoxels_bold_mask_wf, [ + ("outputnode.bold_file", "inputnode.bold_file"), + ]), + (goodvoxels_bold_mask_wf, bold_fsLR_resampling_wf, [ + ("outputnode.goodvoxels_mask", "inputnode.volume_roi"), + ]), + ]) # fmt:skip + + bold_fsLR_resampling_wf.__desc__ += """\ +A "goodvoxels" mask was applied during volume-to-surface sampling in fsLR space, +excluding voxels whose time-series have a locally high coefficient of variation. +""" + bold_grayords_wf = init_bold_grayords_wf( grayord_density=config.workflow.cifti_output, - mem_gb=mem_gb["resampled"], + mem_gb=1, repetition_time=all_metadata[0]["RepetitionTime"], ) ds_bold_cifti = pe.Node( DerivativesDataSink( base_directory=fmriprep_dir, + dismiss_entities=dismiss_echo(), space='fsLR', density=config.workflow.cifti_output, suffix='bold', @@ -583,7 +600,6 @@ def init_bold_wf( ("midthickness_fsLR", "inputnode.midthickness_fsLR"), ("sphere_reg_fsLR", "inputnode.sphere_reg_fsLR"), ("cortex_mask", "inputnode.cortex_mask"), - ("anat_ribbon", "inputnode.anat_ribbon"), ]), (bold_anat_wf, bold_fsLR_resampling_wf, [ ("outputnode.bold_file", "inputnode.bold_file"), @@ -615,7 +631,7 @@ def init_bold_wf( base_directory=fmriprep_dir, desc='confounds', suffix='timeseries', - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name="ds_confounds", run_without_submitting=True, diff --git a/fmriprep/workflows/bold/confounds.py b/fmriprep/workflows/bold/confounds.py index ff876d01d..5c5304be4 100644 --- a/fmriprep/workflows/bold/confounds.py +++ b/fmriprep/workflows/bold/confounds.py @@ -40,6 +40,7 @@ GatherConfounds, RenameACompCor, ) +from ...utils.bids import dismiss_echo def init_bold_confs_wf( @@ -145,6 +146,10 @@ def init_bold_confs_wf( Mask of brain edge voxels """ + from nireports.interfaces.nuisance import ( + CompCorVariancePlot, + ConfoundsCorrelationPlot, + ) from niworkflows.engine.workflows import LiterateWorkflow as Workflow from niworkflows.interfaces.confounds import ExpandModel, SpikeRegressors from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms @@ -153,10 +158,6 @@ def init_bold_confs_wf( from niworkflows.interfaces.nibabel import ApplyMask, Binarize from niworkflows.interfaces.patches import RobustACompCor as ACompCor from niworkflows.interfaces.patches import RobustTCompCor as TCompCor - from niworkflows.interfaces.plotting import ( - CompCorVariancePlot, - ConfoundsCorrelationPlot, - ) from niworkflows.interfaces.reportlets.masks import ROIsPlot from niworkflows.interfaces.utility import TSV2JSON, AddTSVHeader, DictMerge @@ -442,7 +443,7 @@ def init_bold_confs_wf( ) ds_report_bold_rois = pe.Node( - DerivativesDataSink(desc="rois", datatype="figures", dismiss_entities=("echo",)), + DerivativesDataSink(desc="rois", datatype="figures", dismiss_entities=dismiss_echo()), name="ds_report_bold_rois", run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB, @@ -461,7 +462,9 @@ def init_bold_confs_wf( ) ds_report_compcor = pe.Node( - DerivativesDataSink(desc="compcorvar", datatype="figures", dismiss_entities=("echo",)), + DerivativesDataSink( + desc="compcorvar", datatype="figures", dismiss_entities=dismiss_echo() + ), name="ds_report_compcor", run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB, @@ -473,7 +476,9 @@ def init_bold_confs_wf( name="conf_corr_plot", ) ds_report_conf_corr = pe.Node( - DerivativesDataSink(desc="confoundcorr", datatype="figures", dismiss_entities=("echo",)), + DerivativesDataSink( + desc="confoundcorr", datatype="figures", dismiss_entities=dismiss_echo() + ), name="ds_report_conf_corr", run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB, @@ -586,6 +591,7 @@ def _select_cols(table): (crowncompcor, mrg_cc_metadata, [("metadata_file", "in3")]), (mrg_cc_metadata, compcor_plot, [("out", "metadata_files")]), (compcor_plot, ds_report_compcor, [("out_file", "in_file")]), + (inputnode, conf_corr_plot, [("skip_vols", "ignore_initial_volumes")]), (concat, conf_corr_plot, [("confounds_file", "confounds_file"), (("confounds_file", _select_cols), "columns")]), (conf_corr_plot, ds_report_conf_corr, [("out_file", "in_file")]), @@ -672,8 +678,8 @@ def init_carpetplot_wf( tr=metadata["RepetitionTime"], confounds_list=[ ("global_signal", None, "GS"), - ("csf", None, "GSCSF"), - ("white_matter", None, "GSWM"), + ("csf", None, "CSF"), + ("white_matter", None, "WM"), ("std_dvars", None, "DVARS"), ("framewise_displacement", "mm", "FD"), ], @@ -683,7 +689,7 @@ def init_carpetplot_wf( ) ds_report_bold_conf = pe.Node( DerivativesDataSink( - desc="carpetplot", datatype="figures", extension="svg", dismiss_entities=("echo",) + desc="carpetplot", datatype="figures", extension="svg", dismiss_entities=dismiss_echo() ), name="ds_report_bold_conf", run_without_submitting=True, diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index 9ba1bc58c..f81285e62 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -31,7 +31,6 @@ from niworkflows.interfaces.header import ValidateImage from niworkflows.interfaces.nitransforms import ConcatenateXFMs from niworkflows.interfaces.utility import KeySelect -from niworkflows.utils.connections import listify from sdcflows.workflows.apply.correction import init_unwarp_wf from sdcflows.workflows.apply.registration import init_coeff2epi_wf @@ -83,7 +82,8 @@ def get_sbrefs( """ entities = extract_entities(bold_files) entities.pop("echo", None) - entities.update(suffix="sbref", extension=[".nii", ".nii.gz"], **entity_overrides) + entities.update(suffix="sbref", extension=[".nii", ".nii.gz"]) + entities.update(entity_overrides) return sorted( layout.get(return_type="file", **entities), @@ -204,6 +204,10 @@ def init_bold_fit_wf( layout = config.execution.layout + # Fitting operates on the shortest echo + # This could become more complicated in the future + bold_file = bold_series[0] + # Collect sbref files, sorted by EchoTime sbref_files = get_sbrefs( bold_series, @@ -211,9 +215,16 @@ def init_bold_fit_wf( layout=layout, ) - # Fitting operates on the shortest echo - # This could become more complicated in the future - bold_file = bold_series[0] + basename = os.path.basename(bold_file) + sbref_msg = f"No single-band-reference found for {basename}." + if sbref_files and "sbref" in config.workflow.ignore: + sbref_msg = f"Single-band reference file(s) found for {basename} and ignored." + sbref_files = [] + elif sbref_files: + sbref_msg = "Using single-band reference file(s) {}.".format( + ",".join([os.path.basename(sbf) for sbf in sbref_files]) + ) + config.loggers.workflow.info(sbref_msg) # Get metadata from BOLD file(s) entities = extract_entities(bold_series) @@ -287,7 +298,9 @@ def init_bold_fit_wf( name="hmcref_buffer", ) fmapref_buffer = pe.Node(niu.Function(function=_select_ref), name="fmapref_buffer") - hmc_buffer = pe.Node(niu.IdentityInterface(fields=["hmc_xforms"]), name="hmc_buffer") + hmc_buffer = pe.Node( + niu.IdentityInterface(fields=["hmc_xforms", "movpar_file", "rmsd_file"]), name="hmc_buffer" + ) fmapreg_buffer = pe.Node( niu.IdentityInterface(fields=["boldref2fmap_xfm"]), name="fmapreg_buffer" ) @@ -789,7 +802,7 @@ def init_bold_native_wf( # Slice-timing correction if run_stc: - bold_stc_wf = init_bold_stc_wf(name="bold_stc_wf", metadata=metadata) + bold_stc_wf = init_bold_stc_wf(metadata=metadata, mem_gb=mem_gb) workflow.connect([ (inputnode, bold_stc_wf, [("dummy_scans", "inputnode.skip_vols")]), (validate_bold, bold_stc_wf, [("out_file", "inputnode.bold_file")]), @@ -824,7 +837,12 @@ def init_bold_native_wf( ]) # fmt:skip # Resample to boldref - boldref_bold = pe.Node(ResampleSeries(), name="boldref_bold", n_procs=omp_nthreads) + boldref_bold = pe.Node( + ResampleSeries(jacobian="fmap-jacobian" not in config.workflow.ignore), + name="boldref_bold", + n_procs=omp_nthreads, + mem_gb=mem_gb["resampled"], + ) workflow.connect([ (inputnode, boldref_bold, [ @@ -839,7 +857,7 @@ def init_bold_native_wf( ]) # fmt:skip if fieldmap_id: - boldref_fmap = pe.Node(ReconstructFieldmap(inverse=[True]), name="boldref_fmap") + boldref_fmap = pe.Node(ReconstructFieldmap(inverse=[True]), name="boldref_fmap", mem_gb=1) workflow.connect([ (inputnode, boldref_fmap, [ ("boldref", "target_ref_file"), @@ -858,6 +876,7 @@ def init_bold_native_wf( joinsource="echo_index", joinfield=["bold_files"], name="join_echos", + run_without_submitting=True, ) # create optimal combination, adaptive T2* map diff --git a/fmriprep/workflows/bold/outputs.py b/fmriprep/workflows/bold/outputs.py index ee525d8c2..01acf2c21 100644 --- a/fmriprep/workflows/bold/outputs.py +++ b/fmriprep/workflows/bold/outputs.py @@ -35,6 +35,7 @@ from fmriprep import config from fmriprep.config import DEFAULT_MEMORY_MIN_GB from fmriprep.interfaces import DerivativesDataSink +from fmriprep.utils.bids import dismiss_echo def prepare_timing_parameters(metadata: dict): @@ -116,7 +117,7 @@ def prepare_timing_parameters(metadata: dict): slice_timing = timing_parameters.pop("SliceTiming", []) run_stc = len(slice_timing) > 1 and 'slicetiming' not in config.workflow.ignore - timing_parameters["SliceTimingCorrected"] = bool(run_stc) + timing_parameters["SliceTimingCorrected"] = run_stc if len(slice_timing) > 1: st = sorted(slice_timing) @@ -184,7 +185,7 @@ def init_func_fit_reports_wf( Template space and specifications """ - from niworkflows.interfaces.reportlets.registration import ( + from nireports.interfaces.reporting.base import ( SimpleBeforeAfterRPT as SimpleBeforeAfter, ) from sdcflows.interfaces.reportlets import FieldmapReportlet @@ -217,7 +218,7 @@ def init_func_fit_reports_wf( base_directory=output_dir, desc="summary", datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name="ds_report_summary", run_without_submitting=True, @@ -229,7 +230,7 @@ def init_func_fit_reports_wf( base_directory=output_dir, desc="validation", datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name="ds_report_validation", run_without_submitting=True, @@ -333,7 +334,7 @@ def init_func_fit_reports_wf( desc="fmapCoreg", suffix="bold", datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name="ds_sdcreg_report", ) @@ -355,7 +356,7 @@ def init_func_fit_reports_wf( desc="sdc", suffix="bold", datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name="ds_sdc_report", ) @@ -404,7 +405,7 @@ def init_func_fit_reports_wf( desc="coreg", suffix="bold", datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name="ds_epi_t1_report", ) @@ -446,7 +447,7 @@ def init_ds_boldref_wf( desc=desc, suffix="boldref", compress=True, - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name="ds_boldref", run_without_submitting=True, @@ -490,7 +491,7 @@ def init_ds_registration_wf( mode='image', suffix='xfm', extension='.txt', - dismiss_entities=('echo', 'part'), + dismiss_entities=dismiss_echo(["part"]), **{'from': source, 'to': dest}, ), name='ds_xform', @@ -535,7 +536,7 @@ def init_ds_hmc_wf( suffix="xfm", extension=".txt", compress=True, - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), **{"from": "orig", "to": "boldref"}, ), name="ds_xforms", @@ -593,7 +594,7 @@ def init_ds_bold_native_wf( desc='brain', suffix='mask', compress=True, - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name='ds_bold_mask', run_without_submitting=True, @@ -615,7 +616,7 @@ def init_ds_bold_native_wf( compress=True, SkullStripped=multiecho, TaskName=metadata.get('TaskName'), - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), **timing_parameters, ), name='ds_bold', @@ -640,7 +641,7 @@ def init_ds_bold_native_wf( space='boldref', suffix='T2starmap', compress=True, - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), **t2star_meta, ), name='ds_t2star_bold', @@ -726,7 +727,7 @@ def init_ds_volumes_wf( compress=True, SkullStripped=multiecho, TaskName=metadata.get('TaskName'), - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), **timing_parameters, ), name='ds_bold', @@ -769,7 +770,7 @@ def init_ds_volumes_wf( base_directory=output_dir, suffix='boldref', compress=True, - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name='ds_ref', run_without_submitting=True, @@ -781,7 +782,7 @@ def init_ds_volumes_wf( desc='brain', suffix='mask', compress=True, - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name='ds_mask', run_without_submitting=True, @@ -809,7 +810,7 @@ def init_ds_volumes_wf( base_directory=output_dir, suffix='T2starmap', compress=True, - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), **t2star_meta, ), name='ds_t2star_std', @@ -905,7 +906,7 @@ def init_bold_preproc_report_wf( base_directory=reportlets_dir, desc='preproc', datatype="figures", - dismiss_entities=("echo",), + dismiss_entities=dismiss_echo(), ), name='ds_report_bold', mem_gb=DEFAULT_MEMORY_MIN_GB, diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 09cd21349..bde2423f2 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -33,12 +33,11 @@ import os.path as op import typing as ty -import pkg_resources as pkgr from nipype.interfaces import c3, fsl from nipype.interfaces import utility as niu from nipype.pipeline import engine as pe -from ... import config +from ... import config, data from ...interfaces import DerivativesDataSink DEFAULT_MEMORY_MIN_GB = config.DEFAULT_MEMORY_MIN_GB @@ -583,12 +582,12 @@ def init_fsl_bbr_wf( ) FSLDIR = os.getenv('FSLDIR') - if FSLDIR: - flt_bbr.inputs.schedule = op.join(FSLDIR, 'etc/flirtsch/bbr.sch') + if FSLDIR and os.path.exists(schedule := op.join(FSLDIR, 'etc/flirtsch/bbr.sch')): + flt_bbr.inputs.schedule = schedule else: # Should mostly be hit while building docs LOGGER.warning("FSLDIR unset - using packaged BBR schedule") - flt_bbr.inputs.schedule = pkgr.resource_filename('fmriprep', 'data/flirtsch/bbr.sch') + flt_bbr.inputs.schedule = data.load('flirtsch/bbr.sch') # fmt:off workflow.connect([ (inputnode, wm_mask, [('t1w_dseg', 'in_seg')]), diff --git a/fmriprep/workflows/bold/resampling.py b/fmriprep/workflows/bold/resampling.py index 9a3b612e0..771f84037 100644 --- a/fmriprep/workflows/bold/resampling.py +++ b/fmriprep/workflows/bold/resampling.py @@ -44,6 +44,7 @@ from ...config import DEFAULT_MEMORY_MIN_GB from ...interfaces.workbench import MetricDilate, MetricMask, MetricResample +from ...utils.bids import dismiss_echo from .outputs import prepare_timing_parameters @@ -181,6 +182,7 @@ def select_target(subject_id, space): DerivativesDataSink( base_directory=output_dir, extension=".func.gii", + dismiss_entities=dismiss_echo(), TaskName=metadata.get('TaskName'), **timing_parameters, ), @@ -501,7 +503,6 @@ def _calc_lower_thr(in_stats): def init_bold_fsLR_resampling_wf( grayord_density: ty.Literal['91k', '170k'], - estimate_goodvoxels: bool, omp_nthreads: int, mem_gb: float, name: str = "bold_fsLR_resampling_wf", @@ -520,7 +521,6 @@ def init_bold_fsLR_resampling_wf( from fmriprep.workflows.bold.resampling import init_bold_fsLR_resampling_wf wf = init_bold_fsLR_resampling_wf( - estimate_goodvoxels=True, grayord_density='92k', omp_nthreads=1, mem_gb=1, @@ -530,9 +530,6 @@ def init_bold_fsLR_resampling_wf( ---------- grayord_density : :class:`str` Either ``"91k"`` or ``"170k"``, representing the total *grayordinates*. - estimate_goodvoxels : :class:`bool` - Calculate mask excluding voxels with a locally high coefficient of variation to - exclude from surface resampling omp_nthreads : :class:`int` Maximum number of threads an individual process may use mem_gb : :class:`float` @@ -544,35 +541,33 @@ def init_bold_fsLR_resampling_wf( ------ bold_file : :class:`str` Path to BOLD file resampled into T1 space - surfaces : :class:`list` of :class:`str` - Path to left and right hemisphere white, pial and midthickness GIFTI surfaces - morphometrics : :class:`list` of :class:`str` - Path to left and right hemisphere morphometric GIFTI surfaces, which must include thickness + white : :class:`list` of :class:`str` + Path to left and right hemisphere white matter GIFTI surfaces. + pial : :class:`list` of :class:`str` + Path to left and right hemisphere pial GIFTI surfaces. + midthickness : :class:`list` of :class:`str` + Path to left and right hemisphere midthickness GIFTI surfaces. + midthickness_fsLR : :class:`list` of :class:`str` + Path to left and right hemisphere midthickness GIFTI surfaces in fsLR space. sphere_reg_fsLR : :class:`list` of :class:`str` Path to left and right hemisphere sphere.reg GIFTI surfaces, mapping from subject to fsLR - anat_ribbon : :class:`str` - Path to mask of cortical ribbon in T1w space, for calculating goodvoxels + cortex_mask : :class:`list` of :class:`str` + Path to left and right hemisphere cortical masks. + volume_roi : :class:`str` or Undefined + Pre-calculated goodvoxels mask. Not required. Outputs ------- bold_fsLR : :class:`list` of :class:`str` Path to BOLD series resampled as functional GIFTI files in fsLR space - goodvoxels_mask : :class:`str` - Path to mask of voxels, excluding those with locally high coefficients of variation """ import templateflow.api as tf from niworkflows.engine.workflows import LiterateWorkflow as Workflow from niworkflows.interfaces.utility import KeySelect from smriprep import data as smriprep_data - from smriprep.interfaces.workbench import SurfaceResample - from fmriprep.interfaces.gifti import CreateROI - from fmriprep.interfaces.workbench import ( - MetricFillHoles, - MetricRemoveIslands, - VolumeToSurfaceMapping, - ) + from fmriprep.interfaces.workbench import VolumeToSurfaceMapping fslr_density = "32k" if grayord_density == "91k" else "59k" @@ -587,13 +582,13 @@ def init_bold_fsLR_resampling_wf( niu.IdentityInterface( fields=[ 'bold_file', - 'anat_ribbon', 'white', 'pial', 'midthickness', 'midthickness_fsLR', 'sphere_reg_fsLR', 'cortex_mask', + 'volume_roi', ] ), name='inputnode', @@ -612,7 +607,7 @@ def init_bold_fsLR_resampling_wf( ) outputnode = pe.Node( - niu.IdentityInterface(fields=['bold_fsLR', 'goodvoxels_mask']), + niu.IdentityInterface(fields=['bold_fsLR']), name='outputnode', ) @@ -646,8 +641,8 @@ def init_bold_fsLR_resampling_wf( ] atlases = smriprep_data.load_resource('atlases') select_surfaces.inputs.template_roi = [ - str(atlases / 'L.atlasroi.32k_fs_LR.shape.gii'), - str(atlases / 'R.atlasroi.32k_fs_LR.shape.gii'), + str(atlases / f'L.atlasroi.{fslr_density}_fs_LR.shape.gii'), + str(atlases / f'R.atlasroi.{fslr_density}_fs_LR.shape.gii'), ] # RibbonVolumeToSurfaceMapping.sh @@ -661,12 +656,14 @@ def init_bold_fsLR_resampling_wf( metric_dilate = pe.Node( MetricDilate(distance=10, nearest=True), name="metric_dilate", + mem_gb=1, n_procs=omp_nthreads, ) mask_native = pe.Node(MetricMask(), name="mask_native") resample_to_fsLR = pe.Node( MetricResample(method='ADAP_BARY_AREA', area_surfs=True), name="resample_to_fsLR", + mem_gb=1, n_procs=omp_nthreads, ) # ... line 89 @@ -685,6 +682,7 @@ def init_bold_fsLR_resampling_wf( # Resample BOLD to native surface, dilate and mask (inputnode, volume_to_surface, [ ('bold_file', 'volume_file'), + ('volume_roi', 'volume_roi'), ]), (select_surfaces, volume_to_surface, [ ('midthickness', 'surface_file'), @@ -711,27 +709,6 @@ def init_bold_fsLR_resampling_wf( (joinnode, outputnode, [('bold_fsLR', 'bold_fsLR')]), ]) # fmt:skip - if estimate_goodvoxels: - workflow.__desc__ += """\ -A "goodvoxels" mask was applied during volume-to-surface sampling in fsLR space, -excluding voxels whose time-series have a locally high coefficient of variation. -""" - - goodvoxels_bold_mask_wf = init_goodvoxels_bold_mask_wf(mem_gb) - - workflow.connect([ - (inputnode, goodvoxels_bold_mask_wf, [ - ("bold_file", "inputnode.bold_file"), - ("anat_ribbon", "inputnode.anat_ribbon"), - ]), - (goodvoxels_bold_mask_wf, volume_to_surface, [ - ("outputnode.goodvoxels_mask", "volume_roi"), - ]), - (goodvoxels_bold_mask_wf, outputnode, [ - ("outputnode.goodvoxels_mask", "goodvoxels_mask"), - ]), - ]) # fmt:skip - return workflow @@ -812,6 +789,7 @@ def init_bold_grayords_wf( grayordinates=grayord_density, ), name="gen_cifti", + mem_gb=mem_gb, ) workflow.connect([ diff --git a/fmriprep/workflows/bold/stc.py b/fmriprep/workflows/bold/stc.py index ffdcf7b22..39573d8cf 100644 --- a/fmriprep/workflows/bold/stc.py +++ b/fmriprep/workflows/bold/stc.py @@ -53,7 +53,12 @@ def _pre_run_hook(self, runtime): return runtime -def init_bold_stc_wf(metadata: dict, name='bold_stc_wf'): +def init_bold_stc_wf( + *, + mem_gb: dict, + metadata: dict, + name='bold_stc_wf', +): """ Create a workflow for :abbr:`STC (slice-timing correction)`. @@ -119,6 +124,7 @@ def init_bold_stc_wf(metadata: dict, name='bold_stc_wf'): slice_encoding_direction=metadata.get('SliceEncodingDirection', 'k'), tzero=tzero, ), + mem_gb=mem_gb['filesize'] * 2, name='slice_timing_correction', ) diff --git a/fmriprep/workflows/bold/t2s.py b/fmriprep/workflows/bold/t2s.py index b00c81c95..1462023d8 100644 --- a/fmriprep/workflows/bold/t2s.py +++ b/fmriprep/workflows/bold/t2s.py @@ -168,10 +168,10 @@ def init_t2s_reporting_wf(name: str = 't2s_reporting_wf'): a before/after figure comparing the reference BOLD image and T2\* map """ from nipype.pipeline import engine as pe - from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms - from niworkflows.interfaces.reportlets.registration import ( + from nireports.interfaces.reporting.base import ( SimpleBeforeAfterRPT as SimpleBeforeAfter, ) + from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms workflow = pe.Workflow(name=name) diff --git a/fmriprep/workflows/bold/tests/test_base.py b/fmriprep/workflows/bold/tests/test_base.py new file mode 100644 index 000000000..93035c640 --- /dev/null +++ b/fmriprep/workflows/bold/tests/test_base.py @@ -0,0 +1,77 @@ +from pathlib import Path + +import nibabel as nb +import numpy as np +import pytest +from nipype.pipeline.engine.utils import generate_expanded_graph +from niworkflows.utils.testing import generate_bids_skeleton + +from .... import config +from ...tests import mock_config +from ...tests.test_base import BASE_LAYOUT +from ..base import init_bold_wf + + +@pytest.fixture(scope="module", autouse=True) +def _quiet_logger(): + import logging + + logger = logging.getLogger("nipype.workflow") + old_level = logger.getEffectiveLevel() + logger.setLevel(logging.ERROR) + yield + logger.setLevel(old_level) + + +@pytest.fixture(scope="module") +def bids_root(tmp_path_factory): + base = tmp_path_factory.mktemp("boldbase") + bids_dir = base / "bids" + generate_bids_skeleton(bids_dir, BASE_LAYOUT) + yield bids_dir + + +@pytest.mark.parametrize("task", ["rest", "nback"]) +@pytest.mark.parametrize("fieldmap_id", ["phasediff", None]) +@pytest.mark.parametrize("freesurfer", [False, True]) +@pytest.mark.parametrize("level", ["minimal", "resampling", "full"]) +def test_bold_wf( + bids_root: Path, + tmp_path: Path, + task: str, + fieldmap_id: str | None, + freesurfer: bool, + level: str, +): + """Test as many combinations of precomputed files and input + configurations as possible.""" + output_dir = tmp_path / 'output' + output_dir.mkdir() + + img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4)) + + if task == 'rest': + bold_series = [ + str(bids_root / 'sub-01' / 'func' / 'sub-01_task-rest_run-1_bold.nii.gz'), + ] + elif task == 'nback': + bold_series = [ + str(bids_root / 'sub-01' / 'func' / f'sub-01_task-nback_echo-{i}_bold.nii.gz') + for i in range(1, 4) + ] + + # The workflow will attempt to read file headers + for path in bold_series: + img.to_filename(path) + + with mock_config(bids_dir=bids_root): + config.workflow.level = level + config.workflow.run_reconall = freesurfer + wf = init_bold_wf( + bold_series=bold_series, + fieldmap_id=fieldmap_id, + precomputed={}, + ) + + flatgraph = wf._create_flat_graph() + generate_expanded_graph(flatgraph) diff --git a/fmriprep/workflows/bold/tests/test_fit.py b/fmriprep/workflows/bold/tests/test_fit.py new file mode 100644 index 000000000..382c48e7a --- /dev/null +++ b/fmriprep/workflows/bold/tests/test_fit.py @@ -0,0 +1,172 @@ +from pathlib import Path + +import nibabel as nb +import numpy as np +import pytest +from nipype.pipeline.engine.utils import generate_expanded_graph +from niworkflows.utils.testing import generate_bids_skeleton + +from .... import config +from ...tests import mock_config +from ...tests.test_base import BASE_LAYOUT +from ..fit import init_bold_fit_wf, init_bold_native_wf + + +@pytest.fixture(scope="module", autouse=True) +def _quiet_logger(): + import logging + + logger = logging.getLogger("nipype.workflow") + old_level = logger.getEffectiveLevel() + logger.setLevel(logging.ERROR) + yield + logger.setLevel(old_level) + + +@pytest.fixture(scope="module") +def bids_root(tmp_path_factory): + base = tmp_path_factory.mktemp("boldfit") + bids_dir = base / "bids" + generate_bids_skeleton(bids_dir, BASE_LAYOUT) + yield bids_dir + + +def _make_params( + have_hmcref: bool = True, + have_coregref: bool = True, + have_hmc_xfms: bool = True, + have_boldref2fmap_xfm: bool = True, + have_boldref2anat_xfm: bool = True, +): + return ( + have_hmcref, + have_coregref, + have_hmc_xfms, + have_boldref2anat_xfm, + have_boldref2fmap_xfm, + ) + + +@pytest.mark.parametrize("task", ["rest", "nback"]) +@pytest.mark.parametrize("fieldmap_id", ["phasediff", None]) +@pytest.mark.parametrize( + ( + 'have_hmcref', + 'have_coregref', + 'have_hmc_xfms', + 'have_boldref2fmap_xfm', + 'have_boldref2anat_xfm', + ), + [ + (True, True, True, True, True), + (False, False, False, False, False), + _make_params(have_hmcref=False), + _make_params(have_hmc_xfms=False), + _make_params(have_coregref=False), + _make_params(have_coregref=False, have_boldref2fmap_xfm=False), + _make_params(have_boldref2anat_xfm=False), + ], +) +def test_bold_fit_precomputes( + bids_root: Path, + tmp_path: Path, + task: str, + fieldmap_id: str | None, + have_hmcref: bool, + have_coregref: bool, + have_hmc_xfms: bool, + have_boldref2fmap_xfm: bool, + have_boldref2anat_xfm: bool, +): + """Test as many combinations of precomputed files and input + configurations as possible.""" + output_dir = tmp_path / 'output' + output_dir.mkdir() + + img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4)) + + if task == 'rest': + bold_series = [ + str(bids_root / 'sub-01' / 'func' / 'sub-01_task-rest_run-1_bold.nii.gz'), + ] + elif task == 'nback': + bold_series = [ + str(bids_root / 'sub-01' / 'func' / f'sub-01_task-nback_echo-{i}_bold.nii.gz') + for i in range(1, 4) + ] + + # The workflow will attempt to read file headers + for path in bold_series: + img.to_filename(path) + + dummy_nifti = str(tmp_path / 'dummy.nii') + dummy_affine = str(tmp_path / 'dummy.txt') + img.to_filename(dummy_nifti) + np.savetxt(dummy_affine, np.eye(4)) + + # Construct precomputed files + precomputed = {'transforms': {}} + if have_hmcref: + precomputed['hmc_boldref'] = dummy_nifti + if have_coregref: + precomputed['coreg_boldref'] = dummy_nifti + if have_hmc_xfms: + precomputed['transforms']['hmc'] = dummy_affine + if have_boldref2anat_xfm: + precomputed['transforms']['boldref2anat'] = dummy_affine + if have_boldref2fmap_xfm: + precomputed['transforms']['boldref2fmap'] = dummy_affine + + with mock_config(bids_dir=bids_root): + wf = init_bold_fit_wf( + bold_series=bold_series, + precomputed=precomputed, + fieldmap_id=fieldmap_id, + omp_nthreads=1, + ) + + flatgraph = wf._create_flat_graph() + generate_expanded_graph(flatgraph) + + +@pytest.mark.parametrize("task", ["rest", "nback"]) +@pytest.mark.parametrize("fieldmap_id", ["phasediff", None]) +@pytest.mark.parametrize("run_stc", [True, False]) +def test_bold_native_precomputes( + bids_root: Path, + tmp_path: Path, + task: str, + fieldmap_id: str | None, + run_stc: bool, +): + """Test as many combinations of precomputed files and input + configurations as possible.""" + output_dir = tmp_path / 'output' + output_dir.mkdir() + + img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4)) + + if task == 'rest': + bold_series = [ + str(bids_root / 'sub-01' / 'func' / 'sub-01_task-rest_run-1_bold.nii.gz'), + ] + elif task == 'nback': + bold_series = [ + str(bids_root / 'sub-01' / 'func' / f'sub-01_task-nback_echo-{i}_bold.nii.gz') + for i in range(1, 4) + ] + + # The workflow will attempt to read file headers + for path in bold_series: + img.to_filename(path) + + with mock_config(bids_dir=bids_root): + config.workflow.ignore = ['slicetiming'] if not run_stc else [] + wf = init_bold_native_wf( + bold_series=bold_series, + fieldmap_id=fieldmap_id, + omp_nthreads=1, + ) + + flatgraph = wf._create_flat_graph() + generate_expanded_graph(flatgraph) diff --git a/fmriprep/workflows/tests/__init__.py b/fmriprep/workflows/tests/__init__.py index bf1ce32d4..c246ccd13 100644 --- a/fmriprep/workflows/tests/__init__.py +++ b/fmriprep/workflows/tests/__init__.py @@ -27,12 +27,13 @@ from pathlib import Path from tempfile import mkdtemp -from pkg_resources import resource_filename as pkgrf from toml import loads +from ... import data + @contextmanager -def mock_config(): +def mock_config(bids_dir=None): """Create a mock config for documentation and testing purposes.""" from ... import config @@ -40,8 +41,7 @@ def mock_config(): if not _old_fs: os.environ['FREESURFER_HOME'] = mkdtemp() - filename = Path(pkgrf('fmriprep', 'data/tests/config.toml')) - settings = loads(filename.read_text()) + settings = loads(data.load.readable('tests/config.toml').read_text()) for sectionname, configs in settings.items(): if sectionname != 'environment': section = getattr(config, sectionname) @@ -51,9 +51,13 @@ def mock_config(): config.loggers.init() config.init_spaces() + bids_dir = bids_dir or data.load('tests/ds000005').absolute() + config.execution.work_dir = Path(mkdtemp()) - config.execution.bids_dir = Path(pkgrf('fmriprep', 'data/tests/ds000005')).absolute() + config.execution.bids_dir = bids_dir config.execution.fmriprep_dir = Path(mkdtemp()) + config.execution.bids_database_dir = None + config.execution._layout = None config.execution.init() yield diff --git a/fmriprep/workflows/tests/test_base.py b/fmriprep/workflows/tests/test_base.py index 70dcd5590..027609459 100644 --- a/fmriprep/workflows/tests/test_base.py +++ b/fmriprep/workflows/tests/test_base.py @@ -1,23 +1,59 @@ from copy import deepcopy +from pathlib import Path +from unittest.mock import patch import bids +import nibabel as nb +import numpy as np +import pytest +from nipype.pipeline.engine.utils import generate_expanded_graph from niworkflows.utils.testing import generate_bids_skeleton from sdcflows.fieldmaps import clear_registry from sdcflows.utils.wrangler import find_estimators -from ..base import get_estimator +from ... import config +from ..base import get_estimator, init_fmriprep_wf +from ..tests import mock_config BASE_LAYOUT = { "01": { - "anat": [{"suffix": "T1w"}], + "anat": [ + {"run": 1, "suffix": "T1w"}, + {"run": 2, "suffix": "T1w"}, + {"suffix": "T2w"}, + ], "func": [ - { - "task": "rest", - "run": i, - "suffix": "bold", - "metadata": {"PhaseEncodingDirection": "j", "TotalReadoutTime": 0.6}, - } - for i in range(1, 3) + *( + { + "task": "rest", + "run": i, + "suffix": suffix, + "metadata": { + "RepetitionTime": 2.0, + "PhaseEncodingDirection": "j", + "TotalReadoutTime": 0.6, + "EchoTime": 0.03, + "SliceTiming": [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8], + }, + } + for suffix in ("bold", "sbref") + for i in range(1, 3) + ), + *( + { + "task": "nback", + "echo": i, + "suffix": "bold", + "metadata": { + "RepetitionTime": 2.0, + "PhaseEncodingDirection": "j", + "TotalReadoutTime": 0.6, + "EchoTime": 0.015 * i, + "SliceTiming": [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8], + }, + } + for i in range(1, 4) + ), ], "fmap": [ {"suffix": "phasediff", "metadata": {"EchoTime1": 0.005, "EchoTime2": 0.007}}, @@ -37,13 +73,172 @@ } +@pytest.fixture(scope="module", autouse=True) +def _quiet_logger(): + import logging + + logger = logging.getLogger("nipype.workflow") + old_level = logger.getEffectiveLevel() + logger.setLevel(logging.ERROR) + yield + logger.setLevel(old_level) + + +@pytest.fixture(autouse=True) +def _reset_sdcflows_registry(): + yield + clear_registry() + + +@pytest.fixture(scope="module") +def bids_root(tmp_path_factory): + base = tmp_path_factory.mktemp("base") + bids_dir = base / "bids" + generate_bids_skeleton(bids_dir, BASE_LAYOUT) + + img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4)) + + for bold_path in bids_dir.glob('sub-01/*/*.nii.gz'): + img.to_filename(bold_path) + + yield bids_dir + + +def _make_params( + bold2t1w_init: str = "register", + use_bbr: bool | None = None, + dummy_scans: int | None = None, + me_output_echos: bool = False, + medial_surface_nan: bool = False, + project_goodvoxels: bool = False, + cifti_output: bool | str = False, + run_msmsulc: bool = True, + skull_strip_t1w: str = "auto", + use_syn_sdc: str | bool = False, + force_syn: bool = False, + freesurfer: bool = True, + ignore: list[str] = None, + bids_filters: dict = None, +): + if ignore is None: + ignore = [] + if bids_filters is None: + bids_filters = {} + return ( + bold2t1w_init, + use_bbr, + dummy_scans, + me_output_echos, + medial_surface_nan, + project_goodvoxels, + cifti_output, + run_msmsulc, + skull_strip_t1w, + use_syn_sdc, + force_syn, + freesurfer, + ignore, + bids_filters, + ) + + +@pytest.mark.parametrize("level", ["minimal", "resampling", "full"]) +@pytest.mark.parametrize("anat_only", [False, True]) +@pytest.mark.parametrize( + ( + "bold2t1w_init", + "use_bbr", + "dummy_scans", + "me_output_echos", + "medial_surface_nan", + "project_goodvoxels", + "cifti_output", + "run_msmsulc", + "skull_strip_t1w", + "use_syn_sdc", + "force_syn", + "freesurfer", + "ignore", + "bids_filters", + ), + [ + _make_params(), + _make_params(bold2t1w_init="header"), + _make_params(use_bbr=True), + _make_params(use_bbr=False), + _make_params(bold2t1w_init="header", use_bbr=True), + # Currently disabled + # _make_params(bold2t1w_init="header", use_bbr=False), + _make_params(dummy_scans=2), + _make_params(me_output_echos=True), + _make_params(medial_surface_nan=True), + _make_params(cifti_output='91k'), + _make_params(cifti_output='91k', project_goodvoxels=True), + _make_params(cifti_output='91k', project_goodvoxels=True, run_msmsulc=False), + _make_params(cifti_output='91k', run_msmsulc=False), + _make_params(skull_strip_t1w='force'), + _make_params(skull_strip_t1w='skip'), + _make_params(use_syn_sdc='warn', force_syn=True, ignore=['fieldmaps']), + _make_params(freesurfer=False), + _make_params(freesurfer=False, use_bbr=True), + _make_params(freesurfer=False, use_bbr=False), + # Currently unsupported: + # _make_params(freesurfer=False, bold2t1w_init="header"), + # _make_params(freesurfer=False, bold2t1w_init="header", use_bbr=True), + # _make_params(freesurfer=False, bold2t1w_init="header", use_bbr=False), + # Regression test for gh-3154: + _make_params(bids_filters={'sbref': {'suffix': 'sbref'}}), + ], +) +def test_init_fmriprep_wf( + bids_root: Path, + tmp_path: Path, + level: str, + anat_only: bool, + bold2t1w_init: str, + use_bbr: bool | None, + dummy_scans: int | None, + me_output_echos: bool, + medial_surface_nan: bool, + project_goodvoxels: bool, + cifti_output: bool | str, + run_msmsulc: bool, + skull_strip_t1w: str, + use_syn_sdc: str | bool, + force_syn: bool, + freesurfer: bool, + ignore: list[str], + bids_filters: dict, +): + with mock_config(bids_dir=bids_root): + config.workflow.level = level + config.workflow.anat_only = anat_only + config.workflow.bold2t1w_init = bold2t1w_init + config.workflow.use_bbr = use_bbr + config.workflow.dummy_scans = dummy_scans + config.execution.me_output_echos = me_output_echos + config.workflow.medial_surface_nan = medial_surface_nan + config.workflow.project_goodvoxels = project_goodvoxels + config.workflow.run_msmsulc = run_msmsulc + config.workflow.skull_strip_t1w = skull_strip_t1w + config.workflow.cifti_output = cifti_output + config.workflow.run_reconall = freesurfer + config.workflow.ignore = ignore + with patch.dict('fmriprep.config.execution.bids_filters', bids_filters): + wf = init_fmriprep_wf() + + generate_expanded_graph(wf._create_flat_graph()) + + def test_get_estimator_none(tmp_path): bids_dir = tmp_path / "bids" # No IntendedFors/B0Fields generate_bids_skeleton(bids_dir, BASE_LAYOUT) layout = bids.BIDSLayout(bids_dir) - bold_files = sorted(layout.get(suffix='bold', extension='.nii.gz', return_type='file')) + bold_files = sorted( + layout.get(suffix='bold', task='rest', extension='.nii.gz', return_type='file') + ) assert get_estimator(layout, bold_files[0]) == () assert get_estimator(layout, bold_files[1]) == () @@ -65,11 +260,12 @@ def test_get_estimator_b0field_and_intendedfor(tmp_path): layout = bids.BIDSLayout(bids_dir) _ = find_estimators(layout=layout, subject='01') - bold_files = sorted(layout.get(suffix='bold', extension='.nii.gz', return_type='file')) + bold_files = sorted( + layout.get(suffix='bold', task='rest', extension='.nii.gz', return_type='file') + ) assert get_estimator(layout, bold_files[0]) == ('epi',) assert get_estimator(layout, bold_files[1]) == ('auto_00000',) - clear_registry() def test_get_estimator_overlapping_specs(tmp_path): @@ -92,12 +288,13 @@ def test_get_estimator_overlapping_specs(tmp_path): layout = bids.BIDSLayout(bids_dir) _ = find_estimators(layout=layout, subject='01') - bold_files = sorted(layout.get(suffix='bold', extension='.nii.gz', return_type='file')) + bold_files = sorted( + layout.get(suffix='bold', task='rest', extension='.nii.gz', return_type='file') + ) # B0Fields take precedence assert get_estimator(layout, bold_files[0]) == ('epi',) assert get_estimator(layout, bold_files[1]) == ('epi',) - clear_registry() def test_get_estimator_multiple_b0fields(tmp_path): @@ -116,9 +313,10 @@ def test_get_estimator_multiple_b0fields(tmp_path): layout = bids.BIDSLayout(bids_dir) _ = find_estimators(layout=layout, subject='01') - bold_files = sorted(layout.get(suffix='bold', extension='.nii.gz', return_type='file')) + bold_files = sorted( + layout.get(suffix='bold', task='rest', extension='.nii.gz', return_type='file') + ) # Always get an iterable; don't care if it's a list or tuple assert get_estimator(layout, bold_files[0]) == ['epi', 'phasediff'] assert get_estimator(layout, bold_files[1]) == ('epi',) - clear_registry() diff --git a/pyproject.toml b/pyproject.toml index 520a34c84..0fb9a03c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] license = {file = "LICENSE"} requires-python = ">=3.10" @@ -21,7 +22,7 @@ dependencies = [ "looseversion", "nibabel >= 4.0.1", "nipype >= 1.8.5", - "nireports >= 23.1.0", + "nireports @ git+https://github.com/nipreps/nireports.git@main", "nitime", "nitransforms >= 21.0.0", "niworkflows @ git+https://github.com/nipreps/niworkflows.git@master", @@ -31,9 +32,9 @@ dependencies = [ "psutil >= 5.4", "pybids >= 0.15.2", "requests", - "sdcflows @ git+https://github.com/nipreps/sdcflows.git@master", + "sdcflows @ git+https://github.com/nipreps/sdcflows.git@master", "smriprep @ git+https://github.com/nipreps/smriprep.git@master", - "tedana >= 0.0.9", + "tedana >= 23.0.2", "templateflow >= 23.0.0", "toml", "codecarbon", diff --git a/requirements.txt b/requirements.txt index fd7e75174..60ea70687 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,11 +23,11 @@ attrs==23.1.0 # sdcflows bids-validator==1.13.1 # via pybids -bokeh==2.2.3 +bokeh==3.3.1 # via tedana boto==2.49.0 # via datalad -certifi==2023.7.22 +certifi==2023.11.17 # via # requests # sentry-sdk @@ -51,12 +51,14 @@ codecarbon==2.3.1 # fmriprep # fmriprep (pyproject.toml) contourpy==1.2.0 - # via matplotlib -cryptography==41.0.5 + # via + # bokeh + # matplotlib +cryptography==41.0.7 # via secretstorage cycler==0.12.1 # via matplotlib -datalad==0.19.3 +datalad==0.19.4 # via # datalad-next # datalad-osf @@ -75,7 +77,7 @@ fasteners==0.19 # via datalad filelock==3.13.1 # via nipype -fonttools==4.44.0 +fonttools==4.45.0 # via matplotlib formulaic==0.5.2 # via pybids @@ -85,20 +87,20 @@ greenlet==3.0.1 # via sqlalchemy h5py==3.10.0 # via nitransforms -humanize==4.8.0 +humanize==4.9.0 # via # datalad # datalad-next idna==3.4 # via requests -imageio==2.32.0 +imageio==2.33.0 # via scikit-image -importlib-metadata==6.8.0 +importlib-metadata==7.0.0 # via keyring importlib-resources==6.1.1 # via + # nireports # niworkflows - # sdcflows # templateflow indexed-gzip==1.8.7 # via smriprep @@ -116,11 +118,10 @@ jeepney==0.8.0 # via # keyring # secretstorage -jinja2==3.0.1 +jinja2==3.1.2 # via # bokeh # niworkflows - # tedana joblib==1.3.2 # via # nilearn @@ -152,7 +153,7 @@ mapca==0.0.4 # via tedana markupsafe==2.1.3 # via jinja2 -matplotlib==3.8.1 +matplotlib==3.8.2 # via # nireports # nitime @@ -160,8 +161,10 @@ matplotlib==3.8.1 # seaborn # smriprep # tedana -migas==0.3.0 - # via fmriprep +migas==0.4.0 + # via + # fmriprep + # sdcflows more-itertools==10.1.0 # via jaraco-classes msgpack==1.0.7 @@ -199,7 +202,7 @@ nipype==1.8.6 # niworkflows # sdcflows # smriprep -nireports==23.1.0 +nireports==23.2.0 # via # fmriprep # fmriprep (pyproject.toml) @@ -213,7 +216,7 @@ nitransforms==23.0.1 # fmriprep (pyproject.toml) # niworkflows # sdcflows -niworkflows==1.8.1 +niworkflows==1.10.0 # via # fmriprep # fmriprep (pyproject.toml) @@ -267,6 +270,7 @@ packaging==23.2 # smriprep pandas==2.1.3 # via + # bokeh # codecarbon # fmriprep # fmriprep (pyproject.toml) @@ -277,17 +281,17 @@ pandas==2.1.3 # pybids # seaborn # tedana -patool==1.15.0 +patool==2.0.0 # via datalad -pillow==10.0.1 +pillow==10.1.0 # via # bokeh # imageio # matplotlib # scikit-image -platformdirs==4.0.0 +platformdirs==4.1.0 # via datalad -prometheus-client==0.18.0 +prometheus-client==0.19.0 # via codecarbon prov==2.0.0 # via nipype @@ -298,7 +302,7 @@ psutil==5.9.6 # fmriprep (pyproject.toml) py-cpuinfo==9.0.0 # via codecarbon -pybids==0.16.3 +pybids==0.16.4 # via # fmriprep # fmriprep (pyproject.toml) @@ -321,12 +325,11 @@ pyparsing==3.1.1 python-dateutil==2.8.2 # via # arrow - # bokeh # matplotlib # nipype # pandas # prov -python-gitlab==4.1.1 +python-gitlab==4.2.0 # via datalad pytz==2023.3.post1 # via @@ -365,7 +368,7 @@ scikit-learn==1.3.2 # mapca # nilearn # tedana -scipy==1.11.3 +scipy==1.11.4 # via # formulaic # mapca @@ -379,7 +382,7 @@ scipy==1.11.3 # scikit-learn # sdcflows # tedana -sdcflows==2.5.1 +sdcflows==2.8.0 # via # fmriprep # fmriprep (pyproject.toml) @@ -389,7 +392,7 @@ seaborn==0.13.0 # niworkflows secretstorage==3.3.3 # via keyring -sentry-sdk==1.35.0 +sentry-sdk==1.39.0 # via fmriprep simplejson==3.19.2 # via nipype @@ -399,7 +402,7 @@ six==1.16.0 # isodate # osfclient # python-dateutil -smriprep==0.12.2 +smriprep==0.13.2 # via # fmriprep # fmriprep (pyproject.toml) @@ -409,7 +412,7 @@ svgutils==0.3.4 # via # nireports # niworkflows -tedana==23.0.1 +tedana==23.0.2 # via # fmriprep # fmriprep (pyproject.toml) @@ -430,7 +433,7 @@ tifffile==2023.9.26 toml==0.10.2 # via # fmriprep - # fmriprep (pyproject.toml) + # sdcflows tornado==6.3.3 # via bokeh tqdm==4.66.1 @@ -449,7 +452,6 @@ types-python-dateutil==2.8.19.14 # via arrow typing-extensions==4.8.0 # via - # bokeh # datalad # formulaic # sqlalchemy @@ -463,5 +465,7 @@ urllib3==2.1.0 # sentry-sdk wrapt==1.16.0 # via formulaic +xyzservices==2023.10.1 + # via bokeh zipp==3.17.0 # via importlib-metadata