Skip to content

Commit bc6fe17

Browse files
authored
Merge branch 'nipreps:master' into fix/precomputed-mask-no-preproc
2 parents 10549c3 + d24cd43 commit bc6fe17

File tree

4 files changed

+81
-57
lines changed

4 files changed

+81
-57
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ repos:
1212
- id: check-toml
1313
- id: check-added-large-files
1414
- repo: https://github.com/astral-sh/ruff-pre-commit
15-
rev: v0.2.0
15+
rev: v0.6.5
1616
hooks:
1717
- id: ruff
18+
args: [ --fix ]
1819
- id: ruff-format

nibabies/_warnings.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

nibabies/utils/bids.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
import os
99
import sys
1010
import typing as ty
11+
import warnings
1112
from pathlib import Path
1213

14+
import pandas as pd
15+
1316
SUPPORTED_AGE_UNITS = (
1417
'weeks',
1518
'months',
@@ -244,7 +247,11 @@ def parse_bids_for_age_months(
244247

245248
scans_tsv = session_level / f'{prefix}_scans.tsv'
246249
if scans_tsv.exists():
247-
age = _get_age_from_tsv(scans_tsv)
250+
age = _get_age_from_tsv(
251+
scans_tsv,
252+
index_column='filename',
253+
index_val=r'^anat.*',
254+
)
248255

249256
if age is not None:
250257
return age
@@ -258,16 +265,18 @@ def parse_bids_for_age_months(
258265

259266
participants_tsv = Path(bids_root) / 'participants.tsv'
260267
if participants_tsv.exists() and age is None:
261-
age = _get_age_from_tsv(participants_tsv, index_column='participant_id', index_val=subject)
268+
age = _get_age_from_tsv(
269+
participants_tsv, index_column='participant_id', index_value=subject
270+
)
262271

263272
return age
264273

265274

266275
def _get_age_from_tsv(
267-
bids_tsv: Path, index_column: str | None = None, index_val: str | None = None
268-
) -> int | None:
269-
import pandas as pd
270-
276+
bids_tsv: Path,
277+
index_column: str | None = None,
278+
index_value: str | None = None,
279+
) -> float | None:
271280
df = pd.read_csv(str(bids_tsv), sep='\t')
272281
age_col = None
273282

@@ -278,14 +287,18 @@ def _get_age_from_tsv(
278287
if age_col is None:
279288
return
280289

281-
if not index_column or not index_val: # Just grab first value
282-
idx = df.index[0]
283-
else:
284-
idx = df.index[df[index_column] == index_val].item()
290+
df = df[df[index_column].str.fullmatch(index_value)]
291+
292+
# Multiple indices may be present after matching
293+
if len(df) > 1:
294+
warnings.warn(
295+
f'Multiple matches for {index_column}:{index_value} found in {bids_tsv.name}.',
296+
stacklevel=1,
297+
)
285298

286299
try:
287300
# extract age value from row
288-
age = int(df.loc[idx, age_col].item())
301+
age = float(df.loc[df.index[0], age_col].item())
289302
except Exception: # noqa: BLE001
290303
return
291304

@@ -294,7 +307,10 @@ def _get_age_from_tsv(
294307
bids_json = bids_tsv.with_suffix('.json')
295308
age_units = _get_age_units(bids_json)
296309
if age_units is False:
297-
return None
310+
raise FileNotFoundError(
311+
f'Could not verify age unit for {bids_tsv.name} - ensure a sidecar JSON '
312+
'describing column `age` units is available.'
313+
)
298314
else:
299315
age_units = age_col.split('_')[-1]
300316

@@ -318,12 +334,14 @@ def _get_age_units(bids_json: Path) -> ty.Literal['weeks', 'months', 'years', Fa
318334
return False
319335

320336

321-
def age_to_months(age: int, units: ty.Literal['weeks', 'months', 'years']) -> int:
337+
def age_to_months(age: int | float, units: ty.Literal['weeks', 'months', 'years']) -> int:
322338
"""
323339
Convert a given age, in either "weeks", "months", or "years", into months.
324340
325341
>>> age_to_months(1, 'years')
326342
12
343+
>>> age_to_months(0.5, 'years')
344+
6
327345
>>> age_to_months(2, 'weeks')
328346
0
329347
>>> age_to_months(3, 'weeks')
@@ -335,7 +353,7 @@ def age_to_months(age: int, units: ty.Literal['weeks', 'months', 'years']) -> in
335353
YEARS_TO_MONTH = 12
336354

337355
if units == 'weeks':
338-
age = round(age * WEEKS_TO_MONTH)
356+
age *= WEEKS_TO_MONTH
339357
elif units == 'years':
340358
age *= YEARS_TO_MONTH
341-
return age
359+
return int(round(age))

nibabies/utils/tests/test_bids.py

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,59 @@ def create_sidecar(tsv_file: Path, units) -> None:
2424
age_months = {'age_months': [3, 6, 9]}
2525
age_years = {'age_years': [1, 1, 2]}
2626

27+
participants = {'participant_id': ['sub-1', 'sub-2', 'sub-11']}
28+
sessions = {'session_id': ['ses-1', 'ses-2', 'ses-3']}
29+
scans = {
30+
'filename': [
31+
'dwi/sub-01_dwi.nii.gz',
32+
'anat/sub-01_T1w.nii.gz',
33+
'func/sub-01_task-rest_bold.nii.gz',
34+
]
35+
}
36+
2737

2838
@pytest.mark.parametrize(
29-
('idx_col', 'idx_val', 'data', 'sidecar', 'expected'),
39+
('idx_col', 'idx_val', 'data', 'units', 'expected'),
3040
[
31-
('session_id', 'x1', age, False, None),
32-
('session_id', 'x1', age, 'months', 4),
33-
('session_id', 'x1', age, 'weeks', 1), # Convert from 4 weeks -> 1 month
34-
('session_id', 'x1', age, ['months', 'weeks'], None),
35-
('session_id', 'x2', age_weeks, False, 2),
36-
('participant_id', 'x1', age_months, False, 3),
37-
('participant_id', 'x3', age_years, False, 24),
38-
('session_id', 'x3', {**age_months, **age}, False, 9),
39-
(None, None, age_months, False, 3),
41+
('session_id', 'ses-1', age, 'months', 4),
42+
('session_id', 'ses-1', age, 'weeks', 1), # Convert from 4 weeks -> 1 month
43+
('session_id', 'ses-2', age_weeks, False, 2),
44+
('participant_id', 'sub-1', age_months, False, 3),
45+
('participant_id', 'sub-11', age_years, False, 24),
46+
('session_id', 'ses-3', {**age_months, **age}, False, 9),
47+
('filename', r'^anat.*', age_months, False, 6),
4048
],
4149
)
42-
def test_get_age_from_tsv(tmp_path, idx_col, idx_val, data, sidecar, expected):
50+
def test_get_age_from_tsv(tmp_path, idx_col, idx_val, data, units, expected):
4351
tsv_file = tmp_path / 'test-age-parsing.tsv'
44-
base = {}
45-
if idx_col is not None:
46-
base[idx_col] = ['x1', 'x2', 'x3']
47-
create_tsv({**base, **data}, tsv_file)
4852

49-
if sidecar:
50-
create_sidecar(tsv_file, sidecar)
53+
if idx_col == 'participant_id':
54+
base = participants
55+
elif idx_col == 'session_id':
56+
base = sessions
57+
elif idx_col == 'filename':
58+
base = scans
59+
60+
create_tsv({**base, **data}, tsv_file)
61+
if units:
62+
create_sidecar(tsv_file, units)
5163

5264
res = _get_age_from_tsv(tsv_file, idx_col, idx_val)
5365
assert res == expected
66+
67+
68+
def test_get_age_from_tsv_error(tmp_path):
69+
tsv_file = tmp_path / 'participants.tsv'
70+
71+
create_tsv({**participants, **age}, tsv_file)
72+
with pytest.raises(FileNotFoundError):
73+
_get_age_from_tsv(tsv_file, 'participant_id', 'sub-1')
74+
75+
76+
def test_get_age_from_tsv_warning(tmp_path):
77+
tsv_file = tmp_path / 'participants.tsv'
78+
dual_participants = {'participant_id': ['sub-1', 'sub-2', 'sub-2']}
79+
create_tsv({**dual_participants, **age_months}, tsv_file)
80+
81+
with pytest.warns(UserWarning):
82+
_get_age_from_tsv(tsv_file, 'participant_id', 'sub-2')

0 commit comments

Comments
 (0)