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 ,
11
+ seed
9
12
)
10
13
from datetime import (datetime ,
11
14
timedelta ,
15
18
from glob import glob
16
19
17
20
import nibabel
21
+ import string
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 , seed_stdout = True ):
54
+ seed (label_seed )
55
+ rand_char = '' .join (choice (string .ascii_letters ) for _ in range (label_size - 1 ))
56
+ seed (label_seed )
57
+ rand_num = choice (string .digits )
58
+ if seed_stdout :
59
+ print (f'Seed used to generate custom label: { label_seed } ' )
60
+ return rand_char + rand_num
61
+
49
62
def test_maybe_na ():
50
63
for na in '' , ' ' , None , 'n/a' , 'N/A' , 'NA' :
51
64
assert maybe_na (na ) == 'n/a'
@@ -67,7 +80,7 @@ def test_treat_age():
67
80
68
81
SHIM_LENGTH = 6
69
82
TODAY = datetime .today ()
70
-
83
+ LABEL_SEED = int . from_bytes ( os . urandom ( 8 ), byteorder = "big" )
71
84
72
85
A_SHIM = [random () for i in range (SHIM_LENGTH )]
73
86
def test_get_shim_setting (tmpdir ):
@@ -86,9 +99,13 @@ def test_get_shim_setting(tmpdir):
86
99
assert get_shim_setting (json_name ) == A_SHIM
87
100
88
101
89
- def test_get_key_info_for_fmap_assignment (tmpdir ):
102
+ def test_get_key_info_for_fmap_assignment (tmpdir , label_size = 4 , label_seed = LABEL_SEED ):
90
103
"""
91
- Test get_key_info_for_fmap_assignment
104
+ Test get_key_info_for_fmap_assignment.
105
+
106
+ label_size and label_seed are used for the "CustomAcquisitionLabel" matching
107
+ parameter. label_size is the size of the random label while label_seed is
108
+ the seed for the random label creation.
92
109
"""
93
110
94
111
nifti_file = op .join (TESTS_DATA_PATH , 'sample_nifti.nii.gz' )
@@ -123,9 +140,9 @@ def test_get_key_info_for_fmap_assignment(tmpdir):
123
140
)
124
141
assert key_info == [KeyInfoForForce ]
125
142
126
- # 5) matching_parameter = 'AcquisitionLabel '
143
+ # 5) matching_parameter = 'ModalityAcquisitionLabel '
127
144
for d in ['fmap' , 'func' , 'dwi' , 'anat' ]:
128
- os . makedirs (op .join (str (tmpdir ), d ))
145
+ Path (op .join (str (tmpdir ), d )). mkdir ( parents = True , exist_ok = True )
129
146
for (dirname , fname , expected_key_info ) in [
130
147
('fmap' , 'sub-foo_acq-fmri_epi.json' , 'func' ),
131
148
('fmap' , 'sub-foo_acq-bold_epi.json' , 'func' ),
@@ -140,7 +157,24 @@ def test_get_key_info_for_fmap_assignment(tmpdir):
140
157
json_name = op .join (str (tmpdir ), dirname , fname )
141
158
save_json (json_name , {SHIM_KEY : A_SHIM })
142
159
assert [expected_key_info ] == get_key_info_for_fmap_assignment (
143
- json_name , matching_parameter = 'AcquisitionLabel'
160
+ json_name , matching_parameter = 'ModalityAcquisitionLabel'
161
+ )
162
+
163
+ # 6) matching_parameter = 'CustomAcquisitionLabel'
164
+ A_LABEL = gen_rand_label (label_size , label_seed )
165
+ for d in ['fmap' , 'func' , 'dwi' , 'anat' ]:
166
+ Path (op .join (str (tmpdir ), d )).mkdir (parents = True , exist_ok = True )
167
+
168
+ for (dirname , fname , expected_key_info ) in [
169
+ ('fmap' , f'sub-foo_acq-{ A_LABEL } _epi.json' , A_LABEL ),
170
+ ('func' , f'sub-foo_task-{ A_LABEL } _acq-foo_bold.json' , A_LABEL ),
171
+ ('dwi' , f'sub-foo_acq-{ A_LABEL } _dwi.json' , A_LABEL ),
172
+ ('anat' , f'sub-foo_acq-{ A_LABEL } _T1w.json' , A_LABEL ),
173
+ ]:
174
+ json_name = op .join (str (tmpdir ), dirname , fname )
175
+ save_json (json_name , {SHIM_KEY : A_SHIM })
176
+ assert [expected_key_info ] == get_key_info_for_fmap_assignment (
177
+ json_name , matching_parameter = 'CustomAcquisitionLabel'
144
178
)
145
179
146
180
# Finally: invalid matching_parameters:
@@ -500,6 +534,158 @@ def create_dummy_no_shim_settings_bids_session(session_path):
500
534
501
535
return session_struct , expected_result , expected_fmap_groups , expected_compatible_fmaps
502
536
537
+ def create_dummy_no_shim_settings_custom_label_bids_session (session_path , label_size = 4 , label_seed = LABEL_SEED ):
538
+ """
539
+ Creates a dummy BIDS session, with slim json files and empty nii.gz
540
+ The fmap files are pepolar
541
+ The json files don't have ShimSettings
542
+ The fmap files have a custom ACQ label matching:
543
+ - TASK label for <func> modality
544
+ - ACQ label for any other modality (e.g. <dwi>)
545
+
546
+ Parameters:
547
+ ----------
548
+ session_path : str or os.path
549
+ path to the session (or subject) level folder
550
+ label_size : int, optional
551
+ size of the random label
552
+ label_seed : int, optional
553
+ seed for the random label creation
554
+
555
+ Returns:
556
+ -------
557
+ session_struct : dict
558
+ Structure of the directory that was created
559
+ expected_result : dict
560
+ dictionary with fmap names as keys and the expected "IntendedFor" as
561
+ values.
562
+ None
563
+ it returns a third argument (None) to have the same signature as
564
+ create_dummy_pepolar_bids_session
565
+ """
566
+ session_parent , session_basename = op .split (session_path .rstrip (op .sep ))
567
+ if session_basename .startswith ('ses-' ):
568
+ prefix = op .split (session_parent )[1 ] + '_' + session_basename
569
+ else :
570
+ prefix = session_basename
571
+
572
+ # 1) Simulate the file structure for a session:
573
+
574
+ # Dict with the file structure for the session.
575
+ # All json files will be empty.
576
+ # -anat:
577
+ anat_struct = {
578
+ f'{ prefix } _{ mod } .{ ext } ' : dummy_content
579
+ for ext , dummy_content in zip (['nii.gz' , 'json' ], ['' , {}])
580
+ for mod in ['T1w' , 'T2w' ]
581
+ }
582
+ # -dwi:
583
+ label_seed += 1
584
+ DWI_LABEL = gen_rand_label (label_size , label_seed )
585
+ dwi_struct = {
586
+ f'{ prefix } _acq-{ DWI_LABEL } _run-{ runNo } _dwi.{ ext } ' : dummy_content
587
+ for ext , dummy_content in zip (['nii.gz' , 'json' ], ['' , {}])
588
+ for runNo in [1 , 2 ]
589
+ }
590
+ # -func:
591
+ label_seed += 1
592
+ FUNC_LABEL = gen_rand_label (label_size , label_seed )
593
+ func_struct = {
594
+ f'{ prefix } _task-{ FUNC_LABEL } _acq-{ acq } _bold.{ ext } ' : dummy_content
595
+ for ext , dummy_content in zip (['nii.gz' , 'json' ], ['' , {}])
596
+ for acq in ['A' , 'B' ]
597
+ }
598
+ # -fmap:
599
+ fmap_struct = {
600
+ f'{ prefix } _acq-{ acq } _dir-{ d } _run-{ r } _epi.{ ext } ' : dummy_content
601
+ for ext , dummy_content in zip (['nii.gz' , 'json' ], ['' , {}])
602
+ for acq in [DWI_LABEL , FUNC_LABEL ]
603
+ for d in ['AP' , 'PA' ]
604
+ for r in [1 , 2 ]
605
+ }
606
+ expected_fmap_groups = {
607
+ f'{ prefix } _acq-{ acq } _run-{ r } _epi' : [
608
+ f'{ op .join (session_path , "fmap" , prefix )} _acq-{ acq } _dir-{ d } _run-{ r } _epi.json'
609
+ for d in ['AP' , 'PA' ]
610
+ ]
611
+ for acq in [DWI_LABEL , FUNC_LABEL ]
612
+ for r in [1 , 2 ]
613
+ }
614
+
615
+ # structure for the full session (init the OrderedDict as a list to preserve order):
616
+ session_struct = OrderedDict ([
617
+ ('fmap' , fmap_struct ),
618
+ ('anat' , anat_struct ),
619
+ ('dwi' , dwi_struct ),
620
+ ('func' , func_struct ),
621
+ ])
622
+ # add "_scans.tsv" file to the session_struct
623
+ scans_file_content = generate_scans_tsv (session_struct )
624
+ session_struct .update ({'{p}_scans.tsv' .format (p = prefix ): scans_file_content })
625
+
626
+ create_tree (session_path , session_struct )
627
+
628
+ # 2) Now, let's create a dict with the fmap groups compatible for each run
629
+ # -anat: empty
630
+ expected_compatible_fmaps = {
631
+ f'{ op .join (session_path , "anat" , prefix )} _{ mod } .json' : {}
632
+ for mod in ['T1w' , 'T2w' ]
633
+ }
634
+ # -dwi: each of the runs (1, 2) is compatible with both of the dwi fmaps (1, 2):
635
+ expected_compatible_fmaps .update ({
636
+ f'{ op .join (session_path , "dwi" , prefix )} _acq-{ DWI_LABEL } _run-{ runNo } _dwi.json' : {
637
+ key : val for key , val in expected_fmap_groups .items () if key in [
638
+ f'{ prefix } _acq-{ DWI_LABEL } _run-{ r } _epi' for r in [1 , 2 ]
639
+ ]
640
+ }
641
+ for runNo in [1 , 2 ]
642
+ })
643
+ # -func: each of the acq (A, B) is compatible w/ both fmap fMRI runs (1, 2)
644
+ expected_compatible_fmaps .update ({
645
+ f'{ op .join (session_path , "func" , prefix )} _task-{ FUNC_LABEL } _acq-{ acq } _bold.json' : {
646
+ key : val for key , val in expected_fmap_groups .items () if key in [
647
+ f'{ prefix } _acq-{ FUNC_LABEL } _run-{ r } _epi' for r in [1 , 2 ]
648
+ ]
649
+ }
650
+ for acq in ['A' , 'B' ]
651
+ })
652
+
653
+ # 3) Now, let's create a dict with what we expect for the "IntendedFor":
654
+ # NOTE: The "expected_prefix" (the beginning of the path to the
655
+ # "IntendedFor") should be relative to the subject level (see:
656
+ # https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#fieldmap-data)
657
+
658
+ sub_match = re .findall ('(sub-([a-zA-Z0-9]*))' , session_path )
659
+ sub_str = sub_match [0 ][0 ]
660
+ expected_prefix = session_path .split (sub_str )[- 1 ].split (op .sep )[- 1 ]
661
+
662
+ # dict, with fmap names as keys and the expected "IntendedFor" as values.
663
+ expected_result = {
664
+ # (runNo=1 goes with the long list, runNo=2 goes with None):
665
+ f'{ prefix } _acq-{ DWI_LABEL } _dir-{ d } _run-{ runNo } _epi.json' : intended_for
666
+ for runNo , intended_for in zip (
667
+ [1 , 2 ],
668
+ [[op .join (expected_prefix , 'dwi' , f'{ prefix } _acq-{ DWI_LABEL } _run-{ r } _dwi.nii.gz' ) for r in [1 ,2 ]],
669
+ None ]
670
+ )
671
+ for d in ['AP' , 'PA' ]
672
+ }
673
+ expected_result .update (
674
+ {
675
+ # The first "fMRI" run gets all files in the "func" folder;
676
+ # the second shouldn't get any.
677
+ f'{ prefix } _acq-{ FUNC_LABEL } _dir-{ d } _run-{ runNo } _epi.json' : intended_for
678
+ for runNo , intended_for in zip (
679
+ [1 , 2 ],
680
+ [[op .join (expected_prefix , 'func' , f'{ prefix } _task-{ FUNC_LABEL } _acq-{ acq } _bold.nii.gz' )
681
+ for acq in ['A' , 'B' ]],
682
+ None ]
683
+ )
684
+ for d in ['AP' , 'PA' ]
685
+ }
686
+ )
687
+
688
+ return session_struct , expected_result , expected_fmap_groups , expected_compatible_fmaps
503
689
504
690
def create_dummy_magnitude_phase_bids_session (session_path ):
505
691
"""
@@ -685,7 +871,8 @@ def test_find_fmap_groups(tmpdir, simulation_function):
685
871
@pytest .mark .parametrize (
686
872
"simulation_function, match_param" , [
687
873
(create_dummy_pepolar_bids_session , 'Shims' ),
688
- (create_dummy_no_shim_settings_bids_session , 'AcquisitionLabel' ),
874
+ (create_dummy_no_shim_settings_bids_session , 'ModalityAcquisitionLabel' ),
875
+ (create_dummy_no_shim_settings_custom_label_bids_session , 'CustomAcquisitionLabel' ),
689
876
(create_dummy_magnitude_phase_bids_session , 'Shims' )
690
877
]
691
878
)
@@ -726,7 +913,8 @@ def test_find_compatible_fmaps_for_run(tmpdir, simulation_function, match_param)
726
913
for folder , expected_prefix in zip (['no_sessions/sub-1' , 'sessions/sub-1/ses-pre' ], ['' , 'ses-pre' ])
727
914
for sim_func , mp in [
728
915
(create_dummy_pepolar_bids_session , 'Shims' ),
729
- (create_dummy_no_shim_settings_bids_session , 'AcquisitionLabel' ),
916
+ (create_dummy_no_shim_settings_bids_session , 'ModalityAcquisitionLabel' ),
917
+ (create_dummy_no_shim_settings_custom_label_bids_session , 'CustomAcquisitionLabel' ),
730
918
(create_dummy_magnitude_phase_bids_session , 'Shims' )
731
919
]
732
920
]
@@ -816,7 +1004,8 @@ def test_select_fmap_from_compatible_groups(tmpdir, folder, expected_prefix, sim
816
1004
for folder , expected_prefix in zip (['no_sessions/sub-1' , 'sessions/sub-1/ses-pre' ], ['' , 'ses-pre' ])
817
1005
for sim_func , mp in [
818
1006
(create_dummy_pepolar_bids_session , 'Shims' ),
819
- (create_dummy_no_shim_settings_bids_session , 'AcquisitionLabel' ),
1007
+ (create_dummy_no_shim_settings_bids_session , 'ModalityAcquisitionLabel' ),
1008
+ (create_dummy_no_shim_settings_custom_label_bids_session , 'CustomAcquisitionLabel' ),
820
1009
(create_dummy_magnitude_phase_bids_session , 'Shims' )
821
1010
]
822
1011
]
0 commit comments