2424from pathlib import Path
2525from enum import Enum , auto
2626from collections import defaultdict
27+ from contextlib import suppress
2728import re
2829import attr
2930from json import loads
3031from bids .layout import parse_file_entities
3132from bids .utils import listify
3233from niworkflows .utils .bids import relative_to_root
3334from .utils .bimap import EstimatorRegistry
35+ from .utils .misc import create_logger
3436
3537
38+ logger = create_logger ('sdcflows.fieldmaps' )
39+
3640_estimators = EstimatorRegistry ()
3741_intents = defaultdict (set )
3842
@@ -109,12 +113,7 @@ class FieldmapFile:
109113 >>> f.suffix
110114 'T1w'
111115
112- >>> FieldmapFile(
113- ... dsA_dir / "sub-01" / "fmap" / "sub-01_dir-LR_epi.nii.gz",
114- ... find_meta=False
115- ... ) # doctest: +IGNORE_EXCEPTION_DETAIL
116- Traceback (most recent call last):
117- MetadataError:
116+ By default, the immediate JSON sidecar file is consulted for metadata.
118117
119118 >>> f = FieldmapFile(
120119 ... dsA_dir / "sub-01" / "fmap" / "sub-01_dir-LR_epi.nii.gz",
@@ -123,11 +122,26 @@ class FieldmapFile:
123122 0.005
124123
125124 >>> f = FieldmapFile(
126- ... dsA_dir / "sub-01" / "fmap" / "sub-01_dir-LR_epi.nii.gz",
127- ... metadata={"TotalReadoutTime": None},
128- ... ) # doctest: +IGNORE_EXCEPTION_DETAIL
129- Traceback (most recent call last):
130- MetadataError:
125+ ... dsA_dir / "sub-01" / "fmap" / "sub-01_phasediff.nii.gz"
126+ ... )
127+ >>> f.metadata['EchoTime2']
128+ 0.00746
129+
130+ >>> f = FieldmapFile(
131+ ... dsA_dir / "sub-01" / "fmap" / "sub-01_phase2.nii.gz"
132+ ... )
133+ >>> f.metadata['EchoTime']
134+ 0.00746
135+
136+ >>> f = FieldmapFile(
137+ ... dsA_dir / "sub-01" / "fmap" / "sub-01_fieldmap.nii.gz"
138+ ... )
139+ >>> f.metadata['Units']
140+ 'rad/s'
141+
142+ However, it is possible to provide alternative metadata.
143+ It is recommended to load the metadata with an external tool that
144+ fully implements the BIDS inheritance rules to ensure correctness.
131145
132146 >>> f = FieldmapFile(
133147 ... dsA_dir / "sub-01" / "fmap" / "sub-01_dir-LR_epi.nii.gz",
@@ -136,44 +150,56 @@ class FieldmapFile:
136150 >>> f.metadata['TotalReadoutTime']
137151 0.006
138152
153+ If metadata is present, warnings or errors may be presented.
154+
155+ >>> FieldmapFile(
156+ ... dsA_dir / "sub-01" / "fmap" / "sub-01_dir-LR_epi.nii.gz",
157+ ... find_meta=False
158+ ... ) # doctest: +IGNORE_EXCEPTION_DETAIL
159+ Traceback (most recent call last):
160+ MetadataError: Missing 'PhaseEncodingDirection' ...
161+
139162 >>> FieldmapFile(
140163 ... dsA_dir / "sub-01" / "fmap" / "sub-01_phasediff.nii.gz",
141164 ... find_meta=False
142165 ... ) # doctest: +IGNORE_EXCEPTION_DETAIL
143166 Traceback (most recent call last):
144167 MetadataError:
145168
146- >>> f = FieldmapFile(
147- ... dsA_dir / "sub-01" / "fmap" / "sub-01_phasediff.nii.gz"
148- ... )
149- >>> f.metadata['EchoTime2']
150- 0.00746
151-
152169 >>> FieldmapFile(
153170 ... dsA_dir / "sub-01" / "fmap" / "sub-01_phase2.nii.gz",
154171 ... find_meta=False
155172 ... ) # doctest: +IGNORE_EXCEPTION_DETAIL
156173 Traceback (most recent call last):
157174 MetadataError:
158175
159- >>> f = FieldmapFile(
160- ... dsA_dir / "sub-01" / "fmap" / "sub-01_phase2.nii.gz"
161- ... )
162- >>> f.metadata['EchoTime']
163- 0.00746
164-
165176 >>> FieldmapFile(
166177 ... dsA_dir / "sub-01" / "fmap" / "sub-01_fieldmap.nii.gz",
167178 ... find_meta=False
168179 ... ) # doctest: +IGNORE_EXCEPTION_DETAIL
169180 Traceback (most recent call last):
170181 MetadataError:
171182
172- >>> f = FieldmapFile(
173- ... dsA_dir / "sub-01" / "fmap" / "sub-01_fieldmap.nii.gz"
174- ... )
175- >>> f.metadata['Units']
176- 'rad/s'
183+ Readout timing information is required to estimate PEPOLAR fieldmaps,
184+ but it is possible to provide a fallback values.
185+ Therefore, warnings are logged if the metadata are missing.
186+
187+ >>> with caplog.at_level(logging.WARNING, "sdcflows.fieldmaps"):
188+ ... f = FieldmapFile(
189+ ... dsA_dir / "sub-01" / "fmap" / "sub-01_dir-LR_epi.nii.gz",
190+ ... metadata={"TotalReadoutTime": None},
191+ ... ) # doctest: +IGNORE_EXCEPTION_DETAIL
192+ >>> print(caplog.text)
193+ WARNING ... Missing readout timing information ... Explicit fallback must be provided.
194+
195+ >>> with caplog.at_level(logging.WARNING, "sdcflows.fieldmaps"):
196+ ... f = FieldmapFile(
197+ ... dsA_dir / "sub-01" / "fmap" / "sub-01_dir-LR_epi.nii.gz",
198+ ... metadata={"PhaseEncodingDirection": "i", "EstimatedTotalReadoutTime": 0.05},
199+ ... find_meta=False,
200+ ... ) # doctest: +IGNORE_EXCEPTION_DETAIL
201+ >>> print(caplog.text)
202+ WARNING ... Missing readout timing information ... Estimated timing is available.
177203
178204 """
179205
@@ -251,12 +277,19 @@ def __attrs_post_init__(self):
251277
252278 from .utils .epimanip import get_trt
253279
280+ msg = "Missing readout timing information for <%s>. %s"
281+ extra = "Explicit fallback must be provided."
282+ have_trt = False
254283 try :
255284 get_trt (self .metadata , in_file = self .path )
256- except ValueError as exc :
257- raise MetadataError (
258- f"Missing readout timing information for <{ self .path } >."
259- ) from exc
285+ have_trt = True
286+ except ValueError :
287+ with suppress (ValueError ):
288+ get_trt (self .metadata , in_file = self .path , use_estimate = True )
289+ extra = "Estimated timing is available."
290+
291+ if not have_trt :
292+ logger .warning (msg , self .path , extra )
260293
261294 elif self .suffix == "fieldmap" and "Units" not in self .metadata :
262295 raise MetadataError (f"Missing 'Units' for <{ self .path } >." )
0 commit comments