/
wrangler.py
653 lines (580 loc) · 25.3 KB
/
wrangler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
#
# Copyright 2021 The NiPreps Developers <nipreps@gmail.com>
#
# 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/
#
"""Find fieldmaps on the BIDS inputs for :abbr:`SDC (susceptibility distortion correction)`."""
from __future__ import annotations
import logging
from functools import reduce
from itertools import product
from contextlib import suppress
from pathlib import Path
from typing import Optional, Union, List, Dict, Any
from bids.layout import BIDSLayout, BIDSFile
from bids.utils import listify
from .. import fieldmaps as fm
def _resolve_intent(
intent: str,
layout: BIDSLayout,
subject: str
) -> str | None:
root = Path(layout.root)
if intent.startswith("bids::"):
return str(root / intent[6:])
if not intent.startswith("bids:"):
return str(root / f"sub-{subject}" / intent)
return intent
def find_estimators(
*,
layout: BIDSLayout,
subject: str,
sessions: Optional[List[str]] = None,
fmapless: Union[bool, set] = True,
force_fmapless: bool = False,
logger: Optional[logging.Logger] = None,
bids_filters: Optional[dict] = None,
anat_suffix: Union[str, List[str]] = 'T1w',
) -> list:
"""
Apply basic heuristics to automatically find available data for fieldmap estimation.
The "*fieldmap-less*" heuristics only attempt to find ``_dwi`` and ``_bold`` candidates
to pair with a ``_T1w`` anatomical reference.
For more complicated heuristics (for instance, using ``_T2w`` images or ``_sbref``
images,) the :py:class:`~sdcflows.fieldmaps.FieldmapEstimation` object must be
created manually by the user.
Parameters
----------
layout : :obj:`bids.layout.BIDSLayout`
An initialized PyBIDS layout.
subject : :obj:`str`
Participant label for this single-subject workflow.
sessions : :obj:`list` or None
One of more session identifiers. If None, all sessions will be used.
fmapless : :obj:`bool` or :obj:`set`
Indicates if fieldmap-less heuristics should be executed.
When ``fmapless`` is a :obj:`set`, it can contain valid BIDS suffixes
for EPI images (namely, ``"dwi"``, ``"bold"``, ``"asl"``, or ``"sbref"``).
When ``fmapless`` is ``True``, heuristics will use the ``{"bold", "dwi", "asl"}`` set.
force_fmapless : :obj:`bool`
When some other fieldmap estimation methods have been found, fieldmap-less
estimation will be skipped except if ``force_fmapless`` is ``True``.
logger
The logger used to relay messages. If not provided, one will be created.
bids_filters
Optional dictionary of key/values to filter the entities on.
This allows lower level file inclusion/exclusion.
anat_suffix : :obj:`str` or :obj:`list`
String or list of strings to filter anatomical images for fieldmap-less
approaches. If not provided, ``T1w`` is used.
Returns
-------
estimators : :obj:`list`
The list of :py:class:`~sdcflows.fieldmaps.FieldmapEstimation` objects that have
successfully been built (meaning, all necessary inputs and corresponding metadata
are present in the given layout.)
Examples
--------
Our ``ds000054`` dataset, created for *fMRIPrep*, only has one *phasediff* type of fieldmap
with ``magnitude1`` and ``magnitude2`` files:
>>> find_estimators(
... layout=layouts['ds000054'],
... subject="100185",
... fmapless=False,
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<3 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_00000')]
OpenNeuro's dataset with four *PEPOLAR* EPI files, two runs per phase-encoding direction
(AP, PA):
>>> find_estimators(
... layout=layouts['ds001771'],
... subject="36",
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_00001')]
OpenNeuro's ``ds001600`` is an SDC test-dataset containing many different possibilities
for fieldmap estimation:
>>> find_estimators(
... layout=layouts['ds001600'],
... subject="1",
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_00002'),
FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_00003'),
FieldmapEstimation(sources=<3 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_00004'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_00005')]
We can also pick one (simplified) HCP subject for testing purposes:
>>> find_estimators(
... layout=layouts['HCP101006'],
... subject="101006",
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<2 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_00006'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_00007'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_00008')]
Finally, *SDCFlows*' "*dataset A*" and "*dataset B*" contain BIDS structures
with zero-byte NIfTI files and some corresponding metadata:
>>> find_estimators(
... layout=layouts['dsA'],
... subject="01",
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<2 files>, method=<EstimatorType.MAPPED: 4>,
bids_id='auto_...'),
FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_...'),
FieldmapEstimation(sources=<3 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_...'),
FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_...')]
>>> find_estimators(
... layout=layouts['dsB'],
... subject="01",
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<2 files>, method=<EstimatorType.MAPPED: 4>,
bids_id='auto_...'),
FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_...'),
FieldmapEstimation(sources=<3 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_...'),
FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_...')]
After cleaning the registry, we can see how the "*fieldmap-less*" estimation
can be forced:
>>> from .. import fieldmaps as fm
>>> fm.clear_registry()
>>> find_estimators(
... layout=layouts['ds000054'],
... subject="100185",
... fmapless={"bold"},
... force_fmapless=True,
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<3 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...')]
Likewise in a more comprehensive dataset:
>>> find_estimators(
... layout=layouts['ds001771'],
... subject="36",
... force_fmapless=True,
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...')]
Because "*dataset A*" contains very few metadata fields available, "*fieldmap-less*"
heuristics come back empty (BOLD and DWI files are missing
the mandatory ``PhaseEncodingDirection``, in this case):
>>> find_estimators(
... layout=layouts['dsA'],
... subject="01",
... force_fmapless=True,
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<2 files>, method=<EstimatorType.MAPPED: 4>,
bids_id='auto_...'),
FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_...'),
FieldmapEstimation(sources=<3 files>, method=<EstimatorType.PHASEDIFF: 3>,
bids_id='auto_...'),
FieldmapEstimation(sources=<4 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.PEPOLAR: 2>,
bids_id='auto_...')]
This function should also correctly investigate multi-session datasets:
>>> find_estimators(
... layout=layouts['ds000206'],
... subject="05",
... fmapless=False,
... force_fmapless=False,
... ) # doctest: +ELLIPSIS
[]
>>> find_estimators(
... layout=layouts['ds000206'],
... subject="05",
... fmapless=True,
... force_fmapless=False,
... ) # doctest: +ELLIPSIS
[FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...'),
FieldmapEstimation(sources=<2 files>, method=<EstimatorType.ANAT: 5>,
bids_id='auto_...')]
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_...')]
"""
from .misc import create_logger
from bids.layout import Query
from bids.exceptions import BIDSEntityError
# The created logger is set to ERROR log level
logger = logger or create_logger('sdcflows.wrangler')
base_entities = {
"subject": subject,
"extension": [".nii", ".nii.gz"],
"part": ["mag", None],
"scope": "raw", # Ensure derivatives are not captured
}
if bids_filters:
filters = bids_filters.copy() # copy to avoid altering in place
if 'session' in bids_filters and sessions is not None:
raise ValueError("Filters include session, but session is already defined.")
sessions = listify(filters.pop('session', None))
base_entities.update(filters)
subject_root = Path(layout.root) / f"sub-{subject}"
sessions = sessions or layout.get_sessions(subject=subject) or [None]
fmapless = fmapless or {}
if fmapless is True:
fmapless = {"bold", "dwi", "asl"}
estimators = []
# Step 1. Use B0FieldIdentifier metadata
b0_ids = tuple()
with suppress(BIDSEntityError):
# flatten lists from json (tupled in pybids for hashing), then unique
b0_ids = reduce(
set.union,
(listify(ids) for ids in layout.get_B0FieldIdentifiers(**base_entities)),
set()
)
if b0_ids:
logger.debug(
"Dataset includes `B0FieldIdentifier` metadata."
"Any data missing this metadata will be ignored."
)
for b0_id in b0_ids:
# Found B0FieldIdentifier metadata entries
b0_entities = base_entities.copy()
b0_entities["B0FieldIdentifier"] = b0_id
bare_ids = layout.get(**base_entities, B0FieldIdentifier=b0_id)
listed_ids = layout.get(
**base_entities,
B0FieldIdentifier=f'"{b0_id}"', # Double quotes to match JSON, not Python repr
regex_search=True,
)
try:
e = fm.FieldmapEstimation(
[
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
for fmap in bare_ids + listed_ids
]
)
except (ValueError, TypeError) as err:
_log_debug_estimator_fail(
logger, b0_id, bare_ids + listed_ids, layout.root, str(err)
)
else:
_log_debug_estimation(logger, e, layout.root)
estimators.append(e)
# Step 2. If no B0FieldIdentifiers were found, try several heuristics
if not estimators:
# Set up B0 fieldmap strategies:
for fmap in layout.get(
**{
**base_entities,
**{'suffix': ["fieldmap", "phasediff", "phase1"], 'session': sessions}
}
):
try:
e = fm.FieldmapEstimation(
fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
)
except (ValueError, TypeError) as err:
_log_debug_estimator_fail(
logger, "unnamed fieldmap", [fmap], layout.root, str(err)
)
else:
_log_debug_estimation(logger, e, layout.root)
estimators.append(e)
# A bunch of heuristics to select EPI fieldmaps
acqs = (
base_entities.get('acquisitions')
or layout.get_acquisitions(subject=subject, suffix="epi") + [None]
)
contrasts = (
base_entities.get('ceagent')
or layout.get_ceagents(subject=subject, suffix="epi") + [None]
)
for ses, acq, ce in product(sessions, 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:
by_intent = {}
for fmap in layout.get(**{**entities, **{'direction': dirs}}):
fmapfile = fm.FieldmapFile(fmap.path, metadata=fmap.get_metadata())
by_intent.setdefault(
tuple(fmapfile.metadata.get('IntendedFor', ())), []
).append(fmapfile)
for collection in by_intent.values():
try:
e = fm.FieldmapEstimation(collection)
except (ValueError, TypeError) as err:
_log_debug_estimator_fail(
logger, "unnamed PEPOLAR", collection, layout.root, str(err)
)
else:
_log_debug_estimation(logger, e, layout.root)
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(
**{
**base_entities,
**{'suffix': 'epi', 'IntendedFor': Query.REQUIRED, 'session': sessions}
}
)
for epi_fmap in has_intended:
if epi_fmap.path in fm._estimators.sources:
logger.debug("Skipping fieldmap %s (already in use)", epi_fmap.relpath)
continue # skip EPI images already considered above
logger.debug("Found single PE fieldmap %s", epi_fmap.relpath)
epi_base_md = epi_fmap.get_metadata()
# Find existing IntendedFor targets and warn if missing
all_targets = []
for intent in listify(epi_base_md["IntendedFor"]):
target = layout.get_file(_resolve_intent(intent, layout, subject))
if target is None:
logger.debug("Single PE target %s not found", intent)
continue
all_targets.append(target)
# If sbrefs are targets, then the goal is generally to estimate with epi+sbref
# and correct bold/dwi/asl
sbrefs = [
target for target in all_targets if target.entities["suffix"] == "sbref"
]
if sbrefs:
targets = sbrefs
intent_map = []
for sbref in sbrefs:
ents = sbref.get_entities(metadata=False)
ents["suffix"] = ["bold", "dwi", "asl"]
intent_map.append(
[
target
for target in layout.get(**ents)
if target in all_targets
]
)
else:
targets = all_targets
intent_map = [[target] for target in all_targets]
for target, intent in zip(targets, intent_map):
logger.debug("Found single PE target %s", target.relpath)
# The new estimator is IntendedFor the individual targets,
# even if the EPI file is IntendedFor multiple
estimator_md = epi_base_md.copy()
estimator_md["IntendedFor"] = [
str(Path(pathlike).relative_to(subject_root))
for pathlike in intent
]
try:
e = fm.FieldmapEstimation(
[
fm.FieldmapFile(epi_fmap.path, metadata=estimator_md),
fm.FieldmapFile(target.path, metadata=target.get_metadata())
]
)
except (ValueError, TypeError) as err:
_log_debug_estimator_fail(
logger,
"unnamed PEPOLAR",
[epi_fmap, target],
layout.root,
str(err)
)
else:
_log_debug_estimation(logger, e, layout.root)
estimators.append(e)
if estimators and not force_fmapless:
fmapless = False
# Find fieldmap-less schemes
anat_file = layout.get(**{**base_entities, **{'suffix': anat_suffix, 'session': sessions}})
if not fmapless or not anat_file:
logger.debug("Skipping fmap-less estimation")
return estimators
logger.debug("Attempting fmap-less estimation")
estimator_specs = find_anatomical_estimators(
anat_file=anat_file[0],
layout=layout,
subject=subject,
sessions=sessions,
base_entities=base_entities,
suffixes=fmapless,
)
for spec in estimator_specs:
try:
estimator = fm.FieldmapEstimation(spec)
except (ValueError, TypeError) as err:
_log_debug_estimator_fail(logger, "ANAT", spec, layout.root, str(err))
else:
_log_debug_estimation(logger, estimator, layout.root)
estimators.append(estimator)
return estimators
def find_anatomical_estimators(
*,
anat_file: BIDSFile,
layout: BIDSLayout,
subject: str,
sessions: List[str],
base_entities: Dict[str, Any],
suffixes: List[str],
) -> List[List[fm.FieldmapFile]]:
r"""Find anatomical estimators
Given an anatomical reference image, create lists of files for estimating
susceptibility distortion for the EPI images in a dataset.
Parameters
----------
anat_file : :class:`bids.layout.BIDSFile`
Anatomical reference image to use in estimators.
layout : :class:`bids.layout.BIDSLayout`
An initialized PyBIDS layout.
subject : :class:`str`
Participant label for this single-subject workflow.
sessions : :class:`list`
One of more session identifiers. To use all, pass ``[None]``.
base_entities : :class:`dict`
Entities to use to query for images. These should include any filters.
suffixes : :class:`list`
EPI suffixes, for example ``["bold", "dwi", "asl"]``. Associated ``"sbref"``\s
will be found and used in place of BOLD/diffusion EPIs.
Similarly, ``"m0scan"``\s associated with ASL runs with the ``IntendedFor`` or
``B0FieldIdentifier`` metadata will be used in place of ASL runs.
"""
from .epimanip import get_trt
subject_root = Path(layout.root) / f"sub-{subject}"
hits = set() # Avoid duplicates
estimators = []
for ses, suffix in sorted(product(sessions, suffixes)):
suffixes = ["sbref", suffix] # Order indicates preference; prefer sbref
datatype = {
"bold": "func",
"dwi": "dwi",
"asl": "perf",
}[suffix]
candidates = layout.get(
**{
**base_entities,
**{"suffix": suffixes, "session": ses, "datatype": datatype},
}
)
# Filter out candidates without defined PE direction
epi_targets = []
for candidate in candidates:
meta = candidate.get_metadata()
if not meta.get("PhaseEncodingDirection"):
continue
trt = 1.0
with suppress(ValueError):
trt = get_trt(meta, candidate.path)
meta.update({"TotalReadoutTime": trt})
epi_targets.append(fm.FieldmapFile(candidate, metadata=meta))
def sort_key(fmap):
# Return sbref before DWI/BOLD and shortest echo first
return suffixes.index(fmap.suffix), fmap.metadata.get("EchoTime", 1)
for target in sorted(epi_targets, key=sort_key):
if target.path in hits:
continue
query = {**base_entities, **target.entities}
# Find all echos, so strip from query, if present
query.pop("echo", None)
# Include sbref and EPI images in IntendedFor
# No harm in including sbrefs that won't be corrected,
# and ensures the hits set prevents doubling up
intent = [Path(epi) for epi in layout.get(suffix=suffixes, **query)]
metadata = {
"IntendedFor": [str(epi.relative_to(subject_root)) for epi in intent]
}
estimators.append([fm.FieldmapFile(anat_file, metadata=metadata), target])
hits.update(intent)
return estimators
def _log_debug_estimation(
logger: logging.Logger,
estimation: fm.FieldmapEstimation,
bids_root: str,
) -> None:
"""A helper function to log estimation information when running with verbosity."""
logger.debug(
"Found %s estimation from %d sources:\n- %s",
estimation.method.name,
len(estimation.sources),
"\n- ".join(
[str(Path(s.path).relative_to(bids_root)) for s in estimation.sources]
),
)
def _log_debug_estimator_fail(
logger: logging.Logger,
b0_id: str,
files: List[BIDSFile],
bids_root: str,
message: str
) -> None:
"""A helper function to log failures to build an estimator when running with verbosity."""
logger.debug(
"Failed to construct %s estimation from %d sources:\n- %s\nError: %s",
b0_id,
len(files),
"\n- ".join([str(Path(s.path).relative_to(bids_root)) for s in files]),
message,
)