Coverage for nilearn/interfaces/bids/query.py: 13%
81 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""Functions for working with BIDS datasets."""
3from __future__ import annotations
5import glob
6import json
7from pathlib import Path
8from warnings import warn
10from nilearn._utils import fill_doc
11from nilearn._utils.logger import find_stack_level
14def _get_metadata_from_bids(
15 field,
16 json_files,
17 bids_path=None,
18):
19 """Get a metadata field from a BIDS json sidecar files.
21 This assumes that all the json files in the list have the same value
22 for that field,
23 hence the metadata is read only from the first json file in the list.
25 Parameters
26 ----------
27 field : :obj:`str`
28 Name of the field to be read. For example 'RepetitionTime'.
30 json_files : :obj:`list` of :obj:`str`
31 List of path to json files, for example returned by get_bids_files.
33 bids_path : :obj:`str` or :obj:`pathlib.Path`, optional
34 Fullpath to the BIDS dataset.
36 Returns
37 -------
38 float or None
39 value of the field or None if the field is not found.
40 """
41 if json_files:
42 assert isinstance(json_files, list) and isinstance(
43 json_files[0], (Path, str)
44 )
45 with Path(json_files[0]).open() as f:
46 specs = json.load(f)
47 value = specs.get(field)
48 if value is not None:
49 return value
50 else:
51 warn(
52 f"'{field}' not found in file {json_files[0]}.",
53 stacklevel=find_stack_level(),
54 )
55 else:
56 msg_suffix = f" in:\n {bids_path}" if bids_path else ""
57 warn(
58 f"\nNo bold.json found in BIDS folder{msg_suffix}.",
59 stacklevel=find_stack_level(),
60 )
62 return None
65@fill_doc
66def infer_slice_timing_start_time_from_dataset(bids_path, filters, verbose=0):
67 """Return the StartTime metadata field from a BIDS derivatives dataset.
69 This corresponds to the reference time (in seconds) used for the slice
70 timing correction.
72 See https://github.com/bids-standard/bids-specification/issues/836
74 Parameters
75 ----------
76 bids_path : :obj:`str` or :obj:`pathlib.Path`
77 Fullpath to the derivatives folder of the BIDS dataset.
79 filters : :obj:`list` of :obj:`tuple` (:obj:`str`, :obj:`str`), optional
80 Filters are of the form (field, label). Only one filter per field
81 allowed. A file that does not match a filter will be discarded.
82 Filter examples would be ('ses', '01'), ('dir', 'ap') and
83 ('task', 'localizer').
85 %(verbose0)s
87 Returns
88 -------
89 float or None
90 Value of the field or None if the field is not found.
92 """
93 img_specs = get_bids_files(
94 bids_path,
95 modality_folder="func",
96 file_tag="bold",
97 file_type="json",
98 filters=filters,
99 )
100 if not img_specs:
101 if verbose:
102 msg_suffix = f" in:\n {bids_path}"
103 warn(
104 f"\nNo bold.json found in BIDS folder{msg_suffix}.",
105 stacklevel=find_stack_level(),
106 )
107 return None
109 return _get_metadata_from_bids(
110 field="StartTime",
111 json_files=img_specs,
112 bids_path=bids_path,
113 )
116@fill_doc
117def infer_repetition_time_from_dataset(bids_path, filters, verbose=0):
118 """Return the RepetitionTime metadata field from a BIDS dataset.
120 Parameters
121 ----------
122 bids_path : :obj:`str` or :obj:`pathlib.Path`
123 Fullpath to the raw folder of the BIDS dataset.
125 filters : :obj:`list` of :obj:`tuple` (:obj:`str`, :obj:`str`), optional
126 Filters are of the form (field, label). Only one filter per field
127 allowed. A file that does not match a filter will be discarded.
128 Filter examples would be ('ses', '01'), ('dir', 'ap') and
129 ('task', 'localizer').
131 %(verbose0)s
133 Returns
134 -------
135 float or None
136 Value of the field or None if the field is not found.
138 """
139 img_specs = get_bids_files(
140 main_path=bids_path,
141 modality_folder="func",
142 file_tag="bold",
143 file_type="json",
144 filters=filters,
145 )
147 if not img_specs:
148 if verbose:
149 msg_suffix = f" in:\n {bids_path}"
150 warn(
151 f"\nNo bold.json found in BIDS folder{msg_suffix}.",
152 stacklevel=find_stack_level(),
153 )
154 return None
156 return _get_metadata_from_bids(
157 field="RepetitionTime",
158 json_files=img_specs,
159 bids_path=bids_path,
160 )
163def get_bids_files(
164 main_path,
165 file_tag="*",
166 file_type="*",
167 sub_label="*",
168 modality_folder="*",
169 filters=None,
170 sub_folder=True,
171):
172 """Search for files in a :term:`BIDS` dataset following given constraints.
174 This utility function allows to filter files in the :term:`BIDS` dataset by
175 any of the fields contained in the file names. Moreover it allows to search
176 for specific types of files or particular tags.
178 The provided filters have to correspond to a file name field, so
179 any file not containing the field will be ignored. For example the filter
180 ('sub', '01') would return all files corresponding to the first
181 subject that specifically contain in the file name 'sub-01'. If more
182 filters are given then we constraint the possible files names accordingly.
184 Notice that to search in the derivatives folder, it has to be given as
185 part of the main_path. This is useful since the current convention gives
186 exactly the same inner structure to derivatives than to the main
187 :term:`BIDS` dataset folder, so we can search it in the same way.
189 Parameters
190 ----------
191 main_path : :obj:`str` or :obj:`pathlib.Path`
192 Directory of the :term:`BIDS` dataset.
194 file_tag : :obj:`str` accepted by glob, default='*'
195 The final tag of the desired files. For example 'bold' if one is
196 interested in the files related to the neuroimages.
198 file_type : :obj:`str` accepted by glob, default='*'
199 The type of the desired files. For example to be able to request only
200 'nii' or 'json' files for the 'bold' tag.
202 sub_label : :obj:`str` accepted by glob, default='*'
203 Such a common filter is given as a direct option since it applies also
204 at the level of directories. the label is what follows the 'sub' field
205 in the :term:`BIDS` convention as 'sub-label'.
207 modality_folder : :obj:`str` accepted by glob, default='*'
208 Inside the subject and optional session folders a final level of
209 folders is expected in the :term:`BIDS` convention that groups files
210 according to different neuroimaging modalities and any other additions
211 of the dataset provider. For example the 'func' and 'anat' standard
212 folders. If given as the empty string '', files will be searched
213 inside the sub-label/ses-label directories.
215 filters : :obj:`list` of :obj:`tuple` (:obj:`str`, :obj:`str`), \
216 default=None
217 Filters are of the form (field, label). Only one filter per field
218 allowed. A file that does not match a filter will be discarded.
219 Filter examples would be ('ses', '01'), ('dir', 'ap') and
220 ('task', 'localizer').
222 sub_folder : :obj:`bool`, default=True
223 Determines if the files searched are at the level of
224 subject/session folders or just below the dataset main folder.
225 Setting this option to False with other default values would return
226 all the files below the main directory, ignoring files in subject
227 or derivatives folders.
229 Returns
230 -------
231 files : :obj:`list` of :obj:`str`
232 List of file paths found.
234 """
235 main_path = Path(main_path)
236 if sub_folder:
237 files = main_path / "sub-*" / "ses-*"
238 session_folder_exists = glob.glob(str(files))
239 ses_level = "ses-*" if session_folder_exists else ""
240 files = (
241 main_path
242 / f"sub-{sub_label}"
243 / ses_level
244 / modality_folder
245 / f"sub-{sub_label}*_{file_tag}.{file_type}"
246 )
247 else:
248 files = main_path / f"*{file_tag}.{file_type}"
250 files = glob.glob(str(files))
251 files.sort()
253 filters = filters or []
254 if filters:
255 files = [parse_bids_filename(file_, legacy=False) for file_ in files]
256 for entity, label in filters:
257 files = [
258 file_
259 for file_ in files
260 if (entity not in file_["entities"] and label == "")
261 or (
262 entity in file_["entities"]
263 and file_["entities"][entity] == label
264 )
265 ]
266 return [ref_file["file_path"] for ref_file in files]
268 return files
271def parse_bids_filename(img_path, legacy=True):
272 r"""Return dictionary with parsed information from file path.
274 Parameters
275 ----------
276 img_path : :obj:`str`
277 Path to file from which to parse information.
279 legacy : :obj:`bool`, default=True
280 Whether to return a dictionary that uses BIDS terms (``False``)
281 or the legacy content for the output (``True``).
282 ``False`` will become the default in version >= 0.13.2.
284 .. versionadded :: 0.11.2dev
286 Returns
287 -------
288 reference : :obj:`dict`
289 Returns a dictionary with all key-value pairs in the file name
290 parsed and other useful fields.
292 The dictionary will contain ``'file_path'``, ``'file_basename'``.
294 If ``legacy`` is set to ``True``,
295 the dictionary will also contain
296 'file_tag', 'file_type' and 'file_fields'.
297 The 'file_tag' field refers to the last part of the file under the
298 :term:`BIDS` convention that is of the form \*_tag.type.
299 Contrary to the rest of the file name it is not a key-value pair.
300 This notion should be revised in the case we are handling derivatives
301 since so far the convention will keep the tag prepended to any fields
302 added in the case of preprocessed files that also end with another tag.
303 This parser will consider any tag in the middle of the file name as a
304 key with no value and will be included in the 'file_fields' key.
306 If ``legacy`` is set to ``False``,
307 the dictionary will instead contain
308 ``'extension'``, ``'suffix'`` and ``'entities'``.
309 (See the documentation on
310 `typical bids filename <https://bids.neuroimaging.io/getting_started/folders_and_files/files.html#filename-template>`_
311 for more information).
313 """
314 reference = {
315 "file_path": img_path,
316 "file_basename": Path(img_path).name,
317 }
318 parts = reference["file_basename"].split("_")
319 suffix, extension = parts[-1].split(".", 1)
321 if legacy:
322 warn(
323 (
324 "For versions >= 0.13.2 this function will always return "
325 "a dictionary that uses BIDS terms as keys. "
326 "Set 'legacy=False' to start using this new behavior."
327 ),
328 DeprecationWarning,
329 stacklevel=find_stack_level(),
330 )
332 reference["file_tag"] = suffix
333 reference["file_type"] = extension
334 reference["file_fields"] = []
335 for part in parts[:-1]:
336 field = part.split("-")[0]
337 reference["file_fields"].append(field)
338 # In derivatives is not clear if the source file name will
339 # be parsed as a field with no value.
340 reference[field] = None
341 if len(part.split("-")) > 1:
342 value = part.split("-")[1]
343 reference[field] = value
345 else:
346 reference["extension"] = extension
347 reference["suffix"] = suffix
348 reference["entities"] = {}
349 for part in parts[:-1]:
350 entity = part.split("-")[0]
351 # In derivatives is not clear if the source file name will
352 # be parsed as a field with no value.
353 label = None
354 if len(part.split("-")) > 1:
355 value = part.split("-")[1]
356 label = value
357 reference["entities"][entity] = label
359 return reference