4
4
import re
5
5
import os
6
6
import os .path as op
7
+ from pathlib import Path
7
8
from random import (random ,
8
9
shuffle ,
10
+ choice
9
11
)
10
12
from datetime import (datetime ,
11
13
timedelta ,
15
17
from glob import glob
16
18
17
19
import nibabel
20
+ import string
21
+ import numpy as np
18
22
from numpy import testing as np_testing
19
23
20
24
from heudiconv .utils import (
46
50
47
51
import pytest
48
52
53
+ def gen_rand_label (label_size , label_seed ):
54
+ np .random .seed (label_seed )
55
+ rand_char = '' .join (choice (string .ascii_letters ) for _ in range (label_size - 1 ))
56
+ np .random .seed (label_seed )
57
+ rand_num = choice (string .digits )
58
+ return rand_char + rand_num
59
+
49
60
def test_maybe_na ():
50
61
for na in '' , ' ' , None , 'n/a' , 'N/A' , 'NA' :
51
62
assert maybe_na (na ) == 'n/a'
@@ -86,7 +97,7 @@ def test_get_shim_setting(tmpdir):
86
97
assert get_shim_setting (json_name ) == A_SHIM
87
98
88
99
89
- def test_get_key_info_for_fmap_assignment (tmpdir ):
100
+ def test_get_key_info_for_fmap_assignment (tmpdir , label_size = 4 , label_seed = 42 ):
90
101
"""
91
102
Test get_key_info_for_fmap_assignment
92
103
"""
@@ -123,9 +134,9 @@ def test_get_key_info_for_fmap_assignment(tmpdir):
123
134
)
124
135
assert key_info == [KeyInfoForForce ]
125
136
126
- # 5) matching_parameter = 'AcquisitionLabel '
137
+ # 5) matching_parameter = 'ModalityAcquisitionLabel '
127
138
for d in ['fmap' , 'func' , 'dwi' , 'anat' ]:
128
- os . makedirs (op .join (str (tmpdir ), d ))
139
+ Path (op .join (str (tmpdir ), d )). mkdir ( parents = True , exist_ok = True )
129
140
for (dirname , fname , expected_key_info ) in [
130
141
('fmap' , 'sub-foo_acq-fmri_epi.json' , 'func' ),
131
142
('fmap' , 'sub-foo_acq-bold_epi.json' , 'func' ),
@@ -140,7 +151,24 @@ def test_get_key_info_for_fmap_assignment(tmpdir):
140
151
json_name = op .join (str (tmpdir ), dirname , fname )
141
152
save_json (json_name , {SHIM_KEY : A_SHIM })
142
153
assert [expected_key_info ] == get_key_info_for_fmap_assignment (
143
- json_name , matching_parameter = 'AcquisitionLabel'
154
+ json_name , matching_parameter = 'ModalityAcquisitionLabel'
155
+ )
156
+
157
+ # 6) matching_parameter = 'CustomAcquisitionLabel'
158
+ A_LABEL = gen_rand_label (label_size , label_seed )
159
+ for d in ['fmap' , 'func' , 'dwi' , 'anat' ]:
160
+ Path (op .join (str (tmpdir ), d )).mkdir (parents = True , exist_ok = True )
161
+
162
+ for (dirname , fname , expected_key_info ) in [
163
+ ('fmap' , f'sub-foo_acq-{ A_LABEL } _epi.json' , A_LABEL ),
164
+ ('func' , f'sub-foo_task-{ A_LABEL } _acq-foo_bold.json' , A_LABEL ),
165
+ ('dwi' , f'sub-foo_acq-{ A_LABEL } _dwi.json' , A_LABEL ),
166
+ ('anat' , f'sub-foo_acq-{ A_LABEL } _T1w.json' , A_LABEL ),
167
+ ]:
168
+ json_name = op .join (str (tmpdir ), dirname , fname )
169
+ save_json (json_name , {SHIM_KEY : A_SHIM })
170
+ assert [expected_key_info ] == get_key_info_for_fmap_assignment (
171
+ json_name , matching_parameter = 'CustomAcquisitionLabel'
144
172
)
145
173
146
174
# Finally: invalid matching_parameters:
@@ -357,7 +385,7 @@ def create_dummy_pepolar_bids_session(session_path):
357
385
return session_struct , expected_result , expected_fmap_groups , expected_compatible_fmaps
358
386
359
387
360
- def create_dummy_no_shim_settings_bids_session (session_path ):
388
+ def create_dummy_no_shim_settings_bids_session (session_path , label_seed = 42 , label_size = 4 ):
361
389
"""
362
390
Creates a dummy BIDS session, with slim json files and empty nii.gz
363
391
The fmap files are pepolar
@@ -500,6 +528,154 @@ def create_dummy_no_shim_settings_bids_session(session_path):
500
528
501
529
return session_struct , expected_result , expected_fmap_groups , expected_compatible_fmaps
502
530
531
+ def create_dummy_no_shim_settings_custom_label_bids_session (session_path , label_size = 4 , label_seed = 42 ):
532
+ """
533
+ Creates a dummy BIDS session, with slim json files and empty nii.gz
534
+ The fmap files are pepolar
535
+ The json files don't have ShimSettings
536
+ The fmap files have a custom ACQ label matching:
537
+ - TASK label for <func> modality
538
+ - ACQ label for any other modality (e.g. <dwi>)
539
+
540
+ Parameters:
541
+ ----------
542
+ session_path : str or os.path
543
+ path to the session (or subject) level folder
544
+
545
+ Returns:
546
+ -------
547
+ session_struct : dict
548
+ Structure of the directory that was created
549
+ expected_result : dict
550
+ dictionary with fmap names as keys and the expected "IntendedFor" as
551
+ values.
552
+ None
553
+ it returns a third argument (None) to have the same signature as
554
+ create_dummy_pepolar_bids_session
555
+ """
556
+ session_parent , session_basename = op .split (session_path .rstrip (op .sep ))
557
+ if session_basename .startswith ('ses-' ):
558
+ prefix = op .split (session_parent )[1 ] + '_' + session_basename
559
+ else :
560
+ prefix = session_basename
561
+
562
+ # 1) Simulate the file structure for a session:
563
+
564
+ # Dict with the file structure for the session.
565
+ # All json files will be empty.
566
+ # -anat:
567
+ anat_struct = {
568
+ f'{ prefix } _{ mod } .{ ext } ' : dummy_content
569
+ for ext , dummy_content in zip (['nii.gz' , 'json' ], ['' , {}])
570
+ for mod in ['T1w' , 'T2w' ]
571
+ }
572
+ # -dwi:
573
+ label_seed += 1
574
+ DWI_LABEL = gen_rand_label (label_size , label_seed )
575
+ dwi_struct = {
576
+ f'{ prefix } _acq-{ DWI_LABEL } _run-{ runNo } _dwi.{ ext } ' : dummy_content
577
+ for ext , dummy_content in zip (['nii.gz' , 'json' ], ['' , {}])
578
+ for runNo in [1 , 2 ]
579
+ }
580
+ # -func:
581
+ label_seed += 1
582
+ FUNC_LABEL = gen_rand_label (label_size , label_seed )
583
+ func_struct = {
584
+ f'{ prefix } _task-{ FUNC_LABEL } _acq-{ acq } _bold.{ ext } ' : dummy_content
585
+ for ext , dummy_content in zip (['nii.gz' , 'json' ], ['' , {}])
586
+ for acq in ['A' , 'B' ]
587
+ }
588
+ # -fmap:
589
+ fmap_struct = {
590
+ f'{ prefix } _acq-{ acq } _dir-{ d } _run-{ r } _epi.{ ext } ' : dummy_content
591
+ for ext , dummy_content in zip (['nii.gz' , 'json' ], ['' , {}])
592
+ for acq in [DWI_LABEL , FUNC_LABEL ]
593
+ for d in ['AP' , 'PA' ]
594
+ for r in [1 , 2 ]
595
+ }
596
+ expected_fmap_groups = {
597
+ f'{ prefix } _acq-{ acq } _run-{ r } _epi' : [
598
+ f'{ op .join (session_path , "fmap" , prefix )} _acq-{ acq } _dir-{ d } _run-{ r } _epi.json'
599
+ for d in ['AP' , 'PA' ]
600
+ ]
601
+ for acq in [DWI_LABEL , FUNC_LABEL ]
602
+ for r in [1 , 2 ]
603
+ }
604
+
605
+ # structure for the full session (init the OrderedDict as a list to preserve order):
606
+ session_struct = OrderedDict ([
607
+ ('fmap' , fmap_struct ),
608
+ ('anat' , anat_struct ),
609
+ ('dwi' , dwi_struct ),
610
+ ('func' , func_struct ),
611
+ ])
612
+ # add "_scans.tsv" file to the session_struct
613
+ scans_file_content = generate_scans_tsv (session_struct )
614
+ session_struct .update ({'{p}_scans.tsv' .format (p = prefix ): scans_file_content })
615
+
616
+ create_tree (session_path , session_struct )
617
+
618
+ # 2) Now, let's create a dict with the fmap groups compatible for each run
619
+ # -anat: empty
620
+ expected_compatible_fmaps = {
621
+ f'{ op .join (session_path , "anat" , prefix )} _{ mod } .json' : {}
622
+ for mod in ['T1w' , 'T2w' ]
623
+ }
624
+ # -dwi: each of the runs (1, 2) is compatible with both of the dwi fmaps (1, 2):
625
+ expected_compatible_fmaps .update ({
626
+ f'{ op .join (session_path , "dwi" , prefix )} _acq-{ DWI_LABEL } _run-{ runNo } _dwi.json' : {
627
+ key : val for key , val in expected_fmap_groups .items () if key in [
628
+ f'{ prefix } _acq-{ DWI_LABEL } _run-{ r } _epi' for r in [1 , 2 ]
629
+ ]
630
+ }
631
+ for runNo in [1 , 2 ]
632
+ })
633
+ # -func: each of the acq (A, B) is compatible w/ both fmap fMRI runs (1, 2)
634
+ expected_compatible_fmaps .update ({
635
+ f'{ op .join (session_path , "func" , prefix )} _task-{ FUNC_LABEL } _acq-{ acq } _bold.json' : {
636
+ key : val for key , val in expected_fmap_groups .items () if key in [
637
+ f'{ prefix } _acq-{ FUNC_LABEL } _run-{ r } _epi' for r in [1 , 2 ]
638
+ ]
639
+ }
640
+ for acq in ['A' , 'B' ]
641
+ })
642
+
643
+ # 3) Now, let's create a dict with what we expect for the "IntendedFor":
644
+ # NOTE: The "expected_prefix" (the beginning of the path to the
645
+ # "IntendedFor") should be relative to the subject level (see:
646
+ # https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#fieldmap-data)
647
+
648
+ sub_match = re .findall ('(sub-([a-zA-Z0-9]*))' , session_path )
649
+ sub_str = sub_match [0 ][0 ]
650
+ expected_prefix = session_path .split (sub_str )[- 1 ].split (op .sep )[- 1 ]
651
+
652
+ # dict, with fmap names as keys and the expected "IntendedFor" as values.
653
+ expected_result = {
654
+ # (runNo=1 goes with the long list, runNo=2 goes with None):
655
+ f'{ prefix } _acq-{ DWI_LABEL } _dir-{ d } _run-{ runNo } _epi.json' : intended_for
656
+ for runNo , intended_for in zip (
657
+ [1 , 2 ],
658
+ [[op .join (expected_prefix , 'dwi' , f'{ prefix } _acq-{ DWI_LABEL } _run-{ r } _dwi.nii.gz' ) for r in [1 ,2 ]],
659
+ None ]
660
+ )
661
+ for d in ['AP' , 'PA' ]
662
+ }
663
+ expected_result .update (
664
+ {
665
+ # The first "fMRI" run gets all files in the "func" folder;
666
+ # the second shouldn't get any.
667
+ f'{ prefix } _acq-{ FUNC_LABEL } _dir-{ d } _run-{ runNo } _epi.json' : intended_for
668
+ for runNo , intended_for in zip (
669
+ [1 , 2 ],
670
+ [[op .join (expected_prefix , 'func' , f'{ prefix } _task-{ FUNC_LABEL } _acq-{ acq } _bold.nii.gz' )
671
+ for acq in ['A' , 'B' ]],
672
+ None ]
673
+ )
674
+ for d in ['AP' , 'PA' ]
675
+ }
676
+ )
677
+
678
+ return session_struct , expected_result , expected_fmap_groups , expected_compatible_fmaps
503
679
504
680
def create_dummy_magnitude_phase_bids_session (session_path ):
505
681
"""
@@ -685,7 +861,8 @@ def test_find_fmap_groups(tmpdir, simulation_function):
685
861
@pytest .mark .parametrize (
686
862
"simulation_function, match_param" , [
687
863
(create_dummy_pepolar_bids_session , 'Shims' ),
688
- (create_dummy_no_shim_settings_bids_session , 'AcquisitionLabel' ),
864
+ (create_dummy_no_shim_settings_bids_session , 'ModalityAcquisitionLabel' ),
865
+ (create_dummy_no_shim_settings_custom_label_bids_session , 'CustomAcquisitionLabel' ),
689
866
(create_dummy_magnitude_phase_bids_session , 'Shims' )
690
867
]
691
868
)
@@ -726,7 +903,8 @@ def test_find_compatible_fmaps_for_run(tmpdir, simulation_function, match_param)
726
903
for folder , expected_prefix in zip (['no_sessions/sub-1' , 'sessions/sub-1/ses-pre' ], ['' , 'ses-pre' ])
727
904
for sim_func , mp in [
728
905
(create_dummy_pepolar_bids_session , 'Shims' ),
729
- (create_dummy_no_shim_settings_bids_session , 'AcquisitionLabel' ),
906
+ (create_dummy_no_shim_settings_bids_session , 'ModalityAcquisitionLabel' ),
907
+ (create_dummy_no_shim_settings_custom_label_bids_session , 'CustomAcquisitionLabel' ),
730
908
(create_dummy_magnitude_phase_bids_session , 'Shims' )
731
909
]
732
910
]
@@ -816,7 +994,8 @@ def test_select_fmap_from_compatible_groups(tmpdir, folder, expected_prefix, sim
816
994
for folder , expected_prefix in zip (['no_sessions/sub-1' , 'sessions/sub-1/ses-pre' ], ['' , 'ses-pre' ])
817
995
for sim_func , mp in [
818
996
(create_dummy_pepolar_bids_session , 'Shims' ),
819
- (create_dummy_no_shim_settings_bids_session , 'AcquisitionLabel' ),
997
+ (create_dummy_no_shim_settings_bids_session , 'ModalityAcquisitionLabel' ),
998
+ (create_dummy_no_shim_settings_custom_label_bids_session , 'CustomAcquisitionLabel' ),
820
999
(create_dummy_magnitude_phase_bids_session , 'Shims' )
821
1000
]
822
1001
]
0 commit comments