From 48d7228e72d123c5856e45920c6417827c6c0f03 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Mon, 14 Oct 2024 16:48:32 -0400 Subject: [PATCH 1/7] TESTING: Build workflow across different conditions --- nibabies/data/tests/config.toml | 67 +++--- nibabies/workflows/tests/__init__.py | 71 ++++++ nibabies/workflows/tests/test_base.py | 334 ++++++++++++++++++++++++++ 3 files changed, 433 insertions(+), 39 deletions(-) create mode 100644 nibabies/workflows/tests/__init__.py create mode 100644 nibabies/workflows/tests/test_base.py diff --git a/nibabies/data/tests/config.toml b/nibabies/data/tests/config.toml index 9458cddf..9e9fba13 100644 --- a/nibabies/data/tests/config.toml +++ b/nibabies/data/tests/config.toml @@ -1,74 +1,57 @@ [environment] cpu_count = 8 -exec_docker_version = "20.10.12" -exec_env = "nibabies-docker" -free_mem = 0.4 +exec_env = "posix" +free_mem = 2.2 overcommit_policy = "heuristic" overcommit_limit = "50%" -nipype_version = "1.6.1" -templateflow_version = "0.7.2" -version = "21.1.0" +nipype_version = "1.5.0" +templateflow_version = "24.2.2" +version = "24.1.0" [execution] -bids_dir = "/data" -bids_database_dir = "/tmp/bids_db" -bids_description_hash = "c47e9ebb943ca662556808b2aeac3f6c8bb2a242696c32850c64ec47aba80d9e" +bids_dir = "ds000005/" +bids_description_hash = "5d42e27751bbc884eca87cb4e62b9a0cca0cd86f8e578747fe89b77e6c5b21e5" boilerplate_only = false -sloppy = true -debug = [] -derivatives = [ "/opt/derivatives/precomputed",] fs_license_file = "/opt/freesurfer/license.txt" -fs_subjects_dir = "/opt/subjects" -layout = "BIDS Layout: .../data | Subjects: 1 | Sessions: 1 | Runs: 1" -log_dir = "/tmp/logs" -log_level = 20 +fs_subjects_dir = "/opt/freesurfer/subjects" +log_dir = "/tmp/fmriprep/logs" +log_level = 40 low_mem = false md_only_boilerplate = false -nibabies_dir = "/out" -notrack = false -output_dir = "/out" -me_output_echos = false -output_layout = "bids" -output_spaces = "MNIInfant:cohort-1:res-native" +notrack = true +output_dir = "/tmp" +output_spaces = "MNIInfant:cohort-1" reports_only = false -run_uuid = "20220323-202555_01a7d80d-7ff4-4b13-a99c-ec399045e9ff" -segmentation_atlases_dir = "/opt/segmentations" +run_uuid = "20200306-105302_d365772b-fd60-4741-a722-372c2f558b50" participant_label = [ "01",] -templateflow_home = "/home/nibabies/.cache/templateflow" -work_dir = "/scratch" +templateflow_home = "~/.cache/templateflow" +work_dir = "work/" write_graph = false [workflow] anat_only = false -bold2t1w_dof = 6 -bold2t1w_init = "register" -cifti_output = false +bold2anat_dof = 6 fd_radius = 45 fmap_bspline = false force_syn = false hires = true -ignore = [ "slicetiming",] +ignore = [] longitudinal = false medial_surface_nan = false project_goodvoxels = false regressors_all_comps = false regressors_dvars_th = 1.5 regressors_fd_th = 0.5 -run_reconall = true skull_strip_fixed_seed = false skull_strip_template = "UNCInfant:cohort-1" -skull_strip_t1w = "force" -slice_time_ref = 0.5 -spaces = "MNIInfant:cohort-1:res-native MNIInfant:cohort-1" -use_bbr = false -use_syn_sdc = false +surface_recon_method = "auto" [nipype] crashfile_format = "txt" get_linked_libs = false -memory_gb = 8 -nprocs = 4 -omp_nthreads = 2 +memory_gb = 32 +nprocs = 8 +omp_nthreads = 8 plugin = "MultiProc" resource_monitor = false stop_on_first_crash = false @@ -76,3 +59,9 @@ stop_on_first_crash = false [nipype.plugin_args] maxtasksperchild = 1 raise_insufficient = false + +[execution.bids_filters.t1w] +reconstruction = "" + +[execution.bids_filters.t2w] +reconstruction = "" \ No newline at end of file diff --git a/nibabies/workflows/tests/__init__.py b/nibabies/workflows/tests/__init__.py new file mode 100644 index 00000000..4dad52b5 --- /dev/null +++ b/nibabies/workflows/tests/__init__.py @@ -0,0 +1,71 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Utilities and mocks for testing and documentation building.""" + +import os +import shutil +from contextlib import contextmanager +from pathlib import Path +from tempfile import mkdtemp + +from toml import loads + +from nibabies import data +from nibabies.workflows.base import init_execution_spaces + + +@contextmanager +def mock_config(bids_dir=None): + """Create a mock config for documentation and testing purposes.""" + from ... import config + + _old_fs = os.getenv('FREESURFER_HOME') + if not _old_fs: + os.environ['FREESURFER_HOME'] = mkdtemp() + + settings = loads(data.load.readable('tests/config.toml').read_text()) + for sectionname, configs in settings.items(): + if sectionname != 'environment': + section = getattr(config, sectionname) + section.load(configs, init=False) + config.nipype.omp_nthreads = 1 + config.nipype.init() + config.loggers.init() + init_execution_spaces() + + bids_dir = bids_dir or data.load('tests/ds000005').absolute() + + config.execution.work_dir = Path(mkdtemp()) + config.execution.bids_dir = bids_dir + config.execution.nibabies_dir = Path(mkdtemp()) + config.execution.bids_database_dir = None + config.execution._layout = None + config.execution.init() + + yield + + shutil.rmtree(config.execution.work_dir) + shutil.rmtree(config.execution.nibabies_dir) + + if not _old_fs: + del os.environ['FREESURFER_HOME'] diff --git a/nibabies/workflows/tests/test_base.py b/nibabies/workflows/tests/test_base.py new file mode 100644 index 00000000..08a5baf1 --- /dev/null +++ b/nibabies/workflows/tests/test_base.py @@ -0,0 +1,334 @@ +from copy import deepcopy +from pathlib import Path +from unittest.mock import patch + +import bids +import nibabel as nb +import numpy as np +import pandas as pd +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 ... import config +from ..base import get_estimator, init_nibabies_wf +from ..tests import mock_config + +BASE_LAYOUT = { + '01': { + 'anat': [ + {'run': 1, 'suffix': 'T1w'}, + {'run': 2, 'suffix': 'T1w'}, + {'suffix': 'T2w'}, + ], + 'func': [ + *( + { + '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}}, + {'suffix': 'magnitude1', 'metadata': {'EchoTime': 0.005}}, + { + 'suffix': 'epi', + 'direction': 'PA', + 'metadata': {'PhaseEncodingDirection': 'j', 'TotalReadoutTime': 0.6}, + }, + { + 'suffix': 'epi', + 'direction': 'AP', + 'metadata': {'PhaseEncodingDirection': 'j-', 'TotalReadoutTime': 0.6}, + }, + ], + }, +} + + +@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) + + # Ensure age information is available + df = pd.DataFrame({'participant_id': ['sub-01'], 'age_months': ['1']}) + df.to_csv(str(bids_dir / 'participants.tsv'), sep='\t', index=False) + + 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) + + return bids_dir + + +def _make_params( + bold2anat_init: str = 'auto', + 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_anat: 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 ( + bold2anat_init, + use_bbr, + dummy_scans, + me_output_echos, + medial_surface_nan, + project_goodvoxels, + cifti_output, + run_msmsulc, + skull_strip_anat, + 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( + ( + 'bold2anat_init', + 'use_bbr', + 'dummy_scans', + 'me_output_echos', + 'medial_surface_nan', + 'project_goodvoxels', + 'cifti_output', + 'run_msmsulc', + 'skull_strip_anat', + 'use_syn_sdc', + 'force_syn', + 'freesurfer', + 'ignore', + 'bids_filters', + ), + [ + _make_params(), + _make_params(bold2anat_init='t1w'), + _make_params(bold2anat_init='t2w'), + _make_params(bold2anat_init='header'), + _make_params(use_bbr=True), + _make_params(use_bbr=False), + _make_params(bold2anat_init='header', use_bbr=True), + # Currently disabled + # _make_params(bold2anat_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', run_msmsulc=False), + _make_params(skull_strip_anat='force'), + _make_params(skull_strip_anat='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, bold2anat_init="header"), + # _make_params(freesurfer=False, bold2anat_init="header", use_bbr=True), + # _make_params(freesurfer=False, bold2anat_init="header", use_bbr=False), + # Regression test for gh-3154: + _make_params(bids_filters={'sbref': {'suffix': 'sbref'}}), + ], +) +def test_init_fmriprep_wf( + monkeypatch, + bids_root: Path, + tmp_path: Path, + level: str, + anat_only: bool, + bold2anat_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_anat: str, + use_syn_sdc: str | bool, + force_syn: bool, + freesurfer: bool, + ignore: list[str], + bids_filters: dict, +): + monkeypatch.setenv('SUBJECTS_DIR', '/opt/freesurfer/subjects') + with mock_config(bids_dir=bids_root): + config.workflow.level = level + config.workflow.anat_only = anat_only + config.workflow.bold2anat_init = bold2anat_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_anat = skull_strip_anat + config.workflow.cifti_output = cifti_output + config.workflow.run_reconall = freesurfer + config.workflow.ignore = ignore + with patch.dict('nibabies.config.execution.bids_filters', bids_filters): + wf = init_nibabies_wf( + [ + ('01', None), + ] + ) + + 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', task='rest', extension='.nii.gz', return_type='file') + ) + + assert get_estimator(layout, bold_files[0]) == () + assert get_estimator(layout, bold_files[1]) == () + + +def test_get_estimator_b0field_and_intendedfor(tmp_path): + bids_dir = tmp_path / 'bids' + + # Set B0FieldSource for run 1 + spec = deepcopy(BASE_LAYOUT) + spec['01']['func'][0]['metadata']['B0FieldSource'] = 'epi' + spec['01']['fmap'][2]['metadata']['B0FieldIdentifier'] = 'epi' + spec['01']['fmap'][3]['metadata']['B0FieldIdentifier'] = 'epi' + + # Set IntendedFor for run 2 + spec['01']['fmap'][0]['metadata']['IntendedFor'] = 'func/sub-01_task-rest_run-2_bold.nii.gz' + + generate_bids_skeleton(bids_dir, spec) + layout = bids.BIDSLayout(bids_dir) + _ = find_estimators(layout=layout, subject='01') + + 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',) + + +def test_get_estimator_overlapping_specs(tmp_path): + bids_dir = tmp_path / 'bids' + + # Set B0FieldSource for both runs + spec = deepcopy(BASE_LAYOUT) + spec['01']['func'][0]['metadata']['B0FieldSource'] = 'epi' + spec['01']['func'][1]['metadata']['B0FieldSource'] = 'epi' + spec['01']['fmap'][2]['metadata']['B0FieldIdentifier'] = 'epi' + spec['01']['fmap'][3]['metadata']['B0FieldIdentifier'] = 'epi' + + # Set IntendedFor for both runs + spec['01']['fmap'][0]['metadata']['IntendedFor'] = [ + 'func/sub-01_task-rest_run-1_bold.nii.gz', + 'func/sub-01_task-rest_run-2_bold.nii.gz', + ] + + generate_bids_skeleton(bids_dir, spec) + layout = bids.BIDSLayout(bids_dir) + _ = find_estimators(layout=layout, subject='01') + + 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',) + + +def test_get_estimator_multiple_b0fields(tmp_path): + bids_dir = tmp_path / 'bids' + + # Set B0FieldSource for both runs + spec = deepcopy(BASE_LAYOUT) + spec['01']['func'][0]['metadata']['B0FieldSource'] = ('epi', 'phasediff') + spec['01']['func'][1]['metadata']['B0FieldSource'] = 'epi' + spec['01']['fmap'][0]['metadata']['B0FieldIdentifier'] = 'phasediff' + spec['01']['fmap'][1]['metadata']['B0FieldIdentifier'] = 'phasediff' + spec['01']['fmap'][2]['metadata']['B0FieldIdentifier'] = 'epi' + spec['01']['fmap'][3]['metadata']['B0FieldIdentifier'] = 'epi' + + generate_bids_skeleton(bids_dir, spec) + layout = bids.BIDSLayout(bids_dir) + _ = find_estimators(layout=layout, subject='01') + + 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',) From a4ccc21a9e21fd3bbd9cde4f3743fee4ecac59b6 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Mon, 14 Oct 2024 16:49:16 -0400 Subject: [PATCH 2/7] DOC/STY: Fix doctest, include type hint --- nibabies/workflows/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nibabies/workflows/base.py b/nibabies/workflows/base.py index a6800634..eca233a3 100644 --- a/nibabies/workflows/base.py +++ b/nibabies/workflows/base.py @@ -70,11 +70,13 @@ from bids.layout import BIDSLayout from niworkflows.utils.spaces import SpatialReferences + SubjectSession = tuple[str, str | None] + AUTO_T2W_MAX_AGE = 8 -def init_nibabies_wf(subworkflows_list): +def init_nibabies_wf(subworkflows_list: list[SubjectSession]): """ Build *NiBabies*'s pipeline. @@ -92,7 +94,7 @@ def init_nibabies_wf(subworkflows_list): from nibabies.workflows.tests import mock_config from nibabies.workflows.base import init_nibabies_wf with mock_config(): - wf = init_nibabies_wf() + wf = init_nibabies_wf(['01', None]) Parameters ---------- From ccc598c4413729fbe474f2ca96293ae63cbb0ead Mon Sep 17 00:00:00 2001 From: mathiasg Date: Mon, 14 Oct 2024 16:50:06 -0400 Subject: [PATCH 3/7] FIX: Drop session from prefix if no session level --- nibabies/utils/bids.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nibabies/utils/bids.py b/nibabies/utils/bids.py index c4ec6aca..ecdba754 100644 --- a/nibabies/utils/bids.py +++ b/nibabies/utils/bids.py @@ -236,8 +236,8 @@ def parse_bids_for_age_months( # Play nice with sessions subject = f'sub-{subject_id}' - session = f'ses-{session_id}' or '' - prefix = f'{subject}' + f'_{session}' if session else '' + session = f'ses-{session_id}' if session_id else '' + prefix = f'{subject}' + (f'_{session}' if session else '') subject_level = session_level = Path(bids_root) / subject if session_id: From be1297792dfd645d0083350413883309947986e6 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Mon, 14 Oct 2024 21:02:33 -0400 Subject: [PATCH 4/7] TST: Add surface recon method to matrix --- nibabies/workflows/tests/test_base.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/nibabies/workflows/tests/test_base.py b/nibabies/workflows/tests/test_base.py index 08a5baf1..d01c2c30 100644 --- a/nibabies/workflows/tests/test_base.py +++ b/nibabies/workflows/tests/test_base.py @@ -121,7 +121,7 @@ def _make_params( skull_strip_anat: str = 'auto', use_syn_sdc: str | bool = False, force_syn: bool = False, - freesurfer: bool = True, + surface_recon_method: str | None = 'auto', ignore: list[str] = None, bids_filters: dict = None, ): @@ -141,7 +141,7 @@ def _make_params( skull_strip_anat, use_syn_sdc, force_syn, - freesurfer, + surface_recon_method, ignore, bids_filters, ) @@ -162,7 +162,7 @@ def _make_params( 'skull_strip_anat', 'use_syn_sdc', 'force_syn', - 'freesurfer', + 'surface_recon_method', 'ignore', 'bids_filters', ), @@ -185,9 +185,13 @@ def _make_params( _make_params(skull_strip_anat='force'), _make_params(skull_strip_anat='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), + _make_params(surface_recon_method=None), + _make_params(surface_recon_method=None, use_bbr=True), + _make_params(surface_recon_method=None, use_bbr=False), + _make_params(surface_recon_method='auto'), + # _make_params(surface_recon_method='mcribs'), # Requires precomputed segmentation + _make_params(surface_recon_method='infantfs'), + _make_params(surface_recon_method='freesurfer'), # Currently unsupported: # _make_params(freesurfer=False, bold2anat_init="header"), # _make_params(freesurfer=False, bold2anat_init="header", use_bbr=True), @@ -196,7 +200,7 @@ def _make_params( _make_params(bids_filters={'sbref': {'suffix': 'sbref'}}), ], ) -def test_init_fmriprep_wf( +def test_init_nibabies_wf( monkeypatch, bids_root: Path, tmp_path: Path, @@ -213,7 +217,7 @@ def test_init_fmriprep_wf( skull_strip_anat: str, use_syn_sdc: str | bool, force_syn: bool, - freesurfer: bool, + surface_recon_method: str | None, ignore: list[str], bids_filters: dict, ): @@ -230,7 +234,7 @@ def test_init_fmriprep_wf( # config.workflow.run_msmsulc = run_msmsulc config.workflow.skull_strip_anat = skull_strip_anat config.workflow.cifti_output = cifti_output - config.workflow.run_reconall = freesurfer + config.workflow.surface_recon_method = surface_recon_method config.workflow.ignore = ignore with patch.dict('nibabies.config.execution.bids_filters', bids_filters): wf = init_nibabies_wf( From 3b82cf32f314ab0160dd1f2a8c5e91aab0e9fe63 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Mon, 14 Oct 2024 21:03:01 -0400 Subject: [PATCH 5/7] FIX: Forgotten connection (thanks tests!) --- nibabies/workflows/bold/registration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabies/workflows/bold/registration.py b/nibabies/workflows/bold/registration.py index ce030c27..e66a6727 100644 --- a/nibabies/workflows/bold/registration.py +++ b/nibabies/workflows/bold/registration.py @@ -603,7 +603,7 @@ def init_fsl_bbr_wf( flt_bbr.inputs.schedule = data.load('flirtsch/bbr.sch') # fmt:off workflow.connect([ - (inputnode, wm_mask, [('t1w_dseg', 'in_seg')]), + (inputnode, wm_mask, [('anat_dseg', 'in_seg')]), (inputnode, flt_bbr, [('in_file', 'in_file')]), (lta_to_fsl, flt_bbr, [('out_fsl', 'in_matrix_file')]), ]) From 72dd9795367e3893c82c86837ffbdd7fb86a0992 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Tue, 15 Oct 2024 13:33:19 -0400 Subject: [PATCH 6/7] TST: Expand to testing single anatomical pathway --- nibabies/workflows/tests/test_base.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/nibabies/workflows/tests/test_base.py b/nibabies/workflows/tests/test_base.py index d01c2c30..b603a597 100644 --- a/nibabies/workflows/tests/test_base.py +++ b/nibabies/workflows/tests/test_base.py @@ -74,6 +74,13 @@ } +T1W_ONLY = BASE_LAYOUT.copy() +T1W_ONLY['01']['anat'] = [{'suffix': 'T1w'}] + +T2W_ONLY = T1W_ONLY.copy() +T2W_ONLY['01']['anat'] = [{'suffix': 'T2w'}] + + @pytest.fixture(scope='module', autouse=True) def _quiet_logger(): import logging @@ -91,11 +98,17 @@ def _reset_sdcflows_registry(): clear_registry() -@pytest.fixture(scope='module') -def bids_root(tmp_path_factory): +@pytest.fixture(scope='module', params=[BASE_LAYOUT, T1W_ONLY, T2W_ONLY]) +def bids_root(tmp_path_factory, request): + """ + Create a BIDS skeleton for various input types: + - Full inputs (T1w + T2w) + - T1w only + - T2w only + """ base = tmp_path_factory.mktemp('base') bids_dir = base / 'bids' - generate_bids_skeleton(bids_dir, BASE_LAYOUT) + generate_bids_skeleton(bids_dir, request.param) # Ensure age information is available df = pd.DataFrame({'participant_id': ['sub-01'], 'age_months': ['1']}) From e5124cc51edc5f74c7780d6a02d295b36128573e Mon Sep 17 00:00:00 2001 From: mathiasg Date: Tue, 15 Oct 2024 13:33:34 -0400 Subject: [PATCH 7/7] FIX: Another catch --- nibabies/workflows/anatomical/fit.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nibabies/workflows/anatomical/fit.py b/nibabies/workflows/anatomical/fit.py index a887b942..11413053 100644 --- a/nibabies/workflows/anatomical/fit.py +++ b/nibabies/workflows/anatomical/fit.py @@ -940,7 +940,7 @@ def init_infant_anat_fit_wf( workflow.__desc__ = desc - if not recon_method: + if recon_method is None: LOGGER.info('ANAT Skipping Stages 6+') return workflow @@ -1765,6 +1765,10 @@ def init_infant_single_anat_fit_wf( workflow.__desc__ = desc + if recon_method is None: + LOGGER.info('ANAT Skipping Stages 5+') + return workflow + # Stage 5: Surface reconstruction if recon_method == 'mcribs': if reference_anat == 'T1w':