From 5098ddcb18bd6fb2d12a2498026e0d66060f6108 Mon Sep 17 00:00:00 2001 From: Martin HA Date: Wed, 8 Nov 2023 18:13:28 +0000 Subject: [PATCH] Add general madym_T1 wrapper (set to IR_E by default) --- config/madym_T1.config.yaml | 16 ++++ workflow/Snakefile | 8 +- workflow/rules/madym_T1.smk | 36 ++++++++ workflow/schemas/madym_T1.schema.yaml | 121 ++++++++++++++++++++++++++ workflow/scripts/madym_T1.py | 87 ++++++++++++++++++ 5 files changed, 263 insertions(+), 5 deletions(-) create mode 100644 config/madym_T1.config.yaml create mode 100644 workflow/rules/madym_T1.smk create mode 100644 workflow/schemas/madym_T1.schema.yaml create mode 100644 workflow/scripts/madym_T1.py diff --git a/config/madym_T1.config.yaml b/config/madym_T1.config.yaml new file mode 100644 index 0000000..3ca8daa --- /dev/null +++ b/config/madym_T1.config.yaml @@ -0,0 +1,16 @@ +# madym_T1 +# version = v4.23.0 + +method: IR_E + +T1_dir: IR +T1_vols: [OE_400, OE_800, OE_1000, OE_1500, OE_2000, OE_2500] +B1_name: null + +output_dir: T1_IR +log_file: logs/madym_T1_IR.log +config_out: logs/madym_T1_IR.conf + +T1_noise: 0.1 +B1_scaling: 1000 +B1_values: [] \ No newline at end of file diff --git a/workflow/Snakefile b/workflow/Snakefile index 2a09adc..b571ed3 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -17,7 +17,7 @@ include: "rules/common.smk" # include: "rules/OE_deltaR1.smk" include: "rules/DCE_deltaCt.smk" - +include: "rules/madym_T1.smk" # include: "rules/DCE_ETM.smk" # # include: "rules/OE_DCE_hypoxia_mapping.smk" @@ -26,7 +26,5 @@ include: "rules/DCE_deltaCt.smk" rule all: input: - expand("{maps_dir}/{key}{ext}", - maps_dir = data_path("maps_dir"), - key = ['C_t', 'delta_C', 'C_baseline', 'C_enhancing', 'C_p_vals', 'S_p_vals'], - ext = ".nii.gz"), \ No newline at end of file + rules.DCE_deltaCt.output, + rules.madym_T1.output \ No newline at end of file diff --git a/workflow/rules/madym_T1.smk b/workflow/rules/madym_T1.smk new file mode 100644 index 0000000..87dd0d8 --- /dev/null +++ b/workflow/rules/madym_T1.smk @@ -0,0 +1,36 @@ +''' +Wrapper for madym_T1 +''' +import os +from snakemake.utils import validate + +configfile: workflow.source_path("../../config/madym_T1.config.yaml") +validate(config, workflow.source_path("../schemas/madym_T1.schema.yaml")) + +envvars: + "MADYM_ROOT" + +rule madym_T1: + container: + "docker://registry.gitlab.com/manchester_qbi/manchester_qbi_public/madym_cxx/madym_release_no_gui:u22.04" + # "preclinicalmri_depends_no_gui:latest" + input: + # TODO: adjust for other formats, see img_fmt_r in madym_T1.schema.yaml + expand("{T1_dir}/{vols}{ext}", + T1_dir = config["T1_dir"], + vols = config["T1_vols"], + ext = [".nii.gz", ".json"]) + output: + # TODO: adjust for other formats, see img_fmt_w in madym_T1.schema.yaml + expand("{output_dir}/{key}{ext}", + output_dir = config["output_dir"], + key = ["T1", "M0","efficiency"], + ext = [".xtr", ".nii.gz"]), + os.path.join(config["output_dir"], "error_tracker.nii.gz") + # touch(os.path.join(config["output_dir"], "madym_T1.done")) + log: + log = config["log_file"], + cfg = config["config_out"], + audit = os.path.join(config["audit_dir"], config["audit_name"]) + script: + "../scripts/madym_T1.py" \ No newline at end of file diff --git a/workflow/schemas/madym_T1.schema.yaml b/workflow/schemas/madym_T1.schema.yaml new file mode 100644 index 0000000..1588193 --- /dev/null +++ b/workflow/schemas/madym_T1.schema.yaml @@ -0,0 +1,121 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" + + +# The following options are overriden by snakemake's functionality: +# working_directory: set to workdir +# config_file: disabled (replaced by rule's configfile) + +# roi_name + +properties: + + # Inputs + T1_vols: + type: [array, "null"] + default: null + description: Variable flip angle file names, comma separated (no spaces). + T1_dir: + type: [string, "null"] + default: null + description: Folder containing T1 input volumes, can be left empty if already included in option --T1_vols + img_fmt_r: + type: string + default: NIFTI_GZ + # enum: [ANALYZE, ANALYZE_SPARSE, NIFTI, NIFTI_GZ, DICOM] # TODO: adjust madym_T1.smk inputs for other formats + const: NIFTI_GZ + description: Image format for reading input. + error_name: + type: [string, "null"] + default: null + description: Error codes image file name. + + # Outputs + output_dir: + type: [string, "null"] + default: null + description: Output path, will use a temporary directory if empty. + img_fmt_w: + type: string + default: NIFTI_GZ + # enum: [ANALYZE, ANALYZE_SPARSE, NIFTI, NIFTI_GZ, DICOM] # TODO: adjust madym_T1.smk outputs for other formats + const: NIFTI_GZ + description: Image format for writing output. + log_file: + type: string + default: logs/madym_T1.log + description: Folder in which audit output is saved. + config_out: + type: [string, "null"] + default: logs/madym_T1.conf + description: Filename of the output config file. + no_log: + type: boolean + default: false + description: Switch off program logging. + no_audit: + type: boolean + default: true + description: Switch off audit logging. + audit_dir: + type: string + default: audit_logs + description: Folder in which audit output is saved. + audit_name: + type: string + default: madym_T1.audit + description: Audit file name. + quiet: + type: boolean + default: false + description: Do not display logging messages in cout. + + # Params + method: + type: ["null","string"] + default: null + enum: [VFA, VFA_B1, IR, IR_E] + description: T1 method to use to fit, Variable Flip-Angle [B1 corrected], Inversion Recovery [with efficiency weighting] + B1_name: + type: [string, "null"] + default: null + description: Path to the B1 correction map. + B1_scaling: + type: ["number", "null"] + default: null + description: Value applied to scaled values in the B1 correction map. + B1_values: + type: [array, "null"] + default: null + description: B1 correction values, 1D array of length n_samples. + T1_noise: + type: ["number", "null"] + default: null + description: PD noise threshold. + + # Other + nifti_scaling: + type: boolean + default: false + description: If set, applies intensity scaling and offset when reading/writing NIFTI images. + nifti_4D: + type: boolean + default: false + description: If set, reads NIFTI 4D images for T1 mapping and dynamic inputs. + use_BIDS: + type: boolean + default: false + description: If set, writes images using BIDS JSON meta info. + voxel_size_warn_only: + type: boolean + default: false + description: Warn if voxel sizes don't match for subsequent images. + overwrite: + type: boolean + default: true + description: Set overwrite existing analysis in the output directory ON. + +required: + - method + - T1_vols + +# additionalProperties: false \ No newline at end of file diff --git a/workflow/scripts/madym_T1.py b/workflow/scripts/madym_T1.py new file mode 100644 index 0000000..713a34d --- /dev/null +++ b/workflow/scripts/madym_T1.py @@ -0,0 +1,87 @@ +import os +import re + +from QbiMadym import madym_T1 +from QbiMadym.utils import local_madym_root + +import subprocess +command = 'madym_T1.exe' +try: + command = os.path.join(local_madym_root(),'madym_T1') + subprocess.run([command, "--version"], stdout = subprocess.PIPE, stderr = subprocess.PIPE, check=True) +except subprocess.CalledProcessError: + raise NameError(f"{command} is not available.") + +madym_T1.run( + # + working_directory = os.getcwd(), + config_file = None, + # cmd_exe = command, + # + output_dir = snakemake.config["output_dir"], + img_fmt_w = "NIFTI_GZ", + + # Input + # T1_dir = snakemake.config["T1_dir"], + T1_vols = [os.path.join(snakemake.config["T1_dir"], vol) for vol in snakemake.config["T1_vols"]], + img_fmt_r = "NIFTI_GZ", + roi_name = snakemake.config["roi_path"], + error_name = snakemake.config["error_name"], + + # Params + method = snakemake.config["method"], + B1_name = snakemake.config["B1_name"], + B1_scaling = snakemake.config["B1_scaling"], + noise_thresh = snakemake.config["T1_noise"], + nifti_scaling = snakemake.config["nifti_scaling"], + nifti_4D = snakemake.config["nifti_4D"], + use_BIDS = snakemake.config["use_BIDS"], + voxel_size_warn_only = snakemake.config["voxel_size_warn_only"], + # + no_log = False, + quiet = False, + overwrite = True, + return_maps = False, + dummy_run = False, + + no_audit = snakemake.config["no_audit"], + audit_dir = snakemake.config["audit_dir"], + + program_log_name = "smk.log", # see below (*) + config_out = "smk.cfg", + audit_name = "smk.audit" + + # madym_T1_lite options (not exposed) + # ScannerParams = None, + # signals = None, + # TR = None, + # B1_values = None, + # output_name = 'madym_analysis.dat', +) + +# (*) Logs are time-stamped and auto-renamed by madym: +# +# {output_dir}/madym_T1_{date}_{time}_smk.log +# {output_dir}/madym_T1_{date}_{time}_smk.cfg +# {output_dir}/madym_T1_{date}_{time}_override_smk.cfg +# {audit_dir}/madym_T1_{date}_{time}_smk.audit +# +# Rename to consistent snakemake.log entries +for item in snakemake.log.keys(): + + if item == "log" or item == "cfg": + dir = snakemake.config["output_dir"] + elif item == "audit": + dir = snakemake.config["audit_dir"] + else: + raise NameError("Unexpected log key: " + item) + + for filename in os.listdir(dir): + match = re.match(r'madym_T1_(\d+)_(\d+)(_\w)?_smk\.' + item, filename) + if match: + if match.group(3) is None: + logname = snakemake.log[item] + else: + # Modify log.ext to e.g. log_override.ext + logname = re.sub(r'(\.\w+)$', match.group(3) + r'\1', snakemake.log[item]) + os.rename(os.path.join(dir, filename), logname)