diff --git a/src/original/MG_LU/TOPED_fsl_commands.py b/src/original/MG_LU/TOPED_fsl_commands.py new file mode 100644 index 0000000..28a330f --- /dev/null +++ b/src/original/MG_LU/TOPED_fsl_commands.py @@ -0,0 +1,308 @@ +import os +import re +import shutil +import subprocess +from typing import Optional, Tuple + + +class TOPED: + def __init__(self, distribution: str, user: str, linux_workdir: str): + """ Specify WSL distribution, user, and working directory for FSL. + Parameters: + distribution : str The WSL distribution where FSL is installed (e.g., "Ubuntu"). + user : str The Linux username to run commands as. + linux_workdir : str The directory in the Linux filesystem to use for temporary files. + """ + + if not os.path.exists(linux_workdir): + #raise ValueError(f"Linux working directory not found: {linux_workdir}") + print(f"Linux working directory not found, creating: {linux_workdir}") + os.makedirs(linux_workdir) + + self.distribution = distribution + self.user = user + self.linux_workdir = linux_workdir + + # CORE COMMAND FUNCTION + def run_command(self, command: str, workdir: str): + """ Run a generic FSL command through WSL. + Parameters: + command : str The FSL command to execute. + workdir : str The working directory in the Linux filesystem where the command should be executed. + """ + # print the full command in the IDE output when running the fsl_runner. + print(f" command: {command}") + + full_cmd = f"source ~/.profile; source ~/.bashrc; {command}" + return subprocess.run( + ['wsl', '-d', self.distribution, '-u', self.user, '-e', 'bash', '-c', full_cmd], + cwd=workdir, + check=True, + capture_output=True, + text=True + ) + + # TOPUP + def run_topup( + self, + b0_path: str, + acq_parameter_path: str, + config_file_path: Optional[str] = None, + nthr: Optional[int] = None, + logout: Optional[str] = None, + dfout: Optional[str] = None, + jacout: Optional[str] = None, + patient_id: Optional[str] = None, + output_dir: Optional[str] = None + ) -> str: + """ Run FSL TOPUP on a stack of b0 images. + Parameters: + b0_path : str The path to the b0 image to process with TOPUP. + acq_parameter_path : str Path to the acquisition parameters file. + config_file_path : str, optional Path to the TOPUP configuration file, by default None, which uses the default FSL config b02b0.cnf. + nthr : int, optional Number of threads to use, by default None (uses default_threads) + Returns: + whatever specified in the TOPUP command output (dfout, iout, jacout) + Corrected image, fieldmap, and Jacobian determinant paths are generated in the Linux working directory. + """ + workdir = self.get_patient_workdir(patient_id) + + if not os.path.exists(b0_path): + raise ValueError("b0_path invalid") + if not os.path.exists(acq_parameter_path): + raise ValueError("acq_parameter_path invalid") + + output_dir = output_dir or os.path.dirname(b0_path) + + # Copy inputs + b0_name = self.copy_to_wsl(b0_path, workdir) + b0_basename, _ = self.split_nifti_gz(b0_name) + + acqparam = self.copy_to_wsl(acq_parameter_path, workdir) + if config_file_path: + config_filename = self.copy_to_wsl(config_file_path, workdir) + config_arg = f"--config={config_filename} " + else: + # FSL default config + config_arg = "--config=b02b0.cnf " + + # Build command + cmd = ( + f"topup " + f"--imain={b0_name} " + f"--datain={acqparam} " + f"--out={b0_basename} " + f"--iout={self.get_corrected_name(b0_basename)} " + ) + + if logout is not None: cmd += f"--logout={self.get_log_name(b0_basename)} " + if dfout is not None: cmd += f"--dfout={self.get_warpfig_name(b0_basename)} " + if jacout is not None: cmd += f"--jacout={self.get_jacobian_name(b0_basename)} " + if nthr is not None: cmd += f"--nthr={nthr} " + cmd += config_arg + + # Run + self.run_command(cmd, workdir) + + # Copy corrected image back + self.ensure_dir(output_dir) + corrected_filename = self.get_corrected_name(b0_basename, ext=True) + self.copy_from_wsl(corrected_filename, output_dir, workdir) + + # Return topup base for Eddy + return b0_basename + + # Eddy + def run_eddy( + self, + dwi_path: str, + mask_path: str, + acq_parameter_path: str, + index_path: str, + bvecs_path: str, + bvals_path: str, + topup_base: str, + slspec_path: Optional[str] = None, + b_range: Optional[int] = None, + flm: Optional[str] = None, + slm: Optional[str] = None, + niter: Optional[int] = None, + fwhm: Optional[str] = None, + resamp: Optional[str] = None, + fep: Optional[bool] = None, + repol: Optional[bool] = None, + estimate_move_by_susceptibility: Optional[bool] = None, + mporder: Optional[int] = None, + patient_id: Optional[str] = None, + output_dir: Optional[str] = None, + json_path: Optional[str] = None, + interp: Optional[str] = None, + nvoxhp: Optional[int] = None, + ff: Optional[float] = None, + dont_sep_offs_move: Optional[bool] = None, + dont_peas: Optional[bool] = None, + ol_nstd: Optional[float] = None, + ol_nvox: Optional[int] = None, + ol_type: Optional[str] = None, + ol_ss: Optional[str] = None, + ol_pos: Optional[bool] = None, + ol_sqr: Optional[bool] = None, + s2v_niter: Optional[int] = None, + s2v_lambda: Optional[float] = None, + s2v_interp: Optional[str] = None, + mbs_niter: Optional[int] = None, + mbs_lambda: Optional[float] = None, + mbs_ksp: Optional[float] = None, + cnr_maps: Optional[bool] = None, + residuals: Optional[bool] = None, + data_is_shelled: Optional[bool] = None, + nthr: Optional[int] = None + ) -> str: + + """ Run FSL Eddy. + Parameters: + dwi_path : str Path to the input DWI image. This is the stack to be corrected by Eddy & TOPUP. + mask_path : str Path to the mask image. + acqparams_path : str Path to acquisition parameters file + index_path : str Path to index file + bvecs_path : str Path to bvecs file + bvals_path : str Path to bvals file + topup_base : str Base name used for topup output. is is automatically set by TOUP in the linux working directory and is used as input for Eddy. + output_dir : str Output directory + bunch of optional Eddy flags (set to None to use FSL defaults). + Returns: + (Eddy + TOPUP) Corrected DWI image (path to output image) + """ + + workdir = self.get_patient_workdir(patient_id) + + for p in [dwi_path, mask_path, acq_parameter_path, index_path, bvecs_path, bvals_path]: + if not os.path.exists(p): + raise ValueError(f"Missing compulsory file: {p}") + + output_dir = output_dir or os.path.dirname(dwi_path) + + # Copy inputs + dwi_name = self.copy_to_wsl(dwi_path, workdir) + dwi_basename, _ = self.split_nifti_gz(dwi_name) + + mask = self.copy_to_wsl(mask_path, workdir) + acqp = self.copy_to_wsl(acq_parameter_path, workdir) + index = self.copy_to_wsl(index_path, workdir) + bvecs = self.copy_to_wsl(bvecs_path, workdir) + bvals = self.copy_to_wsl(bvals_path, workdir) + + slspec = self.copy_to_wsl(slspec_path, workdir) if slspec_path else None + json_file = self.copy_to_wsl(json_path, workdir) if json_path else None + + # Build command + cmd = ( + f"eddy " + f"--imain={dwi_name} " + f"--mask={mask} " + f"--acqp={acqp} " + f"--index={index} " + f"--bvecs={bvecs} " + f"--bvals={bvals} " + f"--topup={topup_base} " + f"--out={self.get_corrected_name_eddy(dwi_basename)} " + ) + + # Optional params + if slspec: cmd += f"--slspec={slspec} " + if json_file: cmd += f"--json={json_file} " + if b_range is not None: cmd += f"--b_range={b_range} " + + if flm is not None: cmd += f"--flm={flm} " + if slm is not None: cmd += f"--slm={slm} " + if fwhm is not None: cmd += f"--fwhm={fwhm} " + if niter is not None: cmd += f"--niter={niter} " + if interp is not None: cmd += f"--interp={interp} " + if resamp is not None: cmd += f"--resamp={resamp} " + if fep: cmd += f"--fep " + if nvoxhp is not None: cmd += f"--nvoxhp={nvoxhp} " + if ff is not None: cmd += f"--ff={ff} " + if dont_sep_offs_move: cmd += f"--dont_sep_offs_move " + if dont_peas: cmd += f"--dont_peas " + + if repol: cmd += f"--repol " + if ol_nstd is not None: cmd += f"--ol_nstd={ol_nstd} " + if ol_nvox is not None: cmd += f"--ol_nvox={ol_nvox} " + if ol_type is not None: cmd += f"--ol_type={ol_type} " + if ol_ss is not None: cmd += f"--ol_ss={ol_ss} " + if ol_pos: cmd += f"--ol_pos " + if ol_sqr: cmd += f"--ol_sqr " + + if mporder is not None: cmd += f"--mporder={mporder} " + if s2v_niter is not None: cmd += f"--s2v_niter={s2v_niter} " + if s2v_lambda is not None: cmd += f"--s2v_lambda={s2v_lambda} " + if s2v_interp is not None: cmd += f"--s2v_interp={s2v_interp} " + + if estimate_move_by_susceptibility: cmd += f"--estimate_move_by_susceptibility " + if mbs_niter is not None: cmd += f"--mbs_niter={mbs_niter} " + if mbs_lambda is not None: cmd += f"--mbs_lambda={mbs_lambda} " + if mbs_ksp is not None: cmd += f"--mbs_ksp={mbs_ksp} " + + if cnr_maps: cmd += f"--cnr_maps " + if residuals: cmd += f"--residuals " + if data_is_shelled: cmd += f"--data_is_shelled " + if nthr is not None: cmd += f"--nthr={nthr} " + + + # Run + self.run_command(cmd, workdir) + + # Copy output back + self.ensure_dir(output_dir) + output_filename = self.get_corrected_name_eddy(dwi_basename, ext=True) + self.copy_from_wsl(output_filename, output_dir, workdir) + + return os.path.join(output_dir, output_filename) + + + # FILE HANDLING between windows and wsl + def copy_to_wsl(self, path: str, workdir: str) -> str: + filename = os.path.basename(path) + dst = os.path.join(workdir, filename) + shutil.copy(path, dst) + return filename + + def copy_from_wsl(self, filename: str, output_dir: str, workdir: str): + src = os.path.join(workdir, filename) + dst = os.path.join(output_dir, filename) + if not os.path.exists(src): + raise FileNotFoundError(f"Expected output not found: {src}") + shutil.copy(src, dst) + + + # HELPERS - filenames and folders etc. + def split_nifti_gz(self, filename: str) -> Tuple[str, str]: + match = re.match(r"^(.*)(\.nii\.gz)$", filename) + return match.group(1), match.group(2) if match else os.path.splitext(filename) + + def get_warpfig_name(self, base: str): + return f"{base}_warpfig" + + def get_jacobian_name(self, base: str): + return f"{base}_jacobian" + + def get_log_name(self, base: str): + return f"{base}_log" + + def get_corrected_name(self, base: str, ext=False): + return f"{base}_corrected.nii.gz" if ext else f"{base}_corrected" + + def get_corrected_name_eddy(self, base: str, ext=False): + return f"{base}_EddyCorrected.nii.gz" if ext else f"{base}_EddyCorrected" + + def ensure_dir(self, path: str): + if not os.path.exists(path): + os.makedirs(path) + + def get_patient_workdir(self, patient_id: Optional[str]) -> str: + if not patient_id: + return self.linux_workdir + path = os.path.join(self.linux_workdir, patient_id) + if not os.path.exists(path): + os.makedirs(path) + return path \ No newline at end of file diff --git a/src/original/MG_LU/TOPED_fsl_runner.py b/src/original/MG_LU/TOPED_fsl_runner.py new file mode 100644 index 0000000..80100f8 --- /dev/null +++ b/src/original/MG_LU/TOPED_fsl_runner.py @@ -0,0 +1,375 @@ +import os +from typing import Optional +from TOPED_fsl_commands import TOPED # script containing subprocess commands + +""" +About this script: +The script is run in Windows and expects files located in windows directories. + +This script runs all linux commands as a subprocess using the TOPED class in TOPED_fsl_commands.py. + The input data is read from your specified windows directory 'main_dir' and the final output is saved in the same directory. + All processing is done in a specified linux working directory. All files for each patient are saved there and then copied back to the respective folders in the windows directory. +To run the script you will need to specify the parameters in the configuration section below. +Input files for topup and and eddy are identified through keywords specified in the IDENTIFIERS dictionary. + for each input type (e.g. b0, dwi stack, mask, acqp, bvecs, bvals, index), specify a keyword that is contained in the respective file names in your dataset. + This should be unique, i.e. if the keyword for the topup imain input is "b0", the script looks for files containing "b0" in the name and takes that as input. If there are multiple files containing "b0", an error is raised. + The assumed directory strucure is seen below. The script looks for the input files in the 'patient' folders first, then in the common folder if not found in the patient folder. + The structure of the main directory will be copied into the Linux working directory (i.e. the same folders and files will be generated). + +Requirements: +- Python packages (os, re, typing, subprocess, shutil) +- FSL installed with an WSL distro. user needs to be configured. Make sure FSL can be run from the command line in WSL. + + +Supported file inputs: + Topup: + - b0 image (4D file containing multiple b0 volumes). nii.gz (compulsory) + - acqparams.txt (PE directions) (compulsory) + - configuration file (if not specified the default FSL config b02b0.cnf is used) .cnf (optional) + Eddy: + - dwi image (4D stack contaning whatever should be topup+eddy corrected). nii.gz (compulsory) + - mask (typically brain mask, but i dont do brain so its whatever i guess). nii.gz (compulsory) + - acqparams (assumed to be the same as for topup). (compulsory) + - index (to match the dwi stack volumes to the acqparams lines). (compulsory) + - bvecs (as generated by e.g. dcm2niix). make sure they match the full stack. (compulsory) + - bvals (as generated by e.g. dcm2niix). make sure they match the full stack. (compulsory) + - slspec (optional). + - JSON (optional) +""" + +# main_directory +# ├── patient_1 +# │ ├── pt1_b0.nii.gz +# │ ├── pt1_dwi_stack.nii.gz +# │ ├── pt1_mask.nii.gz +# │ └── ... (other patient-specific files, e.g. pt1_jsonfile.json) +# ├── patient_2 +# │ ├── pt2_b0.nii.gz +# │ ├── pt2_dwi_stack.nii.gz +# │ ├── pt2_mask.nii.gz +# │ └── ... (other patient-specific files) +# └── common_files (optional) +# ├── acqparams.txt +# ├── bvecs.txt +# ├── bvals.txt +# ├── index.txt +# ├── slspec.txt +# └── config.cnf + +# names of folders are arbitrary. +# The names of these folders are copied into the linux working directory and are used to identify the respective entries. +# a 'common_files' folder is optional. The purpose is is you e.g. have the same config file or acqparams/bvecs/bvals/index/slspec files for all patients. + +# ======================================================================================================================================================= +"""CONFIGURATION - Adjust these parameters according to your data""" # change things in this section! + +#main directory in WINDOWS containing patient folders and common folder. +MAIN_DIR = r"C:\path\to\your\main_dir" # replace with path to your main_dir. + +# folder inside main_dir that contains files which are common for all patients (e.g. acqparams, bvecs, bvals, index, slspec). +COMMON_FOLDER_NAME = "common_files" # replace with your path, or set to None if there is no common folder. +# The script looks in the patient folders first. if it does not find the required files there, it looks in the common folder. + + +# Filename identifiers - Change the RHS here according to your own file naming conventions!!! +# The script will look for files containing these keywords in the patient folders first (then in the common folder). +IDENTIFIERS = { + "b0": "b0.nii.gz", # e.g. topup --imain. assumed to contain str 'b0' in the filename. + # the name of the b0 file determines the names of generated output files. + # a b0 file named 'patient1_ddmmyyyy_b0.nii.gz' will generate output files which all have the prefix 'patient1_ddmmyyyy'. + "dwi": "stack.nii.gz", # e.g. eddy --imain. DWI stack assumed to contain 'stack' in the filename. Should have the same prefix as the b0 file, e.g. 'patient1_ddmmyyyy_stack.nii.gz' to match with 'patient1_ddmmyyyy_b0.nii.gz'. + "mask": "mask", # eddy --mask. contains 'mask' in the filename. + "bvec": "bvec", + "bval": "bval", + "index": "index", # index for eddy + "acqp": "acq", # acqparams for topup and eddy. + "slspec": "slspec", # optional slice spec for eddy. + "json": "json", # optional json file for eddy. + "config": ".cnf" # optional config file for topup. If not found, the FSL default config b02b0.cnf will be used. +} + +# FSL PARAMETERS - adjust as needed. +TOPUP_PARAMS = { + "nthr": 14, # --nthr to run topup. + "jacout": None, # Default False. + "logout": None, # Default False. + "dfout": None, # Default False. + "output_dir": None # all files are saved in the linux working directory by default (None). The corrected files are copied back into main_dir in windows. + # remaining Topup parameters are set with the config file. You can specify a config file for each patient or a common one in the common folder by placing the .cnf file in the directory. +} + + +EDDY_PARAMS = { + "b_range": None, # None defaults to 50. + "flm": None, # ('linear', 'quadratic', 'cubic'). None defaults to 'quadratic' + "slm": None, # ('none', 'linear', 'quadratic'). None defaults to 'none'. + "fwhm": 10, # Default: 0 mm. Set to 10 for testing. + "niter": 1, # Default: 5. Set to 1 for testing. + "interp": None, # ('spline', 'trilinear'). None defaults to 'spline'. + "resamp": None, # ('jac', 'lsr'). None defaults to 'jac'. + "fep": None, # True/False. None defaults to False. + "nvoxhp": None, # None defaults to 1000. + "ff": None, # [0,10]. None defaults to 10. + "dont_sep_offs_move": None, # True/False. None defaults to False. + "dont_peas": None, # True/False. None defaults to False. + + "repol": None, # True/False. None defaults to False. set to True to enable outlier replacement. + "ol_nstd": None, # When repol=True; None defaults to 4. + "ol_nvox": None, # When repol=True; None defaults to 250. + "ol_type": None, # When repol=True; ('sw', 'gw', 'both'). None defaults to 'sw'. + "ol_ss": None, # When repol=True; ('sw', 'pooled'). None defaults to 'sw'. + "ol_pos": None, # When repol=True; True/False. None defaults to False. + "ol_sqr": None, # When repol=True; True/False. None defaults to False. + + "mporder": None, # Default 0. + "s2v_niter": None, # When mporder>0; None defaults to 5. + "s2v_lambda": None, # When mporder>0; None defaults to 1. + "s2v_interp": None, # When mporder>0; ('trilinear', 'spline'). None defaults to 'trilinear'. + + "estimate_move_by_susceptibility": None, # True/Flase. None defaults to False. + "mbs_niter": None, # When estimate_move_by_susceptibility=True; None defaults to 10. + "mbs_lambda": None, # When estimate_move_by_susceptibility=True; None defaults to 10. + "mbs_ksp": None, # When estimate_move_by_susceptibility=True; None defaults to 10. + + "cnr_maps": None, # True/False. None defaults to False. + "residuals": None, # True/False. None defaults to False. + "data_is_shelled": None,# True/False. None defaults to False. + "nthr": None, # --nthr to run eddy. only relevant for CPU! + + "output_dir": None # all files are saved in the linux working directory by default (None). The corrected files are copied back into original dir in windows. +} + +# WSL commands are run through your distro and in whatever directory you specify as your linux_workdir. +fsl = TOPED( + + distribution="Ubuntu-22.04", # your WSL distribution name. you can find it by running 'wsl -l -v' in powershell. + user="username", # replace with your WSL user. + linux_workdir=r"\\wsl.localhost\Ubuntu-22.04\home\user\yourfolder" # replace with a path to a folder in your WSL distribution. + # yourfolder will be created if it does not exist. + # All output files will be saved here. Corrected files from TOPUP and Eddy will be copied back to the patient folders in windows. + # linux_workdir will be organised into folders matching your main_dir structure, i.e. for each patient folder in main_dir, a corresponding folder is created in linux_workdir and all processing for that patient is done in that folder. +) + + + + + +# ======================================================================================================================================================= +"""RUNNING LOOP THOUGH FOLDERS IN MAIN_DIR, WITH PARAMTERS SPECIFIED ABOVE""" +# ALL lops are run with the parameters as specified in the configuration section above. parameters are global for the run. +# be careful if/when changing this section below (needs to match with TOPED_fsl_commands.py). + + +# HELPER functions to find input files based in IDENTIFIERS dict. +def find_file(folder, keyword) -> Optional[str]: + matches = [f for f in os.listdir(folder) if keyword.lower() in f.lower()] + if len(matches) == 0: + return None + if len(matches) > 1: + raise RuntimeError(f"Multiple files found for '{keyword}' in {folder}") + return os.path.join(folder, matches[0]) + +# function for finding cnf files. +def find_cnf_file(folder): + matches = [f for f in os.listdir(folder) if f.lower().endswith(".cnf")] + if len(matches) == 0: + return None + if len(matches) > 1: + raise RuntimeError(f"Multiple .cnf files found in {folder}! clean this shit up") + return os.path.join(folder, matches[0]) +# function for finding json files. +def find_json_file(folder): + matches = [f for f in os.listdir(folder) if f.lower().endswith(".json")] + if len(matches) == 0: + return None + if len(matches) > 1: + raise RuntimeError(f"Multiple .json files found in {folder}!") + return os.path.join(folder, matches[0]) + + +# searches for the file in the patient folder first, then in the common folder if not found in the patient folder. +def resolve_file(patient_dir, common_dir, key): + local = find_file(patient_dir, IDENTIFIERS[key]) + if local: + return local + if common_dir: + return find_file(common_dir, IDENTIFIERS[key]) + return None + + +# main loops start HERE - runs TOPUP and Eddy for each patient folder in main_dir +common_dir = None +if COMMON_FOLDER_NAME: + _common_path = os.path.join(MAIN_DIR, COMMON_FOLDER_NAME) + if os.path.isdir(_common_path): + common_dir = _common_path + else: + print("note: no common folder found -> checking compulsory files in remaining folders...") + +# Input file check - check all of main_dir, do the files exist for all patients? +# this is so that you wont leeave this overnight and come back to some shit error that could have been prevented. + +print("\n File check: Checking if all required files are found...") +file_check = True + +for patient in os.listdir(MAIN_DIR): + patient_dir = os.path.join(MAIN_DIR, patient) + if not os.path.isdir(patient_dir) or patient == COMMON_FOLDER_NAME: + continue + + issues = [] + + # Locate files using same logic as the main loop + b0_path = find_file(patient_dir, IDENTIFIERS["b0"]) + dwi_path = find_file(patient_dir, IDENTIFIERS["dwi"]) + mask_path = find_file(patient_dir, IDENTIFIERS["mask"]) + acqp_path = resolve_file(patient_dir, common_dir, "acqp") + bvecs_path = resolve_file(patient_dir, common_dir, "bvec") + bvals_path = resolve_file(patient_dir, common_dir, "bval") + index_path = resolve_file(patient_dir, common_dir, "index") + + # TOPUP compulsory inputs + if not b0_path: issues.append("TOPUP: cannot find b0 file") + if not acqp_path: issues.append("TOPUP: cannot find acquisition parameters file") + + # EDDY compulsory (only warn if b0 exists, i.e. TOPUP would run) + if b0_path: + if not dwi_path: issues.append("EDDY: cannot find 4D dwi file") + if not mask_path: issues.append("EDDY: cannot find mask file") + if not bvecs_path: issues.append("EDDY: cannot find bvecs file") + if not bvals_path: issues.append("EDDY: cannot find bvals file") + if not index_path: issues.append("EDDY: cannot find index file") + + if issues: + print(f" FAIL {patient}:") + for issue in issues: + print(f" - {issue}") + file_check = False + else: + print(f" OK {patient}") + +if not file_check: + print("\n WARNING!!!!!!!!! File check failed. Some files cannot be found. You may want to abort and fix the issues. " \ + "\n Consider renaming your files to contain the specified keyword or adjust the IDENTIFIERS dict accordingly. " \ + "\n TOPUP/Eddy will run only for the 'OK' cases.") +else: + print("\n File check passed — all patients have all compulsory inputs.") + + +for patient in os.listdir(MAIN_DIR): + # searches ALL folders in main_dir (except for specified common_folder). -> Attempts to run topup+eddy on all folders in main_dir. + patient_dir = os.path.join(MAIN_DIR, patient) + if not os.path.isdir(patient_dir) or patient == COMMON_FOLDER_NAME: + continue + print(f"\n \nProcessing folder: {patient}--------------------------------------------------------------") + patient_id = patient # this is the name of the folder both in windows and in linux workdir. + + try: + # Locate compulsory files - these files should be in the 'patient' directory, not in common. + b0_path = find_file(patient_dir, IDENTIFIERS["b0"]) + dwi_path = find_file(patient_dir, IDENTIFIERS["dwi"]) + mask_path = find_file(patient_dir, IDENTIFIERS["mask"]) + # these can be in both common and patient dir. + acqp_path = resolve_file(patient_dir, common_dir, "acqp") + bvecs_path = resolve_file(patient_dir, common_dir, "bvec") + bvals_path = resolve_file(patient_dir, common_dir, "bval") + index_path = resolve_file(patient_dir, common_dir, "index") + slspec_path = resolve_file(patient_dir, common_dir, "slspec") + config_path = find_cnf_file(patient_dir) + if not config_path and common_dir: + config_path = find_cnf_file(common_dir) + json_path = find_json_file(patient_dir) + if not json_path and common_dir: + json_path = find_json_file(common_dir) + + # Check TOPUP inputs + if not b0_path: + print(f" o_O No b0 found in {patient} → Cannot run TOPUP. skipping dataset. Rename your b0 file to contain the specified keyword or adjust the IDENTIFIERS dict accordingly.") + continue + if not acqp_path: + raise RuntimeError(" Missing acquisition parameters (acqp). Please add file or update IDENTIFIERS dict.") + + print(f" → Running TOPUP :)") + # RUN TOPUP + topup_base = fsl.run_topup( + b0_path=b0_path, + acq_parameter_path=acqp_path, + config_file_path=config_path, + nthr=TOPUP_PARAMS["nthr"], + logout=TOPUP_PARAMS["logout"], + jacout=TOPUP_PARAMS["jacout"], + dfout=TOPUP_PARAMS["dfout"], + patient_id=patient, + output_dir=TOPUP_PARAMS["output_dir"] or patient_dir + ) + + # Check EDDY inputs + missing_eddy = [] + if not dwi_path: missing_eddy.append("dwi") + if not mask_path: missing_eddy.append("mask") + if not bvecs_path: missing_eddy.append("bvecs") + if not bvals_path: missing_eddy.append("bvals") + if not index_path: missing_eddy.append("index") + + if missing_eddy: + print(f" o_O Eddy missing compulsory parameters: {missing_eddy} → Cannot run EDDY. Rename your files to contain the respective keywords or adjust the IDENTIFIERS dict accordingly.") + continue +# + print(f" → Running Eddy :)") + + # RUN EDDY + fsl.run_eddy( + dwi_path=dwi_path, + mask_path=mask_path, + acq_parameter_path=acqp_path, + index_path=index_path, + bvecs_path=bvecs_path, + bvals_path=bvals_path, + patient_id=patient, + topup_base=topup_base, + + slspec_path=slspec_path, + json_path=json_path, + b_range=EDDY_PARAMS["b_range"], + + flm=EDDY_PARAMS["flm"], + slm=EDDY_PARAMS["slm"], + fwhm=EDDY_PARAMS["fwhm"], + niter=EDDY_PARAMS["niter"], + interp=EDDY_PARAMS["interp"], + resamp=EDDY_PARAMS["resamp"], + fep=EDDY_PARAMS["fep"], + nvoxhp=EDDY_PARAMS["nvoxhp"], + ff=EDDY_PARAMS["ff"], + dont_sep_offs_move=EDDY_PARAMS["dont_sep_offs_move"], + dont_peas=EDDY_PARAMS["dont_peas"], + + repol=EDDY_PARAMS["repol"], + ol_nstd=EDDY_PARAMS["ol_nstd"], + ol_nvox=EDDY_PARAMS["ol_nvox"], + ol_type=EDDY_PARAMS["ol_type"], + ol_ss=EDDY_PARAMS["ol_ss"], + ol_pos=EDDY_PARAMS["ol_pos"], + ol_sqr=EDDY_PARAMS["ol_sqr"], + mporder=EDDY_PARAMS["mporder"], + s2v_niter=EDDY_PARAMS["s2v_niter"], + s2v_lambda=EDDY_PARAMS["s2v_lambda"], + s2v_interp=EDDY_PARAMS["s2v_interp"], + + estimate_move_by_susceptibility=EDDY_PARAMS["estimate_move_by_susceptibility"], + mbs_niter=EDDY_PARAMS["mbs_niter"], + mbs_lambda=EDDY_PARAMS["mbs_lambda"], + mbs_ksp=EDDY_PARAMS["mbs_ksp"], + + cnr_maps=EDDY_PARAMS["cnr_maps"], + residuals=EDDY_PARAMS["residuals"], + data_is_shelled=EDDY_PARAMS["data_is_shelled"], + + + output_dir=EDDY_PARAMS["output_dir"] or patient_dir + ) + + print(f"\n Done. Check your output for {patient} :)") + + except Exception as e: + print(f"ERROR in {patient}: {str(e)}") \ No newline at end of file