Skip to content

Commit 51dd6e9

Browse files
authored
Merge pull request #367 from PennLINC/bids-uris
Support BIDS URIs in CuBIDS apply
2 parents 135aa1f + e852a9f commit 51dd6e9

File tree

4 files changed

+357
-5
lines changed

4 files changed

+357
-5
lines changed

cubids/cubids.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -631,11 +631,16 @@ def change_filename(self, filepath, entities):
631631
# Coerce IntendedFor to a list.
632632
data["IntendedFor"] = listify(data["IntendedFor"])
633633
for item in data["IntendedFor"]:
634-
if item in _get_intended_for_reference(filepath):
634+
if item == _get_participant_relative_path(filepath):
635635
# remove old filename
636636
data["IntendedFor"].remove(item)
637637
# add new filename
638-
data["IntendedFor"].append(_get_intended_for_reference(new_path))
638+
data["IntendedFor"].append(_get_participant_relative_path(new_path))
639+
if item == _get_bidsuri(filepath, self.path):
640+
# remove old filename
641+
data["IntendedFor"].remove(item)
642+
# add new filename
643+
data["IntendedFor"].append(_get_bidsuri(new_path, self.path))
639644

640645
# update the json with the new data dictionary
641646
_update_json(filename_with_if, data)
@@ -757,7 +762,7 @@ def _purge_associations(self, scans):
757762
# sub, ses, modality only (no self.path)
758763
if_scans = []
759764
for scan in scans:
760-
if_scans.append(_get_intended_for_reference(self.path + scan))
765+
if_scans.append(_get_participant_relative_path(self.path + scan))
761766

762767
for path in Path(self.path).rglob("sub-*/*/fmap/*.json"):
763768
# json_file = self.layout.get_file(str(path))
@@ -1428,10 +1433,28 @@ def _file_to_entity_set(filename):
14281433
return _entities_to_entity_set(entities)
14291434

14301435

1431-
def _get_intended_for_reference(scan):
1436+
def _get_participant_relative_path(scan):
1437+
"""Build the relative-from-subject version of a Path to a file.
1438+
1439+
This is what will appear in the IntendedFor field of any association.
1440+
1441+
"""
14321442
return "/".join(Path(scan).parts[-3:])
14331443

14341444

1445+
def _get_bidsuri(filename, dataset_root):
1446+
"""Convert a file path to a bidsuri.
1447+
1448+
Examples
1449+
--------
1450+
>>> _get_bidsuri("/path/to/bids/sub-01/ses-01/dataset_description.json", "/path/to/bids")
1451+
'bids::sub-01/ses-01/dataset_description.json'
1452+
"""
1453+
if dataset_root in filename:
1454+
return filename.replace(dataset_root, "bids::").replace("bids::/", "bids::")
1455+
raise ValueError(f"Only local datasets are supported: {filename}")
1456+
1457+
14351458
def _get_param_groups(
14361459
files,
14371460
fieldmap_lookup,

cubids/tests/test_apply.py

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
"""Test cubids apply."""
2+
3+
import pandas as pd
4+
import pytest
5+
from niworkflows.utils.testing import generate_bids_skeleton
6+
7+
relpath_intendedfor_long = {
8+
"01": [
9+
{
10+
"session": "01",
11+
"anat": [{"suffix": "T1w", "metadata": {"EchoTime": 1}}],
12+
"fmap": [
13+
{
14+
"dir": "AP",
15+
"suffix": "epi",
16+
"metadata": {
17+
"IntendedFor": ["ses-01/dwi/sub-01_ses-01_dir-AP_run-01_dwi.nii.gz"],
18+
},
19+
},
20+
{
21+
"dir": "PA",
22+
"suffix": "epi",
23+
"metadata": {
24+
"IntendedFor": ["ses-01/dwi/sub-01_ses-01_dir-AP_run-01_dwi.nii.gz"],
25+
},
26+
},
27+
],
28+
"dwi": [
29+
{
30+
"dir": "AP",
31+
"run": "01",
32+
"suffix": "dwi",
33+
"metadata": {
34+
"RepetitionTime": 0.8,
35+
},
36+
}
37+
],
38+
},
39+
],
40+
}
41+
bidsuri_intendedfor_long = {
42+
"01": [
43+
{
44+
"session": "01",
45+
"anat": [{"suffix": "T1w", "metadata": {"EchoTime": 1}}],
46+
"fmap": [
47+
{
48+
"dir": "AP",
49+
"suffix": "epi",
50+
"metadata": {
51+
"IntendedFor": [
52+
"bids::sub-01/ses-01/dwi/sub-01_ses-01_dir-AP_run-01_dwi.nii.gz"
53+
],
54+
},
55+
},
56+
{
57+
"dir": "PA",
58+
"suffix": "epi",
59+
"metadata": {
60+
"IntendedFor": [
61+
"bids::sub-01/ses-01/dwi/sub-01_ses-01_dir-AP_run-01_dwi.nii.gz"
62+
],
63+
},
64+
},
65+
],
66+
"dwi": [
67+
{
68+
"dir": "AP",
69+
"run": "01",
70+
"suffix": "dwi",
71+
"metadata": {
72+
"RepetitionTime": 0.8,
73+
},
74+
}
75+
],
76+
},
77+
],
78+
}
79+
relpath_intendedfor_cs = {
80+
"01": [
81+
{
82+
"anat": [{"suffix": "T1w", "metadata": {"EchoTime": 1}}],
83+
"fmap": [
84+
{
85+
"dir": "AP",
86+
"suffix": "epi",
87+
"metadata": {
88+
"IntendedFor": ["dwi/sub-01_dir-AP_run-01_dwi.nii.gz"],
89+
},
90+
},
91+
{
92+
"dir": "PA",
93+
"suffix": "epi",
94+
"metadata": {
95+
"IntendedFor": ["dwi/sub-01_dir-AP_run-01_dwi.nii.gz"],
96+
},
97+
},
98+
],
99+
"dwi": [
100+
{
101+
"dir": "AP",
102+
"run": "01",
103+
"suffix": "dwi",
104+
"metadata": {
105+
"RepetitionTime": 0.8,
106+
},
107+
}
108+
],
109+
},
110+
],
111+
}
112+
bidsuri_intendedfor_cs = {
113+
"01": [
114+
{
115+
"anat": [{"suffix": "T1w", "metadata": {"EchoTime": 1}}],
116+
"fmap": [
117+
{
118+
"dir": "AP",
119+
"suffix": "epi",
120+
"metadata": {
121+
"IntendedFor": ["bids::sub-01/dwi/sub-01_dir-AP_run-01_dwi.nii.gz"],
122+
},
123+
},
124+
{
125+
"dir": "PA",
126+
"suffix": "epi",
127+
"metadata": {
128+
"IntendedFor": ["bids::sub-01/dwi/sub-01_dir-AP_run-01_dwi.nii.gz"],
129+
},
130+
},
131+
],
132+
"dwi": [
133+
{
134+
"dir": "AP",
135+
"run": "01",
136+
"suffix": "dwi",
137+
"metadata": {
138+
"RepetitionTime": 0.8,
139+
},
140+
}
141+
],
142+
},
143+
],
144+
}
145+
146+
147+
@pytest.fixture(scope="module")
148+
def files_data():
149+
"""A dictionary describing a CuBIDS files tsv file for testing."""
150+
dict_ = {
151+
"longitudinal": {
152+
"ParamGroup": [1, 1, 1, 1],
153+
"EntitySet": [
154+
"datatype-anat_suffix-T1w",
155+
"datatype-dwi_direction-AP_run-01_suffix-dwi",
156+
"datatype-fmap_direction-AP_fmap-epi_suffix-epi",
157+
"datatype-fmap_direction-PA_fmap-epi_suffix-epi",
158+
],
159+
"FilePath": [
160+
"/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii.gz",
161+
"/sub-01/ses-01/dwi/sub-01_ses-01_dir-AP_run-01_dwi.nii.gz",
162+
"/sub-01/ses-01/fmap/sub-01_ses-01_dir-AP_epi.nii.gz",
163+
"/sub-01/ses-01/fmap/sub-01_ses-01_dir-PA_epi.nii.gz",
164+
],
165+
"KeyParamGroup": [
166+
"datatype-anat_suffix-T1w__1",
167+
"datatype-dwi_direction-AP_run-01_suffix-dwi__1",
168+
"datatype-fmap_direction-AP_fmap-epi_suffix-epi__1",
169+
"datatype-fmap_direction-PA_fmap-epi_suffix-epi__1",
170+
],
171+
},
172+
"cross-sectional": {
173+
"ParamGroup": [1, 1, 1, 1],
174+
"EntitySet": [
175+
"datatype-anat_suffix-T1w",
176+
"datatype-dwi_direction-AP_run-01_suffix-dwi",
177+
"datatype-fmap_direction-AP_fmap-epi_suffix-epi",
178+
"datatype-fmap_direction-PA_fmap-epi_suffix-epi",
179+
],
180+
"FilePath": [
181+
"/sub-01/anat/sub-01_T1w.nii.gz",
182+
"/sub-01/dwi/sub-01_dir-AP_run-01_dwi.nii.gz",
183+
"/sub-01/fmap/sub-01_dir-AP_epi.nii.gz",
184+
"/sub-01/fmap/sub-01_dir-PA_epi.nii.gz",
185+
],
186+
"KeyParamGroup": [
187+
"datatype-anat_suffix-T1w__1",
188+
"datatype-dwi_direction-AP_run-01_suffix-dwi__1",
189+
"datatype-fmap_direction-AP_fmap-epi_suffix-epi__1",
190+
"datatype-fmap_direction-PA_fmap-epi_suffix-epi__1",
191+
],
192+
},
193+
}
194+
return dict_
195+
196+
197+
@pytest.fixture(scope="module")
198+
def summary_data():
199+
"""A dictionary describing a CuBIDS summary tsv file for testing."""
200+
dict_ = {
201+
"RenameEntitySet": [
202+
None,
203+
"acquisition-VAR_datatype-dwi_direction-AP_run-01_suffix-dwi",
204+
None,
205+
None,
206+
],
207+
"KeyParamGroup": [
208+
"datatype-anat_suffix-T1w__1",
209+
"datatype-dwi_direction-AP_run-01_suffix-dwi__1",
210+
"datatype-fmap_direction-AP_fmap-epi_suffix-epi__1",
211+
"datatype-fmap_direction-PA_fmap-epi_suffix-epi__1",
212+
],
213+
"HasFieldmap": [False, True, False, False],
214+
"UsedAsFieldmap": [False, False, True, True],
215+
"MergeInto": [None, None, None, None],
216+
"EntitySet": [
217+
"datatype-anat_suffix-T1w",
218+
"datatype-dwi_direction-AP_run-01_suffix-dwi",
219+
"datatype-fmap_direction-AP_fmap-epi_suffix-epi",
220+
"datatype-fmap_direction-PA_fmap-epi_suffix-epi",
221+
],
222+
"ParamGroup": [1, 1, 1, 1],
223+
}
224+
return dict_
225+
226+
227+
@pytest.mark.parametrize(
228+
("name", "skeleton", "intended_for", "expected"),
229+
[
230+
(
231+
"relpath_long",
232+
relpath_intendedfor_long,
233+
# XXX: Should not have extra leading zero in run entity, but that's a known bug.
234+
"ses-01/dwi/sub-01_ses-01_acq-VAR_dir-AP_run-001_dwi.nii.gz",
235+
"pass",
236+
),
237+
(
238+
"bidsuri_long",
239+
bidsuri_intendedfor_long,
240+
# XXX: Should not have extra leading zero in run entity, but that's a known bug.
241+
"bids::sub-01/ses-01/dwi/sub-01_ses-01_acq-VAR_dir-AP_run-001_dwi.nii.gz",
242+
"pass",
243+
),
244+
(
245+
"relpath_cs",
246+
relpath_intendedfor_cs,
247+
# XXX: Should not have extra leading zero in run entity, but that's a known bug.
248+
# XXX: CuBIDS enforces longitudinal dataset, so this fails.
249+
"dwi/sub-01_acq-VAR_dir-AP_run-001_dwi.nii.gz",
250+
TypeError,
251+
),
252+
(
253+
"bidsuri_cs",
254+
bidsuri_intendedfor_cs,
255+
# XXX: Should not have extra leading zero in run entity, but that's a known bug.
256+
# XXX: CuBIDS enforces longitudinal dataset, so this fails.
257+
"bids::sub-01/dwi/sub-01_acq-VAR_dir-AP_run-001_dwi.nii.gz",
258+
TypeError,
259+
),
260+
],
261+
)
262+
def test_cubids_apply_intendedfor(
263+
tmpdir,
264+
summary_data,
265+
files_data,
266+
name,
267+
skeleton,
268+
intended_for,
269+
expected,
270+
):
271+
"""Test cubids apply with different IntendedFor types."""
272+
import json
273+
274+
from cubids.workflows import apply
275+
276+
# Generate a BIDS dataset
277+
bids_dir = tmpdir / name
278+
generate_bids_skeleton(str(bids_dir), skeleton)
279+
280+
if "long" in name:
281+
fdata = files_data["longitudinal"]
282+
fmap_json = bids_dir / "sub-01/ses-01/fmap/sub-01_ses-01_dir-AP_epi.json"
283+
else:
284+
fdata = files_data["cross-sectional"]
285+
fmap_json = bids_dir / "sub-01/fmap/sub-01_dir-AP_epi.json"
286+
287+
# Create a CuBIDS summary tsv
288+
summary_tsv = tmpdir / "summary.tsv"
289+
df = pd.DataFrame(summary_data)
290+
df.to_csv(summary_tsv, sep="\t", index=False)
291+
292+
# Create a CuBIDS files tsv
293+
files_tsv = tmpdir / "files.tsv"
294+
df = pd.DataFrame(fdata)
295+
df.to_csv(files_tsv, sep="\t", index=False)
296+
297+
# Run cubids apply
298+
if isinstance(expected, str):
299+
apply(
300+
bids_dir=str(bids_dir),
301+
use_datalad=False,
302+
acq_group_level="subject",
303+
config=None,
304+
edited_summary_tsv=summary_tsv,
305+
files_tsv=files_tsv,
306+
new_tsv_prefix=None,
307+
container=None,
308+
)
309+
310+
with open(fmap_json) as f:
311+
metadata = json.load(f)
312+
313+
assert metadata["IntendedFor"] == [intended_for]
314+
else:
315+
with pytest.raises(expected):
316+
apply(
317+
bids_dir=str(bids_dir),
318+
use_datalad=False,
319+
acq_group_level="subject",
320+
config=None,
321+
edited_summary_tsv=summary_tsv,
322+
files_tsv=files_tsv,
323+
new_tsv_prefix=None,
324+
container=None,
325+
)

cubids/workflows.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ def apply(
467467
str(new_tsv_prefix),
468468
raise_on_error=False,
469469
)
470-
sys.exit(0)
470+
return
471471

472472
# Run it through a container
473473
container_type = _get_container_type(container)

0 commit comments

Comments
 (0)