From 634745611979cf42c5151b5c9062cc105e63504f Mon Sep 17 00:00:00 2001 From: Ford McDonald Date: Mon, 7 Jul 2025 20:04:24 -0400 Subject: [PATCH 1/4] add units field for anatomical scans --- xnat_tools/bids_postprocess.py | 15 +++++++++++++++ xnat_tools/bids_utils.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/xnat_tools/bids_postprocess.py b/xnat_tools/bids_postprocess.py index d82f1484..2db0c185 100644 --- a/xnat_tools/bids_postprocess.py +++ b/xnat_tools/bids_postprocess.py @@ -5,6 +5,7 @@ import typer from xnat_tools.bids_utils import ( + append_anat_units_field, insert_intended_for_fmap, path_string_preprocess, remove_func_acquisition_duration_field, @@ -109,6 +110,13 @@ def bids_postprocess( includesess, ) + append_anat_units_field( + bids_experiment_dir, + includesubj, + session_suffix, + includesess, + ) + else: if includesubj == []: files = os.listdir(bids_experiment_dir) @@ -161,6 +169,13 @@ def bids_postprocess( includesess, ) + append_anat_units_field( + bids_experiment_dir, + includesubj, + session, + includesess, + ) + def main(): """Entry point for console_scripts""" diff --git a/xnat_tools/bids_utils.py b/xnat_tools/bids_utils.py index 86123a21..65b154d5 100644 --- a/xnat_tools/bids_utils.py +++ b/xnat_tools/bids_utils.py @@ -117,6 +117,37 @@ def build_sessions_list(bids_dir, subj, session="", sess_list=None): return sessions +def ensure_json_field(json_path, field, default): + """Add 'Units' key with empty value if missing from JSON.""" + try: + with open(json_path, "r", encoding="utf-8") as f: + data = json.load(f) + + if field not in data: + data[field] = default + + with open(json_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) + _logger.info(f"Added {field} field to {json_path}") + else: + _logger.info(f"{field} field already present in {json_path}") + except Exception as e: + _logger.info(f"Error processing {json_path}: {e}") + + +def append_anat_units_field(bids_dir, sub_list=None, session="", sess_list=None): + anat_dirs = [ + f"{bids_dir}/sub-{subj}/ses-{sess}/anat" for subj in sub_list for sess in sess_list + ] + + for dir in anat_dirs: + anat_jsons = [os.path.join(dir, f) for f in os.listdir(dir) if f.endswith("json")] + + for path in anat_jsons: + # Phase images (with the `part-phase` entity) must have units "rad" or "arbitrary". + ensure_json_field(path, "Units", "arbitrary") + + def remove_func_acquisition_duration_field(bids_dir, sub_list=None, session="", sess_list=None): """Remove AcquisitionDuration from func jsons if RepetitionTime is defined""" @@ -170,6 +201,7 @@ def correct_for_bids_schema_validator(bids_dir, sub_list=None, session="", sess_ sub_list = [x.removeprefix("sub-") for x in os.listdir(bids_dir) if x.startswith("sub-")] remove_func_acquisition_duration_field(bids_dir, sub_list, session, sess_list) + append_anat_units_field(bids_dir, sub_list, session, sess_list) # Extract aquisition token from filename From d0d163eeb5192cbb3601a2ac1736d71f5a68ab9c Mon Sep 17 00:00:00 2001 From: Ford McDonald Date: Mon, 7 Jul 2025 20:06:01 -0400 Subject: [PATCH 2/4] fix docstring --- xnat_tools/bids_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xnat_tools/bids_utils.py b/xnat_tools/bids_utils.py index 65b154d5..c29bdc16 100644 --- a/xnat_tools/bids_utils.py +++ b/xnat_tools/bids_utils.py @@ -118,7 +118,7 @@ def build_sessions_list(bids_dir, subj, session="", sess_list=None): def ensure_json_field(json_path, field, default): - """Add 'Units' key with empty value if missing from JSON.""" + """Add 'Units' key with default value if missing from JSON.""" try: with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) From a021c5655ecfa7822c8996c5e818f7183e8c3046 Mon Sep 17 00:00:00 2001 From: Ford McDonald Date: Tue, 15 Jul 2025 14:55:29 -0400 Subject: [PATCH 3/4] walk through all modalities, inspecting phase jsons for units field --- xnat_tools/bids_postprocess.py | 6 +++--- xnat_tools/bids_utils.py | 28 ++++++++++++++++++---------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/xnat_tools/bids_postprocess.py b/xnat_tools/bids_postprocess.py index 2db0c185..6f77d3cd 100644 --- a/xnat_tools/bids_postprocess.py +++ b/xnat_tools/bids_postprocess.py @@ -5,7 +5,7 @@ import typer from xnat_tools.bids_utils import ( - append_anat_units_field, + append_phase_units_field, insert_intended_for_fmap, path_string_preprocess, remove_func_acquisition_duration_field, @@ -110,7 +110,7 @@ def bids_postprocess( includesess, ) - append_anat_units_field( + append_phase_units_field( bids_experiment_dir, includesubj, session_suffix, @@ -169,7 +169,7 @@ def bids_postprocess( includesess, ) - append_anat_units_field( + append_phase_units_field( bids_experiment_dir, includesubj, session, diff --git a/xnat_tools/bids_utils.py b/xnat_tools/bids_utils.py index c29bdc16..a4a66ccf 100644 --- a/xnat_tools/bids_utils.py +++ b/xnat_tools/bids_utils.py @@ -135,17 +135,25 @@ def ensure_json_field(json_path, field, default): _logger.info(f"Error processing {json_path}: {e}") -def append_anat_units_field(bids_dir, sub_list=None, session="", sess_list=None): - anat_dirs = [ - f"{bids_dir}/sub-{subj}/ses-{sess}/anat" for subj in sub_list for sess in sess_list - ] +def append_phase_units_field(bids_dir, sub_list=None, session="", sess_list=None): + for subj in sub_list: + for sess in sess_list: + ses_dir = os.path.join(bids_dir, f"sub-{subj}", f"ses-{sess}") + if not os.path.isdir(ses_dir): + continue - for dir in anat_dirs: - anat_jsons = [os.path.join(dir, f) for f in os.listdir(dir) if f.endswith("json")] + # Walk through all folders under the session directory (anat, func, etc.) + for root, _, files in os.walk(ses_dir): + json_files = [ + os.path.join(root, f) + for f in files + if f.endswith("json") and "_part-phase" in f + ] - for path in anat_jsons: - # Phase images (with the `part-phase` entity) must have units "rad" or "arbitrary". - ensure_json_field(path, "Units", "arbitrary") + for path in json_files: + # Phase images must have units "rad" or "arbitrary" + # (defaulting to "arbitrary" here) + ensure_json_field(path, "Units", "arbitrary") def remove_func_acquisition_duration_field(bids_dir, sub_list=None, session="", sess_list=None): @@ -201,7 +209,7 @@ def correct_for_bids_schema_validator(bids_dir, sub_list=None, session="", sess_ sub_list = [x.removeprefix("sub-") for x in os.listdir(bids_dir) if x.startswith("sub-")] remove_func_acquisition_duration_field(bids_dir, sub_list, session, sess_list) - append_anat_units_field(bids_dir, sub_list, session, sess_list) + append_phase_units_field(bids_dir, sub_list, session, sess_list) # Extract aquisition token from filename From b3b8e48092c4724c51c4459fb15172e92b5b9485 Mon Sep 17 00:00:00 2001 From: Elizabeth Lorenc Date: Fri, 18 Jul 2025 11:04:00 -0400 Subject: [PATCH 4/4] fix: build sessions list if only bidsdir passed to append phase units func When we correct phase jsons from with the xnat2bids pipeline, we always pass bids dir, subject, session(s), but if people want to call just the correct_for_bids_schema_validator function they're likely to want to run it on an entire BIDS directory. E.g. python -c "from xnat_tools.bids_utils import correct_for_bids_schema_validator; correct_for_bids_schema_validator('~/Documents/BNC/demodat2_mrs/bnc/study-demodat2/bids')" Also switch to using log_info function, which will use logger if we're in the xnat2bids pipeline and simple print statements if people are calling just the correct_for_bids_schema_validator function --- xnat_tools/bids_utils.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/xnat_tools/bids_utils.py b/xnat_tools/bids_utils.py index a4a66ccf..19166dad 100644 --- a/xnat_tools/bids_utils.py +++ b/xnat_tools/bids_utils.py @@ -101,8 +101,6 @@ def build_sessions_list(bids_dir, subj, session="", sess_list=None): subj_path = f"{bids_dir}/sub-{subj}" subj_sub_dirs = os.listdir(subj_path) - log_info(f"Processing participant {subj} at path {subj_path}") - # If a session is provided, only process that session. # If the includesess list is not empty, cocatenate all session # suffixes with "ses-" prefix for the file path. @@ -128,20 +126,26 @@ def ensure_json_field(json_path, field, default): with open(json_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=4) - _logger.info(f"Added {field} field to {json_path}") + log_info(f"Added {field} field to {json_path}") else: - _logger.info(f"{field} field already present in {json_path}") + log_info(f"{field} field already present in {json_path}") except Exception as e: - _logger.info(f"Error processing {json_path}: {e}") + log_info(f"Error processing {json_path}: {e}") def append_phase_units_field(bids_dir, sub_list=None, session="", sess_list=None): for subj in sub_list: - for sess in sess_list: - ses_dir = os.path.join(bids_dir, f"sub-{subj}", f"ses-{sess}") + + # makes list of the sessions to process + sessions = build_sessions_list(bids_dir, subj, session, sess_list) + + for sess in sessions: + ses_dir = os.path.join(bids_dir, f"sub-{subj}", f"{sess}") if not os.path.isdir(ses_dir): continue + log_info(f"Checking for missing phase units in jsons at path {ses_dir}") + # Walk through all folders under the session directory (anat, func, etc.) for root, _, files in os.walk(ses_dir): json_files = [ @@ -167,12 +171,15 @@ def remove_func_acquisition_duration_field(bids_dir, sub_list=None, session="", _logger.info(f"List of sessions sub-directories {sessions}") for sess in sessions: + func_path = f"{bids_dir}/sub-{subj}/{sess}/func" # Don't do anything if this session doesn't contain a func folder if not os.path.exists(func_path): continue + log_info(f"Removing AcquisitionDuration field from jsons at path {func_path}") + func_jsons = [ os.path.join(func_path, f) for f in os.listdir(func_path) if f.endswith("json") ]