Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX: Implement B0FieldIdentifier / B0FieldSource #247

Merged
merged 4 commits into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions sdcflows/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@
}

data_dir = Path(__file__).parent / "tests" / "data"

layouts["dsA"] = BIDSLayout(data_dir / "dsA", validate=False, derivatives=False)
layouts["dsB"] = BIDSLayout(data_dir / "dsB", validate=False, derivatives=False)
layouts.update({
folder.name: BIDSLayout(folder, validate=False, derivatives=False)
for folder in data_dir.glob("ds*") if folder.is_dir()
})


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

doctest_namespace["dsA_dir"] = data_dir / "dsA"
doctest_namespace["dsB_dir"] = data_dir / "dsB"
doctest_namespace["dsC_dir"] = data_dir / "dsC"


@pytest.fixture
Expand Down
11 changes: 11 additions & 0 deletions sdcflows/tests/data/dsC/dataset_description.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Name": "Test Dataset A, only empty files",
"BIDSVersion": "",
"License": "CC0",
"Authors": ["Esteban O."],
"Acknowledgements": "",
"HowToAcknowledge": "",
"Funding": "",
"ReferencesAndLinks": [""],
"DatasetDOI": ""
}
Empty file.
Empty file.
Empty file.
Empty file.
4 changes: 4 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-AP_sbref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"PhaseEncodingDirection": "j-",
"TotalReadoutTime": 0.005
}
Empty file.
Empty file.
4 changes: 4 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-LR_sbref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"PhaseEncodingDirection": "i",
"TotalReadoutTime": 0.005
}
Empty file.
Empty file.
4 changes: 4 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-PA_sbref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"PhaseEncodingDirection": "j",
"TotalReadoutTime": 0.005
}
Empty file.
Empty file.
4 changes: 4 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/dwi/sub-01_dir-RL_sbref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"PhaseEncodingDirection": "i-",
"TotalReadoutTime": 0.005
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"PhaseEncodingDirection": "j",
"TotalReadoutTime": 0.005,
"IntendedFor": [
"dwi/sub-01_dir-AP_dwi.nii.gz",
"dwi/sub-01_dir-AP_sbref.nii.gz",
"func/sub-01_task-rest_bold.nii.gz",
"func/sub-01_task-rest_sbref.nii.gz"
]
}
Empty file.
5 changes: 5 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/fmap/sub-01_dir-AP_epi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"B0FieldIdentifier": "pepolar4pe",
"PhaseEncodingDirection": "j-",
"TotalReadoutTime": 0.005
}
Empty file.
5 changes: 5 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/fmap/sub-01_dir-LR_epi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"B0FieldIdentifier": "pepolar4pe",
"PhaseEncodingDirection": "i",
"TotalReadoutTime": 0.005
}
Empty file.
5 changes: 5 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/fmap/sub-01_dir-PA_epi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"B0FieldIdentifier": "pepolar4pe",
"PhaseEncodingDirection": "j",
"TotalReadoutTime": 0.005
}
Empty file.
5 changes: 5 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/fmap/sub-01_dir-RL_epi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"B0FieldIdentifier": "pepolar4pe",
"PhaseEncodingDirection": "i-",
"TotalReadoutTime": 0.005
}
Empty file.
3 changes: 3 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/fmap/sub-01_fieldmap.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"Units": "rad/s"
}
Empty file.
Empty file.
Empty file.
Empty file.
3 changes: 3 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/fmap/sub-01_phase1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"EchoTime": 0.005
}
Empty file.
3 changes: 3 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/fmap/sub-01_phase2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"EchoTime": 0.00746
}
Empty file.
4 changes: 4 additions & 0 deletions sdcflows/tests/data/dsC/sub-01/fmap/sub-01_phasediff.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"EchoTime1": 0.00500,
"EchoTime2": 0.00746
}
Empty file.
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions sdcflows/tests/data/dsC/task-rest_bold.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"TaskName": "Rest",
"PhaseEncodingDirection": "j-",
"TotalReadoutTime": 0.05
}
132 changes: 89 additions & 43 deletions sdcflows/utils/wrangler.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,35 @@ def find_estimators(*, layout, subject, fmapless=True, force_fmapless=False):
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_00012')]

When the ``B0FieldIdentifier`` metadata is set for one or more fieldmaps, then
the heuristics that use ``IntendedFor`` are dismissed:

>>> find_estimators(
... layout=layouts['dsC'],
... subject="01",
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<5 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='pepolar4pe')]

The only exception to the priority of ``B0FieldIdentifier`` is when fieldmaps
are searched with the ``force_fmapless`` argument on:

>>> fm.clear_registry() # Necessary as `pepolar4pe` is not changing.
>>> find_estimators(
... layout=layouts['dsC'],
... subject="01",
... fmapless=True,
... force_fmapless=True,
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<5 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='pepolar4pe'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_00000')]

"""
from .. import fieldmaps as fm
from bids.layout import Query
from bids.exceptions import BIDSEntityError

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

estimators = []

# Set up B0 fieldmap strategies:
for fmap in layout.get(suffix=["fieldmap", "phasediff", "phase1"], **base_entities):
e = fm.FieldmapEstimation(
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
)
estimators.append(e)
b0_ids = tuple()
with suppress(BIDSEntityError):
b0_ids = layout.get_B0FieldIdentifiers(**base_entities)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to specify extension and scope? I think only subject is relevant. Possibly session.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think extension would be required, yes. But, in the case of a weird dataset where pybids finds a json without its NIfTI this would fail for very little gain.

scope is necessary, because derivatives/ will definitely have B0FieldIdentifier defined if processed with SDCFlows.

subject is necessary, of course,

and session should not be there - the user may want to use fieldmaps with data from another session and save time in others (e.g., My Connectome).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, as long as extension doesn't break things, that's fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scope is necessary, because derivatives/ will definitely have B0FieldIdentifier defined if processed with SDCFlows.

But suppose I have a qMRI sequence where I quantify B0, which would be a derivative, and want to use that to correct raw BOLD series?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is preprocessed, and in Hz, then you don't need an estimator for that - this function finds data that potentially can be used to estimate a fieldmap.

If, on the contrary, some preprocessing is needed (e.g., you will want to approximate some B-Spline coefficients to properly integrate with the correction workflows), then SDCFlows does not support that type of fieldmap. You would definitely pick up some sdcflows.interfaces but essentially, you have to build the workflow yourself. Also, our Fieldmap and FieldmapEstimator objects do not support other types of fieldmaps.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The responsibility of this function is merely implementing the best we can those heuristics we used to have in fMRIPrep, for convenience across the board (dMRIPrep, babies, rodents, etc.).

Copy link
Member Author

@oesteban oesteban Oct 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before I merge, I guess I didn't explicitly give an answer to the actual question.

But suppose I have a qMRI sequence where I quantify B0, which would be a derivative, and want to use that to correct raw BOLD series?

Then, (i) you want it to have a B0FieldIdentifier in it, or at the least the IntendedFor so that the code to find fieldmap matches locates it, (ii) if you want to use SDCFlows' unwarp workflow, then you need to make sure that an anatomical reference is given and B-Spline coefficients representation available - if not, that's something you'll write on your own, (iii) invoke the unwarp workflow.

As you will see with the links, both (i) and (iii) take place within the downstream library (i.e., not implemented in SDCFlows proper).


# A bunch of heuristics to select EPI fieldmaps
sessions = layout.get_sessions()
acqs = tuple(layout.get_acquisitions(suffix="epi") + [None])
contrasts = tuple(layout.get_ceagents(suffix="epi") + [None])

for ses, acq, ce in product(sessions or (None,), acqs, contrasts):
for b0_id in b0_ids:
# Found B0FieldIdentifier metadata entries
entities = base_entities.copy()
entities.update(
{"suffix": "epi", "session": ses, "acquisition": acq, "ceagent": ce}
)
dirs = layout.get_directions(**entities)
if len(dirs) > 1:
entities["B0FieldIdentifier"] = b0_id

_e = fm.FieldmapEstimation([
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
for fmap in layout.get(**entities)
])
estimators.append(_e)

# As no B0FieldIdentifier(s) were found, try several heuristics
if not estimators:
# Set up B0 fieldmap strategies:
for fmap in layout.get(suffix=["fieldmap", "phasediff", "phase1"], **base_entities):
e = fm.FieldmapEstimation(
[
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
for fmap in layout.get(direction=dirs, **entities)
]
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
)
estimators.append(e)

# At this point, only single-PE _epi files WITH ``IntendedFor`` can be automatically processed
# (this will be easier with bids-standard/bids-specification#622 in).
has_intended = tuple()
with suppress(ValueError):
has_intended = layout.get(suffix="epi", IntendedFor=Query.ANY, **base_entities)

for epi_fmap in has_intended:
if epi_fmap.path in fm._estimators.sources:
continue # skip EPI images already considered above

targets = [epi_fmap] + [
layout.get_file(str(subject_root / intent))
for intent in epi_fmap.get_metadata()["IntendedFor"]
]

epi_sources = []
for fmap in targets:
with suppress(fm.MetadataError):
epi_sources.append(
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
# A bunch of heuristics to select EPI fieldmaps
sessions = layout.get_sessions(subject=subject)
acqs = tuple(layout.get_acquisitions(subject=subject, suffix="epi") + [None])
contrasts = tuple(layout.get_ceagents(subject=subject, suffix="epi") + [None])

for ses, acq, ce in product(sessions or (None,), acqs, contrasts):
entities = base_entities.copy()
entities.update(
{"suffix": "epi", "session": ses, "acquisition": acq, "ceagent": ce}
)
dirs = layout.get_directions(**entities)
if len(dirs) > 1:
e = fm.FieldmapEstimation(
[
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
for fmap in layout.get(direction=dirs, **entities)
]
)
estimators.append(e)

# At this point, only single-PE _epi files WITH ``IntendedFor`` can
# be automatically processed.
has_intended = tuple()
with suppress(ValueError):
has_intended = layout.get(suffix="epi", IntendedFor=Query.ANY, **base_entities)

for epi_fmap in has_intended:
if epi_fmap.path in fm._estimators.sources:
continue # skip EPI images already considered above

targets = [epi_fmap] + [
layout.get_file(str(subject_root / intent))
for intent in epi_fmap.get_metadata()["IntendedFor"]
]

epi_sources = []
for fmap in targets:
with suppress(fm.MetadataError):
epi_sources.append(
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
)

with suppress(ValueError, TypeError):
estimators.append(fm.FieldmapEstimation(epi_sources))
with suppress(ValueError, TypeError):
estimators.append(fm.FieldmapEstimation(epi_sources))

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

from .epimanip import get_trt

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

Expand All @@ -306,6 +351,7 @@ def find_estimators(*, layout, subject, fmapless=True, force_fmapless=False):
for candidate in candidates:
meta = candidate.get_metadata()
pe_dir = meta.get("PhaseEncodingDirection")

if not pe_dir:
continue

Expand Down