Skip to content

Commit bdbc91e

Browse files
committed
RF: Add 'AcquisitionLabel' as matching_parameter option for fmap matching
Remove it from the `Shims` option. Add corresponding unit tests and documentation.
1 parent 34c1b41 commit bdbc91e

File tree

3 files changed

+104
-53
lines changed

3 files changed

+104
-53
lines changed

docs/heuristics.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ The parameters that can be specified and the allowed options are defined in ``bi
117117
* ``'ImagingVolume'``: both ``fmaps`` and images will need to have the same the imaging
118118
volume (the header affine transformation: position, orientation and voxel size, as well
119119
as number of voxels along each dimensions).
120+
* ``'AcquisitionLabel'``: it checks for what modality (``anat``, ``func``, ``dwi``) each
121+
``fmap`` is intended by checking the ``_acq-`` label in the filename
120122
* ``'Force'``: forces ``heudiconv`` to consider any ``fmaps`` in the session to be
121123
suitable for any image, no matter what the imaging parameters are.
122124

heudiconv/bids.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class BIDSError(Exception):
5555
AllowedFmapParameterMatching = [
5656
'Shims',
5757
'ImagingVolume',
58+
'AcquisitionLabel',
5859
'Force',
5960
]
6061
# Key info returned by get_key_info_for_fmap_assignment when
@@ -513,30 +514,23 @@ def convert_sid_bids(subject_id):
513514
def get_shim_setting(json_file):
514515
"""
515516
Gets the "ShimSetting" field from a json_file.
516-
If not present:
517-
- for fmap files: use the <acq> entity from the file name
518-
- for other files: use the folder name ('anat', 'func', 'dwi', ...)
517+
If no "ShimSetting" present, return error
519518
520519
Parameters:
521520
----------
522521
json_file : str
523522
524523
Returns:
525524
-------
526-
str with "ShimSetting" value or <acq> entity
525+
str with "ShimSetting" value
527526
"""
528527
data = load_json(json_file)
529-
if SHIM_KEY in data.keys():
528+
try:
530529
shims = data[SHIM_KEY]
531-
else:
532-
modality = op.basename(op.dirname(json_file))
533-
if modality == 'fmap':
534-
# extract the <acq> entity:
535-
shims = re.search('(?<=[/_]acq-)\w+',json_file).group(0).split('_')[0]
536-
if shims.lower() in ['fmri', 'bold']:
537-
shims = 'func'
538-
else:
539-
shims = modality
530+
except KeyError as e:
531+
lgr.error('File %s does not have "ShimSetting".'
532+
'Please use a different "matching_parameters" in your heuristic file', json_file)
533+
raise KeyError
540534
return shims
541535

542536

@@ -620,6 +614,20 @@ def get_key_info_for_fmap_assignment(json_file, matching_parameter='ImagingVolum
620614
nifti_file = glob(remove_suffix(json_file, '.json') + '.nii*')
621615
nifti_header = nb_load(nifti_file).header
622616
key_info = [nifti_header.affine, nifti_header.dim[1:3]]
617+
elif matching_parameter == 'AcquisitionLabel':
618+
# Check the acq label for the fmap and the modality for others:
619+
modality = op.basename(op.dirname(json_file))
620+
if modality == 'fmap':
621+
# extract the <acq> entity:
622+
acq_label = re.search('(?<=[/_]acq-)\w+', json_file).group(0).split('_')[0]
623+
if any(s in acq_label.lower() for s in ['fmri', 'bold', 'func']):
624+
key_info = ['func']
625+
elif any(s in acq_label.lower() for s in ['diff', 'dwi']):
626+
key_info = ['dwi']
627+
elif any(s in acq_label.lower() for s in ['anat', 'struct']):
628+
key_info = ['anat']
629+
else:
630+
key_info = [modality]
623631
elif matching_parameter == 'Force':
624632
# We want to force the matching, so just return some string
625633
# regardless of the image

heudiconv/tests/test_bids.py

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -60,29 +60,21 @@ def test_treat_age():
6060
TODAY = datetime.today()
6161

6262

63-
# Test scenarios:
64-
# -file with "ShimSetting" field
65-
# -file with no "ShimSetting", in "foo" dir, should return "foo"
66-
# -file with no "ShimSetting", in "fmap" dir, acq-CatchThis, should return
67-
# "CatchThis"
68-
# -file with no "ShimSetting", in "fmap" dir, acq-fMRI, should return "func"
6963
A_SHIM = ['{0:.4f}'.format(random()) for i in range(SHIM_LENGTH)]
70-
@pytest.mark.parametrize(
71-
"fname, content, expected_return", [
72-
(op.join('foo', 'bar.json'), {SHIM_KEY: A_SHIM}, A_SHIM),
73-
(op.join('dont_catch_this', 'foo', 'bar.json'), {}, 'foo'),
74-
(op.join('dont_catch_this', 'fmap', 'bar_acq-CatchThis.json'), {}, 'CatchThis'),
75-
(op.join('dont_catch_this', 'fmap', 'bar_acq-fMRI.json'), {}, 'func'),
76-
]
77-
)
78-
def test_get_shim_setting(tmpdir, fname, content, expected_return):
64+
def test_get_shim_setting(tmpdir):
7965
""" Tests for get_shim_setting """
80-
json_name = op.join(str(tmpdir), fname)
81-
json_dir = op.dirname(json_name)
66+
json_dir = op.join(str(tmpdir), 'foo')
8267
if not op.exists(json_dir):
8368
os.makedirs(json_dir)
84-
save_json(json_name, content)
85-
assert get_shim_setting(json_name) == expected_return
69+
json_name = op.join(json_dir, 'sub-foo.json')
70+
# 1) file with no "ShimSetting", should return None
71+
save_json(json_name, {})
72+
with pytest.raises(KeyError):
73+
assert get_shim_setting(json_name)
74+
75+
# -file with "ShimSetting" field
76+
save_json(json_name, {SHIM_KEY: A_SHIM})
77+
assert get_shim_setting(json_name) == A_SHIM
8678

8779

8880
def test_get_key_info_for_fmap_assignment(tmpdir, monkeypatch):
@@ -135,6 +127,26 @@ def mock_nibabel_load(file):
135127
)
136128
assert key_info == [KeyInfoForForce]
137129

130+
# 5) matching_parameter = 'AcquisitionLabel'
131+
for d in ['fmap', 'func', 'dwi', 'anat']:
132+
os.makedirs(op.join(str(tmpdir), d))
133+
for (dirname, fname, expected_key_info) in [
134+
('fmap', 'sub-foo_acq-fmri_epi.json', 'func'),
135+
('fmap', 'sub-foo_acq-bold_epi.json', 'func'),
136+
('fmap', 'sub-foo_acq-func_epi.json', 'func'),
137+
('fmap', 'sub-foo_acq-diff_epi.json', 'dwi'),
138+
('fmap', 'sub-foo_acq-anat_epi.json', 'anat'),
139+
('fmap', 'sub-foo_acq-struct_epi.json', 'anat'),
140+
('func', 'sub-foo_bold.json', 'func'),
141+
('dwi', 'sub-foo_dwi.json', 'dwi'),
142+
('anat', 'sub-foo_T1w.json', 'anat'),
143+
]:
144+
json_name = op.join(str(tmpdir), dirname, fname)
145+
save_json(json_name, {SHIM_KEY: A_SHIM})
146+
assert [expected_key_info] == get_key_info_for_fmap_assignment(
147+
json_name, matching_parameter='AcquisitionLabel'
148+
)
149+
138150
# Finally: invalid matching_parameters:
139151
with pytest.raises(ValueError):
140152
assert get_key_info_for_fmap_assignment(
@@ -207,6 +219,7 @@ def create_dummy_pepolar_bids_session(session_path):
207219
# 1) Simulate the file structure for a session:
208220

209221
# Generate some random ShimSettings:
222+
anat_shims = ['{0:.4f}'.format(random()) for i in range(SHIM_LENGTH)]
210223
dwi_shims = ['{0:.4f}'.format(random()) for i in range(SHIM_LENGTH)]
211224
func_shims_A = ['{0:.4f}'.format(random()) for i in range(SHIM_LENGTH)]
212225
func_shims_B = ['{0:.4f}'.format(random()) for i in range(SHIM_LENGTH)]
@@ -215,7 +228,7 @@ def create_dummy_pepolar_bids_session(session_path):
215228
# -anat:
216229
anat_struct = {
217230
'{p}_{m}.{e}'.format(p=prefix, m=mod, e=ext): dummy_content
218-
for ext, dummy_content in zip(['nii.gz', 'json'], ['', {}])
231+
for ext, dummy_content in zip(['nii.gz', 'json'], ['', {'ShimSetting': anat_shims}])
219232
for mod in ['T1w', 'T2w']
220233
}
221234
# -dwi:
@@ -675,11 +688,13 @@ def test_find_fmap_groups(tmpdir, simulation_function):
675688
# B) same, with no ShimSetting
676689
# C) magnitude/phase, with ShimSetting
677690
@pytest.mark.parametrize(
678-
"simulation_function", [create_dummy_pepolar_bids_session,
679-
create_dummy_no_shim_settings_bids_session,
680-
create_dummy_magnitude_phase_bids_session]
691+
"simulation_function, match_param", [
692+
(create_dummy_pepolar_bids_session, 'Shims'),
693+
(create_dummy_no_shim_settings_bids_session, 'AcquisitionLabel'),
694+
(create_dummy_magnitude_phase_bids_session, 'Shims')
695+
]
681696
)
682-
def test_find_compatible_fmaps_for_run(tmpdir, simulation_function):
697+
def test_find_compatible_fmaps_for_run(tmpdir, simulation_function, match_param):
683698
"""
684699
Test find_compatible_fmaps_for_run.
685700
@@ -688,6 +703,8 @@ def test_find_compatible_fmaps_for_run(tmpdir, simulation_function):
688703
tmpdir
689704
simulation_function : function
690705
function to create the directory tree and expected results
706+
match_param : str
707+
matching_parameter for assigning fmaps
691708
"""
692709
folder = op.join(str(tmpdir), 'sub-foo')
693710
_, _, expected_fmap_groups, expected_compatible_fmaps = simulation_function(folder)
@@ -696,7 +713,7 @@ def test_find_compatible_fmaps_for_run(tmpdir, simulation_function):
696713
compatible_fmaps = find_compatible_fmaps_for_run(
697714
json_file,
698715
expected_fmap_groups,
699-
matching_parameters='Shims'
716+
matching_parameters=match_param
700717
)
701718
assert compatible_fmaps == expected_compatible_fmaps[json_file]
702719

@@ -709,28 +726,42 @@ def test_find_compatible_fmaps_for_run(tmpdir, simulation_function):
709726
# B) same, with no ShimSetting
710727
# C) magnitude/phase, with ShimSetting
711728
@pytest.mark.parametrize(
712-
"folder, expected_prefix, simulation_function", [
713-
(folder, expected_prefix, sim_func)
729+
"folder, expected_prefix, simulation_function, match_param", [
730+
(folder, expected_prefix, sim_func, mp)
714731
for folder, expected_prefix in zip(['no_sessions/sub-1', 'sessions/sub-1/ses-pre'], ['', 'ses-pre'])
715-
for sim_func in [create_dummy_pepolar_bids_session,
716-
create_dummy_no_shim_settings_bids_session,
717-
create_dummy_magnitude_phase_bids_session]
732+
for sim_func, mp in [
733+
(create_dummy_pepolar_bids_session, 'Shims'),
734+
(create_dummy_no_shim_settings_bids_session, 'AcquisitionLabel'),
735+
(create_dummy_magnitude_phase_bids_session, 'Shims')
736+
]
718737
]
719738
)
720-
def test_find_compatible_fmaps_for_session(tmpdir, folder, expected_prefix, simulation_function):
739+
def test_find_compatible_fmaps_for_session(
740+
tmpdir,
741+
folder,
742+
expected_prefix,
743+
simulation_function,
744+
match_param
745+
):
721746
"""
722747
Test find_compatible_fmaps_for_session.
723748
724749
Parameters:
725750
----------
726751
tmpdir
752+
folder : str or os.path
753+
path to BIDS study to be simulated, relative to tmpdir
754+
expected_prefix : str
755+
expected start of the "IntendedFor" elements
727756
simulation_function : function
728757
function to create the directory tree and expected results
758+
match_param : str
759+
matching_parameter for assigning fmaps
729760
"""
730761
session_folder = op.join(str(tmpdir), folder)
731762
_, _, _, expected_compatible_fmaps = simulation_function(session_folder)
732763

733-
compatible_fmaps = find_compatible_fmaps_for_session(session_folder, matching_parameters='Shims')
764+
compatible_fmaps = find_compatible_fmaps_for_session(session_folder, matching_parameters=match_param)
734765

735766
assert compatible_fmaps == expected_compatible_fmaps
736767

@@ -785,15 +816,23 @@ def test_select_fmap_from_compatible_groups(tmpdir, folder, expected_prefix, sim
785816
# B) same, with no ShimSetting
786817
# C) magnitude/phase, with ShimSetting
787818
@pytest.mark.parametrize(
788-
"folder, expected_prefix, simulation_function", [
789-
(folder, expected_prefix, sim_func)
819+
"folder, expected_prefix, simulation_function, match_param", [
820+
(folder, expected_prefix, sim_func, mp)
790821
for folder, expected_prefix in zip(['no_sessions/sub-1', 'sessions/sub-1/ses-pre'], ['', 'ses-pre'])
791-
for sim_func in [create_dummy_pepolar_bids_session,
792-
create_dummy_no_shim_settings_bids_session,
793-
create_dummy_magnitude_phase_bids_session]
822+
for sim_func, mp in [
823+
(create_dummy_pepolar_bids_session, 'Shims'),
824+
(create_dummy_no_shim_settings_bids_session, 'AcquisitionLabel'),
825+
(create_dummy_magnitude_phase_bids_session, 'Shims')
826+
]
794827
]
795828
)
796-
def test_populate_intended_for(tmpdir, folder, expected_prefix, simulation_function):
829+
def test_populate_intended_for(
830+
tmpdir,
831+
folder,
832+
expected_prefix,
833+
simulation_function,
834+
match_param
835+
):
797836
"""
798837
Test populate_intended_for.
799838
Parameters:
@@ -805,11 +844,13 @@ def test_populate_intended_for(tmpdir, folder, expected_prefix, simulation_funct
805844
expected start of the "IntendedFor" elements
806845
simulation_function : function
807846
function to create the directory tree and expected results
847+
match_param : str
848+
matching_parameter for assigning fmaps
808849
"""
809850

810851
session_folder = op.join(str(tmpdir), folder)
811852
session_struct, expected_result, _, _ = simulation_function(session_folder)
812-
populate_intended_for(session_folder, matching_parameters='Shims', criterion='First')
853+
populate_intended_for(session_folder, matching_parameters=match_param, criterion='First')
813854

814855
# Now, loop through the jsons in the fmap folder and make sure it matches
815856
# the expected result:

0 commit comments

Comments
 (0)