Skip to content

Commit afc110d

Browse files
authored
Merge pull request #247 from nipreps/fix/b0field-identifier
FIX: Implement ``B0FieldIdentifier`` / ``B0FieldSource``
2 parents 0e19395 + 6dbfbcb commit afc110d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+169
-46
lines changed

sdcflows/conftest.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@
4040
}
4141

4242
data_dir = Path(__file__).parent / "tests" / "data"
43-
44-
layouts["dsA"] = BIDSLayout(data_dir / "dsA", validate=False, derivatives=False)
45-
layouts["dsB"] = BIDSLayout(data_dir / "dsB", validate=False, derivatives=False)
43+
layouts.update({
44+
folder.name: BIDSLayout(folder, validate=False, derivatives=False)
45+
for folder in data_dir.glob("ds*") if folder.is_dir()
46+
})
4647

4748

4849
def pytest_report_header(config):
@@ -66,6 +67,7 @@ def add_np(doctest_namespace):
6667

6768
doctest_namespace["dsA_dir"] = data_dir / "dsA"
6869
doctest_namespace["dsB_dir"] = data_dir / "dsB"
70+
doctest_namespace["dsC_dir"] = data_dir / "dsC"
6971

7072

7173
@pytest.fixture
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Name": "Test Dataset A, only empty files",
3+
"BIDSVersion": "",
4+
"License": "CC0",
5+
"Authors": ["Esteban O."],
6+
"Acknowledgements": "",
7+
"HowToAcknowledge": "",
8+
"Funding": "",
9+
"ReferencesAndLinks": [""],
10+
"DatasetDOI": ""
11+
}

sdcflows/tests/data/dsC/sub-01/anat/sub-01_FLAIR.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/anat/sub-01_T1w.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/anat/sub-01_T2w.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-AP_dwi.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"PhaseEncodingDirection": "j-",
3+
"TotalReadoutTime": 0.005
4+
}

sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-AP_sbref.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-LR_dwi.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"PhaseEncodingDirection": "i",
3+
"TotalReadoutTime": 0.005
4+
}

sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-LR_sbref.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-PA_dwi.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"PhaseEncodingDirection": "j",
3+
"TotalReadoutTime": 0.005
4+
}

sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-PA_sbref.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-RL_dwi.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"PhaseEncodingDirection": "i-",
3+
"TotalReadoutTime": 0.005
4+
}

sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-RL_sbref.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"PhaseEncodingDirection": "j",
3+
"TotalReadoutTime": 0.005,
4+
"IntendedFor": [
5+
"dwi/sub-01_dir-AP_dwi.nii.gz",
6+
"dwi/sub-01_dir-AP_sbref.nii.gz",
7+
"func/sub-01_task-rest_bold.nii.gz",
8+
"func/sub-01_task-rest_sbref.nii.gz"
9+
]
10+
}

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_acq-single_dir-PA_epi.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"B0FieldIdentifier": "pepolar4pe",
3+
"PhaseEncodingDirection": "j-",
4+
"TotalReadoutTime": 0.005
5+
}

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_dir-AP_epi.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"B0FieldIdentifier": "pepolar4pe",
3+
"PhaseEncodingDirection": "i",
4+
"TotalReadoutTime": 0.005
5+
}

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_dir-LR_epi.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"B0FieldIdentifier": "pepolar4pe",
3+
"PhaseEncodingDirection": "j",
4+
"TotalReadoutTime": 0.005
5+
}

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_dir-PA_epi.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"B0FieldIdentifier": "pepolar4pe",
3+
"PhaseEncodingDirection": "i-",
4+
"TotalReadoutTime": 0.005
5+
}

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_dir-RL_epi.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"Units": "rad/s"
3+
}

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_fieldmap.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_magnitude.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_magnitude1.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_magnitude2.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"EchoTime": 0.005
3+
}

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_phase1.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"EchoTime": 0.00746
3+
}

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_phase2.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"EchoTime1": 0.00500,
3+
"EchoTime2": 0.00746
4+
}

sdcflows/tests/data/dsC/sub-01/fmap/sub-01_phasediff.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/func/sub-01_task-rest_bold.nii.gz

Whitespace-only changes.

sdcflows/tests/data/dsC/sub-01/func/sub-01_task-rest_sbref.nii.gz

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"TaskName": "Rest",
3+
"PhaseEncodingDirection": "j-",
4+
"TotalReadoutTime": 0.05
5+
}

sdcflows/utils/wrangler.py

+89-43
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,35 @@ def find_estimators(*, layout, subject, fmapless=True, force_fmapless=False):
214214
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
215215
bids_id='auto_00012')]
216216
217+
When the ``B0FieldIdentifier`` metadata is set for one or more fieldmaps, then
218+
the heuristics that use ``IntendedFor`` are dismissed:
219+
220+
>>> find_estimators(
221+
... layout=layouts['dsC'],
222+
... subject="01",
223+
... ) # doctest: +ELLIPSIS
224+
[FieldmapEstimation(sources=<5 files>, method=<EstimatorType.PEPOLAR: 2>,
225+
bids_id='pepolar4pe')]
226+
227+
The only exception to the priority of ``B0FieldIdentifier`` is when fieldmaps
228+
are searched with the ``force_fmapless`` argument on:
229+
230+
>>> fm.clear_registry() # Necessary as `pepolar4pe` is not changing.
231+
>>> find_estimators(
232+
... layout=layouts['dsC'],
233+
... subject="01",
234+
... fmapless=True,
235+
... force_fmapless=True,
236+
... ) # doctest: +ELLIPSIS
237+
[FieldmapEstimation(sources=<5 files>, method=<EstimatorType.PEPOLAR: 2>,
238+
bids_id='pepolar4pe'),
239+
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
240+
bids_id='auto_00000')]
241+
217242
"""
218243
from .. import fieldmaps as fm
219244
from bids.layout import Query
245+
from bids.exceptions import BIDSEntityError
220246

221247
base_entities = {
222248
"subject": subject,
@@ -232,57 +258,74 @@ def find_estimators(*, layout, subject, fmapless=True, force_fmapless=False):
232258

233259
estimators = []
234260

235-
# Set up B0 fieldmap strategies:
236-
for fmap in layout.get(suffix=["fieldmap", "phasediff", "phase1"], **base_entities):
237-
e = fm.FieldmapEstimation(
238-
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
239-
)
240-
estimators.append(e)
261+
b0_ids = tuple()
262+
with suppress(BIDSEntityError):
263+
b0_ids = layout.get_B0FieldIdentifiers(**base_entities)
241264

242-
# A bunch of heuristics to select EPI fieldmaps
243-
sessions = layout.get_sessions()
244-
acqs = tuple(layout.get_acquisitions(suffix="epi") + [None])
245-
contrasts = tuple(layout.get_ceagents(suffix="epi") + [None])
246-
247-
for ses, acq, ce in product(sessions or (None,), acqs, contrasts):
265+
for b0_id in b0_ids:
266+
# Found B0FieldIdentifier metadata entries
248267
entities = base_entities.copy()
249-
entities.update(
250-
{"suffix": "epi", "session": ses, "acquisition": acq, "ceagent": ce}
251-
)
252-
dirs = layout.get_directions(**entities)
253-
if len(dirs) > 1:
268+
entities["B0FieldIdentifier"] = b0_id
269+
270+
_e = fm.FieldmapEstimation([
271+
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
272+
for fmap in layout.get(**entities)
273+
])
274+
estimators.append(_e)
275+
276+
# As no B0FieldIdentifier(s) were found, try several heuristics
277+
if not estimators:
278+
# Set up B0 fieldmap strategies:
279+
for fmap in layout.get(suffix=["fieldmap", "phasediff", "phase1"], **base_entities):
254280
e = fm.FieldmapEstimation(
255-
[
256-
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
257-
for fmap in layout.get(direction=dirs, **entities)
258-
]
281+
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
259282
)
260283
estimators.append(e)
261284

262-
# At this point, only single-PE _epi files WITH ``IntendedFor`` can be automatically processed
263-
# (this will be easier with bids-standard/bids-specification#622 in).
264-
has_intended = tuple()
265-
with suppress(ValueError):
266-
has_intended = layout.get(suffix="epi", IntendedFor=Query.ANY, **base_entities)
267-
268-
for epi_fmap in has_intended:
269-
if epi_fmap.path in fm._estimators.sources:
270-
continue # skip EPI images already considered above
271-
272-
targets = [epi_fmap] + [
273-
layout.get_file(str(subject_root / intent))
274-
for intent in epi_fmap.get_metadata()["IntendedFor"]
275-
]
276-
277-
epi_sources = []
278-
for fmap in targets:
279-
with suppress(fm.MetadataError):
280-
epi_sources.append(
281-
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
285+
# A bunch of heuristics to select EPI fieldmaps
286+
sessions = layout.get_sessions(subject=subject)
287+
acqs = tuple(layout.get_acquisitions(subject=subject, suffix="epi") + [None])
288+
contrasts = tuple(layout.get_ceagents(subject=subject, suffix="epi") + [None])
289+
290+
for ses, acq, ce in product(sessions or (None,), acqs, contrasts):
291+
entities = base_entities.copy()
292+
entities.update(
293+
{"suffix": "epi", "session": ses, "acquisition": acq, "ceagent": ce}
294+
)
295+
dirs = layout.get_directions(**entities)
296+
if len(dirs) > 1:
297+
e = fm.FieldmapEstimation(
298+
[
299+
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
300+
for fmap in layout.get(direction=dirs, **entities)
301+
]
282302
)
303+
estimators.append(e)
304+
305+
# At this point, only single-PE _epi files WITH ``IntendedFor`` can
306+
# be automatically processed.
307+
has_intended = tuple()
308+
with suppress(ValueError):
309+
has_intended = layout.get(suffix="epi", IntendedFor=Query.ANY, **base_entities)
310+
311+
for epi_fmap in has_intended:
312+
if epi_fmap.path in fm._estimators.sources:
313+
continue # skip EPI images already considered above
314+
315+
targets = [epi_fmap] + [
316+
layout.get_file(str(subject_root / intent))
317+
for intent in epi_fmap.get_metadata()["IntendedFor"]
318+
]
319+
320+
epi_sources = []
321+
for fmap in targets:
322+
with suppress(fm.MetadataError):
323+
epi_sources.append(
324+
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
325+
)
283326

284-
with suppress(ValueError, TypeError):
285-
estimators.append(fm.FieldmapEstimation(epi_sources))
327+
with suppress(ValueError, TypeError):
328+
estimators.append(fm.FieldmapEstimation(epi_sources))
286329

287330
if estimators and not force_fmapless:
288331
fmapless = False
@@ -295,6 +338,8 @@ def find_estimators(*, layout, subject, fmapless=True, force_fmapless=False):
295338

296339
from .epimanip import get_trt
297340

341+
# Sessions may not be defined at this point if some id was found.
342+
sessions = layout.get_sessions(subject=subject)
298343
for ses, suffix in sorted(product(sessions or (None,), fmapless)):
299344
candidates = layout.get(suffix=suffix, session=ses, **base_entities)
300345

@@ -306,6 +351,7 @@ def find_estimators(*, layout, subject, fmapless=True, force_fmapless=False):
306351
for candidate in candidates:
307352
meta = candidate.get_metadata()
308353
pe_dir = meta.get("PhaseEncodingDirection")
354+
309355
if not pe_dir:
310356
continue
311357

0 commit comments

Comments
 (0)