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

1"""Functions for working with BIDS datasets.""" 

2 

3from __future__ import annotations 

4 

5import glob 

6import json 

7from pathlib import Path 

8from warnings import warn 

9 

10from nilearn._utils import fill_doc 

11from nilearn._utils.logger import find_stack_level 

12 

13 

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. 

20 

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. 

24 

25 Parameters 

26 ---------- 

27 field : :obj:`str` 

28 Name of the field to be read. For example 'RepetitionTime'. 

29 

30 json_files : :obj:`list` of :obj:`str` 

31 List of path to json files, for example returned by get_bids_files. 

32 

33 bids_path : :obj:`str` or :obj:`pathlib.Path`, optional 

34 Fullpath to the BIDS dataset. 

35 

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 ) 

61 

62 return None 

63 

64 

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. 

68 

69 This corresponds to the reference time (in seconds) used for the slice 

70 timing correction. 

71 

72 See https://github.com/bids-standard/bids-specification/issues/836 

73 

74 Parameters 

75 ---------- 

76 bids_path : :obj:`str` or :obj:`pathlib.Path` 

77 Fullpath to the derivatives folder of the BIDS dataset. 

78 

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'). 

84 

85 %(verbose0)s 

86 

87 Returns 

88 ------- 

89 float or None 

90 Value of the field or None if the field is not found. 

91 

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 

108 

109 return _get_metadata_from_bids( 

110 field="StartTime", 

111 json_files=img_specs, 

112 bids_path=bids_path, 

113 ) 

114 

115 

116@fill_doc 

117def infer_repetition_time_from_dataset(bids_path, filters, verbose=0): 

118 """Return the RepetitionTime metadata field from a BIDS dataset. 

119 

120 Parameters 

121 ---------- 

122 bids_path : :obj:`str` or :obj:`pathlib.Path` 

123 Fullpath to the raw folder of the BIDS dataset. 

124 

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'). 

130 

131 %(verbose0)s 

132 

133 Returns 

134 ------- 

135 float or None 

136 Value of the field or None if the field is not found. 

137 

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 ) 

146 

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 

155 

156 return _get_metadata_from_bids( 

157 field="RepetitionTime", 

158 json_files=img_specs, 

159 bids_path=bids_path, 

160 ) 

161 

162 

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. 

173 

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. 

177 

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. 

183 

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. 

188 

189 Parameters 

190 ---------- 

191 main_path : :obj:`str` or :obj:`pathlib.Path` 

192 Directory of the :term:`BIDS` dataset. 

193 

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. 

197 

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. 

201 

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'. 

206 

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. 

214 

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'). 

221 

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. 

228 

229 Returns 

230 ------- 

231 files : :obj:`list` of :obj:`str` 

232 List of file paths found. 

233 

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}" 

249 

250 files = glob.glob(str(files)) 

251 files.sort() 

252 

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] 

267 

268 return files 

269 

270 

271def parse_bids_filename(img_path, legacy=True): 

272 r"""Return dictionary with parsed information from file path. 

273 

274 Parameters 

275 ---------- 

276 img_path : :obj:`str` 

277 Path to file from which to parse information. 

278 

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. 

283 

284 .. versionadded :: 0.11.2dev 

285 

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. 

291 

292 The dictionary will contain ``'file_path'``, ``'file_basename'``. 

293 

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. 

305 

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). 

312 

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) 

320 

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 ) 

331 

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 

344 

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 

358 

359 return reference