44import re
55import os
66import os .path as op
7+ from pathlib import Path
78from random import (random ,
89 shuffle ,
10+ choice ,
11+ seed
912 )
1013from datetime import (datetime ,
1114 timedelta ,
1518from glob import glob
1619
1720import nibabel
21+ import string
1822from numpy import testing as np_testing
1923
2024from heudiconv .utils import (
4650
4751import pytest
4852
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+
4962def test_maybe_na ():
5063 for na in '' , ' ' , None , 'n/a' , 'N/A' , 'NA' :
5164 assert maybe_na (na ) == 'n/a'
@@ -67,7 +80,7 @@ def test_treat_age():
6780
6881SHIM_LENGTH = 6
6982TODAY = datetime .today ()
70-
83+ LABEL_SEED = int . from_bytes ( os . urandom ( 8 ), byteorder = "big" )
7184
7285A_SHIM = [random () for i in range (SHIM_LENGTH )]
7386def test_get_shim_setting (tmpdir ):
@@ -86,9 +99,13 @@ def test_get_shim_setting(tmpdir):
8699 assert get_shim_setting (json_name ) == A_SHIM
87100
88101
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 ):
90103 """
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.
92109 """
93110
94111 nifti_file = op .join (TESTS_DATA_PATH , 'sample_nifti.nii.gz' )
@@ -123,9 +140,9 @@ def test_get_key_info_for_fmap_assignment(tmpdir):
123140 )
124141 assert key_info == [KeyInfoForForce ]
125142
126- # 5) matching_parameter = 'AcquisitionLabel '
143+ # 5) matching_parameter = 'ModalityAcquisitionLabel '
127144 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 )
129146 for (dirname , fname , expected_key_info ) in [
130147 ('fmap' , 'sub-foo_acq-fmri_epi.json' , 'func' ),
131148 ('fmap' , 'sub-foo_acq-bold_epi.json' , 'func' ),
@@ -140,7 +157,24 @@ def test_get_key_info_for_fmap_assignment(tmpdir):
140157 json_name = op .join (str (tmpdir ), dirname , fname )
141158 save_json (json_name , {SHIM_KEY : A_SHIM })
142159 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'
144178 )
145179
146180 # Finally: invalid matching_parameters:
@@ -500,6 +534,158 @@ def create_dummy_no_shim_settings_bids_session(session_path):
500534
501535 return session_struct , expected_result , expected_fmap_groups , expected_compatible_fmaps
502536
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
503689
504690def create_dummy_magnitude_phase_bids_session (session_path ):
505691 """
@@ -685,7 +871,8 @@ def test_find_fmap_groups(tmpdir, simulation_function):
685871@pytest .mark .parametrize (
686872 "simulation_function, match_param" , [
687873 (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' ),
689876 (create_dummy_magnitude_phase_bids_session , 'Shims' )
690877 ]
691878)
@@ -726,7 +913,8 @@ def test_find_compatible_fmaps_for_run(tmpdir, simulation_function, match_param)
726913 for folder , expected_prefix in zip (['no_sessions/sub-1' , 'sessions/sub-1/ses-pre' ], ['' , 'ses-pre' ])
727914 for sim_func , mp in [
728915 (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' ),
730918 (create_dummy_magnitude_phase_bids_session , 'Shims' )
731919 ]
732920 ]
@@ -816,7 +1004,8 @@ def test_select_fmap_from_compatible_groups(tmpdir, folder, expected_prefix, sim
8161004 for folder , expected_prefix in zip (['no_sessions/sub-1' , 'sessions/sub-1/ses-pre' ], ['' , 'ses-pre' ])
8171005 for sim_func , mp in [
8181006 (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' ),
8201009 (create_dummy_magnitude_phase_bids_session , 'Shims' )
8211010 ]
8221011 ]
0 commit comments