From dfdd443b016bec7a534a31b45ca69802cd653f84 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 2 Feb 2018 13:57:56 -0500 Subject: [PATCH 01/75] Started the main 1.0.4 development branch! --- CPAC/__init__.py | 2 +- CPAC/func_preproc/func_preproc.py | 2 +- CPAC/info.py | 2 +- CPAC/resources/configs/pipeline_config_template.yml | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CPAC/__init__.py b/CPAC/__init__.py index 7aa1b4e69e..5f061fb19f 100644 --- a/CPAC/__init__.py +++ b/CPAC/__init__.py @@ -50,7 +50,7 @@ def test(self, label='fast', verbose=1, extra_argv=['--exe'], doctests = False, #__version__ = '0.1-git' try: - version = '1.0.3' + version = '1.0.4' # gitproc = Popen(['git', 'log', '--oneline'], stdout = PIPE) # (stdout, stderr) = gitproc.communicate() diff --git a/CPAC/func_preproc/func_preproc.py b/CPAC/func_preproc/func_preproc.py index c067311594..e287cde405 100644 --- a/CPAC/func_preproc/func_preproc.py +++ b/CPAC/func_preproc/func_preproc.py @@ -303,7 +303,7 @@ def create_func_preproc(use_bet=False, wf_name='func_preproc'): For each volume in RPI-oriented T2 image, the command, aligns the image with the base mean image and calculates the motion, displacement and movement parameters. It also outputs the aligned 4D volume and movement and displacement parameters for each volume. - - Create a brain-only mask. For details see `3dautomask `_:: + - Create a brain-only mask. For details see `3dautomask `_:: 3dAutomask -prefix rest_3dc_RPI_3dv_automask.nii.gz diff --git a/CPAC/info.py b/CPAC/info.py index a9de5190e8..a48c0b1ef9 100644 --- a/CPAC/info.py +++ b/CPAC/info.py @@ -10,7 +10,7 @@ # version _version_major = 1 _version_minor = 0 -_version_micro = 3 +_version_micro = 4 _version_extra = '' def get_cpac_gitversion(): diff --git a/CPAC/resources/configs/pipeline_config_template.yml b/CPAC/resources/configs/pipeline_config_template.yml index 09f14536f7..d7d1b879aa 100644 --- a/CPAC/resources/configs/pipeline_config_template.yml +++ b/CPAC/resources/configs/pipeline_config_template.yml @@ -206,7 +206,6 @@ fmap_distcorr_frac: [0.5] # Set the Delta-TE value, used for preparing the field map, the time delay between the first and second echo images. -# Note, TE can be specified in the data configuration YAML file via scan parameters setup. If it is not specified there, this value will be used instead. fmap_distcorr_deltaTE : 2.46 From 90b2fa749e530fdde5c2aa9e0f01666592ba96fd Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Tue, 20 Feb 2018 17:33:12 -0500 Subject: [PATCH 02/75] Allow the user to select which anatomical scan/run to be used during data config generation, and also fixed an issue with using wildcards. Updated the data settings template for v1.0.4 as well. --- .../interface/windows/dataconfig_window.py | 74 +++++++++---- CPAC/pipeline/cpac_pipeline.py | 15 +-- .../configs/data_config_TEMPLATE.yml | 2 +- .../configs/data_settings_template.yml | 48 ++++++--- .../configs/pipeline_config_template.yml | 2 +- CPAC/utils/build_data_config.py | 101 +++++++++++++++--- 6 files changed, 185 insertions(+), 57 deletions(-) diff --git a/CPAC/GUI/interface/windows/dataconfig_window.py b/CPAC/GUI/interface/windows/dataconfig_window.py index e6d0de98e5..22694e8f3f 100644 --- a/CPAC/GUI/interface/windows/dataconfig_window.py +++ b/CPAC/GUI/interface/windows/dataconfig_window.py @@ -12,6 +12,7 @@ from ..utils.constants import control, dtype import os import yaml +import CPAC import pkg_resources as p import sys @@ -19,7 +20,7 @@ ID_RUN_EXT = 11 ID_RUN_MEXT = 12 -# DataConfig wx.Frame class + class DataConfig(wx.Frame): # Init method @@ -27,7 +28,7 @@ def __init__(self, parent): wx.Frame.__init__(self, parent, title="CPAC - Data Configuration " "Setup", - size=(940, 620)) + size=(1040, 620)) mainSizer = wx.BoxSizer(wx.VERTICAL) @@ -46,7 +47,7 @@ def __init__(self, parent): values=["BIDS", "Custom"], wkf_switch=True) - self.page.add(label="BIDS Base Directory ", + self.page.add(label="[BIDS only] BIDS Base Directory ", control=control.DIR_COMBO_BOX, name="bidsBaseDir", type=dtype.STR, @@ -56,7 +57,7 @@ def __init__(self, parent): "the entire dataset.", values="") - self.page.add(label="Anatomical File Path Template ", + self.page.add(label="[Custom only] Anatomical File Path Template ", control=control.TEXT_BOX, name="anatomicalTemplate", type=dtype.STR, @@ -75,7 +76,7 @@ def __init__(self, parent): style=wx.EXPAND | wx.ALL, size=(532,-1)) - self.page.add(label="Functional File Path Template ", + self.page.add(label="[Custom only] Functional File Path Template ", control=control.TEXT_BOX, name="functionalTemplate", type=dtype.STR, @@ -113,6 +114,36 @@ def __init__(self, parent): style= wx.EXPAND | wx.ALL, size = (300,-1)) + self.page.add(label="(Optional) Which Anatomical Scan? ", + control=control.TEXT_BOX, + name="anatomical_scan", + type=dtype.STR, + comment="Scan/Run ID for the Anatomical Scan\n\n" + "Sometimes, there are multiple anatomical " + "scans for each participant in a dataset.\n\n" + "If this is the case, you can choose which " + "anatomical scan to use for this participant " + "by entering the identifier that makes the " + "scan unique.\n\nExamples:\n\nBIDS dataset\n" + "../anat/sub-001_run-1_T1w.nii.gz\n" + "../anat/sub-001_run-2_T1w.nii.gz\n" + "Pick the second with 'run-2'.\n\n" + "Custom dataset\n" + "Example use case: let's say most anatomicals " + "in your dataset are '../mprage.nii.gz', but " + "some participants only have '../anat1.nii.gz' " + "and '../anat2.nii.gz'. You want the " + "mprage.nii.gz files included, but only the " + "anat2.nii.gz in the others.\n\nPlace a " + "wildcard (*) in the anatomical filepath " + "template above (../*.nii.gz), then enter " + "'anat2' in this field to 'break the tie' for " + "participants that have the 'anat1' and " + "'anat2' scans.", + values="None", + style=wx.EXPAND | wx.ALL, + size=(532,-1)) + # Add AWS credentials path self.page.add(label="(Optional) AWS credentials file ", control=control.COMBO_BOX, @@ -123,7 +154,7 @@ def __init__(self, parent): 'local files.', values='None') - self.page.add(label="(Optional) Scan Parameters File ", + self.page.add(label="(Optional) [Custom only] Scan Parameters File ", control=control.COMBO_BOX, name="scanParametersCSV", type=dtype.COMBO, @@ -138,7 +169,8 @@ def __init__(self, parent): "configuration file.", values="None") - self.page.add(label="(Optional) Field Map Phase File Path Template ", + self.page.add(label="(Optional) [Custom only] Field Map Phase File " + "Path Template ", control=control.TEXT_BOX, name="fieldMapPhase", type=dtype.STR, @@ -157,7 +189,8 @@ def __init__(self, parent): style=wx.EXPAND | wx.ALL, size=(532,-1)) - self.page.add(label="(Optional) Field Map Magnitude File Path Template ", + self.page.add(label="(Optional) [Custom only] Field Map Magnitude " + "File Path Template ", control=control.TEXT_BOX, name="fieldMapMagnitude", type=dtype.STR, @@ -414,9 +447,9 @@ def display(win, msg): key_order = ['dataFormat', 'bidsBaseDir', 'anatomicalTemplate', 'functionalTemplate', 'outputSubjectListLocation', - 'subjectListName', 'awsCredentialsFile', - 'scanParametersCSV', 'fieldMapPhase', - 'fieldMapMagnitude', 'subjectList', + 'subjectListName', 'anatomical_scan', + 'awsCredentialsFile', 'scanParametersCSV', + 'fieldMapPhase', 'fieldMapMagnitude', 'subjectList', 'exclusionSubjectList', 'siteList', 'exclusionSiteList', 'sessionList', 'exclusionSessionList', 'scanList', 'exclusionScanList'] @@ -487,7 +520,7 @@ def display(win, msg): return elif "Custom" in config_dict["dataFormat"][0]: - if len(config_dict["bidsBaseDir"][0]) > 0: + if "/" in str(config_dict["bidsBaseDir"]): err = wx.MessageDialog(self, "BIDS base directory " "provided, but data format " "is set to Custom instead " @@ -534,7 +567,7 @@ def display(win, msg): else: dlg = wx.FileDialog( self, message="Save file as ...", - defaultDir=os.getcwd(), + defaultDir=str(config_dict["outputSubjectListLocation"]), defaultFile="data_settings_{0}.yaml".format(subject_list_name), wildcard="YAML files(*.yaml, *.yml)|*.yaml;*.yml", style=wx.SAVE) @@ -545,7 +578,8 @@ def display(win, msg): with open(path, "wt") as f: f.write( - "# CPAC Data Settings File\n# Version 1.0.3\n") + "# CPAC Data Settings File\n# Version {0}" + "\n".format(CPAC.__version__)) f.write( "#\n# http://fcp-indi.github.io for more info.\n#\n" "# Use this file to generate the data configuration " @@ -556,14 +590,14 @@ def display(win, msg): "--data_settings_file input flag.\n\n\n") for key in key_order: - val = config_dict[key][0] + value = config_dict[key][0] help = config_dict[key][2] - if "/" in val or "%s" in val or 'None' in val or \ - key == 'subjectListName': - value = val - else: - value =[item.strip() for item in val.split(',')] + # if "/" in val or "%s" in val or 'None' in val or \ + # key == 'subjectListName': + # value = val + # else: + # value =[item.strip() for item in val.split(',')] help = help.replace("\n", "\n# ") diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 80d6d04c29..c86048dae4 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -337,11 +337,12 @@ def create_log_node(wflow, output, indx, scan_id=None): log_wf.inputs.inputspec.inputs = log_dir return log_wf - def logStandardError(sectionName, errLine, errNum): + def logStandardError(sectionName, errLine, errNum, errInfo=None): - logger.info( - "\n\n" + 'ERROR: %s - %s' % (sectionName, errLine) + "\n\n" + \ - "Error name: cpac_pipeline_%s" % (errNum) + "\n\n") + logger.info("\n\nERROR: {0} - {1}\n\nError name: cpac_pipeline" + "_{2}\n\n".format(sectionName, errLine, errNum)) + if errInfo: + logger.info("Error details: {0}\n\n".format(errInfo)) def logConnectionError(workflow_name, numStrat, resourcePool, errNum): @@ -5047,9 +5048,9 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): qc_plot_id[8] = 'fd_plot' - except: - logStandardError('QC', 'unable to get resources for FD plot', '0053') - raise + except Exception as e: + logStandardError('QC', 'unable to get resources for FD plot', '0053', e) + raise Exception # make QC montages for Skull Stripping Visualization diff --git a/CPAC/resources/configs/data_config_TEMPLATE.yml b/CPAC/resources/configs/data_config_TEMPLATE.yml index e9e1736244..0eb390cf00 100644 --- a/CPAC/resources/configs/data_config_TEMPLATE.yml +++ b/CPAC/resources/configs/data_config_TEMPLATE.yml @@ -1,5 +1,5 @@ # CPAC Data Configuration File -# Version 1.0.3 +# Version 1.0.4 # # http://fcp-indi.github.io for more info. # diff --git a/CPAC/resources/configs/data_settings_template.yml b/CPAC/resources/configs/data_settings_template.yml index b37e7cd7bf..fdd5ff1ab7 100644 --- a/CPAC/resources/configs/data_settings_template.yml +++ b/CPAC/resources/configs/data_settings_template.yml @@ -1,5 +1,5 @@ # CPAC Data Settings File -# Version 1.0.3 +# Version 1.0.4 # # http://fcp-indi.github.io for more info. # @@ -7,15 +7,15 @@ # Select if data is organized using BIDS standard or a custom format. -# Options: ['BIDS'] or ['Custom'] -dataFormat: [''] +# Options: 'BIDS' or 'Custom' +dataFormat: BIDS # Base directory of BIDS-organized data. # BIDS Data Format only. # # This should be the path to the overarching directory containing the entire dataset. -bidsBaseDir: +bidsBaseDir: None # File Path Template for Anatomical Files @@ -28,7 +28,7 @@ bidsBaseDir: # /data/{site}/{participant}/anat.nii.gz # # See the User Guide for more detailed instructions. -anatomicalTemplate: +anatomicalTemplate: None # File Path Template for Functional Files @@ -41,7 +41,7 @@ anatomicalTemplate: # /data/{site}/{participant}/{series}/func.nii.gz # # See the User Guide for more detailed instructions. -functionalTemplate: +functionalTemplate: None # Required if downloading data from a non-public S3 bucket on Amazon Web Services instead of using local files. @@ -56,6 +56,26 @@ outputSubjectListLocation: subjectListName: +# Scan/Run ID for the Anatomical Scan +# +# Sometimes, there are multiple anatomical scans for each participant in a dataset. +# +# If this is the case, you can choose which anatomical scan to use for this participant by entering the identifier that makes the scan unique. +# +# Examples: +# +# BIDS dataset +# ../anat/sub-001_run-1_T1w.nii.gz +# ../anat/sub-001_run-2_T1w.nii.gz +# Pick the second with 'run-2'. +# +# Custom dataset +# Example use case: let's say most anatomicals in your dataset are '../mprage.nii.gz', but some participants only have '../anat1.nii.gz' and '../anat2.nii.gz'. You want the mprage.nii.gz files included, but only the anat2.nii.gz in the others. +# +# Place a wildcard (*) in the anatomical filepath template above (../*.nii.gz), then enter 'anat2' in this field to 'break the tie' for participants that have the 'anat1' and 'anat2' scans. +anatomical_scan: None + + # For Slice Timing Correction. # Custom Data Format only. # @@ -93,7 +113,7 @@ fieldMapMagnitude: None # Include only a sub-set of the participants present in the folders defined above. # -# List participants in this box (ex: ['sub101', 'sub102']) or provide the path to a text file with one participant ID on each line. +# List participants in this box (ex: sub101, sub102) or provide the path to a text file with one participant ID on each line. # # If 'None' is specified, CPAC will include all participants. subjectList: None @@ -101,7 +121,7 @@ subjectList: None # Exclude a sub-set of the participants present in the folders defined above. # -# List participants in this box (ex: ['sub101', 'sub102']) or provide the path to a text file with one participant ID on each line. +# List participants in this box (ex: sub101, sub102) or provide the path to a text file with one participant ID on each line. # # If 'None' is specified, CPAC will not exclude any participants. exclusionSubjectList: None @@ -109,7 +129,7 @@ exclusionSubjectList: None # Include only a sub-set of the sites present in the folders defined above. # -# List sites in this box (ex: ['NYU', 'UCLA']) or provide the path to a text file with one site name on each line. +# List sites in this box (ex: NYU, UCLA) or provide the path to a text file with one site name on each line. # # If 'None' is specified, CPAC will include all sites. siteList: None @@ -117,7 +137,7 @@ siteList: None # Exclude a sub-set of the sites present in the folders defined above. # -# List sites in this box (ex: ['NYU', 'UCLA']) or provide the path to a text file with one site name on each line. +# List sites in this box (ex: NYU, UCLA) or provide the path to a text file with one site name on each line. # # If 'None' is specified, CPAC will include all sites. exclusionSiteList: None @@ -125,7 +145,7 @@ exclusionSiteList: None # Include only a sub-set of the sessions present in the folders defined above. # -# List sessions in this box (ex: ['session-1', 'session-2']) or provide the path to a text file with one session name on each line. +# List sessions in this box (ex: session-1, session-2) or provide the path to a text file with one session name on each line. # # If 'None' is specified, CPAC will include all sessions. sessionList: None @@ -133,7 +153,7 @@ sessionList: None # Exclude a sub-set of the sessions present in the folders defined above. # -# List sessions in this box (ex: ['session-1', 'session-2']) or provide the path to a text file with one session name on each line. +# List sessions in this box (ex: session-1, session-2) or provide the path to a text file with one session name on each line. # # If 'None' is specified, CPAC will include all sessions. exclusionSessionList: None @@ -141,7 +161,7 @@ exclusionSessionList: None # Include only a sub-set of the series present in the folders defined above. # -# List series in this box (ex: ['func-1', 'func-2']) or provide the path to a text file with one series name on each line. +# List series in this box (ex: func-1, func-2) or provide the path to a text file with one series name on each line. # # If 'None' is specified, CPAC will include all series. scanList: None @@ -149,7 +169,7 @@ scanList: None # Exclude a sub-set of the series present in the folders defined above. # -# List series in this box (ex: ['func-1', 'func-2']) or provide the path to a text file with one series name on each line. +# List series in this box (ex: func-1, func-2) or provide the path to a text file with one series name on each line. # # If 'None' is specified, CPAC will include all series. exclusionScanList: None diff --git a/CPAC/resources/configs/pipeline_config_template.yml b/CPAC/resources/configs/pipeline_config_template.yml index d7d1b879aa..57ce35ecc1 100644 --- a/CPAC/resources/configs/pipeline_config_template.yml +++ b/CPAC/resources/configs/pipeline_config_template.yml @@ -1,5 +1,5 @@ # CPAC Pipeline Configuration YAML file -# Version 1.0.3 +# Version 1.0.4 # # http://fcp-indi.github.io for more info. # diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index 35ebb6b697..f0ced85a2d 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -281,9 +281,9 @@ def format_incl_excl_dct(site_incl_list=None, participant_incl_list=None, return incl_dct -def get_BIDS_data_dct(bids_base_dir, file_list=None, aws_creds_path=None, - inclusion_dct=None, exclusion_dct=None, - config_dir=None): +def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, + aws_creds_path=None, inclusion_dct=None, + exclusion_dct=None, config_dir=None): import os import glob @@ -297,6 +297,10 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, aws_creds_path=None, anat = os.path.join(bids_base_dir, "sub-{participant}/anat/sub-{participant}_T1w.nii.gz") + if anat_scan: + anat_sess = anat_sess.replace("_T1w", "_*_T1w") + anat = anat.replace("_T1w", "_*_T1w") + func_sess = os.path.join(bids_base_dir, "sub-{participant}" "/ses-{session}/func/sub-" @@ -330,7 +334,7 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, aws_creds_path=None, sub_jsons_glob = os.path.join(bids_base_dir, "*sub-*/*bold.json") ses_jsons_glob = os.path.join(bids_base_dir, "*sub-*/ses-*/*bold.json") - site_dir_glob = os.path.join(bids_base_dir, "*", "sub-*/*.nii*") + site_dir_glob = os.path.join(bids_base_dir, "*", "sub-*/*/*.nii*") ses = False site_dir = False @@ -558,10 +562,10 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, aws_creds_path=None, def get_nonBIDS_data(anat_template, func_template, file_list=None, - scan_params_dct=None, fmap_phase_template=None, - fmap_mag_template=None, aws_creds_path=None, - inclusion_dct=None, exclusion_dct=None, sites_dct=None, - verbose=False): + anat_scan=None, scan_params_dct=None, + fmap_phase_template=None, fmap_mag_template=None, + aws_creds_path=None, inclusion_dct=None, + exclusion_dct=None, sites_dct=None, verbose=False): # go over the file paths, validate for nifti's? # work with the template @@ -572,6 +576,7 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, # - throw error/warning if anat and func templates are identical # - all permutations of scan parameters json/csv's at different levels + import os import glob import fnmatch @@ -642,8 +647,15 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, if '.nii' not in anat_path: continue + # pick the right anatomical scan + if anat_scan: + if anat_scan not in os.path.basename(anat_path): + continue + path_dct = {} + # reduce the template down to only the substrings that do not have + # these tags or IDs site_parts = anat_template.split('{site}') partic_parts = [] @@ -652,10 +664,18 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, ses_parts = [] for part in partic_parts: ses_parts = ses_parts + part.split('{session}') + if "*" in anat_template: + wild_parts = [] + for part in ses_parts: + wild_parts = wild_parts + part.split('*') + ses_parts = wild_parts new_template = anat_template new_path = anat_path + # go through the non-label/non-ID substrings and parse them out, + # going from left to right and chopping out both sides of the whole + # string until only the tag, and the ID, are left for idx in range(0, len(ses_parts)): part1 = ses_parts[idx] try: @@ -663,9 +683,29 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, except IndexError: break + # example: /home/{site}/ses-{session}/.. + # part1 = /home/, part2 = /ses- + # first split -> ['', '{site}/ses-{session}/..'] + # (pick second item) + # second split -> ['{site}', '{session}/..'] + # (pick first item) label = new_template.split(part1, 1)[1] label = label.split(part2, 1)[0] + if label == "*": + continue + + # example: /home/{site}/*/ses-{session}/.. + # /home/NYU/folder1/ses-1/.. + # part1 = /home/, part2 = /*/ses- + # first split -> ['', 'NYU/folder1/ses-1/..'] + # (pick second item) + # what it would be -> ['NYU/folder1/ses-1/..'] because /*/ses-1 doesn't exist in there + # what we want -> ['NYU', '1/..'] + # (pick first item) + # so, '/*/ses-', we want to transform into '/folder1/ses-' + # TODO: BASICALLY we want to convert the TEMPLATE so that its actual * will be filled with the current real path's real substring! + id = new_path.split(part1, 1)[1] id = id.split(part2, 1)[0] @@ -691,11 +731,10 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, "Scan not included:\n{0}" \ "\n\n".format(anat_path) else: - warn = "\n\n[!] WARNING: While parsing your input data " \ - "files, a file path was found with conflicting " \ - "IDs for the same data level.\n\n" \ - "File path: {0}\n" \ - "Level: {1}\n" \ + warn = "\n\n[!] WARNING: While parsing your input " \ + "data files, a file path was found with " \ + "conflicting IDs for the same data level." \ + "\n\nFile path: {0}\nLevel: {1}\n" \ "Conflicting IDs: {2}, {3}\n\n" \ "This file has not been added to the data " \ "configuration.".format(anat_path, label, @@ -758,6 +797,12 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, if sub_id in exclusion_dct['participants']: continue + if "*" in anat_path: + if "s3://" in anat_path: + err = "\n\n[!] Cannot use wildcards (*) in AWS S3 bucket " \ + "(s3://) paths!" + paths = glob.glob(anat_path) + temp_sub_dct = {'subject_id': sub_id, 'unique_id': ses_id, 'site': site_id, @@ -800,6 +845,11 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, scan_parts = [] for part in ses_parts: scan_parts = scan_parts + part.split('{scan}') + if "*" in func_template: + wild_parts = [] + for part in scan_parts: + wild_parts = wild_parts + part.split('*') + scan_parts = wild_parts new_template = func_template new_path = func_path @@ -814,6 +864,9 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, label = new_template.split(part1, 1)[1] label = label.split(part2, 1)[0] + if label == "*": + continue + id = new_path.split(part1, 1)[1] id = id.split(part2, 1)[0] @@ -1020,6 +1073,11 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, scan_parts = [] for part in ses_parts: scan_parts = scan_parts + part.split('{scan}') + if "*" in fmap_phase_template: + wild_parts = [] + for part in scan_parts: + wild_parts = wild_parts + part.split('*') + scan_parts = wild_parts new_template = fmap_phase_template new_path = fmap_phase @@ -1034,6 +1092,9 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, label = new_template.split(part1, 1)[1] label = label.split(part2, 1)[0] + if label == "*": + continue + id = new_path.split(part1, 1)[1] id = id.split(part2, 1)[0] @@ -1152,6 +1213,11 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, scan_parts = [] for part in ses_parts: scan_parts = scan_parts + part.split('{scan}') + if "*" in fmap_mag_template: + wild_parts = [] + for part in scan_parts: + wild_parts = wild_parts + part.split('*') + scan_parts = wild_parts new_template = fmap_mag_template new_path = fmap_mag @@ -1166,6 +1232,9 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, label = new_template.split(part1, 1)[1] label = label.split(part2, 1)[0] + if label == "*": + continue + id = new_path.split(part1, 1)[1] id = id.split(part2, 1)[0] @@ -1287,6 +1356,7 @@ def run(data_settings_yml): import os import yaml + import CPAC print "\nGenerating data configuration file.." @@ -1323,6 +1393,7 @@ def run(data_settings_yml): data_dct = get_BIDS_data_dct(settings_dct['bidsBaseDir'], file_list=file_list, + anat_scan=settings_dct['anatomical_scan'], aws_creds_path=settings_dct['awsCredentialsFile'], inclusion_dct=incl_dct, exclusion_dct=excl_dct, @@ -1366,6 +1437,7 @@ def run(data_settings_yml): data_dct = get_nonBIDS_data(settings_dct['anatomicalTemplate'], settings_dct['functionalTemplate'], file_list=file_list, + anat_scan=settings_dct['anatomical_scan'], scan_params_dct=params_dct, fmap_phase_template=settings_dct['fieldMapPhase'], fmap_mag_template=settings_dct['fieldMapMagnitude'], @@ -1410,7 +1482,8 @@ def run(data_settings_yml): with open(data_config_outfile, "wt") as f: # Make sure YAML doesn't dump aliases (so it's more human # read-able) - f.write("# CPAC Data Configuration File\n# Version 1.0.3\n") + f.write("# CPAC Data Configuration File\n# Version {0}" + "\n".format(CPAC.__version__)) f.write("#\n# http://fcp-indi.github.io for more info.\n#\n" "# Tip: This file can be edited manually with " "a text editor for quick modifications.\n\n") From f7af2a7ae842f9361a1f76741bb8e342ecc4373d Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Wed, 21 Feb 2018 17:43:57 -0500 Subject: [PATCH 03/75] Updated to clean up QA code --- CPAC/pipeline/cpac_pipeline.py | 71 +++++++++++++++------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 80d6d04c29..2ed0c63849 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -1405,16 +1405,8 @@ def getNodeList(strategy): # we might prefer to use the TR stored in the NIFTI header # if not, use the value in the scan_params node try: - if c.TR: - if isinstance(c.TR, str): - if "None" in c.TR or "none" in c.TR: - pass - else: - workflow.connect(scan_params, 'tr', - func_slice_timing_correction, 'tr') - else: - workflow.connect(scan_params, 'tr', - func_slice_timing_correction, 'tr') + workflow.connect(scan_params, 'tr', + func_slice_timing_correction, 'tr') except Exception as xxx: logger.info( "Error connecting input 'tr' to func_slice_timing_" @@ -4866,8 +4858,7 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): QUALITY CONTROL - to be re-implemented later """"""""""""""""""""""""""""""""""""""""""""""""""" - # TODO - QA pages: re-introduce - ''' + if 1 in c.generateQualityControlImages: #register color palettes @@ -4916,7 +4907,7 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): output_names=['new_fname'], function=gen_std_dev), name='std_dev_%d' % num_strat) - + #all functional calls are in the pipeline figure# std_dev_anat = pe.Node(util.Function(input_names=['func_', 'ref_', 'xfm_', @@ -4942,7 +4933,7 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): montage_snr = create_montage('montage_snr_%d' % num_strat, 'red_to_blue', 'snr') - +#Are you making connections here in the pipeline because there is not inputspec and outputspec in the qc scripts indivually???# workflow.connect(preproc, out_file, std_dev, 'func_') @@ -5027,16 +5018,16 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): if 'gen_motion_stats' in nodes: try: + if c.fdCalc == 'Power': + fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_power') + else: + fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_jenkinson') + if ("De-Spiking" in c.runMotionSpike and 1 in c.runNuisance): + excluded, out_file_ex = strat.get_node_from_resource_pool('despiking_frames_excluded') + elif ("Scrubbing" in c.runMotionSpike and 1 in c.runNuisance): + excluded, out_file_ex = strat.get_node_from_resource_pool('scrubbing_frames_excluded') - fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement') - excluded, out_file_ex = strat.get_node_from_resource_pool('scrubbing_frames_excluded') - - fd_plot = pe.Node(util.Function(input_names=['arr', - 'ex_vol', - 'measure'], - output_names=['hist_path'], - function=gen_plot_png), - name='fd_plot_%d' % num_strat) + fd_plot = pe.Node(util.Function(input_names=['arr','ex_vol','measure'],output_names=['hist_path'],function=gen_plot_png),name='fd_plot_%d' % num_strat) fd_plot.inputs.measure = 'FD' workflow.connect(fd, out_file, fd_plot, 'arr') @@ -5190,7 +5181,7 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): # make QC montage for Mean Functional in MNI with MNI edge try: - m_f_i, out_file = strat.get_node_from_resource_pool('mean_functional_in_mni') + m_f_i, out_file = strat.get_node_from_resource_pool('mean_functional_to_standard') montage_mfi = create_montage('montage_mfi_%d' % num_strat, 'red', 'MNI_edge_on_mean_func_mni') ### @@ -5294,21 +5285,21 @@ def QA_montages(measure, idx): # ReHo QA montages - if 1 in c.runReHo: + #if 1 in c.runReHo: - if 1 in c.runRegisterFuncToMNI: - QA_montages('reho_to_standard', 15) - - if c.fwhm != None: - QA_montages('reho_to_standard_smooth', 16) - - if 1 in c.runZScoring: - - if c.fwhm != None: - QA_montages('reho_to_standard_smooth_fisher_zstd', 17) - - else: - QA_montages('reho_to_standard_fisher_zstd', 18) +# if 1 in c.runRegisterFuncToMNI: +# QA_montages('reho_to_standard', 15) +# +# if c.fwhm != None: +# QA_montages('reho_to_standard_smooth', 16) +# +# if 1 in c.runZScoring: +# +# if c.fwhm != None: +# QA_montages('reho_to_standard_smooth_fisher_zstd', 17) +# +# else: +# QA_montages('reho_to_standard_fisher_zstd', 18) @@ -5363,7 +5354,7 @@ def QA_montages(measure, idx): # Dual Regression QA montages - if (1 in c.runDualReg) and (1 in c.runSpatialRegression): + if ("DualReg" in sca_analysis_dict.keys()) and (1 in c.runSpatialRegression): QA_montages('dr_tempreg_maps_files', 31) QA_montages('dr_tempreg_maps_zstat_files', 32) @@ -5398,7 +5389,7 @@ def QA_montages(measure, idx): num_strat += 1 - ''' + logger.info('\n\n' + 'Pipeline building completed.' + '\n\n') From e8106ce88f9bb4a7248ec3a7a057f11f59206495 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Wed, 21 Feb 2018 17:45:50 -0500 Subject: [PATCH 04/75] Updated templates to remove jinja2 error --- CPAC/resources/templates/cpac_runner.html | 46 +++++++++---------- .../templates/logger_group_index.html | 32 ++++++------- .../templates/logger_subject_index.html | 22 ++++----- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/CPAC/resources/templates/cpac_runner.html b/CPAC/resources/templates/cpac_runner.html index 997018cb60..ed51f4ff9a 100755 --- a/CPAC/resources/templates/cpac_runner.html +++ b/CPAC/resources/templates/cpac_runner.html @@ -2,13 +2,13 @@ - Log File for {{ subject_id }} + Log File for ( subject_id ) - + - + @@ -76,7 +76,7 @@

Subjects


- +

© CPAC 2013

@@ -86,9 +86,9 @@

Subjects

================================================== --> - - - + + + - {% endfor %} - {% endfor %} + (% for subject_id in subject_ids %) + (% for scan_id in scan_ids[subject_id] %) + + (% endfor %) + (% endfor %) - - + + + From ce20be846939c027ceec1fe74a0dec25314b46d8 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 23 Feb 2018 19:20:32 -0500 Subject: [PATCH 05/75] Introducing scan/series-level nesting in the data config (in progress, big changes), for func- scans, scan parameter files, and also field map files. Processing of different acq- and run- tags for BIDS datasets included. --- CPAC/pipeline/cpac_pipeline.py | 16 - .../configs/data_config_TEMPLATE.yml | 38 +- CPAC/utils/build_data_config.py | 651 ++++++++++++------ CPAC/utils/datasource.py | 162 +++-- 4 files changed, 550 insertions(+), 317 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index c86048dae4..6b3455fa5c 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -1082,24 +1082,8 @@ def getNodeList(strategy): except KeyError: func_paths_dict = sub_dict['rest'] - # for now, pull in field maps if they exist - fmap_phasediff = None - fmap_mag = None - if 'fmap' in sub_dict.keys(): - - try: - fmap_phasediff = sub_dict['fmap']['phase_diff'] - fmap_mag = sub_dict['fmap']['magnitude'] - except KeyError as e: - err = "[!] Some of the required field map files are " \ - "missing from your data configuration.\n\nDetails: " \ - "{0}\n\n".format(e) - raise Exception(err) - try: funcFlow = create_func_datasource(func_paths_dict, - fmap_phasediff, - fmap_mag, 'func_gather_%d' % num_strat) funcFlow.inputs.inputnode.subject = subject_id funcFlow.inputs.inputnode.creds_path = input_creds_path diff --git a/CPAC/resources/configs/data_config_TEMPLATE.yml b/CPAC/resources/configs/data_config_TEMPLATE.yml index 0eb390cf00..44a5027c2b 100644 --- a/CPAC/resources/configs/data_config_TEMPLATE.yml +++ b/CPAC/resources/configs/data_config_TEMPLATE.yml @@ -10,21 +10,23 @@ - anat: /path/to/site01/sub01/ses01/anatomical.nii.gz creds_path: null func: - scan_1: /path/to/site01/sub01/ses01/scan_1_func.nii.gz - scan_parameters: - acquisition: seq+z - firsttr (start volume index): '' - lasttr (final volume index): '' - reference: 27 - tr: 3.0 + scan_1: + scan: /path/to/site01/sub01/ses01/scan_1_func.nii.gz + scan_parameters: + acquisition: seq+z + firsttr (start volume index): '' + lasttr (final volume index): '' + reference: 27 + tr: 3.0 site: site01 subject_id: sub01 unique_id: ses01 - anat: /path/to/site01/sub02/ses02/anatomical.nii.gz creds_path: null func: - scan_1: /path/to/site01/sub02/ses02/scan_1_func.nii.gz - scan_parameters: None + scan_1: + scan: /path/to/site01/sub02/ses02/scan_1_func.nii.gz + scan_parameters: None site: site01 subject_id: sub02 unique_id: ses02 @@ -34,8 +36,22 @@ - anat: s3://s3_bucket/path/to/site_A/sub200/anatomical.nii.gz creds_path: null (or) /path/to/AWS_credentials.csv func: - scan_name_REST: s3://s3_bucket/path/to/site_A/sub200/scan_name_REST_func.nii.gz - scan_parameters: s3://s3_bucket/path/to/site_A/scan_name_REST_func.json + scan_name_REST: + scan: s3://s3_bucket/path/to/site_A/sub200/scan_name_REST_func.nii.gz + scan_parameters: s3://s3_bucket/path/to/site_A/scan_name_REST_func.json site: site_A subject_id: sub200 unique_id: ses-1 + + +# with field map files for distortion correction +- anat: /path/to/site01/sub01/ses01/anatomical.nii.gz + creds_path: null + func: + scan_1: + scan: /path/to/site01/sub01/ses01/scan_1_func.nii.gz + fmap_phase: /path/to/site01/sub01/ses01/scan_1_phase-diff.nii.gz + fmap_mag: /path/to/site01/sub01/ses01/scan_1_magnitude.nii.gz + site: site01 + subject_id: sub01 + unique_id: ses01 \ No newline at end of file diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index f0ced85a2d..6af4e9c648 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -29,6 +29,123 @@ def gather_file_paths(base_directory, verbose=False): return path_list +def pull_s3_sublist(data_folder, creds_path=None, keep_prefix=True): + + import os + from indi_aws import fetch_creds + + if creds_path: + creds_path = os.path.abspath(creds_path) + + s3_path = data_folder.split("s3://")[1] + bucket_name = s3_path.split("/")[0] + bucket_prefix = s3_path.split(bucket_name + "/")[1] + + print "Pulling from {0} ...".format(data_folder) + + s3_list = [] + bucket = fetch_creds.return_bucket(creds_path, bucket_name) + + # ensure slash at end of bucket_prefix, so that if the final + # directory name is a substring in other directory names, these + # other directories will not be pulled into the file list + if "/" not in bucket_prefix[-1]: + bucket_prefix += "/" + + # Build S3-subjects to download + for bk in bucket.objects.filter(Prefix=bucket_prefix): + if keep_prefix: + fullpath = os.path.join("s3://", bucket_name, str(bk.key)) + s3_list.append(fullpath) + else: + s3_list.append(str(bk.key).replace(bucket_prefix, "")) + + print "Finished pulling from S3. " \ + "{0} file paths found.".format(len(s3_list)) + + if not s3_list: + err = "\n\n[!] No input data found matching your data settings in " \ + "the AWS S3 bucket provided:\n{0}\n\n".format(data_folder) + raise Exception(err) + + return s3_list + + +def get_file_list(base_directory, creds_path=None, write_txt=None, + write_pkl=None, write_info=False): + + import os + + if "s3://" in base_directory: + # AWS S3 bucket + file_list = pull_s3_sublist(base_directory, creds_path) + else: + # local + base_directory = os.path.abspath(base_directory) + file_list = gather_file_paths(base_directory) + + if len(file_list) == 0: + warn = "\n\n[!] No files were found in the base directory you " \ + "provided.\n\nDirectory: {0}\n\n".format(base_directory) + raise Exception(warn) + + if write_txt: + if ".txt" not in write_txt: + write_txt = "{0}.txt".format(write_txt) + write_txt = os.path.abspath(write_txt) + with open(write_txt, "wt") as f: + for path in file_list: + f.write("{0}\n".format(path)) + print "\nFilepath list text file written:\n" \ + "{0}".format(write_txt) + + if write_pkl: + import pickle + if ".pkl" not in write_pkl: + write_pkl = "{0}.pkl".format(write_pkl) + write_pkl = os.path.abspath(write_pkl) + with open(write_pkl, "wt") as f: + pickle.dump(file_list, f) + print "\nFilepath list pickle file written:\n" \ + "{0}".format(write_pkl) + + if write_info: + niftis = [] + jsons = [] + scan_jsons = [] + csvs = [] + tsvs = [] + part_tsvs = [] + for path in file_list: + if ".nii" in path: + niftis.append(path) + elif ".json" in path: + jsons.append(path) + if "bold.json" in path: + scan_jsons.append(path) + elif ".csv" in path: + csvs.append(path) + elif ".tsv" in path: + tsvs.append(path) + if "participants.tsv" in path: + part_tsvs.append(path) + + print "\nBase directory: {0}".format(base_directory) + print "File paths found: {0}".format(len(file_list)) + print "..NIFTI files: {0}".format(len(niftis)) + print "..JSON files: {0}".format(len(jsons)) + if jsons: + print "....{0} of which are scan parameter JSON files" \ + "".format(len(scan_jsons)) + print "..CSV files: {0}".format(len(csvs)) + print "..TSV files: {0}".format(len(tsvs)) + if tsvs: + print "....{0} of which are participants.tsv files" \ + "".format(len(part_tsvs)) + + return file_list + + def download_single_s3_path(s3_path, download_dir=None, creds_path=None, overwrite=False): """Download a single file from an AWS s3 bucket. @@ -79,48 +196,6 @@ def download_single_s3_path(s3_path, download_dir=None, creds_path=None, return local_dl -def pull_s3_sublist(data_folder, creds_path=None, keep_prefix=True): - - import os - from indi_aws import fetch_creds - - if creds_path: - creds_path = os.path.abspath(creds_path) - - s3_path = data_folder.split("s3://")[1] - bucket_name = s3_path.split("/")[0] - bucket_prefix = s3_path.split(bucket_name + "/")[1] - - print "Pulling from {0} ...".format(data_folder) - - s3_list = [] - bucket = fetch_creds.return_bucket(creds_path, bucket_name) - - # ensure slash at end of bucket_prefix, so that if the final - # directory name is a substring in other directory names, these - # other directories will not be pulled into the file list - if "/" not in bucket_prefix[-1]: - bucket_prefix += "/" - - # Build S3-subjects to download - for bk in bucket.objects.filter(Prefix=bucket_prefix): - if keep_prefix: - fullpath = os.path.join("s3://", bucket_name, str(bk.key)) - s3_list.append(fullpath) - else: - s3_list.append(str(bk.key).replace(bucket_prefix, "")) - - print "Finished pulling from S3. " \ - "{0} file paths found.".format(len(s3_list)) - - if not s3_list: - err = "\n\n[!] No input data found matching your data settings in " \ - "the AWS S3 bucket provided:\n{0}\n\n".format(data_folder) - raise Exception(err) - - return s3_list - - def extract_scan_params_csv(scan_params_csv): """ Function to extract the site-based scan parameters from a csv file @@ -286,6 +361,7 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, exclusion_dct=None, config_dir=None): import os + import re import glob if not config_dir: @@ -330,23 +406,38 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, part_tsv_glob = os.path.join(bids_base_dir, "*participants.tsv") + ''' site_jsons_glob = os.path.join(bids_base_dir, "*bold.json") sub_jsons_glob = os.path.join(bids_base_dir, "*sub-*/*bold.json") - ses_jsons_glob = os.path.join(bids_base_dir, "*sub-*/ses-*/*bold.json") + ses_jsons_glob = os.path.join(bids_base_dir, "*ses-*bold.json") + ses_scan_jsons_glob = os.path.join(bids_base_dir, "*ses-*task-*bold.json") + scan_jsons_glob = os.path.join(bids_base_dir, "*task-*bold.json") + ''' site_dir_glob = os.path.join(bids_base_dir, "*", "sub-*/*/*.nii*") + json_globs = [os.path.join(bids_base_dir, "sub-*/ses-*/func/*bold.json"), + os.path.join(bids_base_dir, "sub-*/ses-*/*bold.json"), + os.path.join(bids_base_dir, "sub-*/func/*bold.json"), + os.path.join(bids_base_dir, "sub-*/*bold.json"), + os.path.join(bids_base_dir, "*bold.json")] + + site_json_globs = [os.path.join(bids_base_dir, "*/sub-*/ses-*/func/*bold.json"), + os.path.join(bids_base_dir, "*/sub-*/ses-*/*bold.json"), + os.path.join(bids_base_dir, "*/sub-*/func/*bold.json"), + os.path.join(bids_base_dir, "*/sub-*/*bold.json"), + os.path.join(bids_base_dir, "*/*bold.json")] + ses = False site_dir = False part_tsv = None + site_jsons = [] + jsons = [] + if file_list: import fnmatch - site_jsons = [] - sub_jsons = [] - ses_jsons = [] - for filepath in file_list: if fnmatch.fnmatch(filepath, site_dir_glob) and \ @@ -363,12 +454,14 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, # check if there is a participants.tsv file part_tsv = filepath - if fnmatch.fnmatch(filepath, ses_jsons_glob): - ses_jsons.append(filepath) - if fnmatch.fnmatch(filepath, sub_jsons_glob): - sub_jsons.append(filepath) - if fnmatch.fnmatch(filepath, site_jsons_glob): - site_jsons.append(filepath) + for glob_str in site_json_globs: + site_dir = True + if fnmatch.fnmatch(filepath, glob_str): + site_jsons.append(filepath) + + for glob_str in json_globs: + if fnmatch.fnmatch(filepath, glob_str): + jsons.append(filepath) else: if len(glob.glob(site_dir_glob)) > 0: @@ -384,9 +477,12 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, # check if there is a participants.tsv file part_tsv = glob.glob(part_tsv_glob) - site_jsons = glob.glob(site_jsons_glob) - sub_jsons = glob.glob(sub_jsons_glob) - ses_jsons = glob.glob(ses_jsons_glob) + + for glob_str in site_json_globs: + site_jsons = site_jsons + glob.glob(glob_str) + + for glob_str in json_globs: + jsons = jsons + glob.glob(glob_str) sites_dct = {} sites_subs_dct = {} @@ -467,75 +563,124 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, scan_params_dct = {} - if len(ses_jsons) > 0: - for json_file in ses_jsons: - ids = os.path.basename(json_file).rstrip(".json").split("_") + if site_jsons: + # if there is a site-level directory in the BIDS dataset (not BIDS + # compliant) + # example: /bids_dir/site01/sub-01/func/sub-01_task-rest_bold.json + # instead of /bids_dir/sub-01/func/sub-01_task-rest_bold.json + for json_file in site_jsons: + + # get site ID + site_id = json_file.replace("{0}/".format(bids_base_dir), "").split("/")[0] + if "site-" in site_id: + site_id = site_id.replace("site-", "") + + # get other IDs, from the BIDS format tags, such as "sub-01" or + # "task-rest". also handle things like "task-rest_run-1" + ids = re.split("/|_", json_file) + + sub_id = "All" + ses_id = "All" + scan_id = "All" + run_id = None + acq_id = None + for id in ids: if "sub-" in id: sub_id = id.replace("sub-", "") if "ses-" in id: ses_id = id.replace("ses-", "") - - site_id = "All" - if sites_subs_dct: - if sub_id in sites_subs_dct.keys(): - site_id = sites_subs_dct[sub_id] + if "task-" in id: + scan_id = id.replace("task-", "") + if "run-" in id: + run_id = id.replace("run-", "") + if "acq-" in id: + acq_id = id.replace("acq-", "") + + if run_id or acq_id: + json_filename = os.path.basename(json_file) + if "task-" in json_filename: + # if we have a scan ID already, get the "full" scan ID + # including any run- and acq- tags, since the rest of + # CPAC encodes all of the task-, acq-, and run- tags + # together for each series/scan + scan_id = json_filename.split("task-")[1].split("_bold")[0] + elif "_" in json_filename: + # if this is an all-scan JSON file, but specific only to + # the run or acquisition: create a tag that reads + # {All}_run-1, for example, to be interpreted later when + # matching scan params JSONs to each func scan + scan_id = "[All]" + for id in json_filename.split("_"): + if "run-" in id or "acq-" in id: + scan_id = "{0}_{1}".format(scan_id, id) if site_id not in scan_params_dct.keys(): scan_params_dct[site_id] = {} if sub_id not in scan_params_dct[site_id].keys(): scan_params_dct[site_id][sub_id] = {} + if ses_id not in scan_params_dct[site_id][sub_id].keys(): + scan_params_dct[site_id][sub_id][ses_id] = {} + + scan_params_dct[site_id][sub_id][ses_id][scan_id] = json_file - scan_params_dct[site_id][sub_id][ses_id] = json_file + # scan_id can now be something like {All}_run-2, change other code + + elif jsons: + for json_file in jsons: + + # get other IDs, from the BIDS format tags, such as "sub-01" or + # "task-rest". also handle things like "task-rest_run-1" + ids = re.split("/|_", json_file) + + site_id = "All" + sub_id = "All" + ses_id = "All" + scan_id = "All" + run_id = None + acq_id = None - if len(sub_jsons) > 0: - for json_file in sub_jsons: - ids = os.path.basename(json_file).rstrip(".json").split("_") for id in ids: if "sub-" in id: sub_id = id.replace("sub-", "") - - site_id = "All" - if sites_subs_dct: - if sub_id in sites_subs_dct.keys(): - site_id = sites_subs_dct[sub_id] + if "ses-" in id: + ses_id = id.replace("ses-", "") + if "task-" in id: + scan_id = id.replace("task-", "") + if "run-" in id: + run_id = id.replace("run-", "") + if "acq-" in id: + acq_id = id.replace("acq-", "") + + if run_id or acq_id: + json_filename = os.path.basename(json_file) + if "task-" in json_filename: + # if we have a scan ID already, get the "full" scan ID + # including any run- and acq- tags, since the rest of + # CPAC encodes all of the task-, acq-, and run- tags + # together for each series/scan + scan_id = json_filename.split("task-")[1].split("_bold")[ + 0] + elif "_" in json_filename: + # if this is an all-scan JSON file, but specific only to + # the run or acquisition: create a tag that reads + # {All}_run-1, for example, to be interpreted later when + # matching scan params JSONs to each func scan + scan_id = "[All]" + for id in json_filename.split("_"): + if "run-" in id or "acq-" in id: + scan_id = "{0}_{1}".format(scan_id, id) if site_id not in scan_params_dct.keys(): scan_params_dct[site_id] = {} if sub_id not in scan_params_dct[site_id].keys(): scan_params_dct[site_id][sub_id] = {} + if ses_id not in scan_params_dct[site_id][sub_id].keys(): + scan_params_dct[site_id][sub_id][ses_id] = {} - scan_params_dct[site_id][sub_id]["All"] = json_file - - if len(site_jsons) > 0: - if len(site_jsons) > 1: - if not site_dir: - print "\nMore than one dataset-level JSON file. These may " \ - "be scan-specific JSON files. Scan-specific scan " \ - "parameters will be implemented soon. Files detected:" - for json in site_jsons: - print "...{0}".format(json) - print "\n" - else: - # if this kicks, then there is a site directory level, and the - # site-specific JSON is sitting in it alongside the sub-* - # participant ID folders - for json in site_jsons: - json_levels = json.split("/") - site_name = json_levels[-2] - if site_name not in scan_params_dct.keys(): - scan_params_dct[site_name] = {} - if "All" not in scan_params_dct[site_name].keys(): - scan_params_dct[site_name]["All"] = {} - scan_params_dct[site_name]["All"]["All"] = json - else: - if "All" not in scan_params_dct.keys(): - scan_params_dct["All"] = {} - if "All" not in scan_params_dct["All"].keys(): - scan_params_dct["All"]["All"] = {} + scan_params_dct[site_id][sub_id][ses_id][scan_id] = json_file - for json_file in site_jsons: - scan_params_dct["All"]["All"]["All"] = json_file + # scan_id can now be something like {All}_run-2, change other code if ses: # if there is a session level in the BIDS dataset @@ -606,9 +751,11 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, func_glob = func_glob.replace('{series}', '{scan}') if '{scan}' in anat_glob: - err = "\n[!] CPAC does not support multiple anatomical scans at " \ - "this time. You are seeing this message because you have a " \ - "{scan} or {series} keyword in your anatomical path template.\n" + err = "\n[!] CPAC does not support multiple anatomical scans. You " \ + "are seeing this message because you have a {scan} or " \ + "{series} keyword in your anatomical path template.\n\nSee " \ + "the help details for the 'Which Anatomical Scan?' " \ + "(anatomical_scan) setting for more information.\n\n" raise Exception(err) for keyword in keywords: @@ -695,17 +842,6 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, if label == "*": continue - # example: /home/{site}/*/ses-{session}/.. - # /home/NYU/folder1/ses-1/.. - # part1 = /home/, part2 = /*/ses- - # first split -> ['', 'NYU/folder1/ses-1/..'] - # (pick second item) - # what it would be -> ['NYU/folder1/ses-1/..'] because /*/ses-1 doesn't exist in there - # what we want -> ['NYU', '1/..'] - # (pick first item) - # so, '/*/ses-', we want to transform into '/folder1/ses-' - # TODO: BASICALLY we want to convert the TEMPLATE so that its actual * will be filled with the current real path's real substring! - id = new_path.split(part1, 1)[1] id = id.split(part2, 1)[0] @@ -715,21 +851,19 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, else: warn = None if path_dct[label] != id: - if str(path_dct[label]) in id and "run-" in id: - # TODO: this is here only because we do not support - # TODO: multiple anat scans yet!! - if "run-1" in id: - warn = None - pass - else: - warn = "\n\n[!] WARNING: Multiple anatomical " \ - "scans found for a single participant. " \ - "Multiple anatomical scans are not yet " \ - "supported. Review the completed data " \ - "configuration file to ensure the " \ - "proper scans are included together.\n\n" \ - "Scan not included:\n{0}" \ - "\n\n".format(anat_path) + if str(path_dct[label]) in id: + warn = "\n\n[!] WARNING: Multiple anatomical " \ + "scans found for a single participant. " \ + "One anatomical scan must be selected " \ + "for the pipeline run. You can use the " \ + "'Which Anatomical Scan?' (anatomical_" \ + "scan) setting in the data config" \ + "uration builder to select which one to " \ + "use. Otherwise, review the completed " \ + "data configuration file to ensure the " \ + "proper scans are included together.\n\n" \ + "Scan not included:\n{0}" \ + "\n\n".format(anat_path) else: warn = "\n\n[!] WARNING: While parsing your input " \ "data files, a file path was found with " \ @@ -949,56 +1083,154 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, if scan_id in exclusion_dct['scans']: continue - temp_func_dct = {scan_id: func_path} + temp_func_dct = {scan_id: {"scan": func_path}} # scan parameters time scan_params = None if scan_params_dct: - # TODO: implement scan-specific level once scan-level nesting is - # TODO: available - if site_id in scan_params_dct.keys(): + # all site-specific if sub_id in scan_params_dct[site_id].keys(): + # all site-specific and sub-specific if ses_id in scan_params_dct[site_id][sub_id].keys(): - # site, sub, session specific scan params - scan_params = scan_params_dct[site_id][sub_id][ses_id] + if scan_id in scan_params_dct[site_id][sub_id][ses_id].keys(): + # site, sub, session, scan specific scan params + scan_params = scan_params_dct[site_id][sub_id][ses_id][scan_id] + elif "run-" in scan_id or "acq-" in scan_id: + # not scan specific, but run and/or acquisition + # specific + run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) + if run_acq_id in scan_params_dct[site_id][sub_id][ses_id].keys(): + scan_params = scan_params_dct[site_id][sub_id][ses_id][run_acq_id] + elif 'All' in scan_params_dct[site_id][sub_id][ses_id].keys(): + # site, sub, session specific scan params + scan_params = scan_params_dct[site_id][sub_id][ses_id]['All'] elif 'All' in scan_params_dct[site_id][sub_id].keys(): - # site and sub specific scan params, same across all - # sessions - scan_params = scan_params_dct[site_id][sub_id]['All'] + if scan_id in scan_params_dct[site_id][sub_id]['All'].keys(): + # site, sub, scan specific scan params + scan_params = scan_params_dct[site_id][sub_id]['All'][scan_id] + elif "run-" in scan_id or "acq-" in scan_id: + # not scan specific, but run and/or acquisition + # specific + run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) + if run_acq_id in scan_params_dct[site_id][sub_id]['All'].keys(): + scan_params = scan_params_dct[site_id][sub_id]['All'][run_acq_id] + elif 'All' in scan_params_dct[site_id][sub_id]['All'].keys(): + # site and sub specific scan params, same across + # all sessions and scans + scan_params = scan_params_dct[site_id][sub_id]['All']['All'] elif "All" in scan_params_dct[site_id].keys(): + # same across all subs if ses_id in scan_params_dct[site_id]["All"].keys(): - # site and session specific scan params, same - # across all subs - scan_params = scan_params_dct[site_id]["All"][ses_id] + if scan_id in scan_params_dct[site_id]["All"][ses_id].keys(): + # site, session, scan specific, same across all + # subs + scan_params = scan_params_dct[site_id]["All"][ses_id][scan_id] + elif "run-" in scan_id or "acq-" in scan_id: + # not scan specific, but run and/or acquisition + # specific + run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) + if run_acq_id in scan_params_dct[site_id]["All"][ses_id].keys(): + scan_params = scan_params_dct[site_id]["All"][ses_id][run_acq_id] + elif "All" in scan_params_dct[site_id]["All"][ses_id].keys(): + # site and session specific scan params, same + # across all subs and scans + scan_params = scan_params_dct[site_id]["All"][ses_id]["All"] elif "All" in scan_params_dct[site_id]["All"].keys(): - # site specific scan params, same across all subs - # and sessions - scan_params = scan_params_dct[site_id]["All"]["All"] + # TODO: but what about task-rest_acq-1 but for all runs??? + # TODO: scan_id would be rest_acq-1_run-1, but scan_params_dct tag would be rest_acq-1 + if scan_id in scan_params_dct[site_id]["All"]["All"].keys(): + # site and scan specific, same across all subs and + # all sessions + scan_params = scan_params_dct[site_id]["All"]["All"][scan_id] + elif "run-" in scan_id and "acq-" in scan_id: + # not scan specific, but run and acquisition + # specific + run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) + if run_acq_id in scan_params_dct[site_id]["All"]["All"].keys(): + scan_params = scan_params_dct[site_id]["All"]["All"][run_acq_id] + elif "acq-" in scan_id: + # not scan or run specific, but acquisition + # specific + acq_id = "[All]_{0}".format(scan_id.split("acq-")[1].split("_")[0]) + if site_id == "NKI_TRT": + print acq_id + print scan_params_dct[site_id]["All"]["All"].keys() + if acq_id in scan_params_dct[site_id]["All"]["All"].keys(): + scan_params = scan_params_dct[site_id]["All"]["All"][acq_id] + elif "All" in scan_params_dct[site_id]["All"]["All"].keys(): + # site specific scan params, same across all subs, + # sessions, and scans + scan_params = scan_params_dct[site_id]["All"]["All"]["All"] elif "All" in scan_params_dct.keys(): + # not site specific if sub_id in scan_params_dct["All"].keys(): + # sub-specific if ses_id in scan_params_dct["All"][sub_id].keys(): - # sub and session specific scan params, same across - # all sites - scan_params = scan_params_dct["All"][sub_id][ses_id] + if scan_id in scan_params_dct["All"][sub_id][ses_id].keys(): + # sub, session, scan specific, same across all + # sites (or no site specified) + scan_params = scan_params_dct["All"][sub_id][ses_id][scan_id] + elif "run-" in scan_id or "acq-" in scan_id: + # not scan specific, but run and/or acquisition + # specific + run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) + if run_acq_id in scan_params_dct["All"][sub_id][ses_id].keys(): + scan_params = scan_params_dct["All"][sub_id][ses_id][run_acq_id] + elif "All" in scan_params_dct["All"][sub_id][ses_id].keys(): + # sub, session specific, same across all scans and + # sites (or site and scan not specified) + scan_params = scan_params_dct["All"][sub_id][ses_id][scan_id]["All"] elif "All" in scan_params_dct["All"][sub_id].keys(): - # sub specific scan params, same across all sites and - # sessions - scan_params = scan_params_dct["All"][sub_id]["All"] + if scan_id in scan_params_dct["All"][sub_id]["All"].keys(): + # sub and scan specific, same across all sites and + # sessions (or site and session not specified) + scan_params = scan_params_dct["All"][sub_id]["All"][scan_id] + elif "run-" in scan_id or "acq-" in scan_id: + # not scan specific, but run and/or acquisition + # specific + run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) + if run_acq_id in scan_params_dct["All"][sub_id]["All"].keys(): + scan_params = scan_params_dct["All"][sub_id]["All"][run_acq_id] + elif "All" in scan_params_dct["All"][sub_id]["All"].keys(): + # sub specific, same across all sites, sessions, + # and scans (or site, session, and scan not + # specified) + scan_params = scan_params_dct["All"][sub_id]["All"]["All"] elif "All" in scan_params_dct["All"].keys(): if ses_id in scan_params_dct["All"]["All"].keys(): - # session-specific scan params, same across all sites - # and subs - scan_params = scan_params_dct["All"]["All"][ses_id] + if scan_id in scan_params_dct["All"]["All"][ses_id].keys(): + # session and scan specific only + scan_params = scan_params_dct["All"]["All"][ses_id][scan_id] + elif "run-" in scan_id or "acq-" in scan_id: + # not scan specific, but run and/or acquisition + # specific + run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) + if run_acq_id in scan_params_dct["All"]["All"][ses_id].keys(): + scan_params = scan_params_dct["All"]["All"][ses_id][run_acq_id] + elif "All" in scan_params_dct["All"]["All"][ses_id].keys(): + # session specific only + scan_params = scan_params_dct["All"]["All"][ses_id]["All"] elif "All" in scan_params_dct["All"]["All"].keys(): - # same scan params across all sites and sessions - scan_params = scan_params_dct["All"]["All"]["All"] + if scan_id in scan_params_dct["All"]["All"]["All"].keys(): + # scan specific only + scan_params = scan_params_dct["All"]["All"]["All"][scan_id] + elif "run-" in scan_id or "acq-" in scan_id: + # not scan specific, but run and/or acquisition + # specific + run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) + if run_acq_id in scan_params_dct["All"]["All"]["All"].keys(): + scan_params = scan_params_dct["All"]["All"]["All"][run_acq_id] + elif "All" in scan_params_dct["All"]["All"]["All"].keys(): + # same scan params across all sites, subs, + # sessions, and scans + scan_params = scan_params_dct["All"]["All"]["All"]["All"] if scan_params: - temp_func_dct.update({'scan_parameters': scan_params}) + temp_func_dct[scan_id].update({'scan_parameters': scan_params}) if site_id not in data_dct.keys(): if verbose: @@ -1104,6 +1336,7 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, else: warn = None if path_dct[label] != id: + ''' if str(path_dct[label]) in id and "run-" in id: # TODO: this is here only because we do not # TODO: support scan-level nesting yet!!! @@ -1117,15 +1350,16 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, "File not included:\n{0}" \ "\n\n".format(fmap_phase) else: - warn = "\n\n[!] WARNING: While parsing your input data " \ - "files, a file path was found with conflicting " \ - "IDs for the same data level.\n\n" \ - "File path: {0}\n" \ - "Level: {1}\n" \ - "Conflicting IDs: {2}, {3}\n\n" \ - "This file has not been added to the data " \ - "configuration.".format(fmap_phase, label, - path_dct[label], id) + ''' + warn = "\n\n[!] WARNING: While parsing your input data " \ + "files, a file path was found with conflicting " \ + "IDs for the same data level.\n\n" \ + "File path: {0}\n" \ + "Level: {1}\n" \ + "Conflicting IDs: {2}, {3}\n\n" \ + "This file has not been added to the data " \ + "configuration.".format(fmap_phase, label, + path_dct[label], id) if warn: print warn skip = True @@ -1166,7 +1400,7 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, else: scan_id = None - temp_fmap_dct = {"phase_diff": fmap_phase} + temp_fmap_dct = {"fmap_phase": fmap_phase} if site_id not in data_dct.keys(): if verbose: @@ -1186,17 +1420,10 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, continue if scan_id: - # if the field map files are at scan level - if 'fmap' not in data_dct[site_id][sub_id][ses_id][scan_id].keys(): - data_dct[site_id][sub_id][ses_id][scan_id]['fmap'] = temp_fmap_dct - else: - data_dct[site_id][sub_id][ses_id][scan_id]['fmap'].update(temp_fmap_dct) + data_dct[site_id][sub_id][ses_id]['func'][scan_id].update(temp_fmap_dct) else: - # if the field map fields are at session level - if 'fmap' not in data_dct[site_id][sub_id][ses_id].keys(): - data_dct[site_id][sub_id][ses_id]['fmap'] = temp_fmap_dct - else: - data_dct[site_id][sub_id][ses_id]['fmap'].update(temp_fmap_dct) + for scan in data_dct[site_id][sub_id][ses_id]['func'].keys(): + data_dct[site_id][sub_id][ses_id]['func'][scan].update(temp_fmap_dct) for fmap_mag in fmap_mag_pool: @@ -1244,6 +1471,7 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, else: warn = None if path_dct[label] != id: + ''' if str(path_dct[label]) in id and "run-" in id: # TODO: this is here only because we do not # TODO: support scan-level nesting yet!!! @@ -1257,15 +1485,16 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, "File not included:\n{0}" \ "\n\n".format(fmap_phase) else: - warn = "\n\n[!] WARNING: While parsing your input data " \ - "files, a file path was found with conflicting " \ - "IDs for the same data level.\n\n" \ - "File path: {0}\n" \ - "Level: {1}\n" \ - "Conflicting IDs: {2}, {3}\n\n" \ - "This file has not been added to the data " \ - "configuration.".format(fmap_mag, label, - path_dct[label], id) + ''' + warn = "\n\n[!] WARNING: While parsing your input data " \ + "files, a file path was found with conflicting " \ + "IDs for the same data level.\n\n" \ + "File path: {0}\n" \ + "Level: {1}\n" \ + "Conflicting IDs: {2}, {3}\n\n" \ + "This file has not been added to the data " \ + "configuration.".format(fmap_mag, label, + path_dct[label], id) if warn: print warn skip = True @@ -1306,7 +1535,7 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, else: scan_id = None - temp_fmap_dct = {"magnitude": fmap_mag} + temp_fmap_dct = {"fmap_mag": fmap_mag} if site_id not in data_dct.keys(): if verbose: @@ -1325,29 +1554,11 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, "session:\n{0}\n{1}\n".format(fmap_mag, ses_id) continue - # TODO: reintroduce once scan-level nesting is implemented - ''' if scan_id: - # if the field map files are at scan level - if 'fmap' not in data_dct[site_id][sub_id][ses_id][scan_id].keys(): - data_dct[site_id][sub_id][ses_id]['func'][scan_id]['fmap'] = temp_fmap_dct - else: - data_dct[site_id][sub_id][ses_id]['func'][scan_id]['fmap'].update(temp_fmap_dct) - - else: - # if the field map fields are at session level - if 'fmap' not in data_dct[site_id][sub_id][ses_id].keys(): - data_dct[site_id][sub_id][ses_id]['fmap'] = temp_fmap_dct - else: - data_dct[site_id][sub_id][ses_id]['fmap'].update(temp_fmap_dct) - ''' - - # if the field map fields are at session level - if 'fmap' not in data_dct[site_id][sub_id][ses_id].keys(): - data_dct[site_id][sub_id][ses_id]['fmap'] = temp_fmap_dct + data_dct[site_id][sub_id][ses_id]['func'][scan_id].update(temp_fmap_dct) else: - data_dct[site_id][sub_id][ses_id]['fmap'].update( - temp_fmap_dct) + for scan in data_dct[site_id][sub_id][ses_id]['func'].keys(): + data_dct[site_id][sub_id][ses_id]['func'][scan].update(temp_fmap_dct) return data_dct @@ -1366,6 +1577,10 @@ def run(data_settings_yml): if "None" in settings_dct["awsCredentialsFile"]: settings_dct["awsCredentialsFile"] = None + if "None" in settings_dct["anatomical_scan"] or \ + "none" in settings_dct["anatomical_scan"]: + settings_dct["anatomical_scan"] = None + incl_dct = format_incl_excl_dct(settings_dct['siteList'], settings_dct['subjectList'], settings_dct['sessionList'], @@ -1379,17 +1594,8 @@ def run(data_settings_yml): if 'BIDS' in settings_dct['dataFormat'] or \ 'bids' in settings_dct['dataFormat']: - if "s3://" in settings_dct['bidsBaseDir']: - # hosted on AWS S3 bucket - file_list = pull_s3_sublist(settings_dct['bidsBaseDir'], - settings_dct['awsCredentialsFile']) - else: - # local (not on AWS S3 bucket), BIDS files - if not os.path.isdir(settings_dct['bidsBaseDir']): - err = "\n[!] The BIDS base directory you provided does not " \ - "exist:\n{0}\n\n".format(settings_dct['bidsBaseDir']) - raise Exception(err) - file_list = None + file_list = get_file_list(settings_dct["bidsBaseDir"], + creds_path=settings_dct["awsCredentialsFile"]) data_dct = get_BIDS_data_dct(settings_dct['bidsBaseDir'], file_list=file_list, @@ -1463,8 +1669,7 @@ def run(data_settings_yml): for session in data_dct[site][sub].keys(): if 'func' in data_dct[site][sub][session].keys(): for scan in data_dct[site][sub][session]['func'].keys(): - if scan != "scan_parameters": - num_scan += 1 + num_scan += 1 data_config_outfile = \ os.path.join(settings_dct['outputSubjectListLocation'], diff --git a/CPAC/utils/datasource.py b/CPAC/utils/datasource.py index 6387841c90..bf0295a11a 100644 --- a/CPAC/utils/datasource.py +++ b/CPAC/utils/datasource.py @@ -3,15 +3,15 @@ def get_rest(scan, rest_dict): - return rest_dict[scan] + # return the time-series NIFTI file of the chosen series/scan + return rest_dict[scan]["scan"] def extract_scan_params_dct(scan_params_dct): return scan_params_dct -def create_func_datasource(rest_dict, fmap_phase=None, fmap_mag=None, - wf_name='func_datasource'): +def create_func_datasource(rest_dict, wf_name='func_datasource'): import nipype.pipeline.engine as pe import nipype.interfaces.utility as util @@ -29,52 +29,104 @@ def create_func_datasource(rest_dict, fmap_phase=None, fmap_mag=None, 'magnitude']), name='outputspec') - # if the input data is taken from a BIDS directory scan_names = rest_dict.keys() + inputnode.iterables = [('scan', scan_names)] - # TODO: this will need to be changed once scan-level nesting is - # TODO: implemented- the file name (for BIDS scan params JSON files) will - # TODO: have to be processed for the particular scan (task, and run) and - # TODO: then linked with the scan below (check the iterable inputnode - # TODO: parameter) - if "scan_parameters" in scan_names: - scan_names.remove("scan_parameters") - - if isinstance(rest_dict["scan_parameters"], str): - if "s3://" in rest_dict["scan_parameters"]: - # if the scan parameters file is on AWS S3, download it - s3_scan_params = \ - pe.Node(util.Function(input_names=['file_path', - 'creds_path', - 'img_type'], - output_names=['local_path'], - function=check_for_s3), - name='s3_scan_params') - - s3_scan_params.inputs.file_path = rest_dict["scan_parameters"] + for scan in scan_names: - wf.connect(inputnode, 'creds_path', - s3_scan_params, 'creds_path') - wf.connect(s3_scan_params, 'local_path', - outputnode, 'scan_params') + scan_resources = rest_dict[scan] - elif isinstance(rest_dict["scan_parameters"], dict): - get_scan_params_dct = \ - pe.Node(util.Function(input_names=['scan_params_dct'], - output_names=['scan_params_dct'], - function=extract_scan_params_dct), - name='get_scan_params_dct') - get_scan_params_dct.inputs.scan_params_dct = \ - rest_dict["scan_parameters"] - wf.connect(get_scan_params_dct, 'scan_params_dct', - outputnode, 'scan_params') + # actual 4D time series file + if "scan" not in scan_resources.keys(): + err = "\n\n[!] The {0} scan is missing its actual time-series " \ + "scan file, which should be a filepath labeled with the " \ + "'scan' key.\n\n".format(scan) + raise Exception(err) - for scan in scan_names: - if '.' in scan or '+' in scan or '*' in scan: - raise Exception('\n[!] Scan names cannot contain any special ' - 'characters. Please update this and try again.') + scan_file = scan_resources["scan"] + + if '.' in scan_file or '+' in scan_file or '*' in scan_file: + raise Exception('\n\n[!] Scan names cannot contain any special ' + 'characters (., +, *, etc.). Please update this ' + 'and try again.\n\nScan: {0}' + '\n\n'.format(scan_file)) + + # scan parameters CSV + if "scan_parameters" in scan_resources.keys(): + if isinstance(scan_resources["scan_parameters"], str): + if "s3://" in scan_resources["scan_parameters"]: + # if the scan parameters file is on AWS S3, download it + s3_scan_params = \ + pe.Node(util.Function(input_names=['file_path', + 'creds_path', + 'img_type'], + output_names=['local_path'], + function=check_for_s3), + name='s3_scan_params') + + s3_scan_params.inputs.file_path = \ + scan_resources["scan_parameters"] + + wf.connect(inputnode, 'creds_path', + s3_scan_params, 'creds_path') + wf.connect(s3_scan_params, 'local_path', + outputnode, 'scan_params') + + elif isinstance(scan_resources["scan_parameters"], dict): + get_scan_params_dct = \ + pe.Node(util.Function(input_names=['scan_params_dct'], + output_names=['scan_params_dct'], + function=extract_scan_params_dct), + name='get_scan_params_dct') + get_scan_params_dct.inputs.scan_params_dct = \ + scan_resources["scan_parameters"] + wf.connect(get_scan_params_dct, 'scan_params_dct', + outputnode, 'scan_params') - inputnode.iterables = [('scan', scan_names)] + # field map files (if applicable) + if "fmap_phase" in scan_resources.keys(): + + fmap_phase = scan_resources["fmap_phase"] + + if "fmap_mag" not in scan_resources.keys(): + err = "\n\n[!] The field map phase difference file has " \ + "been listed for scan {0}, but there is no field " \ + "map magnitude file.\n\nPhase difference file " \ + "listed: {1}\n\n".format(scan, fmap_phase) + raise Exception(err) + + s3_fmap_phase = pe.Node(util.Function(input_names=['file_path', + 'creds_path', + 'img_type'], + output_names=['local_path'], + function=check_for_s3), + name='s3_fmap_phase') + s3_fmap_phase.inputs.file_path = fmap_phase + s3_fmap_phase.inputs.img_type = "other" + wf.connect(inputnode, 'creds_path', s3_fmap_phase, 'creds_path') + wf.connect(s3_fmap_phase, 'local_path', outputnode, 'phase_diff') + + if "fmap_mag" in scan_resources.keys(): + + fmap_mag = scan_resources["fmap_mag"] + + if "fmap_phase" not in scan_resources.keys(): + err = "\n\n[!] The field map magnitude file has been" \ + "listed for scan {0}, but there is no field map phase" \ + "difference file.\n\nPhase magnitude file " \ + "listed: {1}\n\n".format(scan, fmap_mag) + raise Exception(err) + + s3_fmap_mag = pe.Node(util.Function(input_names=['file_path', + 'creds_path', + 'img_type'], + output_names=['local_path'], + function=check_for_s3), + name='s3_fmap_mag') + s3_fmap_mag.inputs.file_path = fmap_mag + s3_fmap_mag.inputs.img_type = "other" + wf.connect(inputnode, 'creds_path', s3_fmap_mag, 'creds_path') + wf.connect(s3_fmap_mag, 'local_path', outputnode, 'magnitude') selectrest = pe.Node(util.Function(input_names=['scan', 'rest_dict'], output_names=['rest'], @@ -99,30 +151,6 @@ def create_func_datasource(rest_dict, fmap_phase=None, fmap_mag=None, wf.connect(check_s3_node, 'local_path', outputnode, 'rest') wf.connect(inputnode, 'scan', outputnode, 'scan') - if fmap_phase and fmap_mag: - s3_fmap_phase = pe.Node(util.Function(input_names=['file_path', - 'creds_path', - 'img_type'], - output_names=['local_path'], - function=check_for_s3), - name='s3_fmap_phase') - s3_fmap_phase.inputs.file_path = fmap_phase - s3_fmap_phase.inputs.img_type = "other" - wf.connect(inputnode, 'creds_path', s3_fmap_phase, 'creds_path') - - s3_fmap_mag = pe.Node(util.Function(input_names=['file_path', - 'creds_path', - 'img_type'], - output_names=['local_path'], - function=check_for_s3), - name='s3_fmap_mag') - s3_fmap_mag.inputs.file_path = fmap_mag - s3_fmap_mag.inputs.img_type = "other" - wf.connect(inputnode, 'creds_path', s3_fmap_mag, 'creds_path') - - wf.connect(s3_fmap_phase, 'local_path', outputnode, 'phase_diff') - wf.connect(s3_fmap_mag, 'local_path', outputnode, 'magnitude') - return wf From d872ec45119d2aad4c22eab1f46b63f1c5b276b7 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Tue, 27 Feb 2018 15:03:07 -0500 Subject: [PATCH 06/75] Reintroduced the automatic generation of the group-level analysis sublist text file, removed phenotypic file template generation. --- .../interface/windows/dataconfig_window.py | 6 - CPAC/utils/build_data_config.py | 163 +++++++++++++++++- CPAC/utils/extract_data.py | 5 +- 3 files changed, 159 insertions(+), 15 deletions(-) diff --git a/CPAC/GUI/interface/windows/dataconfig_window.py b/CPAC/GUI/interface/windows/dataconfig_window.py index e6d0de98e5..02d507a346 100644 --- a/CPAC/GUI/interface/windows/dataconfig_window.py +++ b/CPAC/GUI/interface/windows/dataconfig_window.py @@ -354,12 +354,6 @@ def run(self, config): # Build the subject list from the data config CPAC.utils.build_data_config.run(config) - # Generate group analysis files and such - CPAC.utils.extract_data.generate_supplementary_files(sublist_outdir, sublist_name) - - # will be changed at some point - sublist_name = "data_config_{0}.yml".format(sublist_name) - # check GUI's data config list dialog box for duplicate names while True: parent = self.Parent diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index 35ebb6b697..f817df9ada 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -121,6 +121,139 @@ def pull_s3_sublist(data_folder, creds_path=None, keep_prefix=True): return s3_list +def generate_group_analysis_sublist(): + """Create the group-level analysis inclusion list. + """ + + + + + + +def generate_group_analysis_files(data_config_outdir, data_config_name): + """Create the group-level analysis inclusion list. + """ + + import os + from sets import Set + import csv + import yaml + + data_config_path = os.path.join(data_config_outdir, data_config_name) + + try: + with open(data_config_path, 'r') as f: + subjects_list = yaml.load(f) + except: + err = "\n\n[!] Data configuration file couldn't be read!\nFile " \ + "path: {0}\n".format(data_config_path) + + subject_scan_set = Set() + subID_set = Set() + session_set = Set() + subject_set = Set() + scan_set = Set() + data_list = [] + + try: + for sub in subjects_list: + if sub['unique_id']: + subject_id = sub['subject_id'] + "_" + sub['unique_id'] + else: + subject_id = sub['subject_id'] + + try: + for scan in sub['func'].keys(): + subject_scan_set.add((subject_id, scan)) + subID_set.add(sub['subject_id']) + session_set.add(sub['unique_id']) + subject_set.add(subject_id) + scan_set.add(scan) + except KeyError: + try: + for scan in sub['rest'].keys(): + subject_scan_set.add((subject_id, scan)) + subID_set.add(sub['subject_id']) + session_set.add(sub['unique_id']) + subject_set.add(subject_id) + scan_set.add(scan) + except KeyError: + # one of the participants in the subject list has no + # functional scans + subID_set.add(sub['subject_id']) + session_set.add(sub['unique_id']) + subject_set.add(subject_id) + + except TypeError as e: + print 'Subject list could not be populated!' + print 'This is most likely due to a mis-formatting in your ' \ + 'inclusion and/or exclusion subjects txt file or your ' \ + 'anatomical and/or functional path templates.' + print 'Error: %s' % e + err_str = 'Check formatting of your anatomical/functional path ' \ + 'templates and inclusion/exclusion subjects text files' + raise TypeError(err_str) + + for item in subject_scan_set: + list1 = [] + list1.append(item[0] + "/" + item[1]) + for val in subject_set: + if val in item: + list1.append(1) + else: + list1.append(0) + + for val in scan_set: + if val in item: + list1.append(1) + else: + list1.append(0) + + data_list.append(list1) + + # generate the phenotypic file templates for group analysis + file_name = os.path.join(data_config_outdir, 'phenotypic_template_%s.csv' + % data_config_name) + + try: + f = open(file_name, 'wb') + except: + print '\n\nCPAC says: I couldn\'t save this file to your drive:\n' + print file_name, '\n\n' + print 'Make sure you have write access? Then come back. Don\'t ' \ + 'worry.. I\'ll wait.\n\n' + raise IOError + + writer = csv.writer(f) + + writer.writerow(['participant', 'EV1', '..']) + for sub in sorted(subID_set): + writer.writerow([sub, '']) + + f.close() + + print "Template Phenotypic file for group analysis - %s" % file_name + + # generate the group analysis subject lists + file_name = os.path.join(data_config_outdir, + 'participant_list_group_analysis_%s.txt' + % data_config_name) + + try: + with open(file_name, 'w') as f: + for sub in sorted(subID_set): + print >> f, sub + except: + print '\n\nCPAC says: I couldn\'t save this file to your drive:\n' + print file_name, '\n\n' + print 'Make sure you have write access? Then come back. Don\'t ' \ + 'worry.. I\'ll wait.\n\n' + raise IOError + + print "Participant list required later for group analysis - %s\n\n" \ + % file_name + + def extract_scan_params_csv(scan_params_csv): """ Function to extract the site-based scan parameters from a csv file @@ -1399,13 +1532,20 @@ def run(data_settings_yml): "data_config_{0}.yml" "".format(settings_dct['subjectListName'])) + group_list_outfile = \ + os.path.join(settings_dct['outputSubjectListLocation'], + "group_analysis_participants_{0}.txt" + "".format(settings_dct['subjectListName'])) + # put data_dct contents in an ordered list for the YAML dump data_list = [] + group_list = [] for site in sorted(data_dct.keys()): for sub in sorted(data_dct[site].keys()): for ses in sorted(data_dct[site][sub].keys()): data_list.append(data_dct[site][sub][ses]) + group_list.append("{0}_{1}".format(sub, ses)) with open(data_config_outfile, "wt") as f: # Make sure YAML doesn't dump aliases (so it's more human @@ -1419,16 +1559,29 @@ def run(data_settings_yml): f.write(yaml.dump(data_list, default_flow_style=False, Dumper=noalias_dumper)) + with open(group_list_outfile, "wt") as f: + # write the inclusion list (mainly the group analysis sublist) + # text file + for id in sorted(group_list): + f.write("{0}\n".format(id)) + if os.path.exists(data_config_outfile): - print "\nCPAC DATA SETTINGS file entered:" \ - "\n{0}".format(data_settings_yml) - print "\nCPAC DATA CONFIGURATION file created:" \ - "\n{0}\n".format(data_config_outfile) + print "\nCPAC DATA SETTINGS file entered (use this preset file " \ + "to modify/regenerate the data configuration file):" \ + "\n{0}\n".format(data_settings_yml) print "Number of:" print "...sites: {0}".format(num_sites) print "...participants: {0}".format(num_subs) print "...participant-sessions: {0}".format(num_sess) - print "...functional scans: {0}\n".format(num_scan) + print "...functional scans: {0}".format(num_scan) + print "\nCPAC DATA CONFIGURATION file created (use this for " \ + "individual-level analysis):" \ + "\n{0}\n".format(data_config_outfile) + + if os.path.exists(group_list_outfile): + print "Group-level analysis participant-session list text " \ + "file created (use this for group-level analysis):\n{0}" \ + "\n".format(group_list_outfile) else: err = "\n\n[!] No anatomical input files were found given the data " \ diff --git a/CPAC/utils/extract_data.py b/CPAC/utils/extract_data.py index ddb34a1837..181830d7cf 100644 --- a/CPAC/utils/extract_data.py +++ b/CPAC/utils/extract_data.py @@ -5,9 +5,6 @@ import logging import yaml -#logging.basicConfig(filename=os.path.join(os.getcwd(), 'extract_data_logs.log'), filemode='w', level=logging.DEBUG,\ -# format="%(levelname)s %(asctime)s %(lineno)d %(message)s") - def extract_data(c, param_map): """ @@ -568,12 +565,12 @@ def generate_supplementary_files(data_config_outdir, data_config_name): print "Template Phenotypic file for group analysis with repeated " \ "measures (multiple scans) - %s" % file_name + """ # generate the group analysis subject lists file_name = os.path.join(data_config_outdir, 'participant_list_group_analysis_%s.txt' % data_config_name) - """ try: with open(file_name, 'w') as f: From 9d819d03c9ec9275f32284b1077ea7693debfb8d Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Tue, 27 Feb 2018 18:34:34 -0500 Subject: [PATCH 07/75] Started introducing FSL FLAME presets. --- CPAC/utils/build_sublist.py | 1158 --------------------- CPAC/utils/create_fsl_model.py | 55 +- CPAC/utils/create_group_analysis_files.py | 114 ++ setup.py | 11 +- 4 files changed, 123 insertions(+), 1215 deletions(-) delete mode 100644 CPAC/utils/build_sublist.py create mode 100644 CPAC/utils/create_group_analysis_files.py diff --git a/CPAC/utils/build_sublist.py b/CPAC/utils/build_sublist.py deleted file mode 100644 index 1edec1e0a0..0000000000 --- a/CPAC/utils/build_sublist.py +++ /dev/null @@ -1,1158 +0,0 @@ -# utils/build_sublist.py -# - -''' -This module has functions to build a subject list from S3 and -local filepaths -''' - -# Init global variables -site_kw = '{site}' -ppant_kw = '{participant}' -sess_kw = '{session}' -ser_kw = '{series}' -kw_strs = [site_kw, ppant_kw, sess_kw, ser_kw] - - -def read_subj_txtfile(filepath): - with open(filepath,"r") as f: - sub_ids = f.read().splitlines() - sub_ids = filter(None,sub_ids) - return sub_ids - - -# Check for glob-style patterns -def check_for_glob_patterns(delim, filepath, suffix_flag=False): - ''' - Function to check the delimeter for any glob patterns (?, []); - if some are found, they are replaced by the filepath's characters - in those locations - this allows for a more accurate keyword - extraction - - Parameters - ---------- - delim : string - the non-wildcard characters before/after the desired keyword - filepath : string - filepath to the file of interest - suffix_flag : boolean - flag to indicate if delimeter is prefix or suffix - - Returns - ------- - delim : string - the glob-matched delimeter with the filepath - ''' - - # If it's a suffix, traverse backwards - if suffix_flag: - delim = delim[::-1] - filepath = filepath[::-1] - first_brak = ']' - last_brak = '[' - else: - first_brak = '[' - last_brak = ']' - - # Init variables - delim_list = list(delim) - filepath_test = filepath - in_brak_flg = False - - # Iterate through prefix delimeter - for idx, char in enumerate(delim): - if in_brak_flg: - if char == last_brak: - in_brak_flg = False - delim_list[idx] = '' - continue - if char == '?' or char == first_brak: - if char == first_brak: - in_brak_flg = True - const_str = ''.join(delim[:idx]) - fp_idx = filepath.find(const_str) - filepath_test = filepath[fp_idx+len(const_str):] - char_match = filepath_test[0] - delim_list[idx] = char_match - delim = ''.join(delim_list) - - # Join list - delim = ''.join(delim_list) - - # If it's a suffix, reverse it - if suffix_flag: - delim = delim[::-1] - - # Return the glob-matches prefix delimiter - return delim - - -# Check format of filepath templates -def check_template_format(file_template, site_kw, ppant_kw, sess_kw, ser_kw): - ''' - Function to validate the file templalte contains all required - keywords - - Parameters - ---------- - file_template : string - the file pattern template to check - site_kw : string - the keyword indicating the site-level strings in the filepaths - ppant_kw : string - the keyword indicating the ppant-level strings in the filepaths - sess_kw : string - the keyword indicating the session-level strings in the filepaths - ser_kw : string - the keyword indicating the series-level strings in the filepaths - - Returns - ------- - None - - Raises - ------ - Exception - raises an exception if the filepaths don't contain the required - keywords or if the filepaths have repeated keywords - ''' - - # Import packages - import logging - - # Get logger - logger = logging.getLogger('sublist_builder') - - # Check for ppant, series-level directories - if not ppant_kw in file_template: - err_msg = 'Please provide \'%s\' level directories in '\ - 'filepath template where participant-level '\ - 'directories are present in file template: %s' \ - % (ppant_kw, file_template) - logger.error(err_msg) - raise Exception(err_msg) - - # Check to make sure all keywords are only used once in template - if file_template.count(site_kw) > 1 or file_template.count(ppant_kw) > 1 or \ - file_template.count(sess_kw) > 1 or file_template.count(ser_kw) > 1: - err_msg = 'There are multiple instances of key words in the provided '\ - 'file template: %s. Fix this and try again.' % file_template - logger.error(err_msg) - raise Exception(err_msg) - - -# Extract keyword from filepath -def extract_keyword_from_path(filepath, keyword, template): - ''' - Function to extract the key string from a filepath, given a - keyword and a file pattern template - - Parameters - ---------- - filepath : string - filepath to the file of interest - keyword : string - string of the keyword - template : string - file pattern template containing the keyword - - Returns - ------- - key_str : string - extracted string where the keyword was located in the filepath - ''' - - # Import packages - import logging - - # Init variables - # creates a list of directory levels of the filepath, and of the template - temp_split = template.split('/') - fp_split = filepath.split('/') - - # Get logger - # logger = logging.getLogger('sublist_builder') - - # Extract directory name of the keyword, from the template - kw_dirname = [dir for dir in temp_split if keyword in dir] - - # If the keyword is in the template, extract string from filepath - if len(kw_dirname) > 0: - # Get the directory fullname from template, as well as any surrounding - kw_dirname = kw_dirname[0] - # kw_idx is the index of where in the template path the keyword is - kw_idx = temp_split.index(kw_dirname) - # Extract directory with key string in it from filepath, i.e. if this - # is for {participant}, key_str will be the participant ID string from - # the filepath - key_str = fp_split[kw_idx] - # Get the prefix and suffix surrounding keyword - kw_prefix = kw_dirname.split(keyword)[0] - kw_suffix = kw_dirname.split(keyword)[1] - - # Replace other keywords in prefix/suffix with wildcards '*' - for kw in kw_strs: - # If keyword was found, grab any text in that position to split - # out - if kw in kw_prefix: - kw_prefix = kw_prefix.replace(kw, '*') - if kw in kw_suffix: - kw_suffix = kw_suffix.replace(kw, '*') - - # Strip out prefix patterns - # If it ends with a '*', get everything before it - while kw_prefix.endswith('*'): - kw_prefix = kw_prefix[:-1] - - # Make sure what is left is more than '' - # This will not run if the keyword was the only thing in the directory - # level - if kw_prefix != '': - # Find the previous '*' from the right - prev_star_in_prefix = kw_prefix.rfind('*') - # If there is '*', grab from it to end of prefix as delim - if prev_star_in_prefix >= 0: - prefix_delim = kw_prefix[prev_star_in_prefix+1:] - # Otherwise, just use the whole prefix as delim - else: - prefix_delim = kw_prefix - # Check for glob-style characters in delimeter - prefix_delim = check_for_glob_patterns(prefix_delim, key_str) - - # Split the filepath by prefix delim - prefix_list = key_str.split(prefix_delim) - - # If it was found and split-able, take everything past the first - # ocurrence of the delim (because delim could also be in suffix) - if len(prefix_list) > 1: - key_str = prefix_delim.join(prefix_list[1:]) - # Othwerwise, it wasn't found or split-able, use what we had - else: - key_str = prefix_list[0] - - # Strip out suffix patterns - # If it starts with a '*', get everything after it - while kw_suffix.startswith('*'): - kw_suffix = kw_suffix[1:] - - # Make sure what is left is more than '' - # This will not run if the keyword was the only thing in the directory - # level - if kw_suffix != '': - # Find the next '*' from the left - next_star_in_suffix = kw_suffix.find('*') - # If there is another '*', grab non-wildcards up until '*' as - # delim - if next_star_in_suffix >= 0: - suffix_delim = kw_suffix[:next_star_in_suffix] - # Otherwise, just use the whole prefix as delim - else: - suffix_delim = kw_suffix - # Check for glob-style characters in delimeter - suffix_delim = check_for_glob_patterns(suffix_delim, key_str, - suffix_flag=True) - - # Split the filepath by suffix delim - suffix_list = key_str.split(suffix_delim) - - # If it was found and split-able, take everything up to the last - # ocurrence of the delim (because delim could also be in prefix) - if len(suffix_delim) > 1: - key_str = suffix_delim.join(suffix_list[:-1]) - # Otherwise, it wasn't found or split-able, use what we had - else: - key_str = suffix_list[0] - - # Check to see if we split out everything, if so, just grab - # whole directory - if key_str == '': - msg = 'Could not distinguish %s from filepath %s using the ' \ - 'file pattern template %s.\nInstead, using entire ' \ - 'directory: %s for %s.\nCheck data organization and ' \ - 'file pattern template' \ - % (keyword, filepath, template, key_str, keyword) - logger.info(msg) - key_str = fp_split[kw_idx] - else: - # logger.info('Keyword %s not found in template %s' - # % (keyword, template)) - key_str = '' - - # Remove any nifti extensions - key_str = key_str.rstrip('.gz').rstrip('nii') - - # Return the key string - return key_str - - -# Extract site-based scan parameters -def extract_scan_params(scan_params_csv): - """ - Function to extract the site-based scan parameters from a csv file - and return a dictionary of their values - - Parameters - ---------- - scan_params_csv : string - filepath to the scan parameters csv file - - Returns - ------- - site_dict : dictionary - a dictionary where site names are the keys and the scan - parameters for that site are the values stored as a dictionary - """ - - # Import packages - import csv - - # Init variables - csv_open = open(scan_params_csv, 'r') - site_dict = {} - - # Init csv dictionary reader - reader = csv.DictReader(csv_open) - - placeholders = ['None', 'none', 'All', 'all', '', ' '] - - # Iterate through the csv and pull in parameters - for dict_row in reader: - - if dict_row['Site'] in placeholders: - site = 'All' - else: - site = dict_row['Site'] - - ses = 'All' - if 'Session' in dict_row.keys(): - if dict_row['Session'] not in placeholders: - ses = dict_row['Session'] - - if ses != 'All': - # for scan-specific scan parameters (less common) - if site in site_dict.keys(): - site_dict[site][ses] = {key.lower(): val - for key, val in dict_row.items() - if key != 'Site' and key != 'Session'} - else: - site_dict[site] = {ses: {key.lower(): val - for key, val in dict_row.items() - if key != 'Site' and key != 'Session'}} - - # Assumes all other fields are formatted properly, but TR might - # not be - site_dict[site][ses]['tr'] = \ - site_dict[site][ses].pop('tr (seconds)') - - else: - # site-specific scan parameters only (more common) - if site not in site_dict.keys(): - site_dict[site] = \ - {ses: {key.lower(): val for key, val in dict_row.items() - if key != 'Site' and key != 'Session'}} - else: - site_dict[site][ses] = \ - {key.lower(): val for key, val in dict_row.items() - if key != 'Site' and key != 'Session'} - - # Assumes all other fields are formatted properly, but TR might - # not be - site_dict[site][ses]['tr'] = \ - site_dict[site][ses].pop('tr (seconds)') - - # Return site dictionary - return site_dict - - -# Filter out unwanted sites/subjects -def filter_sub_paths(sub_paths, include_sites, include_subs, exclude_subs, - site_kw, ppant_kw, path_template): - ''' - Function to filter out unwanted sites and subjects from the - collected filepaths - - Parameters - ---------- - sub_paths : list - a list of the subject data files - include_sites : list or string - indicates which sites to keep filepaths from - include_subs : list or string - indicates which subjects to keep filepaths from - exclude_subs : list or string - indicates which subjects to remove filepaths from - path_template : string - filepath template in the form of: - '.../base_dir/{site}/{participant}/{session}/.. - {series}/file.nii.gz' - - Returns - ------- - keep_subj_paths : list - a list of the filepaths to use in the filtered subject list - ''' - - # Import packages - import os - import logging - - # Init variables - logger = logging.getLogger('sublist_builder') - - # Check if {site} was specified - keep_site_paths = [] - if site_kw in path_template and include_sites is not None: - # Filter out sites that are not included - if type(include_sites) is not list: - include_sites = [include_sites] - logger.info('Only including sites: %s' % include_sites) - site_matches = filter(lambda sp: \ - extract_keyword_from_path(sp, - site_kw, - path_template) in \ - include_sites, sub_paths) - # note which site IDs in "subjects to include" are missing - missing = list(include_sites) - for site_id in site_matches: - for include in include_sites: - if include in site_id: - if include in missing: - missing.remove(include) - if len(missing) > 0: - logger.info("Site IDs marked in 'Sites to Include' not found:" \ - "\n%s" % str(missing)) - keep_site_paths.extend(site_matches) - else: - logger.info('Not filtering out any potential sites...') - keep_site_paths = sub_paths - - # Only keep subjects in inclusion list or remove those in exclusion list - if include_subs is not None and exclude_subs is not None: - err_msg = 'Please only populate subjects to include or exclude '\ - '- not both!' - logger.error(err_msg) - raise Exception(err_msg) - # Include only - elif include_subs is not None: - keep_subj_paths = [] - if ".txt" in include_subs: - if os.path.exists(include_subs): - include_subs = read_subj_txtfile(include_subs) - else: - err = "\n\n[!] The filepath to the subject inclusion " \ - "list does not exist!\nFilepath: %s\n\n" \ - % include_subs - raise Exception(err) - if type(include_subs) is not list: - include_subs = [include_subs] - logger.info('Only including subjects: %s' % include_subs) - subj_matches = filter(lambda sp: \ - extract_keyword_from_path(sp, - ppant_kw, - path_template) in \ - include_subs, sub_paths) - # note which participant IDs in "subjects to include" are missing - missing = list(include_subs) - for subj_path in subj_matches: - for include in include_subs: - if include in subj_path: - if include in missing: - missing.remove(include) - if len(missing) > 0: - logger.info("Participant IDs marked in 'Subjects to Include' "\ - "not found:\n%s" % str(missing)) - keep_subj_paths.extend(subj_matches) - - if keep_site_paths: - keep_subj_paths = list(set(keep_subj_paths) & set(keep_site_paths)) - - # Or exclude only - elif exclude_subs is not None: - keep_subj_paths = [] - if ".txt" in exclude_subs: - if os.path.exists(exclude_subs): - exclude_subs = read_subj_txtfile(exclude_subs) - else: - err = "\n\n[!] The filepath to the subject exclusion " \ - "list does not exist!\nFilepath: %s\n\n" \ - % exclude_subs - raise Exception(err) - if type(exclude_subs) is not list: - exclude_subs = [exclude_subs] - logger.info('Including all subjects but: %s' % exclude_subs) - subj_matches = filter(lambda sp: \ - extract_keyword_from_path(sp, - ppant_kw, - path_template) not in \ - exclude_subs, sub_paths) - keep_subj_paths.extend(subj_matches) - - if keep_site_paths: - keep_subj_paths = list(set(keep_subj_paths) & set(keep_site_paths)) - - else: - keep_subj_paths = keep_site_paths - - # Filter out any duplicates - keep_subj_paths = list(set(keep_subj_paths)) - logger.info('Filtered down to %d files' % len(keep_subj_paths)) - - # Return kept paths - return keep_subj_paths - - -# Get site, ppant, session-level directory indicies -def return_dir_indices(path_template): - ''' - Function to return the site, participant, and session-level - directory indicies based on splitting the path template by - directory seperation '/' - Parameters - ---------- - path_template : string - filepath template in the form of: - 's3://bucket_name/base_dir/{site}/{participant}/{session}/.. - ../file.nii.gz' - Returns - ------- - site_idx : integer - the directory level of site folders - ppant_idx : integer - the directory level of participant folders - sess_idx : integer - the directory level of the session folders - ''' - - # Get folder level indices of site and subject - anat - fp_split = path_template.split('/') - # Session level isn't required, but recommended - sess_idx = None - sess_extra = None - for dir_level in fp_split: - if "{site}" in dir_level: - site_idx = fp_split.index(dir_level) - site_extra = filter(bool,dir_level.split("{site}")) - if "{participant}" in dir_level: - ppant_idx = fp_split.index(dir_level) - ppant_extra = filter(bool,dir_level.split("{participant}")) - if "{session}" in dir_level: - sess_idx = fp_split.index(dir_level) - sess_extra = filter(bool,dir_level.split("{session}")) - - # Return extra characters - return site_idx, ppant_idx, sess_idx, site_extra, ppant_extra, sess_extra - - -# Return matching filepaths -def return_local_matched_paths(path_template, bids_flag=False): - ''' - Function to return the filepaths from local directories given a - file pattern template - - Parameters - ---------- - path_template : string - filepath template in the form of: - '/base_dir/{site}/{participant}/{session}/../file.nii.gz' - bids_flag : boolean (optional); default=False - flag to indicate if the dataset to gather is organized to the - BIDS standard - - Returns - ------- - matched_paths : list - a list of strings of the local filepaths - ''' - - # Import packages - import glob - import logging - import os - - # Get logger - logger = logging.getLogger('sublist_builder') - - # Gather local files - # if bids_flag: - # file_pattern = path_template - # else: - file_pattern = path_template.replace('{site}', '*').\ - replace('{participant}', '*').\ - replace('{session}', '*').\ - replace('{series}', '*') - - # in case the user doesn't end their glob path with ".nii" or ".nii.gz" - if ".nii" not in file_pattern: - "".join([file_pattern, ".nii*"]) - - local_filepaths = glob.glob(file_pattern) - - if len(local_filepaths) == 0: - err = "\n\n[!] No data filepaths were found with the path template " \ - "given:\n%s\n\n" % path_template - raise Exception(err) - - # Restrict filepaths and pattern to be of same directory depth - # as fnmatch will expand /*/ recursively to .../*/*/... - matched_paths = [] - for lfp in local_filepaths: - s3_split = lfp.split('/') - fp_split = file_pattern.split('/') - if len(s3_split) == len(fp_split): - matched_paths.append(lfp) - - # Get absolute paths - matched_paths = [os.path.abspath(fp) for fp in matched_paths] - - # Print how many found - num_local_files = len(matched_paths) - logger.info('Found %d files!' % num_local_files) - - # Return the filepaths as a list - return matched_paths - - -def return_local_filepaths(base_dir): - - import os - - print('Gathering file paths from directory:\n{0}\n'.format(base_dir)) - - local_filepaths = [] - for root, dirs, files in os.walk(base_dir): - for filename in files: - fullpath = os.path.join(root, filename) - local_filepaths.append(fullpath) - - print('{0} file paths found.\n'.format(str(len(local_filepaths)))) - - return local_filepaths - - -def return_s3_filepaths(base_dir, creds_path=None): - ''' - Function to return the filepaths from an S3 bucket given a file - pattern template and, optionally, credentials - - Parameters - ---------- - creds_path : string (optional); default=None - filepath to a credentials file containing the AWS credentials - to access the S3 bucket objects - - Returns - ------- - matched_s3_paths : list - a list of strings of the filepaths from the S3 bucket - ''' - - # Import packages - import logging - import os - - from indi_aws import fetch_creds - - # # Check for errors - # if not bids_base_dir: - # if not ('{site}' in path_template and '{participant}' in path_template): - # err_msg = 'Please provide \'{site}\' and \'{particpant}\' in '\ - # 'filepath template where site and participant-level '\ - # 'directories are present' - # raise Exception(err_msg) - - # if running this with "Custom" (non-BIDS) file templates - if '{site}' in base_dir: - base_dir = base_dir.split('{site}')[0] - elif '{participant}' in base_dir: - base_dir = base_dir.split('{participant}')[0] - - # Init variables - bucket_name = base_dir.split('/')[2] - s3_prefix = '/'.join(base_dir.split('/')[:3]) - - # Get logger - logger = logging.getLogger('sublist_builder') - - # Extract base prefix to search through in S3 - prefix = base_dir.replace(s3_prefix, '').lstrip('/') - - # Attempt to get bucket - try: - bucket = fetch_creds.return_bucket(creds_path, bucket_name) - except Exception as exc: - err_msg = 'There was an error in retrieving S3 bucket: %s.\n' \ - 'Error: %s' % (bucket_name, exc) - logger.error(err_msg) - raise Exception(err_msg) - - # Get filepaths from S3 with prefix - logger.info('Gathering files from S3 to parse...') - s3_filepaths = [] - for s3_obj in bucket.objects.filter(Prefix=prefix): - s3_filepaths.append(str(s3_obj.key)) - - # Prepend 's3://bucket_name/' on found paths - s3_filepaths = [os.path.join(s3_prefix, s3_fp) for s3_fp in s3_filepaths] - - return s3_filepaths - - -def process_s3_paths(s3_filepaths, path_template): - - import logging - import fnmatch - - # Get logger - logger = logging.getLogger('sublist_builder') - - # File pattern filter - file_pattern = path_template.replace('{site}', '*').replace('{participant}', '*').replace('{session}', '*').replace('{series}', '*') - - # Get only matching s3 paths - s3_filepaths = fnmatch.filter(s3_filepaths, file_pattern) - - # Restrict filepaths and pattern to be of same directory depth - # as fnmatch will expand /*/ recursively to .../*/*/... - matched_s3_paths = [] - for s3fp in s3_filepaths: - s3_split = s3fp.split('/') - fp_split = file_pattern.split('/') - if len(s3_split) == len(fp_split): - matched_s3_paths.append(s3fp) - - # Print how many found - num_s3_files = len(matched_s3_paths) - logger.info('Found %d files!' % num_s3_files) - - # Return the filepaths as a list - return matched_s3_paths - - -def parse_BIDS_filepaths(bids_filepaths, bids_base_dir, creds_path=None): - """Return a data configuration dictionary (subject list dictionary) when - given a list of BIDS-formatted input data file paths.""" - - sub_dict = {} - json_dict = {} - - for path in bids_filepaths: - # get the stuff after the base dir by itself - dirs_path = path.split(bids_base_dir)[1].lstrip('/') - dirs = dirs_path.split('/') - filename = dirs[-1] - - if '.nii' in path: - if '.nii' not in filename or '_' not in filename: - continue - # 'anat' or 'func' - type = dirs[-2] - - if type != 'anat' and type != 'func': - print '\nFilepath not in BIDS format! Skipping..\n' \ - 'Filepath: {0}\n'.format(path) - continue - - sub_id = None - ses_id = None - for dir in dirs: - if 'sub-' in dir and '.nii' not in dir: - sub_id = dir - sub_idx = dirs.index(dir) - if 'ses-' in dir and '.nii' not in dir: - ses_id = dir - - if sub_idx and sub_idx != 0: - site_id = dirs[sub_idx - 1] - else: - site_id = None - - # parse filename and get scan ID - if type == 'func': - scan_id = None - run_id = None - for file_part in filename.split('_'): - if 'task-' in file_part: - scan_id = file_part.split('task-')[1] - if 'run-' in file_part: - run_id = file_part - if not scan_id: - continue - if run_id: - scan_id = '_'.join([scan_id, run_id]) - - # assign defaults if needed - if not site_id: - site_id = 'site-1' - if not ses_id: - ses_id = 'ses-1' - - key = ('{0}_{1}_{2}'.format(sub_id, site_id, ses_id)) - - # populate sub_dict - if key not in sub_dict.keys(): - if type == 'anat': - sub_dict[key] = {'anat': path, - 'subject_id': sub_id, - 'unique_id': ses_id, - 'site': site_id, - 'creds_path': creds_path} - if type == 'func': - sub_dict[key] = {'func': {scan_id: path}, - 'creds_path': creds_path} - else: - tmp_dict = sub_dict[key] - - if type == 'anat': - if 'anat' not in tmp_dict.keys(): - tmp_dict.update({'anat': path}) - elif type == 'func': - if 'func' not in tmp_dict.keys(): - tmp_dict['func'] = {scan_id: path} - else: - if scan_id not in tmp_dict['func']: - tmp_dict['func'].update({scan_id: path}) - - sub_dict[key] = tmp_dict - - elif '.json' in path: - site_id = None - sub_id = None - ses_id = None - scan_id = None - - if len(dirs) > 2: - # more specific json - for dir in dirs: - if 'sub-' in dir and '.json' not in dir: - sub_id = dir - sub_idx = dirs.index(dir) - if 'ses-' in dir and '.json' not in dir: - ses_id = dir - if '.json' in dir: - scan_id = dir.replace('.json', '') - else: - # site-level json - if len(dirs) > 1: - if '.json' in dirs[1]: - site_id = dirs[0] - if 'task-' in dirs[1]: - scan_id = dirs[1].replace('.json', '') - else: - site_id = 'site-1' - if '.json' in dirs[0]: - scan_id = dirs[0].replace('.json', '') - - json_dict[(site_id, sub_id, ses_id, scan_id)] = path - - # grab the scan parameter JSONs, if they are there - for id_tuple in json_dict.keys(): - json_path = json_dict[id_tuple] - site_id = id_tuple[0] - sub_id = id_tuple[1] - ses_id = id_tuple[2] - scan_id = id_tuple[3] - - if scan_id: - if 'task-' not in scan_id: - continue - else: - continue - - if site_id and not sub_id and not ses_id: - # site-level json - for id_string in sub_dict.keys(): - if site_id in id_string: - tmp_dict = sub_dict[id_string] - if 'func' not in tmp_dict.keys(): - tmp_dict['func'] = {'scan_parameters': json_path} - else: - if scan_id not in tmp_dict['func']: - tmp_dict['func'].update({'scan_parameters': json_path}) - sub_dict[id_string] = tmp_dict - - if sub_id: - # more specific - for id_string in sub_dict.keys(): - if sub_id in id_string: - if ses_id: - if ses_id in id_string: - tmp_dict = sub_dict[id_string] - else: - tmp_dict = sub_dict[id_string] - if 'func' not in tmp_dict.keys(): - tmp_dict['func'] = {'scan_parameters': json_path} - else: - if scan_id not in tmp_dict['func']: - tmp_dict['func'].update({'scan_parameters': json_path}) - sub_dict[id_string] = tmp_dict - - return sub_dict - - -def build_sublist(data_config_yml): - ''' - Function to build a C-PAC-compatible subject list, given - anatomical and functional file paths - - Parameters - ---------- - data_config_yml : string - filepath to the data config yaml file - - Returns - ------- - sublist : list - C-PAC subject list of subject dictionaries - ''' - - # Import packages - import logging - import os - import yaml - - from CPAC.utils import bids_metadata - from CPAC.utils.utils import setup_logger - - # Init variables - tmp_dict = {} - s3_str = 's3://' - - # Load in data config from yaml to dictionary - data_config_dict = yaml.load(open(data_config_yml, 'r')) - - # Get inclusion, exclusion, scan csv parameters, and output location - data_format = data_config_dict['dataFormat'][0] - bids_base_dir = data_config_dict['bidsBaseDir'] - anat_template = data_config_dict['anatomicalTemplate'] - func_template = data_config_dict['functionalTemplate'] - include_subs = data_config_dict['subjectList'] - exclude_subs = data_config_dict['exclusionSubjectList'] - include_sites = data_config_dict['siteList'] - scan_params_csv = data_config_dict['scanParametersCSV'] - sublist_outdir = data_config_dict['outputSubjectListLocation'] - sublist_name = data_config_dict['subjectListName'] - - # Get C-PAC logger if it's available - log_path = os.path.join(sublist_outdir, - 'sublist_build_%s.log' % \ - os.path.basename(data_config_yml).split('.')[0]) - logger = setup_logger('sublist_builder', log_path, logging.INFO, - to_screen=True) - - # Older data configs won't have this field - try: - creds_path = data_config_dict['awsCredentialsFile'] - except KeyError: - creds_path = None - - # Change any 'None' to None of optional arguments - if bids_base_dir == 'None': - bids_base_dir = None - if include_subs == 'None': - include_subs = None - if exclude_subs == 'None': - exclude_subs = None - if include_sites == 'None': - include_sites = None - if scan_params_csv == 'None': - scan_params_csv = None - if creds_path == 'None': - creds_path = None - - ''' - # Get dataformat if BIDS or not - if data_format == 'BIDS': - bids_flag = True - anat_template = return_bids_template(bids_base_dir, 'anat') - func_template = return_bids_template(bids_base_dir, 'func') - else: - bids_flag = False - - ### WE NEED A TEMPLATE FOR RETURN S3 PATHS??? - - # Get directory indices - # If data is BIDS - if bids_flag: - anat_site_idx = func_site_idx = None - anat_path_dirs = anat_template.split('/') - - anat_ppant_idx = func_ppant_idx = \ - anat_path_dirs.index('{participant}') - - if '{site}' in anat_path_dirs: - anat_site_idx = func_site_idx = anat_path_dirs.index('{site}') - else: - anat_site_idx = func_site_idx = None - - if '{session}' in anat_path_dirs: - anat_sess_idx = func_sess_idx = anat_path_dirs.index('{session}') - else: - anat_sess_idx = func_sess_idx = None - ''' - - # Read in scan parameters and return site-based dictionary - if scan_params_csv is not None: - site_scan_params = extract_scan_params(scan_params_csv) - else: - site_scan_params = {} - - # Iterate through file paths and build subject list - if data_format == 'BIDS': - if 's3://' in bids_base_dir: - # on the AWS S3 bucket - bids_filepaths = return_s3_filepaths(bids_base_dir, creds_path) - else: - # local - bids_filepaths = return_local_filepaths(bids_base_dir) - - sub_dict = parse_BIDS_filepaths(bids_filepaths, bids_base_dir, - creds_path) - - elif data_format == 'Custom': - # See if the templates are s3 files - if s3_str in anat_template and s3_str in func_template: - # Get anatomical filepaths from s3 - print 'Fetching anatomical files...' - anat_s3_paths = return_s3_filepaths(anat_template, creds_path) - anat_paths = process_s3_paths(anat_s3_paths, anat_template) - # Get functional filepaths from s3 - print 'Fetching functional files...' - func_s3_paths = return_s3_filepaths(func_template, creds_path) - func_paths = process_s3_paths(func_s3_paths, func_template) - - # If one is in S3 and the other is not, raise error - not supported - elif ( - s3_str in anat_template.lower() and s3_str not in func_template.lower()) or \ - ( - s3_str not in anat_template.lower() and s3_str in func_template.lower()): - err_msg = 'Both anatomical and functional files should either be ' \ - 'on S3 or local. Separating the files is currently not ' \ - 'supported.' - raise Exception(err_msg) - - # Otherwise, it's local files - else: - # Get anatomical filepaths - print 'Gathering anatomical files...' - anat_paths = return_local_matched_paths(anat_template) - # Get functional filepaths - print 'Gathering functional files...' - func_paths = return_local_matched_paths(func_template) - - #TODO: this might be in the wrong spot!!! - # Filter out unwanted anat and func filepaths - logger.info('Filtering anatomical files...') - anat_paths = filter_sub_paths(anat_paths, include_sites, - include_subs, exclude_subs, - site_kw, ppant_kw, anat_template) - print 'Filtered down to %d anatomical files' % len(anat_paths) - - func_paths = filter_sub_paths(func_paths, include_sites, - include_subs, exclude_subs, - site_kw, ppant_kw, func_template) - - # If all data is filtered out, raise exception - if len(anat_paths) == 0 or len(func_paths) == 0: - err_msg = 'Unable to find any files after filtering sites and ' \ - 'subjects! Check site and subject inclusion fields as ' \ - 'well as filepaths and template pattern!' - logger.error(err_msg) - raise Exception(err_msg) - - for anat in anat_paths: - subj = extract_keyword_from_path(anat, "{participant}", - anat_template) - try: - sess = extract_keyword_from_path(anat, "{session}", - anat_template) - except TypeError: - sess = "ses-1" - site = extract_keyword_from_path(anat, "{site}", anat_template) - - subj_d = {'anat': anat, 'creds_path': creds_path, 'func': {}, - 'subject_id': subj, 'unique_id': sess} - tmp_key = '_'.join([subj, site, sess]) - tmp_dict[tmp_key] = subj_d - - # Now go through and populate functional scans dictionaries - for func in func_paths: - func_sp = func.split('/') - subj = extract_keyword_from_path(func, "{participant}", - func_template) - try: - sess = extract_keyword_from_path(func, "{session}", - func_template) - except TypeError: - sess = "ses-1" - site = extract_keyword_from_path(func, "{site}", func_template) - - # Build tmp key and get subject dictionary from tmp dictionary - tmp_key = '_'.join([subj, site, sess]) - # Try and find the associated anat scan - try: - subj_d = tmp_dict[tmp_key] - except KeyError as exc: - logger.info('Unable to find anatomical image for %s. ' - 'Skipping...' % tmp_key) - continue - - # If there is no scan sub-folder, make scan - # the name of the image itself without extension - if "{series}" in func_template: - scan = extract_keyword_from_path(func, "{series}", - func_template) - else: - scan = func_sp[-1].split('.nii')[0] - - # Deal with scan parameters - scan_params = None - if scan_params_csv is not None: - try: - scan_params = site_scan_params[site] - # check for scan-specific level - if scan in scan_params.keys(): - #TODO: WHAT WAS HERE????????!!!! - pass - - except KeyError as exc: - print 'Site %s missing from scan parameters csv, ' \ - 'skipping...' % site - - # Set the rest dictionary with the scan - subj_d['func'][scan] = func - subj_d['func']["scan_parameters"] = scan_params - # And replace it back in the dictionary - tmp_dict[tmp_key] = subj_d - - sub_dict = tmp_dict - - # all done - # Build a subject list from dictionary values - sublist = [] - for data_bundle in sub_dict.values(): - if 'anat' not in data_bundle.keys() or \ - 'func' not in data_bundle.keys(): - continue - if data_bundle['anat'] != '' and data_bundle['func'] != {}: - sublist.append(data_bundle) - # Check to make sure subject list has at least one valid data bundle - if len(sublist) == 0: - err_msg = 'Unable to find both anatomical and functional data for ' \ - 'any subjects. Check data organization and file patterns!' - logger.error(err_msg) - raise Exception(err_msg) - - # Write subject list out to out location - sublist_out_yml = os.path.join(sublist_outdir, - 'CPAC_subject_list_%s.yml' % sublist_name) - with open(sublist_out_yml, 'w') as out_sublist: - # Make sure YAML doesn't dump aliases (so it's more human read-able) - noalias_dumper = yaml.dumper.SafeDumper - noalias_dumper.ignore_aliases = lambda self, data: True - out_sublist.write(yaml.dump(sublist, default_flow_style=False, - Dumper=noalias_dumper)) - - print('Participant list created - {0}'.format(sublist_out_yml)) - - # Return the subject list - return sublist diff --git a/CPAC/utils/create_fsl_model.py b/CPAC/utils/create_fsl_model.py index ec7551a9d3..3941b10f81 100644 --- a/CPAC/utils/create_fsl_model.py +++ b/CPAC/utils/create_fsl_model.py @@ -19,7 +19,6 @@ def load_pheno_file(pheno_file): return pheno_dataframe - def load_group_participant_list(group_participant_list_file): import os @@ -47,7 +46,6 @@ def load_group_participant_list(group_participant_list_file): return group_subs_dataframe - def process_pheno_file(pheno_file_dataframe, group_subs_dataframe, \ participant_id_label): @@ -188,7 +186,6 @@ def process_pheno_file(pheno_file_dataframe, group_subs_dataframe, \ return pheno_file_rows - def create_pheno_dict(pheno_file_rows, ev_selections, participant_id_label): # creates the phenotype data dictionary in a format Patsy requires, @@ -257,7 +254,6 @@ def create_pheno_dict(pheno_file_rows, ev_selections, participant_id_label): else: pheno_data_dict[key].append(float(line[key])) - # this needs to run after each list in each key has been fully # populated above for key in pheno_data_dict.keys(): @@ -287,7 +283,6 @@ def create_pheno_dict(pheno_file_rows, ev_selections, participant_id_label): # replace pheno_data_dict[key] = new_demeaned_evs - # converts non-categorical EV lists into NumPy arrays # so that Patsy may read them in properly if 'categorical' in ev_selections.keys(): @@ -299,7 +294,6 @@ def create_pheno_dict(pheno_file_rows, ev_selections, participant_id_label): return pheno_data_dict - def get_measure_dict(param_file): # load the CPAC-generated power parameters file and parse it @@ -358,7 +352,6 @@ def get_measure_dict(param_file): return measure_dict - def get_custom_roi_info(roi_means_dict): # check @@ -393,7 +386,6 @@ def get_custom_roi_info(roi_means_dict): return roi_dict_dict - def model_group_var_separately(grouping_var, formula, pheno_data_dict, \ ev_selections, coding_scheme): @@ -449,8 +441,7 @@ def model_group_var_separately(grouping_var, formula, pheno_data_dict, \ grouping_var_id_dict[cat_ev_level] = [idx] idx += 1 - - + split_EVs = {} for key in pheno_data_dict.keys(): @@ -501,8 +492,7 @@ def model_group_var_separately(grouping_var, formula, pheno_data_dict, \ # put split EVs into pheno data dict pheno_data_dict.update(split_EVs) - - + # parse through ev_selections, find the categorical names within the # design formula and insert C(, Sum) into the design formula # this is required for Patsy to process the categorical EVs @@ -520,15 +510,12 @@ def model_group_var_separately(grouping_var, formula, pheno_data_dict, \ formula = formula.replace(EV_name, 'C(' + EV_name + \ ', Sum)') - # remove intercept when modeling group variances separately formula = formula + " - 1" - return pheno_data_dict, formula, grouping_var_id_dict - def check_multicollinearity(matrix): import numpy as np @@ -561,7 +548,6 @@ def check_multicollinearity(matrix): 'check your model design.\n\n' - def write_mat_file(design_matrix, output_dir, model_name, \ depatsified_EV_names, current_output=None): @@ -593,7 +579,6 @@ def write_mat_file(design_matrix, output_dir, model_name, \ if not os.path.exists(output_dir): os.makedirs(output_dir) - with open(out_file, 'wt') as f: print >>f, '/NumWaves\t%d' %dimy @@ -613,7 +598,6 @@ def write_mat_file(design_matrix, output_dir, model_name, \ np.savetxt(f, design_matrix, fmt='%1.5e', delimiter='\t') - def create_grp_file(design_matrix, grouping_var_id_dict, output_dir, \ model_name, current_output=None): @@ -640,14 +624,12 @@ def create_grp_file(design_matrix, grouping_var_id_dict, output_dir, \ i += 1 - filename = model_name + ".grp" out_file = os.path.join(output_dir, filename) if not os.path.exists(output_dir): os.makedirs(output_dir) - with open(out_file, "wt") as f: print >>f, '/NumWaves\t1' @@ -656,7 +638,6 @@ def create_grp_file(design_matrix, grouping_var_id_dict, output_dir, \ np.savetxt(f, design_matrix_ones, fmt='%d', delimiter='\t') - def create_design_matrix(pheno_file, ev_selections, formula, \ subject_id_label, sub_list=None, \ coding_scheme="Treatment", grouping_var=None, \ @@ -1052,7 +1033,6 @@ def positive(dmat, a, coding, group_sep, grouping_var): if ev.startswith(term): con[evs[ev]]= -1 - # else not modeling group variances separately else: @@ -1077,20 +1057,17 @@ def positive(dmat, a, coding, group_sep, grouping_var): return con - def greater_than(dmat, a, b, coding, group_sep, grouping_var): c1 = positive(dmat, a, coding, group_sep, grouping_var) c2 = positive(dmat, b, coding, group_sep, grouping_var) return c1-c2 - def negative(dmat, a, coding, group_sep, grouping_var): con = 0-positive(dmat, a, coding, group_sep, grouping_var) return con - def create_dummy_string(length): ppstring = "" for i in range(0, length): @@ -1099,7 +1076,6 @@ def create_dummy_string(length): return ppstring - def create_con_file(con_dict, col_names, file_name, current_output, out_dir): import os @@ -1135,7 +1111,6 @@ def create_con_file(con_dict, col_names, file_name, current_output, out_dir): f.write("\n") - def create_fts_file(ftest_list, con_dict, model_name, current_output, \ out_dir): @@ -1184,7 +1159,6 @@ def create_fts_file(ftest_list, con_dict, model_name, current_output, \ for i in range(fts_n.shape[0]): print >>f, ' '.join(fts_n[i].astype('str')) - except Exception as e: filepath = os.path.join(out_dir, "model_files", current_output, \ @@ -1197,7 +1171,6 @@ def create_fts_file(ftest_list, con_dict, model_name, current_output, \ raise Exception(errmsg) - def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ column_names, coding_scheme, group_sep): @@ -1247,7 +1220,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ length = None length = len(list(lst[0])) - # lst = list of tuples, "tp" # tp = tuple in the format (contrast_name, 0, 0, 0, 0, ...) # with the zeroes being the vector of contrasts for that contrast @@ -1280,7 +1252,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ contrasts = np.array(contrasts, dtype=np.float16) fts_columns = np.array(fts_columns) - # if there are f-tests, create the array for them if fTest: @@ -1295,8 +1266,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ fts_n = fts_columns.T - - if len(column_names) != (num_EVs_in_con_file): err_string = "\n\n[!] CPAC says: The number of EVs in your model " \ @@ -1313,7 +1282,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ raise Exception(err_string) - for design_mat_col, con_csv_col in zip(column_names, evs[1:]): if con_csv_col not in design_mat_col: @@ -1325,9 +1293,7 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ "%s\nYour contrasts matrix columns: %s\n\n" \ % (column_names, evs[1:]) - raise Exception(errmsg) - - + raise Exception(errmsg) out_dir = os.path.join(output_dir, model_name + '.con') @@ -1359,7 +1325,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ np.savetxt(f, contrasts, fmt='%1.5e', delimiter='\t') - if fTest: print "\nFound f-tests in your model, writing f-tests file (.fts)..\n" @@ -1384,7 +1349,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ print >>f, ' '.join(fts_n[i].astype('str')) - def process_contrast(parsed_contrast, operator, ev_selections, group_sep, \ grouping_var, coding_scheme): @@ -1460,11 +1424,9 @@ def process_contrast(parsed_contrast, operator, ev_selections, group_sep, \ parsed_EVs_in_contrast[1] + ":" + \ parsed_EVs_in_contrast[2]] - return parsed_EVs_in_contrast - def run(group_config, current_output, param_file=None, \ derivative_means_dict=None, roi_means_dict=None, \ model_out_dir=None, CPAC_run=True): @@ -1485,7 +1447,6 @@ def run(group_config, current_output, param_file=None, \ raise Exception("Error in reading %s configuration file" \ % group_config) - # pull in the gpa settings! ph = c.pheno_file @@ -1513,7 +1474,6 @@ def run(group_config, current_output, param_file=None, \ output_dir = c.output_dir - # make sure the group analysis output directory exists try: if not os.path.exists(output_dir): @@ -1524,21 +1484,17 @@ def run(group_config, current_output, param_file=None, \ 'you have write access in this file structure.\n\n\n' raise Exception - - measure_dict = {} # extract motion measures from CPAC-generated power params file if param_file != None: measure_dict = get_measure_dict(param_file) - # combine the motion measures dictionary with the measure_mean # dictionary (if it exists) if derivative_means_dict: measure_dict["Measure_Mean"] = derivative_means_dict - # create the .mat and .grp files for FLAME design_matrix, regressor_names = create_design_matrix(ph, sublist, \ ev_selections, formula, \ @@ -1546,7 +1502,6 @@ def run(group_config, current_output, param_file=None, \ grouping_var, measure_dict, \ roi_means_dict, model_out_dir, \ model_name, current_output) - # Create contrasts_dict dictionary for the .con file generation later contrasts_list = contrasts @@ -1559,7 +1514,6 @@ def run(group_config, current_output, param_file=None, \ # remove all spaces parsed_contrast = contrast.replace(' ', '') - EVs_in_contrast = [] parsed_EVs_in_contrast = [] @@ -1616,7 +1570,6 @@ def run(group_config, current_output, param_file=None, \ negative(design_matrix, parsed_EVs_in_contrast[0], \ coding_scheme, group_sep, grouping_var) - if len(contrast_items) > 2: idx = 0 @@ -1679,7 +1632,6 @@ def run(group_config, current_output, param_file=None, \ idx += 1 - # finally write out the .con file and .fts file (if f-tests) if (custom_contrasts == None) or (custom_contrasts == '') or \ ("None" in custom_contrasts): @@ -1693,7 +1645,6 @@ def run(group_config, current_output, param_file=None, \ if f_tests: create_fts_file(f_tests, contrasts_dict, model_name, \ current_output, model_out_dir) - else: diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_group_analysis_files.py new file mode 100644 index 0000000000..efe714e513 --- /dev/null +++ b/CPAC/utils/create_group_analysis_files.py @@ -0,0 +1,114 @@ + + +def read_group_list_text_file(group_list_text_file): + """Read in the group-level analysis participant-session list text file.""" + + with open(group_list_text_file, "r") as f: + group_list = f.readlines() + + # each item here includes both participant and session, and this also will + # become the main ID column in the written design matrix CSV + group_list = [str(x).rstrip("\n") for x in group_list if x != ""] + + return group_list + + +def read_pheno_csv_into_df(pheno_csv): + """Read the phenotypic file CSV into a Pandas DataFrame.""" + + import pandas as pd + + with open(pheno_csv, "r") as f: + pheno_df = pd.read_csv(f) + + return pheno_df + + +def get_pheno_evs(pheno_csv, ev_selections): + """Get the columns of phenotypic data matched with their participant- + session labels as a Pandas DataFrame.""" + pass + + +def create_design_matrix_df(group_list, pheno_df=None, + ev_selections=None, pheno_sub_label=None): + """Create the design matrix intended for group-level analysis via the FSL + FLAME tool. + + This does NOT create the final .mat file that FSL FLAME takes in. This is + an intermediary design matrix CSV meant for the user to review. + + If there is a phenotype CSV provided, this function will align the + participant-session ID labels in the CPAC individual-level analysis output + directory with the values listed in the phenotype file. + """ + + import pandas as pd + + # map the participant-session IDs to just participant IDs + group_list_map = {} + for part_ses in group_list: + group_list_map[part_ses.split("_")[0]] = part_ses + + # initialize a Pandas DataFrame for the new design matrix + design_df = pd.DataFrame() + + # initialize the rows (no columns yet!) + design_df["Participant-Session"] = group_list + + if pheno_df: + # if a phenotype CSV file is provided with the data + + # TODO: next- this aligning of the pheno and the design DF + # TODO: use the group_map to line up the pheno_sub_label column in the + # TODO: pheno DF with the design DF rows- then grab your desired EV + # TODO: column(s) and merge it with the design DF basically + + # align the pheno's participant ID column with the group sublist text + # file + if pheno_sub_label: + pass + + # add any selected EVs to the design matrix from the phenotype file + for ev in ev_selections: + pass + + return design_df + + +def create_contrasts_template_df(design_df): + """Create the template Pandas DataFrame for the contrasts matrix CSV. + + The headers in the contrasts matrix needs to match the headers of the + design matrix.""" + pass + + +def preset_single_group_avg(group_list, pheno_df=None, covariate=None, + pheno_sub_label=None): + """Set up the design matrix CSV for running a single group average + (one-sample T-test).""" + + ev_selections = None + if pheno_df and covariate and pheno_sub_label: + # if we're adding an additional covariate + ev_selections = [covariate] + + design_df = create_design_matrix_df(group_list, pheno_df, + ev_selections=ev_selections, + pheno_sub_label=pheno_sub_label) + + design_df["Group Mean"] = 1 + + return design_df + + +def write_dataframe_to_csv(design_df): + """Write out a matrix Pandas DataFrame into a CSV file.""" + pass + + +def run(group_list_text_file): + + group_list = read_group_list_text_file(group_list_text_file) + diff --git a/setup.py b/setup.py index b7845c0a61..c9b9d56863 100755 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ from build_helpers import INFO_VARS + def configuration(parent_package='', top_path=None): from numpy.distutils.misc_util import Configuration, \ get_numpy_include_dirs @@ -102,11 +103,11 @@ def main(**extra_args): author_email=INFO_VARS['AUTHOR_EMAIL'], platforms=INFO_VARS['PLATFORMS'], version=INFO_VARS['VERSION'], - requires = INFO_VARS['REQUIRES'], - install_requires = INFO_VARS['INSTALL_REQUIRES'], - configuration = configuration, - cmdclass = cmdclass, - scripts = glob('scripts/*'), + requires=INFO_VARS['REQUIRES'], + install_requires=INFO_VARS['INSTALL_REQUIRES'], + configuration=configuration, + cmdclass=cmdclass, + scripts=glob('scripts/*'), entry_points={ 'console_scripts': [ 'cpac_extract_parameters=CPAC.utils.extract_parameters:main' From 2ffdd7d2216dd906c57f01a4b9a0d27d0643e81b Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 9 Mar 2018 15:11:12 -0500 Subject: [PATCH 08/75] First three group analysis presets operational. --- CPAC/GUI/interface/pages/nuisance.py | 2 +- .../GUI/interface/utils/modelconfig_window.py | 322 +++++++---- CPAC/pipeline/cpac_ga_model_generator.py | 173 ++++-- CPAC/pipeline/cpac_group_runner.py | 36 +- CPAC/utils/create_flame_model_files.py | 31 +- CPAC/utils/create_group_analysis_files.py | 515 ++++++++++++++++-- scripts/cpac_cli.py | 75 +++ 7 files changed, 931 insertions(+), 223 deletions(-) create mode 100644 scripts/cpac_cli.py diff --git a/CPAC/GUI/interface/pages/nuisance.py b/CPAC/GUI/interface/pages/nuisance.py index bc66615950..2f5a592dd4 100644 --- a/CPAC/GUI/interface/pages/nuisance.py +++ b/CPAC/GUI/interface/pages/nuisance.py @@ -111,7 +111,7 @@ def __init__(self, parent, counter = 0): control=control.TEXT_BOX, name='spikeThreshold', type=dtype.LNUM, - values="5%", + values="0.2", validator=CharValidator("no-alpha"), comment="(Motion Spike De-Noising only) Specify the " "maximum acceptable Framewise Displacement " diff --git a/CPAC/GUI/interface/utils/modelconfig_window.py b/CPAC/GUI/interface/utils/modelconfig_window.py index e6281e4bb2..9e1c7882bf 100644 --- a/CPAC/GUI/interface/utils/modelconfig_window.py +++ b/CPAC/GUI/interface/utils/modelconfig_window.py @@ -214,24 +214,17 @@ def __init__(self, parent, gpa_settings=None): size = (200,100), combo_type = 8) - self.page.set_sizer() - - - if 'group_sep' in self.gpa_settings.keys(): + if 'group_sep' in self.gpa_settings.keys(): for ctrl in self.page.get_ctrl_list(): - name = ctrl.get_name() - if name == 'group_sep': - if self.gpa_settings['group_sep'] == True: ctrl.set_value('On') elif self.gpa_settings['group_sep'] == False: ctrl.set_value('Off') - mainSizer.Add(self.window, 1, wx.EXPAND) btnPanel = wx.Panel(self.panel, -1) @@ -303,7 +296,15 @@ def __init__(self, parent, gpa_settings=None): ctrl.set_value(self.gpa_settings['group_sep']) if name == 'grouping_var': - ctrl.set_value(self.gpa_settings['grouping_var']) + grouping_var = self.gpa_settings['grouping_var'] + + if isinstance(grouping_var, list) or "[" in grouping_var: + new_grouping_var = "" + for cov in grouping_var: + new_grouping_var += "{0},".format(cov) + new_grouping_var = new_grouping_var.rstrip(",") + + ctrl.set_value(new_grouping_var) if ("list" in name) and (name != "participant_list"): @@ -328,7 +329,6 @@ def __init__(self, parent, gpa_settings=None): ctrl.set_value(new_derlist) - def cancel(self, event): self.Close() @@ -338,7 +338,6 @@ def display(self, win, msg): win.SetFocus() win.Refresh() raise ValueError - ''' button: LOAD SETTINGS ''' def load(self, event): @@ -363,8 +362,7 @@ def load(self, event): # load the group analysis .yml config file (in dictionary form) # into the self.gpa_settings dictionary which holds all settings self.gpa_settings = config_map - - + if self.gpa_settings is None: errDlgFileTest = wx.MessageDialog( self, "Error reading file - group analysis " \ @@ -393,7 +391,6 @@ def load(self, event): ctrl.set_value(phenoHeaderItems) ctrl.set_selection(self.gpa_settings['ev_selections']) - # populate the rest of the controls for ctrl in self.page.get_ctrl_list(): @@ -425,21 +422,30 @@ def load(self, event): ctrl.set_value(None) elif name == 'z_threshold' or name == 'p_threshold': - value = value[0] - ctrl.set_value(value) + try: + value = value[0] + ctrl.set_value(value) + except TypeError: + # if the user has put it in as a float and not a list + ctrl.set_value(str(value)) elif name == 'group_sep': value = s_map.get(value) - ctrl.set_value(value) - - elif name != 'model_setup' and name != 'derivative_list': ctrl.set_value(value) - - dlg.Destroy() + elif name == 'grouping_var': + if isinstance(value, list) or "[" in value: + grouping_var = "" + for cov in value: + grouping_var += "{0},".format(cov) + grouping_var = grouping_var.rstrip(",") + ctrl.set_value(grouping_var) + elif name != 'model_setup' and name != 'derivative_list': + ctrl.set_value(value) + dlg.Destroy() def read_phenotypic(self, pheno_file, ev_selections): @@ -450,8 +456,6 @@ def read_phenotypic(self, pheno_file, ev_selections): # Read in the phenotypic CSV file into a dictionary named pheno_dict # while preserving the header fields as they correspond to the data p_reader = csv.DictReader(open(os.path.abspath(ph), 'rU'), skipinitialspace=True) - - #pheno_dict_list = [] # dictionary to store the data in a format Patsy can use # i.e. a dictionary where each header is a key, and the value is a @@ -493,7 +497,6 @@ def read_phenotypic(self, pheno_file, ev_selections): return pheno_data_dict - ''' button: LOAD PHENOTYPE FILE ''' def populateEVs(self, event): @@ -577,7 +580,6 @@ def testFile(filepath, paramName): for sub in self.subs: if sub in row: break - for ctrl in self.page.get_ctrl_list(): @@ -616,7 +618,6 @@ def testFile(filepath, paramName): ctrl.set_value(formula_string) - ''' button: NEXT ''' def load_next_stage(self, event): @@ -628,8 +629,7 @@ def load_next_stage(self, event): name = ctrl.get_name() self.gpa_settings[name] = str(ctrl.get_selection()) - - + ### CHECK PHENOFILE if can open etc. # function for file path checking @@ -649,12 +649,10 @@ def testFile(filepath, paramName): errDlgFileTest.ShowModal() errDlgFileTest.Destroy() raise Exception - - + testFile(self.gpa_settings['participant_list'], 'Participant List') testFile(self.gpa_settings['pheno_file'], 'Phenotype/EV File') - phenoFile = open(os.path.abspath(self.gpa_settings['pheno_file']),"rU") phenoHeaderString = phenoFile.readline().rstrip('\r\n') @@ -672,8 +670,6 @@ def testFile(filepath, paramName): errSubID.Destroy() raise Exception - - for ctrl in self.page.get_ctrl_list(): name = ctrl.get_name() @@ -696,12 +692,10 @@ def testFile(filepath, paramName): self.gpa_settings['group_sep'] = ctrl.get_selection() - elif name == 'grouping_var': self.gpa_settings['grouping_var'] = ctrl.get_selection() - if ("list" in name) and (name != "participant_list"): self.gpa_settings[name] = [] @@ -711,7 +705,6 @@ def testFile(filepath, paramName): else: self.gpa_settings[name] = str(ctrl.get_selection()) - self.pheno_data_dict = self.read_phenotypic(self.gpa_settings['pheno_file'], \ self.gpa_settings['ev_selections']) @@ -750,7 +743,6 @@ def testFile(filepath, paramName): formula_strip = formula_strip.replace(')',' ') EVs_to_test = formula_strip.split() - # ensure the design formula only has valid EVs in it for EV in EVs_to_test: @@ -789,7 +781,6 @@ def testFile(filepath, paramName): raise Exception - if int_check != 1: errmsg = 'CPAC says: The interaction \'%s\' you ' \ @@ -805,7 +796,6 @@ def testFile(filepath, paramName): errSubID.Destroy() raise Exception - # ensure these interactions are input correctly elif (':' in EV) or ('/' in EV) or ('*' in EV): @@ -866,7 +856,6 @@ def testFile(filepath, paramName): raise Exception - # design formula/input parameters checks if "Custom_ROI_Mean" in formula and \ @@ -886,7 +875,6 @@ def testFile(filepath, paramName): raise Exception - if "Custom_ROI_Mean" not in formula and \ (self.gpa_settings['custom_roi_mask'] != None and \ self.gpa_settings['custom_roi_mask'] != "" and \ @@ -906,7 +894,7 @@ def testFile(filepath, paramName): errSubID.Destroy() raise Exception - + # if there is a custom ROI mean mask file provided, and the user # includes it as a regressor in their design matrix formula, calculate @@ -945,7 +933,6 @@ def testFile(filepath, paramName): for num in range(0,num_rois): custom_roi_labels.append("Custom_ROI_Mean_%d" % int(num+1)) - # pull in phenotype file try: @@ -956,13 +943,11 @@ def testFile(filepath, paramName): "details: %s\n\n" % (self.gpa_settings["pheno_file"], e) raise Exception(err) - # enforce the sub ID label to "Participant" pheno_df.rename(columns={self.gpa_settings["participant_id_label"]:"Participant"}, \ inplace=True) pheno_df["Participant"] = pheno_df["Participant"].astype(str) - # let's create dummy columns for MeanFD, Measure_Mean, and # Custom_ROI_Mask (if included in the Design Matrix Formula) just so we # can get an accurate list of EVs Patsy will generate @@ -992,7 +977,6 @@ def testFile(filepath, paramName): formula = formula.replace("Custom_ROI_Mean",add_formula_string) - repeated_sessions = False # if repeated measures @@ -1011,7 +995,6 @@ def testFile(filepath, paramName): self.gpa_settings["ev_selections"]["categorical"].append("Series") formula = formula + " + Series" - # if modeling group variances separately if str(self.gpa_settings["group_sep"]) == "On": @@ -1033,66 +1016,199 @@ def testFile(filepath, paramName): raise Exception - if self.gpa_settings["grouping_var"] not in formula: - - warn_string = "Note: You have specified '%s' as your " \ - "grouping variable for modeling the group variances " \ - "separately, but you have not included this variable " \ - "in your design formula.\n\nPlease include this " \ - "variable in your design, or choose a different " \ - "grouping variable." % self.gpa_settings["grouping_var"] + if self.gpa_settings["grouping_var"] not in formula: + + # if it's because we have multiple grouping variables in a + # list + if isinstance(self.gpa_settings["grouping_var"], list) or \ + "[" in self.gpa_settings["grouping_var"]: + for item in list(self.gpa_settings["grouping_var"]): + if item not in formula: + warn_string = "Note: You have specified '%s' as your " \ + "grouping variable for modeling the group variances " \ + "separately, but you have not included this variable " \ + "in your design formula.\n\nPlease include this " \ + "variable in your design, or choose a different " \ + "grouping variable." % item + + errSubID = wx.MessageDialog(self, warn_string, + 'Grouping Variable not in Design', wx.OK | wx.ICON_ERROR) + errSubID.ShowModal() + errSubID.Destroy() - errSubID = wx.MessageDialog(self, warn_string, - 'Grouping Variable not in Design', wx.OK | wx.ICON_ERROR) - errSubID.ShowModal() - errSubID.Destroy() + raise Exception - raise Exception + elif "," in self.gpa_settings["grouping_var"]: + for item in self.gpa_settings["grouping_var"].split(","): + if item not in formula: + warn_string = "Note: You have specified '%s' as your " \ + "grouping variable for modeling the group variances " \ + "separately, but you have not included this variable " \ + "in your design formula.\n\nPlease include this " \ + "variable in your design, or choose a different " \ + "grouping variable." % item + + errSubID = wx.MessageDialog(self, warn_string, + 'Grouping Variable not in Design', wx.OK | wx.ICON_ERROR) + errSubID.ShowModal() + errSubID.Destroy() + + raise Exception + + else: + warn_string = "Note: You have specified '%s' as your " \ + "grouping variable for modeling the group variances " \ + "separately, but you have not included this variable " \ + "in your design formula.\n\nPlease include this " \ + "variable in your design, or choose a different " \ + "grouping variable." % self.gpa_settings["grouping_var"] + + errSubID = wx.MessageDialog(self, warn_string, + 'Grouping Variable not in Design', wx.OK | wx.ICON_ERROR) + errSubID.ShowModal() + errSubID.Destroy() + + raise Exception if self.gpa_settings["grouping_var"] not in \ - self.gpa_settings["ev_selections"]["categorical"]: - - warn_string = "Note: The grouping variable must be one of " \ - "the categorical covariates." + self.gpa_settings["ev_selections"]["categorical"]: + + # if it's because we have multiple grouping variables in a + # list + if isinstance(self.gpa_settings["grouping_var"], list) or \ + "[" in self.gpa_settings["grouping_var"]: + for item in list(self.gpa_settings["grouping_var"]): + if item not in formula: + warn_string = "Note: The grouping variable must be one of " \ + "the categorical covariates." + + errSubID = wx.MessageDialog(self, warn_string, + 'Grouping Variable not Categorical', wx.OK | wx.ICON_ERROR) + errSubID.ShowModal() + errSubID.Destroy() - errSubID = wx.MessageDialog(self, warn_string, - 'Grouping Variable not Categorical', wx.OK | wx.ICON_ERROR) - errSubID.ShowModal() - errSubID.Destroy() + raise Exception - raise Exception + elif "," in self.gpa_settings["grouping_var"]: + for item in self.gpa_settings["grouping_var"].split(","): + if item not in formula: + warn_string = "Note: The grouping variable must be one of " \ + "the categorical covariates." + + errSubID = wx.MessageDialog(self, warn_string, + 'Grouping Variable not Categorical', wx.OK | wx.ICON_ERROR) + errSubID.ShowModal() + errSubID.Destroy() + + raise Exception + + else: + warn_string = "Note: The grouping variable must be one of " \ + "the categorical covariates." + + errSubID = wx.MessageDialog(self, warn_string, + 'Grouping Variable not Categorical', wx.OK | wx.ICON_ERROR) + errSubID.ShowModal() + errSubID.Destroy() + raise Exception # get ev list ev_list = parse_out_covariates(formula) - # split up the groups - pheno_df, grp_vector, new_ev_list, cat_list = split_groups(pheno_df, \ - self.gpa_settings["grouping_var"], ev_list, \ - self.gpa_settings["ev_selections"]["categorical"]) - - self.gpa_settings["ev_selections"]["categorical"] = cat_list - - # make the grouping variable categorical for Patsy (if we try to - # do this automatically below, it will categorical-ize all of - # the substrings too) - formula = formula.replace(self.gpa_settings["grouping_var"], \ - "C(" + self.gpa_settings["grouping_var"] \ - + ")") - if self.gpa_settings["coding_scheme"] == "Sum": - formula = formula.replace(")", ", Sum)") - - # update design formula - rename = {} - for old_ev in ev_list: - for new_ev in new_ev_list: - if old_ev + "__FOR" in new_ev: - if old_ev not in rename.keys(): - rename[old_ev] = [] - rename[old_ev].append(new_ev) - - for old_ev in rename.keys(): - formula = formula.replace(old_ev, " + ".join(rename[old_ev])) + if isinstance(self.gpa_settings["grouping_var"], list) or \ + "," in self.gpa_settings["grouping_var"]: + # if this happens, it's because the grouping variable has been + # set to multiple (dummy-coded) categorical covariates that + # have been set up by the new group analysis presets feature + # this code also lives in cpac_ga_model_generator! + # must consolidate! + group_ev = self.gpa_settings["grouping_var"] + + if not isinstance(group_ev, list): + group_ev = group_ev.split(",") + + grp_vector = [] + + if len(group_ev) == 2: + for x, y in zip(pheno_df[group_ev[0]], pheno_df[group_ev[1]]): + if x == 1: + grp_vector.append(1) + elif y == 1: + grp_vector.append(2) + else: + err = "\n\n[!] The two categorical covariates you " \ + "provided as the two separate groups (in order " \ + "to model each group's variances separately) " \ + "either have more than 2 levels (1/0), or are " \ + "not encoded as 1's and 0's.\n\nCovariates:\n" \ + "{0}\n{1}\n\n".format(group_ev[0], + group_ev[1]) + raise Exception(err) + + elif len(group_ev) == 3: + for x, y, z in zip(pheno_df[group_ev[0]], pheno_df[group_ev[1]], + pheno_df[group_ev[2]]): + if x == 1: + grp_vector.append(1) + elif y == 1: + grp_vector.append(2) + elif z == 1: + grp_vector.append(3) + else: + err = "\n\n[!] The three categorical covariates you " \ + "provided as the three separate groups (in order " \ + "to model each group's variances separately) " \ + "either have more than 2 levels (1/0), or are " \ + "not encoded as 1's and 0's.\n\nCovariates:\n" \ + "{0}\n{1}\n{2}\n\n".format(group_ev[0], + group_ev[1], + group_ev[2]) + raise Exception(err) + + else: + # we're only going to see this if someone plays around + # with their preset or config file manually + err = "\n\n[!] If you are seeing this message, it's because:\n" \ + "1. You are using the group-level analysis presets\n" \ + "2. You are running a model with multiple groups having " \ + "their variances modeled separately (i.e. multiple " \ + "values in the FSL FLAME .grp input file), and\n" \ + "3. For some reason, the configuration has been set up " \ + "in a way where CPAC currently thinks you're including " \ + "only one group, or more than three, neither of which " \ + "are supported.\n\nGroups provided:\n{0}" \ + "\n\n".format(str(group_ev)) + raise Exception(err) + + else: + # split up the groups + pheno_df, grp_vector, new_ev_list, cat_list = split_groups(pheno_df, \ + self.gpa_settings["grouping_var"], ev_list, \ + self.gpa_settings["ev_selections"]["categorical"]) + + self.gpa_settings["ev_selections"]["categorical"] = cat_list + + # make the grouping variable categorical for Patsy (if we try + # to do this automatically below, it will categorical-ize all + # of the substrings too) + formula = formula.replace(self.gpa_settings["grouping_var"], \ + "C(" + self.gpa_settings["grouping_var"] \ + + ")") + if self.gpa_settings["coding_scheme"] == "Sum": + formula = formula.replace(")", ", Sum)") + + # update design formula + rename = {} + for old_ev in ev_list: + for new_ev in new_ev_list: + if old_ev + "__FOR" in new_ev: + if old_ev not in rename.keys(): + rename[old_ev] = [] + rename[old_ev].append(new_ev) + + for old_ev in rename.keys(): + formula = formula.replace(old_ev, " + ".join(rename[old_ev])) # remove duplicates self.gpa_settings["ev_selections"]["categorical"] = \ @@ -1100,12 +1216,21 @@ def testFile(filepath, paramName): # categorical-ize design formula if 'categorical' in self.gpa_settings['ev_selections']: + + # pad with spaces if they aren't present + formula = formula.replace("+", " + ") + formula = formula.replace("-", " - ") + formula = formula.replace("=", " = ") + formula = formula.replace("(", " ( ").replace(")", " ) ") + formula = formula.replace("*", " * ") + formula = formula.replace("/", " / ") + for EV_name in self.gpa_settings['ev_selections']['categorical']: if self.gpa_settings['coding_scheme'] == 'Treatment': - formula = formula.replace(EV_name, 'C(' + EV_name + ')') + formula = formula.replace(" {0} ".format(EV_name), 'C(' + EV_name + ')') elif self.gpa_settings['coding_scheme'] == 'Sum': - formula = formula.replace(EV_name, 'C(' + EV_name + ', Sum)') + formula = formula.replace(" {0} ".format(EV_name), 'C(' + EV_name + ', Sum)') # let's avoid an Intercept unless the user explicitly wants one # (then they need to include "+ Intercept" into the formula) @@ -1135,7 +1260,6 @@ def testFile(filepath, paramName): print "Patsy error: %s\n\n" % e raise Exception - column_names = dmatrix.design_info.column_names subFile = open(os.path.abspath(self.gpa_settings['participant_list'])) diff --git a/CPAC/pipeline/cpac_ga_model_generator.py b/CPAC/pipeline/cpac_ga_model_generator.py index 3eda58de2b..ca16df28c0 100755 --- a/CPAC/pipeline/cpac_ga_model_generator.py +++ b/CPAC/pipeline/cpac_ga_model_generator.py @@ -308,7 +308,7 @@ def parse_out_covariates(design_formula): for op in patsy_ops: if op in design_formula: - design_formula = design_formula.replace(op," ") + design_formula = design_formula.replace(op, " ") words = design_formula.split(" ") @@ -323,8 +323,7 @@ def split_groups(pheno_df, group_ev, ev_list, cat_list): new_ev_list = [] new_cat_list = [] - print group_ev - print cat_list + if group_ev not in cat_list: err = "\n\n[!] The grouping variable must be one of the categorical "\ "covariates!\n\n" @@ -399,10 +398,18 @@ def patsify_design_formula(formula, categorical_list, encoding="Treatment"): elif encoding == "Sum": closer = ", Sum)" + # pad with spaces if they aren't present + formula = formula.replace("+", " + ") + formula = formula.replace("-", " - ") + formula = formula.replace("=", " = ") + formula = formula.replace("(", " ( ").replace(")", " ) ") + formula = formula.replace("*", " * ") + formula = formula.replace("/", " / ") + for ev in categorical_list: if ev in formula: new_ev = "C(" + ev + closer - formula = formula.replace(ev, new_ev) + formula = formula.replace(" {0} ".format(ev), new_ev) # remove Intercept - if user wants one, they should add "+ Intercept" when # specifying the design formula @@ -521,13 +528,13 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ # determine if f-tests are included or not custom_confile = group_config_obj.custom_contrasts - if ((custom_confile == None) or (custom_confile == '') or \ + if ((custom_confile is None) or (custom_confile == '') or ("None" in custom_confile) or ("none" in custom_confile)): custom_confile = None if (len(group_config_obj.f_tests) == 0) or \ - (group_config_obj.f_tests == None): + (group_config_obj.f_tests is None): fTest = False else: fTest = True @@ -559,9 +566,9 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ # create path for output directory out_dir = os.path.join(group_config_obj.output_dir, - "group_analysis_results_%s" % pipeline_ID, - "group_model_%s" % model_name, resource_id, - series_or_repeated_label, preproc_strat) + "group_analysis_results_%s" % pipeline_ID, + "group_model_%s" % model_name, resource_id, + series_or_repeated_label, preproc_strat) if 'sca_roi' in resource_id: out_dir = os.path.join(out_dir, @@ -611,7 +618,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ # create new subject list based on which subjects are left after checking # for missing outputs new_participant_list = [] - for part in list(model_df["Participant"]): + for part in list(model_df["participant_id"]): # do this instead of using "set" just in case, to preserve order # only reason there may be duplicates is because of multiple-series # repeated measures runs @@ -624,7 +631,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ group_config_obj.update('participant_list',new_sub_file) - num_subjects = len(list(model_df["Participant"])) + num_subjects = len(list(model_df["participant_id"])) # start processing the dataframe further design_formula = group_config_obj.design_formula @@ -660,7 +667,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ else: individual_masks_dir = os.path.join(model_path, "individual_masks") create_dir(individual_masks_dir, "individual masks") - for unique_id, series_id, raw_filepath in zip(model_df["Participant"], + for unique_id, series_id, raw_filepath in zip(model_df["participant_id"], model_df["Series"], model_df["Raw_Filepath"]): mask_for_means_path = os.path.join(individual_masks_dir, @@ -739,39 +746,106 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ if group_config_obj.group_sep: - # model group variances separately - old_ev_list = ev_list - - model_df, grp_vector, ev_list, cat_list = split_groups(model_df, \ - group_config_obj.grouping_var, \ - ev_list, cat_list) - - # make the grouping variable categorical for Patsy (if we try to - # do this automatically below, it will categorical-ize all of - # the substrings too) - design_formula = design_formula.replace(group_config_obj.grouping_var, \ - "C(" + group_config_obj.grouping_var + ")") - if group_config_obj.coding_scheme == "Sum": - design_formula = design_formula.replace(")", ", Sum)") - - # update design formula - rename = {} - for old_ev in old_ev_list: - for new_ev in ev_list: - if old_ev + "__FOR" in new_ev: - if old_ev not in rename.keys(): - rename[old_ev] = [] - rename[old_ev].append(new_ev) - - for old_ev in rename.keys(): - design_formula = design_formula.replace(old_ev, \ - " + ".join(rename[old_ev])) + # check if the group_ev parameter is a list instead of a string: + # this was added to handle the new group-level analysis presets. this is + # the only modification that was required to the group analysis workflow, + # and it handles cases where the group variances must be modeled + # separately, by creating separate groups for the FSL FLAME .grp file. + # the group_ev parameter gets sent in as a list if coming from any + # of the presets that deal with multiple groups- in these cases, + # the pheno_df/design matrix is already set up properly for the + # multiple groups, and we need to bypass all of the processing + # that usually occurs when the "modeling group variances + # separately" option is enabled in the group analysis config YAML + group_ev = group_config_obj.grouping_var + + if isinstance(group_ev, list): + + grp_vector = [] + + if len(group_ev) == 2: + for x, y in zip(model_df[group_ev[0]], model_df[group_ev[1]]): + if x == 1: + grp_vector.append(1) + elif y == 1: + grp_vector.append(2) + else: + err = "\n\n[!] The two categorical covariates you " \ + "provided as the two separate groups (in order " \ + "to model each group's variances separately) " \ + "either have more than 2 levels (1/0), or are " \ + "not encoded as 1's and 0's.\n\nCovariates:\n" \ + "{0}\n{1}\n\n".format(group_ev[0], group_ev[1]) + raise Exception(err) + + elif len(group_ev) == 3: + for x, y, z in zip(model_df[group_ev[0]], model_df[group_ev[1]], + model_df[group_ev[2]]): + if x == 1: + grp_vector.append(1) + elif y == 1: + grp_vector.append(2) + elif z == 1: + grp_vector.append(3) + else: + err = "\n\n[!] The three categorical covariates you " \ + "provided as the three separate groups (in order " \ + "to model each group's variances separately) " \ + "either have more than 2 levels (1/0), or are " \ + "not encoded as 1's and 0's.\n\nCovariates:\n" \ + "{0}\n{1}\n{2}\n\n".format(group_ev[0], + group_ev[1], + group_ev[2]) + raise Exception(err) + + else: + # we're only going to see this if someone plays around with + # their preset or config file manually + err = "\n\n[!] If you are seeing this message, it's because:\n" \ + "1. You are using the group-level analysis presets\n" \ + "2. You are running a model with multiple groups having " \ + "their variances modeled separately (i.e. multiple " \ + "values in the FSL FLAME .grp input file), and\n" \ + "3. For some reason, the configuration has been set up " \ + "in a way where CPAC currently thinks you're including " \ + "only one group, or more than three, neither of which " \ + "are supported.\n\nGroups provided:\n{0}" \ + "\n\n".format(str(group_ev)) + raise Exception(err) + else: + # model group variances separately + old_ev_list = ev_list + + model_df, grp_vector, ev_list, cat_list = split_groups(model_df, \ + group_config_obj.grouping_var, \ + ev_list, cat_list) + + # make the grouping variable categorical for Patsy (if we try to + # do this automatically below, it will categorical-ize all of + # the substrings too) + design_formula = design_formula.replace(group_config_obj.grouping_var, \ + "C(" + group_config_obj.grouping_var + ")") + if group_config_obj.coding_scheme == "Sum": + design_formula = design_formula.replace(")", ", Sum)") + + # update design formula + rename = {} + for old_ev in old_ev_list: + for new_ev in ev_list: + if old_ev + "__FOR" in new_ev: + if old_ev not in rename.keys(): + rename[old_ev] = [] + rename[old_ev].append(new_ev) + + for old_ev in rename.keys(): + design_formula = design_formula.replace(old_ev, + " + ".join(rename[old_ev])) # prep design formula for Patsy - design_formula = patsify_design_formula(design_formula, cat_list, \ - group_config_obj.coding_scheme[0]) - print design_formula + design_formula = patsify_design_formula(design_formula, cat_list, + group_config_obj.coding_scheme[0]) + # send to Patsy try: dmatrix = patsy.dmatrix(design_formula, model_df) @@ -779,24 +853,17 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ err = "\n\n[!] Something went wrong with processing the group model "\ "design matrix using the Python Patsy package. Patsy might " \ "not be properly installed, or there may be an issue with the "\ - "formatting of the design matrix.\n\nPatsy-formatted design " \ - "formula: %s\n\nError details: %s\n\n" \ - % (model_df.columns, design_formula, e) + "formatting of the design matrix.\n\nDesign matrix columns: " \ + "%s\n\nPatsy-formatted design formula: %s\n\nError details: " \ + "%s\n\n" % (model_df.columns, design_formula, e) raise Exception(err) - print dmatrix.design_info.column_names - print dmatrix - # check the model for multicollinearity - Patsy takes care of this, but # just in case check_multicollinearity(np.array(dmatrix)) # prepare for final stages column_names = dmatrix.design_info.column_names - - # what is this for? - design_matrix = np.array(dmatrix, dtype=np.float16) - # check to make sure there are more time points than EVs! if len(column_names) >= num_subjects: @@ -863,7 +930,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ model_name, resource_id, model_path) dmat_csv_path = os.path.join(model_path, "design_matrix.csv") - write_design_matrix_csv(dmatrix, model_df["Participant"], column_names, + write_design_matrix_csv(dmatrix, model_df["participant_id"], column_names, dmat_csv_path) # workflow time diff --git a/CPAC/pipeline/cpac_group_runner.py b/CPAC/pipeline/cpac_group_runner.py index d6efe4905e..43805acb97 100644 --- a/CPAC/pipeline/cpac_group_runner.py +++ b/CPAC/pipeline/cpac_group_runner.py @@ -294,7 +294,7 @@ def create_output_dict_list(nifti_globs, pipeline_output_folder, new_row_dict = {} - new_row_dict["Participant"] = unique_id + new_row_dict["participant_id"] = unique_id new_row_dict["Series"] = series_id new_row_dict["Filepath"] = filepath @@ -337,7 +337,7 @@ def create_output_df_dict(output_dict_list, inclusion_list=None): # drop whatever is not in the inclusion lists if inclusion_list: - new_df = new_df[new_df.Participant.isin(inclusion_list)] + new_df = new_df[new_df.participant_id.isin(inclusion_list)] # unique_resource_id is tuple (resource_id,strat_info) if unique_resource_id not in output_df_dict.keys(): @@ -375,8 +375,8 @@ def pheno_sessions_to_repeated_measures(pheno_df, sessions_list): participant_id_cols = {} i = 0 - for participant_unique_id in list(pheno_df["Participant"]): - part_col = [0] * len(pheno_df["Participant"]) + for participant_unique_id in list(pheno_df["participant_id"]): + part_col = [0] * len(pheno_df["participant_id"]) for session in sessions_list: if session in participant_unique_id: # generate/update sessions categorical column @@ -394,7 +394,7 @@ def pheno_sessions_to_repeated_measures(pheno_df, sessions_list): i += 1 pheno_df["Session"] = sessions_col - pheno_df["Participant_ID"] = part_ids_col + pheno_df["participant"] = part_ids_col # add new participant ID columns for new_col in participant_id_cols.keys(): @@ -428,9 +428,9 @@ def pheno_series_to_repeated_measures(pheno_df, series_list, \ participant_id_cols = {} i = 0 - for participant_unique_id in pheno_df["Participant"]: + for participant_unique_id in pheno_df["participant_id"]: - part_col = [0] * len(pheno_df["Participant"]) + part_col = [0] * len(pheno_df["participant_id"]) header_title = "participant_%s" % participant_unique_id if header_title not in participant_id_cols.keys(): @@ -461,7 +461,7 @@ def balance_repeated_measures(pheno_df, sessions_list, series_list=None): from collections import Counter - part_ID_count = Counter(pheno_df["Participant_ID"]) + part_ID_count = Counter(pheno_df["participant_ID"]) if series_list: sessions_x_series = len(sessions_list) * len(series_list) @@ -472,7 +472,7 @@ def balance_repeated_measures(pheno_df, sessions_list, series_list=None): for part_ID in part_ID_count.keys(): if part_ID_count[part_ID] != sessions_x_series: - pheno_df = pheno_df[pheno_df.Participant_ID != part_ID] + pheno_df = pheno_df[pheno_df.participant != part_ID] del pheno_df["participant_%s" % part_ID] dropped_parts.append(part_ID) @@ -589,9 +589,9 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): pheno_df = load_pheno_csv_into_df(group_model.pheno_file) # enforce the sub ID label to "Participant" - pheno_df.rename(columns={group_model.participant_id_label:"Participant"}, \ + pheno_df.rename(columns={group_model.participant_id_label:"participant_id"}, \ inplace=True) - pheno_df["Participant"] = pheno_df["Participant"].astype(str) + pheno_df["participant_id"] = pheno_df["participant_id"].astype(str) # unique_resource = (output_measure_type, preprocessing strategy) # output_df_dict[unique_resource] = dataframe @@ -617,7 +617,7 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): inclusion_list = load_text_file(group_model.participant_list, \ "group-level analysis participant list") output_df = \ - output_df[output_df["Participant"].isin(inclusion_list)] + output_df[output_df["participant_id"].isin(inclusion_list)] new_pheno_df = pheno_df.copy() @@ -656,7 +656,7 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # sessions and all series, because in repeated measures/ # within-subject, if one goes, they all have to go new_pheno_df = \ - new_pheno_df[pheno_df["Participant"].isin(output_df["Participant"])] + new_pheno_df[pheno_df["participant_id"].isin(output_df["participant_id"])] if len(new_pheno_df) == 0: err = "\n\n[!] There is a mis-match between the "\ @@ -664,7 +664,7 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): "ant list and the phenotype file.\n\n" raise Exception(err) - join_columns = ["Participant"] + join_columns = ["participant_id"] # if Series is one of the categorically-encoded covariates, # make sure we only are including the series the user has @@ -710,8 +710,8 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): series_df = series_df_tuple[1] # trim down the pheno DF to match the output DF and merge - newer_pheno_df = new_pheno_df[pheno_df["Participant"].isin(series_df["Participant"])] - newer_pheno_df = pd.merge(new_pheno_df, series_df, how="inner", on=["Participant"]) + newer_pheno_df = new_pheno_df[pheno_df["participant_id"].isin(series_df["participant_id"])] + newer_pheno_df = pd.merge(new_pheno_df, series_df, how="inner", on=["participant_id"]) # this can be removed/modified once sessions are no # longer integrated in the full unique participant IDs @@ -739,8 +739,8 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # series_df = output_df but with only one of the Series series_df = series_df_tuple[1] # trim down the pheno DF to match the output DF and merge - newer_pheno_df = new_pheno_df[pheno_df["Participant"].isin(series_df["Participant"])] - newer_pheno_df = pd.merge(new_pheno_df, series_df, how="inner", on=["Participant"]) + newer_pheno_df = new_pheno_df[pheno_df["participant_id"].isin(series_df["participant_id"])] + newer_pheno_df = pd.merge(new_pheno_df, series_df, how="inner", on=["participant_id"]) # send it in analysis_dict[(model_name, group_config_file, resource_id, strat_info, series)] = newer_pheno_df diff --git a/CPAC/utils/create_flame_model_files.py b/CPAC/utils/create_flame_model_files.py index 0181f220bc..2242831d97 100644 --- a/CPAC/utils/create_flame_model_files.py +++ b/CPAC/utils/create_flame_model_files.py @@ -197,7 +197,6 @@ def create_fts_file(ftest_list, con_names, model_name, return out_file - def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ column_names, coding_scheme, group_sep): @@ -208,16 +207,17 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ import os import numpy as np - # Read the header of the contrasts file, which should contain the columns of the design matrix and f-tests (if any) + # Read the header of the contrasts file, which should contain the columns + # of the design matrix and f-tests (if any) with open(con_file,"r") as f: evs = f.readline() evs = evs.rstrip('\r\n').split(',') - - if evs[0].strip().lower()!="contrasts": + if evs[0].strip().lower() != "contrasts": print "Error: first cell in contrasts file should contain 'Contrasts' " raise Exception + # remove "Contrasts" label and replace it with "Intercept" evs[0] = "Intercept" @@ -234,11 +234,12 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ print "Error: Could not successfully read in contrast file: ",con_file raise Exception - lst = contrasts_data.tolist() - # lst = list of rows of the contrast matrix (each row represents a contrast, - # i.e. a name of the contrast, and then coefficients for each of the design matrix columns, and finally - # coefficients for each of the f tests specifying whether this contrast is part of that particular f test). + # lst = list of rows of the contrast matrix (each row represents a + # contrast, i.e. a name of the contrast, and then coefficients for each of + # the design matrix columns, and finally coefficients for each of the + # f tests specifying whether this contrast is part of that particular + # f test). ftst = [] fts_columns = [] @@ -266,9 +267,10 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ if group_sep == False: if False: - # The following insertion gives an error further down the line, because this - # suggests that there will be an intercept column in the design matrix - # but such an intercept is never actually added. + # The following insertion gives an error further down the + # line, because this suggests that there will be an intercept + # column in the design matrix but such an intercept is never + # actually added. if coding_scheme == "Treatment": con_vector.insert(0, 0) elif coding_scheme == "Sum": @@ -284,7 +286,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ contrasts = np.array(contrasts, dtype=np.float16) fts_columns = np.array(fts_columns) - # if there are f-tests, create the array for them if fTest: @@ -302,8 +303,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ fts_n = fts_columns.T - - if len(column_names) != (num_EVs_in_con_file): err_string = "\n\n[!] CPAC says: The number of EVs in your model " \ @@ -320,7 +319,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ raise Exception(err_string) - for design_mat_col, con_csv_col in zip(column_names, evs[1:]): ## TODO: Possible source for errors: the script seems to suggest it checks @@ -338,8 +336,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ raise Exception(errmsg) - - out_file = os.path.join(output_dir, model_name + '.con') with open(out_file,"wt") as f: @@ -365,7 +361,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ col_string = col_string + ev + '\t' print >>f, col_string, '\n' - print >>f, '/Matrix' np.savetxt(f, contrasts, fmt='%1.5e', delimiter='\t') diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_group_analysis_files.py index efe714e513..b6b1a45b34 100644 --- a/CPAC/utils/create_group_analysis_files.py +++ b/CPAC/utils/create_group_analysis_files.py @@ -1,4 +1,7 @@ +# TODO: create a function that can help easily map raw pheno files that do not +# TODO: have the participant_session id that CPAC uses + def read_group_list_text_file(group_list_text_file): """Read in the group-level analysis participant-session list text file.""" @@ -24,14 +27,81 @@ def read_pheno_csv_into_df(pheno_csv): return pheno_df -def get_pheno_evs(pheno_csv, ev_selections): - """Get the columns of phenotypic data matched with their participant- - session labels as a Pandas DataFrame.""" - pass +def write_group_list_text_file(group_list, out_file=None): + """Write out the group-level analysis participant list as a text file.""" + + import os + + if not out_file: + out_file = os.path.join(os.getcwd(), "group_analysis_participant_" + "list.txt") + else: + out_file = os.path.abspath(out_file) + dir_path = out_file.split(os.path.basename(out_file))[0] + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + with open(out_file, "wt") as f: + for part_id in group_list: + f.write("{0}\n".format(part_id)) + + return out_file + + +def write_dataframe_to_csv(matrix_df, out_file=None): + """Write out a matrix Pandas DataFrame into a CSV file.""" + + import os + + if not out_file: + out_file = os.path.join(os.getcwd(), "matrix.csv") + else: + out_file = os.path.abspath(out_file) + dir_path = out_file.split(os.path.basename(out_file))[0] + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + matrix_df.to_csv(out_file, index=False) + + +def write_config_dct_to_yaml(config_dct, out_file=None): + """Write out a configuration dictionary into a YAML file.""" + + import os + import CPAC + + if not out_file: + out_file = os.path.join(os.getcwd(), "gpa_fsl_config.yml") + else: + out_file = os.path.abspath(out_file) + dir_path = out_file.split(os.path.basename(out_file))[0] + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + if not out_file.endswith(".yml"): + out_file = "{0}.yml".format(out_file) + + field_order = ['participant_list', 'pheno_file', 'ev_selections', + 'participant_id_label', 'design_formula', 'mean_mask', + 'custom_roi_mask', 'derivative_list', 'coding_scheme', + 'group_sep', 'grouping_var', 'z_threshold', 'p_threshold', + 'sessions_list', 'series_list', 'contrasts', 'f_tests', + 'custom_contrasts', 'model_name', 'output_dir'] + + with open(out_file, "wt") as f: + f.write("# CPAC Group-Level Analysis Configuration File\n" + "# Version {0}\n".format(CPAC.__version__)) + f.write("#\n# http://fcp-indi.github.io for more info.\n#\n" + "# Tip: This file can be edited manually with " + "a text editor for quick modifications.\n\n") + for key in field_order: + val = config_dct[key] + f.write("{0}: {1}\n\n".format(key, val)) def create_design_matrix_df(group_list, pheno_df=None, - ev_selections=None, pheno_sub_label=None): + ev_selections=None, pheno_sub_label=None, + pheno_ses_label=None, pheno_site_label=None): """Create the design matrix intended for group-level analysis via the FSL FLAME tool. @@ -48,51 +118,142 @@ def create_design_matrix_df(group_list, pheno_df=None, # map the participant-session IDs to just participant IDs group_list_map = {} for part_ses in group_list: - group_list_map[part_ses.split("_")[0]] = part_ses + sub_id = part_ses.split("_")[0] + ses_id = part_ses.split("_")[1] + group_list_map[part_ses] = [sub_id, ses_id, part_ses] - # initialize a Pandas DataFrame for the new design matrix - design_df = pd.DataFrame() + # create a dataframe mapping the 'sub01_ses-1' CPAC-style unique IDs to + # subject and session columns, like this: + # sub01_ses-1 sub01 ses-1 + # sub02_ses-1 sub02 ses-1 + map_df = pd.DataFrame.from_dict(group_list_map, orient='index') - # initialize the rows (no columns yet!) - design_df["Participant-Session"] = group_list + # also, rename the columns to be easier + map_df = map_df.rename( + columns={0: 'participant', 1: 'session', 2: 'participant_id'}) - if pheno_df: - # if a phenotype CSV file is provided with the data + # sort by sub_id and then ses_id + map_df = map_df.sort_values(by=['participant_id']) - # TODO: next- this aligning of the pheno and the design DF - # TODO: use the group_map to line up the pheno_sub_label column in the - # TODO: pheno DF with the design DF rows- then grab your desired EV - # TODO: column(s) and merge it with the design DF basically + # drop unique_id column (does it ever need to really be included?) + # was just keeping it in up until here for mental book-keeping if anything + map_df = map_df[['participant_id', 'participant', 'session']] + + if pheno_df is None: + # no phenotypic matrix provided; simpler design models + design_df = map_df[['participant_id']] + + else: + # if a phenotype CSV file is provided with the data # align the pheno's participant ID column with the group sublist text # file - if pheno_sub_label: - pass - - # add any selected EVs to the design matrix from the phenotype file - for ev in ev_selections: - pass + if not pheno_sub_label: + # TODO: exception message + raise Exception("there's a pheno file, but no pheno sub label") + else: + # rename the pheno sub label thingy + pheno_df = pheno_df.rename( + columns={pheno_sub_label: 'participant_id'}) + if ev_selections: + ev_selections.insert(0, 'participant_id') + + if pheno_ses_label: + # if sessions are important in the model, do this also + pheno_df = pheno_df.rename( + columns={pheno_ses_label: 'session'}) + if ev_selections: + ev_selections.append(1, 'session') + + if pheno_site_label: + # and if sites are important as well, same here + pheno_df = pheno_df.rename( + columns={pheno_site_label: 'site'}) + if ev_selections: + ev_selections.append(2, 'site') + + if ev_selections: + # get specific covariates! + pheno_df = pheno_df[ev_selections] + + # merge + if pheno_ses_label: + design_df = pheno_df.merge(map_df, on=['participant_id']) + else: + design_df = pheno_df.merge(map_df[['participant_id']], + on='participant_id') return design_df -def create_contrasts_template_df(design_df): +def create_contrasts_template_df(design_df, contrasts_dct_list=None): """Create the template Pandas DataFrame for the contrasts matrix CSV. The headers in the contrasts matrix needs to match the headers of the design matrix.""" - pass + + import pandas as pd + + contrast_cols = list(design_df.columns) + contrast_cols.remove('participant_id') + + # TODO: + # if session, if site, remove + + if contrasts_dct_list: + # if we are initializing the contrasts matrix with pre-set contrast + # vectors - just check for accuracy here + for contrast_dct in contrasts_dct_list: + # contrast_dct is a dictionary with each column name mapped to its + # contrast vector value, like this: + # {contrast: "Group Mean", "Group Mean": 1, "age": 0} + if (len(contrast_dct.keys()) - 1) != len(contrast_cols): + # it's -1 because of the "contrast" column in contrast_dct + # TODO: message + raise Exception("number of columns in the contrast vector " + "does not match the number of covariate " + "columns in the design matrix") + + else: + # if default, start it up with a blank "template" contrast vector + contrast_one = {"contrasts": "contrast_1"} + contrast_two = {"contrasts": "contrast_2"} + + for col in contrast_cols: + contrast_one.update({col: 0}) + contrast_two.update({col: 0}) + + contrasts_dct_list = [contrast_one, contrast_two] + + contrast_cols.insert(0, "contrasts") + + # now, make the actual dataframe + contrasts_df = pd.DataFrame(contrasts_dct_list) + + # order the columns properly + contrasts_df = contrasts_df[contrast_cols] + + return contrasts_df def preset_single_group_avg(group_list, pheno_df=None, covariate=None, - pheno_sub_label=None): + pheno_sub_label=None, output_dir=None, + model_name="one_sample_T-test"): """Set up the design matrix CSV for running a single group average (one-sample T-test).""" + import os + + if not output_dir: + output_dir = os.getcwd() + + id_cols = ["participant_id", "participant", "session", "site"] + ev_selections = None - if pheno_df and covariate and pheno_sub_label: - # if we're adding an additional covariate - ev_selections = [covariate] + if pheno_df is not None: + if covariate and pheno_sub_label: + # if we're adding an additional covariate + ev_selections = [covariate] design_df = create_design_matrix_df(group_list, pheno_df, ev_selections=ev_selections, @@ -100,15 +261,301 @@ def preset_single_group_avg(group_list, pheno_df=None, covariate=None, design_df["Group Mean"] = 1 - return design_df + group_mean_contrast = {"contrasts": "Group Mean"} + # make these loops in case we expand this to handle more than one + # covariate past the Group Mean + for col in design_df.columns: + if col not in id_cols: + if col == "Group Mean": + group_mean_contrast.update({col: 1}) + else: + group_mean_contrast.update({col: 0}) -def write_dataframe_to_csv(design_df): - """Write out a matrix Pandas DataFrame into a CSV file.""" - pass + contrasts = [group_mean_contrast] + + if covariate: + covariate_contrast = {"contrasts": covariate} + + for col in design_df.columns: + if col not in id_cols: + if col == covariate: + covariate_contrast.update({col: 1}) + else: + covariate_contrast.update({col: 0}) + + contrasts.append(covariate_contrast) + + contrasts_df = create_contrasts_template_df(design_df, contrasts) + + # create design and contrasts matrix file paths + design_mat_path = os.path.join(output_dir, model_name, + "design_matrix_{0}.csv".format(model_name)) + + contrasts_mat_path = os.path.join(output_dir, model_name, + "contrasts_matrix_{0}.csv" + "".format(model_name)) + + # start group config yaml dictionary + design_formula = "Group Mean" + if covariate: + design_formula = "{0} + {1}".format(design_formula, covariate) + + group_config = {"pheno_file": design_mat_path, + "participant_id_label": pheno_sub_label, + "ev_selections": {"demean": [covariate], + "categorical": ["Group Mean"]}, + "design_formula": design_formula, + "group_sep": "Off", + "grouping_var": None, + "sessions_list": [], + "series_list": [], + "custom_contrasts": contrasts_mat_path, + "model_name": model_name, + "output_dir": output_dir} + + return design_df, contrasts_df, group_config + + +def preset_unpaired_two_group(group_list, pheno_df, groups, pheno_sub_label, + output_dir=None, + model_name="two_sample_unpaired_T-test"): + """Set up the design matrix and contrasts matrix for running an unpaired + two-group difference (two-sample unpaired T-test).""" + + import os + + if not output_dir: + output_dir = os.getcwd() + + id_cols = ["participant_id", "participant", "session", "site"] + + # if the two groups are encoded in one categorical EV/column, then we will + # have to dummy code them out + # if this is the case, then "groups" will be a list with only one + # element in it- the one EV/column that is to be split up into two + ev_selections = [] + for group in groups: + ev_selections.append(group) + + design_df = create_design_matrix_df(group_list, pheno_df, + ev_selections=ev_selections, + pheno_sub_label=pheno_sub_label) + if len(groups) == 1: + # we're going to split the one categorical EV into two + new_groups = [] + + # get full range of values in one-column categorical EV + group_set = list(set(design_df[groups[0]])) + + # this preset is for an unpaired two-group difference- should only be + # two groups encoded in this EV! + if len(group_set) > 2: + # TODO: message + raise Exception("more than two groups provided, but this is a" + "model for a two-group difference") + + # create the two new dummy-coded columns + # column 1 + # new column name + new_name = "{0}-{1}".format(groups[0], group_set[0]) + # create new column encoded in 0's + design_df[new_name] = 0 + # map the relevant values into 1's + design_df[new_name] = design_df[groups[0]].map({group_set[0]: 1, + group_set[1]: 0}) + # update groups list + new_groups.append(new_name) + + # column 2 + # new column name + new_name = "{0}-{1}".format(groups[0], group_set[1]) + # create new column encoded in 0's + design_df[new_name] = 0 + # map the relevant values into 1's + design_df[new_name] = design_df[groups[0]].map({group_set[1]: 1, + group_set[0]: 0}) + # update groups list + new_groups.append(new_name) + + # drop original EV/column + del design_df[groups[0]] + + # update groups list + groups = new_groups + + # start the contrasts + contrast_one = {"contrasts": "{0} - {1}".format(groups[0], groups[1])} + contrast_two = {"contrasts": "{0} - {1}".format(groups[1], groups[0])} + + # make these loops in case we expand this to handle additional covariates + # past the "prescribed" ones in the model/preset + for col in design_df.columns: + if col not in id_cols: + if col == groups[0]: + contrast_one.update({col: 1}) + contrast_two.update({col: -1}) + elif col == groups[1]: + contrast_one.update({col: -1}) + contrast_two.update({col: 1}) + else: + contrast_one.update({col: 0}) + contrast_two.update({col: 0}) + + contrasts = [contrast_one, contrast_two] + + contrasts_df = create_contrasts_template_df(design_df, contrasts) + + # create design and contrasts matrix file paths + design_mat_path = os.path.join(output_dir, model_name, + "design_matrix_{0}.csv".format(model_name)) + + contrasts_mat_path = os.path.join(output_dir, model_name, + "contrasts_matrix_{0}.csv" + "".format(model_name)) + + # start group config yaml dictionary + design_formula = "{0} + {1}".format(groups[0], groups[1]) + + group_config = {"pheno_file": design_mat_path, + "participant_id_label": pheno_sub_label, + "ev_selections": {"demean": [], + "categorical": groups}, + "design_formula": design_formula, + "group_sep": "On", + "grouping_var": groups, + "sessions_list": [], + "series_list": [], + "custom_contrasts": contrasts_mat_path, + "model_name": model_name, + "output_dir": os.path.join(output_dir, model_name)} + + return design_df, contrasts_df, group_config + + +def run(group_list_text_file, derivative_list, z_thresh, p_thresh, + preset=None, pheno_file=None, pheno_sub_label=None, output_dir=None, + model_name=None, covariate=None): + + # TODO: set this up to run regular group analysis with no changes to its + # TODO: original flow- use the generated pheno as the pheno, use the + # TODO: contrasts DF as a custom contrasts matrix, and auto-generate the + # TODO: group analysis config YAML as well - but factor in how to have the + # TODO: user easily/seamlessly decide on derivatives, output dir, and + # TODO: model name + + # TODO: up next- create some kind of CLI to run this easily from the + # TODO: command line- test it for single grp AVG, then fix the .grp thing + # TODO: THEN continue expanding this + + # NOTE: the input parameters above may come in as a dictionary instead + # or something + + import os + + if pheno_file and not pheno_sub_label: + # TODO: message + raise Exception("pheno file provided, but no pheno sub label") + + if pheno_sub_label and not pheno_file: + # TODO: message + raise Exception("pheno sub label provided, but no pheno file") + + if isinstance(group_list_text_file, list): + group_list = group_list_text_file + + # write out a group analysis sublist text file so that it can be + # linked in the group analysis config yaml + out_list = os.path.join(output_dir, model_name, + "gpa_participant_list_" + "{0}.txt".format(model_name)) + group_list_text_file = write_group_list_text_file(group_list, + out_list) + else: + group_list = read_group_list_text_file(group_list_text_file) + + group_config = {"participant_list": group_list_text_file, + "mean_mask": ["Group Mask"], + "custom_roi_mask": None, + "derivative_list": derivative_list, + "coding_scheme": ["Treatment"], + "z_threshold": [z_thresh], + "p_threshold": [p_thresh], + "contrasts": [], + "f_tests": []} + + if not preset: + # TODO: this + pass + + elif preset == "single_grp": + design_df, contrasts_df, group_config_update = \ + preset_single_group_avg(group_list, pheno_df=None, covariate=None, + pheno_sub_label=None, + output_dir=output_dir, + model_name=model_name) + + group_config.update(group_config_update) + + elif preset == "single_grp_cov": + + if not pheno_file: + # TODO: message + raise Exception("pheno file not provided") + + if not covariate: + # TODO: message + raise Exception("covariate not provided") + + pheno_df = read_pheno_csv_into_df(pheno_file) + + design_df, contrasts_df, group_config_update = \ + preset_single_group_avg(group_list, pheno_df, covariate=covariate, + pheno_sub_label=pheno_sub_label, + output_dir=output_dir, + model_name=model_name) + + group_config.update(group_config_update) + + elif preset == "unpaired_two": + + if not pheno_file: + # TODO: message + raise Exception("pheno file not provided") + + if not covariate: + # TODO: message + raise Exception("the two groups were not provided") + + # we're assuming covariate will be coming in as a string of either one + # covariate name, or a string with two covariates separated by a comma + # either way, it needs to be in list form in this case, not string + covariate = covariate.split(",") -def run(group_list_text_file): + pheno_df = read_pheno_csv_into_df(pheno_file) + + # in this case, "covariate" gets sent in as a list of two covariates + design_df, contrasts_df, group_config_update = \ + preset_unpaired_two_group(group_list, pheno_df, + groups=covariate, + pheno_sub_label=pheno_sub_label, + output_dir=output_dir, + model_name=model_name) + + group_config.update(group_config_update) - group_list = read_group_list_text_file(group_list_text_file) + else: + # TODO: not a real preset! + raise Exception("not one of the valid presets") + # write design matrix CSV + write_dataframe_to_csv(design_df, group_config["pheno_file"]) + + # write custom contrasts matrix CSV + write_dataframe_to_csv(contrasts_df, group_config["custom_contrasts"]) + + # write group-level analysis config YAML + out_config = os.path.join(output_dir, model_name, + "gpa_fsl_config_{0}.yml".format(model_name)) + write_config_dct_to_yaml(group_config, out_config) diff --git a/scripts/cpac_cli.py b/scripts/cpac_cli.py new file mode 100644 index 0000000000..a70732c895 --- /dev/null +++ b/scripts/cpac_cli.py @@ -0,0 +1,75 @@ +#!/usr/bin/python + +# TODO: use Python Click to make this nice, or if not, use argparse + +# TODO: probably need Click to nest the analysis presets and then THEIR +# TODO: required inputs + + +def main(): + + import os + import argparse + + from CPAC.utils import create_group_analysis_files + + parser = argparse.ArgumentParser() + + parser.add_argument("cpac_outputs", type=str, + help="the path to the CPAC pipeline output directory " + "containing individual-level analysis output " + "files for each participant") + parser.add_argument("analysis_preset", type=str, + help="the type of group-level analysis to run using " + "FSL FLAME\n\nOptions:\n" + "single_grp: Single Group Average\n" + "single_grp_cov: Single Group Average w/ " + "Covariate\n" + "unpaired_two: Unpaired Two-Group Difference " + "(two-sample unpaired T-test)") + parser.add_argument("z_thresh", type=str, + help="the z-threshold") + parser.add_argument("p_thresh", type=str, + help="the p-threshold (cluster significance)") + parser.add_argument("model_name", type=str, + help="name for the model") + parser.add_argument("derivatives", type=str, + help="list of derivative names separated by spaces") + parser.add_argument("--include", type=str, default=None, + help="the path to the group-level analysis " + "participant list text file") + parser.add_argument("--output_dir", type=str, default=None, + help="the output directory") + parser.add_argument("--pheno_file", type=str, default=None, + help="the additional covariate for single-group " + "average") + parser.add_argument("--pheno_sub_label", type=str, default=None, + help="the additional covariate for single-group " + "average") + parser.add_argument("--covariate", type=str, default=None, + help="the additional covariate for single-group " + "average") + + args = parser.parse_args() + + if not args.output_dir: + output_dir = os.getcwd() + else: + output_dir = args.output_dir + + if not args.include: + include = [x for x in os.listdir(args.cpac_outputs) if os.path.isdir(x)] + else: + include = args.include + + derivatives_list = args.derivatives.split(" ") + + create_group_analysis_files.run(include, derivatives_list, args.z_thresh, + args.p_thresh, args.analysis_preset, + args.pheno_file, args.pheno_sub_label, + output_dir, args.model_name, + args.covariate) + + +if __name__ == "__main__": + main() From c787d7f2c1a06c0c66d461fa501642b73529ac75 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Mon, 12 Mar 2018 11:17:31 -0400 Subject: [PATCH 09/75] final iteration(?) - see extended commit -QC Pipeline names have bee updated -rearranged layout of QC for cleaner-isa code -switched on montages; fixed error -Fixed bug in the FD plot error - entire pipeline crash if any workflow was switched off in the pipeline.config files -Wrote outputs to output directory instead of log directory --- CPAC/pipeline/cpac_pipeline.py | 305 ++++++++++++++++----------------- 1 file changed, 145 insertions(+), 160 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 2ed0c63849..8ef8ef2d5a 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -1405,8 +1405,16 @@ def getNodeList(strategy): # we might prefer to use the TR stored in the NIFTI header # if not, use the value in the scan_params node try: - workflow.connect(scan_params, 'tr', - func_slice_timing_correction, 'tr') + if c.TR: + if isinstance(c.TR, str): + if "None" in c.TR or "none" in c.TR: + pass + else: + workflow.connect(scan_params, 'tr', + func_slice_timing_correction, 'tr') + else: + workflow.connect(scan_params, 'tr', + func_slice_timing_correction, 'tr') except Exception as xxx: logger.info( "Error connecting input 'tr' to func_slice_timing_" @@ -2982,10 +2990,10 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ analysis_type = analysis_type.replace(" ", "") - if analysis_type not in ts_analysis_dict.keys(): + if analysis_type not in ts_analysis_dict.keys(): ts_analysis_dict[analysis_type] = [] - ts_analysis_dict[analysis_type].append(roi_path) + ts_analysis_dict[analysis_type].append(roi_path) if 1 in c.runSCA: @@ -4858,9 +4866,17 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): QUALITY CONTROL - to be re-implemented later """"""""""""""""""""""""""""""""""""""""""""""""""" - + # TODO - QA pages: re-introduce + if 1 in c.generateQualityControlImages: + preproc, out_file = strat.get_node_from_resource_pool('preprocessed') + brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') + func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') + anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') + mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') + + #register color palettes register_pallete(os.path.realpath( os.path.join(CPAC.__path__[0], 'qc', 'red.py')), 'red') @@ -4872,19 +4888,17 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): os.path.join(CPAC.__path__[0], 'qc', 'red_to_blue.py')), 'red_to_blue') register_pallete(os.path.realpath( os.path.join(CPAC.__path__[0], 'qc', 'cyan_to_yellow.py')), 'cyan_to_yellow') - - hist = pe.Node(util.Function(input_names=['measure_file', - 'measure'], - output_names=['hist_path'], - function=gen_histogram), - name='histogram') + + hist = pe.Node(util.Function(input_names=['measure_file','measure'],output_names = ['hist_path'],function = gen_histogram),name = 'histogram') + + for strat in strat_list: nodes = getNodeList(strat) #make SNR plot - + try: hist_ = hist.clone('hist_snr_%d' % num_strat) @@ -4896,115 +4910,99 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): function=drop_percent_), name='dp_snr_%d' % num_strat) drop_percent.inputs.percent_ = 99 - - preproc, out_file = strat.get_node_from_resource_pool('preprocessed') - brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') - func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') - anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') - mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') - + + montage_snr = create_montage('montage_snr_%d' % num_strat, + 'red_to_blue', 'snr') + std_dev = pe.Node(util.Function(input_names=['mask_', 'func_'], output_names=['new_fname'], - function=gen_std_dev), - name='std_dev_%d' % num_strat) - #all functional calls are in the pipeline figure# + function=gen_std_dev), + name='std_dev_%d' % num_strat) + std_dev_anat = pe.Node(util.Function(input_names=['func_', - 'ref_', - 'xfm_', - 'interp_'], - output_names=['new_fname'], - function=gen_func_anat_xfm), - name='std_dev_anat_%d' % num_strat) - - snr = pe.Node(util.Function(input_names=['std_dev', 'mean_func_anat'], - output_names=['new_fname'], - function=gen_snr), - name='snr_%d' % num_strat) - - ### + 'ref_', + 'xfm_', + 'interp_'], + output_names=['new_fname'], + function=gen_func_anat_xfm), + name='std_dev_anat_%d' % num_strat) + + snr = pe.Node(util.Function(input_names=['std_dev', 'mean_func_anat'],output_names=['new_fname'],function=gen_snr),name='snr_%d' % num_strat) snr_val = pe.Node(util.Function(input_names=['measure_file'], - output_names=['snr_storefl'], - function=cal_snr_val), - name='snr_val%d' % num_strat) - - + output_names=['snr_storefl'], + function=cal_snr_val),name='snr_val%d' % num_strat) + + std_dev_anat.inputs.interp_ = 'trilinear' - - montage_snr = create_montage('montage_snr_%d' % num_strat, - 'red_to_blue', 'snr') - -#Are you making connections here in the pipeline because there is not inputspec and outputspec in the qc scripts indivually???# + + workflow.connect(preproc, out_file, - std_dev, 'func_') - + std_dev, 'func_') + workflow.connect(brain_mask, mask_file, - std_dev, 'mask_') - + std_dev, 'mask_') + workflow.connect(std_dev, 'new_fname', - std_dev_anat, 'func_') - + std_dev_anat, 'func_') + workflow.connect(func_to_anat_xfm, xfm_file, std_dev_anat, 'xfm_') - + workflow.connect(anat_ref, ref_file, std_dev_anat, 'ref_') - + workflow.connect(std_dev_anat, 'new_fname', - snr, 'std_dev') - + snr,'std_dev') + workflow.connect(mfa, mfa_file, - snr, 'mean_func_anat') - + snr, 'mean_func_anat') + workflow.connect(snr, 'new_fname', - hist_, 'measure_file') - + hist_, 'measure_file') + workflow.connect(snr, 'new_fname', - drop_percent, 'measure_file') - + drop_percent, 'measure_file') + workflow.connect(snr, 'new_fname', - snr_val, 'measure_file') ### - - + snr_val, 'measure_file') ### + + workflow.connect(drop_percent, 'modified_measure_file', - montage_snr, 'inputspec.overlay') - + montage_snr, 'inputspec.overlay') + workflow.connect(anat_ref, ref_file, - montage_snr, 'inputspec.underlay') - - + montage_snr, 'inputspec.underlay') + + strat.update_resource_pool({'qc___snr_a': (montage_snr, 'outputspec.axial_png'), 'qc___snr_s': (montage_snr, 'outputspec.sagittal_png'), 'qc___snr_hist': (hist_, 'hist_path'), - 'qc___snr_val': (snr_val, 'snr_storefl')}) ### + 'qc___snr_val': (snr_val, 'snr_storefl')}) if not 3 in qc_montage_id_a: qc_montage_id_a[3] = 'snr_a' qc_montage_id_s[3] = 'snr_s' qc_hist_id[3] = 'snr_hist' - + + except: logStandardError('QC', 'unable to get resources for SNR plot', '0051') raise - - #make motion parameters plot - + try: - + mov_param, out_file = strat.get_node_from_resource_pool('movement_parameters') mov_plot = pe.Node(util.Function(input_names=['motion_parameters'], - output_names=['translation_plot', - 'rotation_plot'], - function=gen_motion_plt), - name='motion_plt_%d' % num_strat) - - workflow.connect(mov_param, out_file, - mov_plot, 'motion_parameters') - strat.update_resource_pool({'qc___movement_trans_plot': (mov_plot, 'translation_plot'), - 'qc___movement_rot_plot': (mov_plot, 'rotation_plot')}) - + output_names=['translation_plot', + 'rotation_plot'], + function=gen_motion_plt), + name='motion_plt_%d' % num_strat) + workflow.connect(mov_param, out_file, mov_plot, 'motion_parameters') + strat.update_resource_pool({'qc___movement_trans_plot': (mov_plot, 'translation_plot'),'qc___movement_rot_plot': (mov_plot, 'rotation_plot')}) + if not 6 in qc_plot_id: qc_plot_id[6] = 'movement_trans_plot' - + if not 7 in qc_plot_id: qc_plot_id[7] = 'movement_rot_plot' @@ -5013,71 +5011,63 @@ def calc_avg(output_resource, strat, num_strat, map_node=0): logStandardError('QC', 'unable to get resources for Motion Parameters plot', '0052') raise - - # make FD plot and volumes removed +# make FD plot and volumes removed if 'gen_motion_stats' in nodes: - - try: - if c.fdCalc == 'Power': - fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_power') - else: - fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_jenkinson') - if ("De-Spiking" in c.runMotionSpike and 1 in c.runNuisance): - excluded, out_file_ex = strat.get_node_from_resource_pool('despiking_frames_excluded') - elif ("Scrubbing" in c.runMotionSpike and 1 in c.runNuisance): - excluded, out_file_ex = strat.get_node_from_resource_pool('scrubbing_frames_excluded') - - fd_plot = pe.Node(util.Function(input_names=['arr','ex_vol','measure'],output_names=['hist_path'],function=gen_plot_png),name='fd_plot_%d' % num_strat) - fd_plot.inputs.measure = 'FD' - workflow.connect(fd, out_file, - fd_plot, 'arr') - workflow.connect(excluded, out_file_ex, - fd_plot, 'ex_vol') - strat.update_resource_pool({'qc___fd_plot': (fd_plot, 'hist_path')}) - if not 8 in qc_plot_id: - qc_plot_id[8] = 'fd_plot' - - - except: - logStandardError('QC', 'unable to get resources for FD plot', '0053') - raise - - # make QC montages for Skull Stripping Visualization - + if 1 in c.runNuisance: + + try: + if c.fdCalc == 'Power': + fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_power') + else: + fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_jenkinson') + if ("De-Spiking" in c.runMotionSpike): + excluded, out_file_ex = strat.get_node_from_resource_pool('despiking_frames_excluded') + elif ("Scrubbing" in c.runMotionSpike): + excluded, out_file_ex = strat.get_node_from_resource_pool('scrubbing_frames_excluded') + + + fd_plot = pe.Node(util.Function(input_names=['arr', + 'ex_vol', + 'measure'], + output_names=['hist_path'], + function=gen_plot_png), + name='fd_plot_%d' % num_strat) + fd_plot.inputs.measure = 'FD' + workflow.connect(fd, out_file,fd_plot, 'arr') + workflow.connect(excluded, out_file_ex,fd_plot, 'ex_vol') + strat.update_resource_pool({'qc___fd_plot': (fd_plot, 'hist_path')}) + if not 8 in qc_plot_id: + qc_plot_id[8] = 'fd_plot' + + + except: + logStandardError('QC', 'unable to get resources for FD plot', '0053') + raise + +# # make QC montages for Skull Stripping Visualization try: - anat_underlay, out_file = strat.get_node_from_resource_pool('anatomical_brain') - skull, out_file_s = strat.get_node_from_resource_pool('anatomical_reorient') - - - montage_skull = create_montage('montage_skull_%d' % num_strat, - 'red', 'skull_vis') ### - - skull_edge = pe.Node(util.Function(input_names=['file_'], - output_names=['new_fname'], - function=make_edge), - name='skull_edge_%d' % num_strat) - - - workflow.connect(skull, out_file_s, - skull_edge, 'file_') - - workflow.connect(anat_underlay, out_file, - montage_skull, 'inputspec.underlay') - - workflow.connect(skull_edge, 'new_fname', - montage_skull, 'inputspec.overlay') - - strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, 'outputspec.axial_png'), - 'qc___skullstrip_vis_s': (montage_skull, 'outputspec.sagittal_png')}) - - if not 1 in qc_montage_id_a: + anat_underlay, out_file = strat.get_node_from_resource_pool('anatomical_brain') + skull, out_file_s = strat.get_node_from_resource_pool('anatomical_reorient') + + montage_skull = create_montage('montage_skull_%d' % num_strat,'red', 'skull_vis') ### + + skull_edge = pe.Node(util.Function(input_names=['file_'],output_names=['new_fname'],function=make_edge),name='skull_edge_%d' % num_strat) + + workflow.connect(skull, out_file_s,skull_edge, 'file_') + + workflow.connect(anat_underlay, out_file,montage_skull,'inputspec.underlay') + + workflow.connect(skull_edge, 'new_fname',montage_skull,'inputspec.overlay') + + strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, 'outputspec.axial_png'),'qc___skullstrip_vis_s': (montage_skull, 'outputspec.sagittal_png')}) + + if not 1 in qc_montage_id_a: qc_montage_id_a[1] = 'skullstrip_vis_a' qc_montage_id_s[1] = 'skullstrip_vis_s' - + except: - logStandardError('QC', 'Cannot generate QC montages for Skull Stripping: Resources Not Found', '0054') - raise - + logStandardError('QC', 'Cannot generate QC montages for Skull Stripping: Resources Not Found', '0054') + raise ### make QC montages for mni normalized anatomical image @@ -5226,15 +5216,13 @@ def QA_montages(measure, idx): drop_percent = pe.MapNode(util.Function(input_names=['measure_file', 'percent_'], - output_names=['modified_measure_file'], - function=drop_percent_), + output_names=['modified_measure_file'], function=drop_percent_), name='dp_%s_%d' % (measure, num_strat), iterfield=['measure_file']) drop_percent.inputs.percent_ = 99.999 overlay, out_file = strat.get_node_from_resource_pool(measure) - montage = create_montage('montage_%s_%d' % (measure, num_strat), - 'cyan_to_yellow', measure) + montage = create_montage('montage_%s_%d' % (measure, num_strat),'cyan_to_yellow', measure) montage.inputs.inputspec.underlay = c.template_brain_only_for_func workflow.connect(overlay, out_file, @@ -5264,7 +5252,6 @@ def QA_montages(measure, idx): # ALFF and f/ALFF QA montages if 1 in c.runALFF: - if 1 in c.runRegisterFuncToMNI: QA_montages('alff_to_standard', 7) QA_montages('falff_to_standard', 8) @@ -5284,10 +5271,9 @@ def QA_montages(measure, idx): QA_montages('falff_to_standard_zstd', 14) - # ReHo QA montages - #if 1 in c.runReHo: - -# if 1 in c.runRegisterFuncToMNI: +# ReHo QA montages +# if 1 in c.runReHo: +# if 1 in c.runRegisterFuncToMNI: # QA_montages('reho_to_standard', 15) # # if c.fwhm != None: @@ -5305,7 +5291,6 @@ def QA_montages(measure, idx): # SCA ROI QA montages if (1 in c.runSCA) and (1 in c.runROITimeseries): - if 1 in c.runRegisterFuncToMNI: QA_montages('sca_roi_to_standard', 19) @@ -5344,17 +5329,17 @@ def QA_montages(measure, idx): if "MultReg" in sca_analysis_dict.keys(): #(1 in c.runMultRegSCA) and (1 in c.runROITimeseries): if 1 in c.runRegisterFuncToMNI: - QA_montages('sca_tempreg_maps_files', 27) - QA_montages('sca_tempreg_maps_zstat_files', 28) + QA_montages('sca_tempreg_maps_files', 27) + QA_montages('sca_tempreg_maps_zstat_files', 28) - if c.fwhm != None: + if c.fwhm != None: QA_montages('sca_tempreg_maps_files_smooth', 29) QA_montages('sca_tempreg_maps_zstat_files_smooth', 30) # Dual Regression QA montages - if ("DualReg" in sca_analysis_dict.keys()) and (1 in c.runSpatialRegression): + if ("DualReg" in sca_analysis_dict.keys()) and ("SpatialReg" in ts_analysis_dict.keys()): QA_montages('dr_tempreg_maps_files', 31) QA_montages('dr_tempreg_maps_zstat_files', 32) From e1920062feb7ad59db50cb4404e50d7b2959802c Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Mon, 12 Mar 2018 18:45:17 -0400 Subject: [PATCH 10/75] Assorted fixes and patches to the group model generator, and the GUI, allowing the presets to work seamlessly. --- .../GUI/interface/utils/modelconfig_window.py | 12 ++- CPAC/pipeline/cpac_ga_model_generator.py | 90 ++++++++++--------- CPAC/pipeline/cpac_group_runner.py | 69 +++++++------- CPAC/utils/create_flame_model_files.py | 18 ++-- CPAC/utils/create_group_analysis_files.py | 6 +- 5 files changed, 109 insertions(+), 86 deletions(-) diff --git a/CPAC/GUI/interface/utils/modelconfig_window.py b/CPAC/GUI/interface/utils/modelconfig_window.py index 9e1c7882bf..f96e79ba37 100644 --- a/CPAC/GUI/interface/utils/modelconfig_window.py +++ b/CPAC/GUI/interface/utils/modelconfig_window.py @@ -381,7 +381,17 @@ def load(self, event): phenoHeaderString = phenoFile.readline().rstrip('\r\n') phenoHeaderItems = phenoHeaderString.split(',') - phenoHeaderItems.remove(self.gpa_settings['participant_id_label']) + try: + phenoHeaderItems.remove(self.gpa_settings['participant_id_label']) + except ValueError: + err = "\n[!] The participant ID label you provided in " \ + "the group analysis configuration file ({0}) was " \ + "not found as a column label in the phenotypic " \ + "file.\n\nPhenotypic file: {1}\n\n" \ + "".format(self.gpa_settings['participant_id_label'], + os.path.abspath( + self.gpa_settings['pheno_file'])) + raise Exception(err) # update the 'Model Setup' box and populate it with the EVs and # their associated checkboxes for categorical and demean diff --git a/CPAC/pipeline/cpac_ga_model_generator.py b/CPAC/pipeline/cpac_ga_model_generator.py index ca16df28c0..6bef661566 100755 --- a/CPAC/pipeline/cpac_ga_model_generator.py +++ b/CPAC/pipeline/cpac_ga_model_generator.py @@ -484,9 +484,9 @@ def create_contrasts_dict(dmatrix_obj, contrasts_list, output_measure): return contrasts_vectors -def prep_group_analysis_workflow(model_df, pipeline_config_path, \ - model_name, group_config_path, resource_id, preproc_strat, \ - series_or_repeated_label): +def prep_group_analysis_workflow(model_df, pipeline_config_path, model_name, + group_config_path, resource_id, + preproc_strat, series_or_repeated_label): # # this function runs once per derivative type and preproc strat combo @@ -520,8 +520,6 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ p_threshold = float(group_config_obj.p_threshold[0]) - sub_id_label = group_config_obj.participant_id_label - ftest_list = [] readme_flags = [] @@ -530,7 +528,6 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ if ((custom_confile is None) or (custom_confile == '') or ("None" in custom_confile) or ("none" in custom_confile)): - custom_confile = None if (len(group_config_obj.f_tests) == 0) or \ @@ -541,7 +538,6 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ ftest_list = group_config_obj.f_tests else: - if not os.path.exists(custom_confile): errmsg = "\n[!] CPAC says: You've specified a custom contrasts " \ ".CSV file for your group model, but this file cannot " \ @@ -549,7 +545,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ "entered.\n\nFilepath: %s\n\n" % custom_confile raise Exception(errmsg) - with open(custom_confile,"r") as f: + with open(custom_confile, "r") as f: evs = f.readline() evs = evs.rstrip('\r\n').split(',') @@ -572,18 +568,19 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ if 'sca_roi' in resource_id: out_dir = os.path.join(out_dir, - re.search('sca_ROI_(\d)+',os.path.splitext(\ + re.search('sca_ROI_(\d)+', os.path.splitext(\ os.path.splitext(os.path.basename(\ model_df["Filepath"][0]))[0])[0]).group(0)) if 'dr_tempreg_maps_zstat_files_to_standard_smooth' in resource_id: out_dir = os.path.join(out_dir, - re.search('temp_reg_map_z_(\d)+',os.path.splitext(\ + re.search('temp_reg_map_z_(\d)+', os.path.splitext(\ os.path.splitext(os.path.basename(\ model_df["Filepath"][0]))[0])[0]).group(0)) if 'centrality' in resource_id: - names = ['degree_centrality_binarize', 'degree_centrality_weighted', \ + names = ['degree_centrality_binarize', + 'degree_centrality_weighted', 'eigenvector_centrality_binarize', 'eigenvector_centrality_weighted', 'lfcd_binarize', 'lfcd_weighted'] @@ -594,7 +591,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ break if 'tempreg_maps' in resource_id: - out_dir = os.path.join(out_dir, re.search('\w*[#]*\d+', \ + out_dir = os.path.join(out_dir, re.search('\w*[#]*\d+', os.path.splitext(os.path.splitext(os.path.basename(\ model_df["Filepath"][0]))[0])[0]).group(0)) @@ -604,11 +601,11 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ out_dir.split("group_analysis_results_%s" % pipeline_ID)[1] # generate working directory for this output's group analysis run - work_dir = os.path.join(pipeline_config_obj.workingDirectory, \ - "group_analysis", second_half_out.lstrip("/")) + work_dir = os.path.join(pipeline_config_obj.workingDirectory, + "group_analysis", second_half_out.lstrip("/")) - log_dir = os.path.join(pipeline_config_obj.logDirectory, \ - "group_analysis", second_half_out.lstrip("/")) + log_dir = os.path.join(pipeline_config_obj.logDirectory, + "group_analysis", second_half_out.lstrip("/")) # create the actual directories create_dir(model_path, "group analysis output") @@ -625,11 +622,11 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ if part not in new_participant_list: new_participant_list.append(part) - new_sub_file = write_new_sub_file(model_path, \ - group_config_obj.participant_list, \ + new_sub_file = write_new_sub_file(model_path, + group_config_obj.participant_list, new_participant_list) - group_config_obj.update('participant_list',new_sub_file) + group_config_obj.update('participant_list', new_sub_file) num_subjects = len(list(model_df["participant_id"])) @@ -653,12 +650,11 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ merge_outfile = model_name + "_" + resource_id + "_merged.nii.gz" merge_outfile = os.path.join(model_path, merge_outfile) - merge_file = create_merged_copefile(list(model_df["Filepath"]), \ + merge_file = create_merged_copefile(list(model_df["Filepath"]), merge_outfile) # create merged group mask - merge_mask_outfile = model_name + "_" + resource_id + \ - "_merged_mask.nii.gz" + merge_mask_outfile = model_name + "_" + resource_id + "_merged_mask.nii.gz" merge_mask_outfile = os.path.join(model_path, merge_mask_outfile) merge_mask = create_merge_mask(merge_file, merge_mask_outfile) @@ -668,8 +664,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ individual_masks_dir = os.path.join(model_path, "individual_masks") create_dir(individual_masks_dir, "individual masks") for unique_id, series_id, raw_filepath in zip(model_df["participant_id"], - model_df["Series"], model_df["Raw_Filepath"]): - + model_df["Series"], model_df["Raw_Filepath"]): mask_for_means_path = os.path.join(individual_masks_dir, "%s_%s_%s_mask.nii.gz" % (unique_id, series_id, resource_id)) mask_for_means = create_merge_mask(raw_filepath, @@ -686,7 +681,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ custom_roi_mask = group_config_obj.custom_roi_mask if (custom_roi_mask == None) or (custom_roi_mask == "None") or \ - (custom_roi_mask == "none") or (custom_roi_mask == ""): + (custom_roi_mask == "none") or (custom_roi_mask == ""): err = "\n\n[!] You included 'Custom_ROI_Mean' in your design " \ "formula, but you didn't supply a custom ROI mask file." \ "\n\nDesign formula: %s\n\n" % design_formula @@ -694,8 +689,8 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ # make sure the custom ROI mask file is the same resolution as the # output files - if not, resample and warn the user - roi_mask = check_mask_file_resolution(list(model_df["Raw_Filepath"])[0], \ - custom_roi_mask, mask_for_means, \ + roi_mask = check_mask_file_resolution(list(model_df["Raw_Filepath"])[0], + custom_roi_mask, mask_for_means, model_path, resource_id) # trim the custom ROI mask to be within mask constraints @@ -732,7 +727,9 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ if "Series" not in cat_list: cat_list.append("Series") for col in list(model_df.columns): - if "participant_" in col: + # should only grab the repeated measures-designed participant_{ID} + # columns, not the "participant_id" column! + if "participant_" in col and "_id" not in col: design_formula = design_formula + " + %s" % col cat_list.append(col) @@ -747,10 +744,11 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ if group_config_obj.group_sep: # check if the group_ev parameter is a list instead of a string: - # this was added to handle the new group-level analysis presets. this is - # the only modification that was required to the group analysis workflow, - # and it handles cases where the group variances must be modeled - # separately, by creating separate groups for the FSL FLAME .grp file. + # this was added to handle the new group-level analysis presets. this + # is the only modification that was required to the group analysis + # workflow, and it handles cases where the group variances must be + # modeled separately, by creating separate groups for the FSL FLAME + # .grp file. # the group_ev parameter gets sent in as a list if coming from any # of the presets that deal with multiple groups- in these cases, # the pheno_df/design matrix is already set up properly for the @@ -759,7 +757,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ # separately" option is enabled in the group analysis config YAML group_ev = group_config_obj.grouping_var - if isinstance(group_ev, list): + if isinstance(group_ev, list) or "," in group_ev: grp_vector = [] @@ -817,14 +815,14 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ # model group variances separately old_ev_list = ev_list - model_df, grp_vector, ev_list, cat_list = split_groups(model_df, \ - group_config_obj.grouping_var, \ + model_df, grp_vector, ev_list, cat_list = split_groups(model_df, + group_config_obj.grouping_var, ev_list, cat_list) # make the grouping variable categorical for Patsy (if we try to # do this automatically below, it will categorical-ize all of # the substrings too) - design_formula = design_formula.replace(group_config_obj.grouping_var, \ + design_formula = design_formula.replace(group_config_obj.grouping_var, "C(" + group_config_obj.grouping_var + ")") if group_config_obj.coding_scheme == "Sum": design_formula = design_formula.replace(")", ", Sum)") @@ -863,8 +861,15 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ check_multicollinearity(np.array(dmatrix)) # prepare for final stages - column_names = dmatrix.design_info.column_names - + dmatrix_column_names = dmatrix.design_info.column_names + + # make sure "column_names" is in the same order as the original EV column + # header ordering in model_df + column_names = [] + for col in model_df.columns: + if col in dmatrix_column_names: + column_names.append(col) + # check to make sure there are more time points than EVs! if len(column_names) >= num_subjects: err = "\n\n[!] CPAC says: There are more EVs than there are " \ @@ -878,22 +883,23 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, \ "subjects in your group analysis subject list, this may be " \ "because not every subject in the subject list has an output " \ "for %s in the individual-level analysis output directory.\n\n"\ - % (resource_id, num_subjects, len(column_names), column_names, \ - resource_id) + "Design formula going in: %s\n\n"\ + % (resource_id, num_subjects, len(column_names), column_names, + resource_id, design_formula) raise Exception(err) # time for contrasts contrasts_list = None contrasts_vectors = None - if ((custom_confile == None) or (custom_confile == '') or \ + if ((custom_confile == None) or (custom_confile == '') or ("None" in custom_confile) or ("none" in custom_confile)): # if no custom contrasts matrix CSV provided (i.e. the user # specified contrasts in the GUI) contrasts_list = group_config_obj.contrasts contrasts_vectors = create_contrasts_dict(dmatrix, contrasts_list, - resource_id) + resource_id) # check the merged file's order check_merged_file(model_df["Filepath"], merge_file) diff --git a/CPAC/pipeline/cpac_group_runner.py b/CPAC/pipeline/cpac_group_runner.py index 43805acb97..6828a621fc 100644 --- a/CPAC/pipeline/cpac_group_runner.py +++ b/CPAC/pipeline/cpac_group_runner.py @@ -490,7 +490,6 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # example: # /home/cpac_run_1/output/pipeline_040_ANTS - import os import pandas as pd # Load the MAIN PIPELINE config file into 'c' as a CONFIGURATION OBJECT @@ -506,10 +505,9 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): group_models = [] for group_config_file in c.modelConfigs: - group_models.append((group_config_file, \ + group_models.append((group_config_file, load_config_yml(group_config_file))) - # get the lowest common denominator of group model config choices # - create full participant list # - create full output measure list @@ -523,8 +521,8 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): group_model = group_model_tuple[1] - inclusion = load_text_file(group_model.participant_list, \ - "group-level analysis participant list") + inclusion = load_text_file(group_model.participant_list, + "group-level analysis participant list") full_inclusion_list = full_inclusion_list + inclusion full_output_measure_list = full_output_measure_list + \ @@ -532,18 +530,18 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # if any of the models will require motion parameters if ("MeanFD" in group_model.design_formula) or \ - ("MeanDVARS" in group_model.design_formula): + ("MeanDVARS" in group_model.design_formula): get_motion = True # make sure "None" gets processed properly here... if (group_model.custom_roi_mask == "None") or \ - (group_model.custom_roi_mask == "none"): + (group_model.custom_roi_mask == "none"): custom_roi_mask = None else: custom_roi_mask = group_model.custom_roi_mask if ("Measure_Mean" in group_model.design_formula) or \ - (custom_roi_mask != None): + (custom_roi_mask != None): get_raw_score = True full_inclusion_list = list(set(full_inclusion_list)) @@ -559,11 +557,11 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # - each dataframe will contain output filepaths and their associated # information, and each dataframe will include ALL SERIES/SCANS # - the dataframes will be pruned for each model LATER - output_df_dict = gather_outputs(pipeline_output_folder, \ - full_output_measure_list, \ - full_inclusion_list, \ - get_motion, \ - get_raw_score) + output_df_dict = gather_outputs(pipeline_output_folder, + full_output_measure_list, + full_inclusion_list, + get_motion, + get_raw_score) # alright, group model processing time # going to merge the phenotype DFs with the output file DF @@ -589,7 +587,7 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): pheno_df = load_pheno_csv_into_df(group_model.pheno_file) # enforce the sub ID label to "Participant" - pheno_df.rename(columns={group_model.participant_id_label:"participant_id"}, \ + pheno_df.rename(columns={group_model.participant_id_label:"participant_id"}, inplace=True) pheno_df["participant_id"] = pheno_df["participant_id"].astype(str) @@ -614,8 +612,8 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # prune the output_df for this specific group model and output + # preprocessing strategy - inclusion_list = load_text_file(group_model.participant_list, \ - "group-level analysis participant list") + inclusion_list = load_text_file(group_model.participant_list, + "group-level analysis participant list") output_df = \ output_df[output_df["participant_id"].isin(inclusion_list)] @@ -638,15 +636,15 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): if repeated_sessions == True: new_pheno_df = pheno_sessions_to_repeated_measures( \ - new_pheno_df, \ + new_pheno_df, group_model.sessions_list) # create new rows for all of the series, if applicable # ex. if 10 subjects and two sessions, 10 rows -> 20 rows if repeated_series == True: new_pheno_df = pheno_series_to_repeated_measures( \ - new_pheno_df, \ - group_model.series_list, \ + new_pheno_df, + group_model.series_list, repeated_sessions) # drop the pheno rows - if there are participants missing in @@ -679,15 +677,15 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): new_pheno_df[new_pheno_df["Series"].isin(group_model.series_list)] join_columns.append("Series") # pull together the pheno DF and the output files DF! - new_pheno_df = pd.merge(new_pheno_df, output_df, how="inner",\ - on=join_columns) + new_pheno_df = pd.merge(new_pheno_df, output_df, + how="inner", on=join_columns) if repeated_sessions == True: # this can be removed/modified once sessions are no # longer integrated in the full unique participant IDs new_pheno_df, dropped_parts = \ - balance_repeated_measures(new_pheno_df, \ - group_model.sessions_list, \ + balance_repeated_measures(new_pheno_df, + group_model.sessions_list, group_model.series_list) run_label = "repeated_measures_multiple_sessions_and_series" @@ -706,23 +704,28 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): series = series_df_tuple[0] - # series_df is output_df but with only one of the Series + # series_df is output_df but with only one of the + # Series series_df = series_df_tuple[1] - # trim down the pheno DF to match the output DF and merge + # TODO: is this a mistake? + # trim down the pheno DF to match the output DF and + # merge newer_pheno_df = new_pheno_df[pheno_df["participant_id"].isin(series_df["participant_id"])] newer_pheno_df = pd.merge(new_pheno_df, series_df, how="inner", on=["participant_id"]) # this can be removed/modified once sessions are no # longer integrated in the full unique participant IDs newer_pheno_df, dropped_parts = \ - balance_repeated_measures(newer_pheno_df, \ - group_model.sessions_list, \ + balance_repeated_measures(newer_pheno_df, + group_model.sessions_list, None) # unique_resource = - # (output_measure_type, preprocessing strategy) - analysis_dict[(model_name, group_config_file, resource_id, strat_info, "repeated_measures_%s" % series)] = newer_pheno_df + # (output_measure_type, preprocessing strategy) + analysis_dict[(model_name, group_config_file, + resource_id, strat_info, + "repeated_measures_%s" % series)] = newer_pheno_df else: # no repeated measures @@ -740,9 +743,13 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): series_df = series_df_tuple[1] # trim down the pheno DF to match the output DF and merge newer_pheno_df = new_pheno_df[pheno_df["participant_id"].isin(series_df["participant_id"])] - newer_pheno_df = pd.merge(new_pheno_df, series_df, how="inner", on=["participant_id"]) + newer_pheno_df = pd.merge(new_pheno_df, series_df, + how="inner", + on=["participant_id"]) + # send it in - analysis_dict[(model_name, group_config_file, resource_id, strat_info, series)] = newer_pheno_df + analysis_dict[(model_name, group_config_file, resource_id, + strat_info, series)] = newer_pheno_df return analysis_dict diff --git a/CPAC/utils/create_flame_model_files.py b/CPAC/utils/create_flame_model_files.py index 2242831d97..a3ba290131 100644 --- a/CPAC/utils/create_flame_model_files.py +++ b/CPAC/utils/create_flame_model_files.py @@ -197,8 +197,8 @@ def create_fts_file(ftest_list, con_names, model_name, return out_file -def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ - column_names, coding_scheme, group_sep): +def create_con_ftst_file(con_file, model_name, current_output, output_dir, + column_names, coding_scheme, group_sep): """ Create the contrasts and fts file @@ -209,27 +209,29 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ # Read the header of the contrasts file, which should contain the columns # of the design matrix and f-tests (if any) - with open(con_file,"r") as f: + with open(con_file, "r") as f: evs = f.readline() evs = evs.rstrip('\r\n').split(',') if evs[0].strip().lower() != "contrasts": - print "Error: first cell in contrasts file should contain 'Contrasts' " + print "Error: first cell in contrasts file should contain " \ + "'Contrasts' " raise Exception # remove "Contrasts" label and replace it with "Intercept" evs[0] = "Intercept" # Count the number of f tests defined - count_ftests = len([ ev for ev in evs if "f_test" in ev ]) + count_ftests = len([ev for ev in evs if "f_test" in ev ]) # Whether any f tests are defined - fTest = count_ftests >0 + fTest = count_ftests > 0 # Now read the actual contrasts try: - contrasts_data = np.genfromtxt(con_file, names=True, delimiter=',', dtype=None) + contrasts_data = np.genfromtxt(con_file, names=True, delimiter=',', + dtype=None) except: print "Error: Could not successfully read in contrast file: ",con_file raise Exception @@ -265,7 +267,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ # add Intercept column if group_sep == False: - if False: # The following insertion gives an error further down the # line, because this suggests that there will be an intercept @@ -289,7 +290,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ # if there are f-tests, create the array for them if fTest: - ## TODO: Probably it would be more accurate to check that each ## f test itself contains enough contrasts, rather than whether ## there are in principle enough contrasts to form f tests. diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_group_analysis_files.py index b6b1a45b34..a34233e80a 100644 --- a/CPAC/utils/create_group_analysis_files.py +++ b/CPAC/utils/create_group_analysis_files.py @@ -259,7 +259,7 @@ def preset_single_group_avg(group_list, pheno_df=None, covariate=None, ev_selections=ev_selections, pheno_sub_label=pheno_sub_label) - design_df["Group Mean"] = 1 + design_df["Group_Mean"] = 1 group_mean_contrast = {"contrasts": "Group Mean"} @@ -267,7 +267,7 @@ def preset_single_group_avg(group_list, pheno_df=None, covariate=None, # covariate past the Group Mean for col in design_df.columns: if col not in id_cols: - if col == "Group Mean": + if col == "Group_Mean": group_mean_contrast.update({col: 1}) else: group_mean_contrast.update({col: 0}) @@ -297,7 +297,7 @@ def preset_single_group_avg(group_list, pheno_df=None, covariate=None, "".format(model_name)) # start group config yaml dictionary - design_formula = "Group Mean" + design_formula = "Group_Mean" if covariate: design_formula = "{0} + {1}".format(design_formula, covariate) From 34709c403b2b8de2bbff46a6c1f8ce3e99d1a930 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Tue, 13 Mar 2018 17:50:20 -0400 Subject: [PATCH 11/75] Group analysis zstat outputs are now named after their associated contrast labels. In addition, fixed a few things in the new preset-generating code, and introduced needed error and config checks/messages throughout the group analysis workflow. --- CPAC/GUI/interface/utils/generic_class.py | 9 ++- .../GUI/interface/utils/modelconfig_window.py | 7 +- CPAC/group_analysis/group_analysis.py | 72 +++++++++++++++---- CPAC/pipeline/cpac_ga_model_generator.py | 34 ++++----- CPAC/pipeline/cpac_group_runner.py | 63 +++++++++++----- CPAC/utils/create_group_analysis_files.py | 5 +- .../utils/create_group_analysis_info_files.py | 26 ++++--- 7 files changed, 144 insertions(+), 72 deletions(-) diff --git a/CPAC/GUI/interface/utils/generic_class.py b/CPAC/GUI/interface/utils/generic_class.py index 73ca8d0dfc..dbef764a1b 100644 --- a/CPAC/GUI/interface/utils/generic_class.py +++ b/CPAC/GUI/interface/utils/generic_class.py @@ -437,7 +437,14 @@ def set_value(self, val): val = val.replace("'", "") val = val.split(", ") - self.ctrl.SetCheckedStrings(val) + try: + self.ctrl.SetCheckedStrings(val) + except AssertionError: + err = "\n[!] The derivative name you provided in the " \ + "derivative_list field in the group analysis " \ + "configuration file does not match any of CPAC's " \ + "outputs.\n\nName provided: {0}\n".format(val) + raise Exception(err) strings = self.ctrl.GetCheckedStrings() sample_list = self.get_values() for s in strings: diff --git a/CPAC/GUI/interface/utils/modelconfig_window.py b/CPAC/GUI/interface/utils/modelconfig_window.py index f96e79ba37..e80ac3f345 100644 --- a/CPAC/GUI/interface/utils/modelconfig_window.py +++ b/CPAC/GUI/interface/utils/modelconfig_window.py @@ -449,6 +449,8 @@ def load(self, event): for cov in value: grouping_var += "{0},".format(cov) grouping_var = grouping_var.rstrip(",") + else: + grouping_var = value ctrl.set_value(grouping_var) @@ -1300,8 +1302,7 @@ def testFile(filepath, paramName): raise Exception # open the next window! - modelDesign_window.ModelDesign(self.parent, self.gpa_settings, \ - dmatrix, column_names) #var_list_for_contrasts) - + modelDesign_window.ModelDesign(self.parent, self.gpa_settings, + dmatrix, column_names) self.Close() diff --git a/CPAC/group_analysis/group_analysis.py b/CPAC/group_analysis/group_analysis.py index b6a02f4e19..2b86fbfdfd 100644 --- a/CPAC/group_analysis/group_analysis.py +++ b/CPAC/group_analysis/group_analysis.py @@ -235,9 +235,25 @@ def create_group_analysis(ftest=False, wf_name='groupAnalysis'): name='fsl_flameo') fsl_flameo.inputs.run_mode = 'ols' - ### create analysis specific mask + # rename the FLAME zstat outputs after the contrast string labels for + # easier interpretation + label_zstat_imports = ["import os"] + label_zstat = pe.Node(util.Function(input_names=['zstat_list', + 'con_file'], + output_names=['new_zstat_list'], + function=label_zstat_files, + imports=label_zstat_imports), + name='label_zstat') + + rename_zstats = pe.MapNode(interface=util.Rename(), + name='rename_zstats', + iterfield=['in_file', + 'format_string']) + rename_zstats.inputs.keep_ext = True + + # create analysis specific mask # fslmaths merged.nii.gz -abs -bin -Tmean -mul volume out.nii.gz - #-Tmean: mean across time + # -Tmean: mean across time # create group_reg file # this file can provide an idea of how well the subjects # in our analysis overlay with each other and the MNI brain. @@ -246,13 +262,13 @@ def create_group_analysis(ftest=False, wf_name='groupAnalysis'): merge_mean_mask = pe.Node(interface=fsl.ImageMaths(), name='merge_mean_mask') - #function node to get the operation string for fslmaths command + # function node to get the operation string for fslmaths command get_opstring = pe.Node(util.Function(input_names=['in_file'], output_names=['out_file'], function=get_operation), name='get_opstring') - #connections + # connections ''' grp_analysis.connect(inputnode, 'zmap_files', merge_to_4d, 'in_files') @@ -270,11 +286,17 @@ def create_group_analysis(ftest=False, wf_name='groupAnalysis'): grp_analysis.connect(inputnode, 'grp_file', fsl_flameo, 'cov_split_file') - if ftest: + grp_analysis.connect(fsl_flameo, 'zstats', label_zstat, 'zstat_list') + grp_analysis.connect(inputnode, 'con_file', label_zstat, 'con_file') - #calling easythresh for zfstats file - grp_analysis.connect(inputnode, 'fts_file', - fsl_flameo, 'f_con_file') + grp_analysis.connect(fsl_flameo, 'zstats', rename_zstats, 'in_file') + + grp_analysis.connect(label_zstat, 'new_zstat_list', + rename_zstats, 'format_string') + + if ftest: + # calling easythresh for zfstats file + grp_analysis.connect(inputnode, 'fts_file', fsl_flameo, 'f_con_file') easy_thresh_zf = easy_thresh('easy_thresh_zf') @@ -299,9 +321,9 @@ def create_group_analysis(ftest=False, wf_name='groupAnalysis'): grp_analysis.connect(easy_thresh_zf, 'outputspec.rendered_image', outputnode, 'rendered_image_zf') - #calling easythresh for zstats files + # calling easythresh for zstats files easy_thresh_z = easy_thresh('easy_thresh_z') - grp_analysis.connect(fsl_flameo, 'zstats', + grp_analysis.connect(rename_zstats, 'out_file', easy_thresh_z, 'inputspec.z_stats') grp_analysis.connect(inputnode, 'merge_mask', easy_thresh_z, 'inputspec.merge_mask') @@ -325,10 +347,8 @@ def create_group_analysis(ftest=False, wf_name='groupAnalysis'): outputnode, 'fstats') grp_analysis.connect(inputnode, 'merged_file', outputnode, 'merged') - grp_analysis.connect(fsl_flameo, 'zstats', - outputnode, 'zstats') - + grp_analysis.connect(rename_zstats, 'out_file', outputnode, 'zstats') grp_analysis.connect(easy_thresh_z, 'outputspec.cluster_threshold', outputnode, 'cluster_threshold') @@ -344,7 +364,6 @@ def create_group_analysis(ftest=False, wf_name='groupAnalysis'): return grp_analysis - def get_operation(in_file): """ Method to create operation string @@ -375,3 +394,28 @@ def get_operation(in_file): return op_string except: raise IOError("Unable to load the input nifti image") + + +def label_zstat_files(zstat_list, con_file): + """Take in the z-stat file outputs of FSL FLAME and rename them after the + contrast labels of the contrasts provided.""" + + cons = [] + new_zstat_list = [] + + with open(con_file, "r") as f: + con_file_lines = f.readlines() + + for line in con_file_lines: + if "ContrastName" in line: + con_label = line.split(" ", 1)[1].replace(" ", "") + con_label = con_label.replace("\t", "").replace("\n", "") + cons.append(con_label) + + for zstat_file, con_name in zip(zstat_list, cons): + #filename = os.path.basename(zstat_file) + new_name = "zstat_{0}".format(con_name) + #new_zstat_list.append(zstat_file.replace(filename, new_name)) + new_zstat_list.append(new_name) + + return new_zstat_list diff --git a/CPAC/pipeline/cpac_ga_model_generator.py b/CPAC/pipeline/cpac_ga_model_generator.py index 6bef661566..04bb76a8da 100755 --- a/CPAC/pipeline/cpac_ga_model_generator.py +++ b/CPAC/pipeline/cpac_ga_model_generator.py @@ -495,6 +495,7 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, model_name, import os import patsy + import pandas as pd import numpy as np import nipype.pipeline.engine as pe @@ -761,6 +762,9 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, model_name, grp_vector = [] + if "," in group_ev: + group_ev = group_ev.split(",") + if len(group_ev) == 2: for x, y in zip(model_df[group_ev[0]], model_df[group_ev[1]]): if x == 1: @@ -929,14 +933,18 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, model_name, readme_flags.append("cat_demeaned") + dmatrix_df = pd.DataFrame(dmatrix, + columns=dmatrix.design_info.column_names) + dmatrix_df = dmatrix_df[column_names] + # send off the info so the FLAME input model files can be generated! - mat_file, grp_file, con_file, fts_file = create_flame_model_files(dmatrix, + mat_file, grp_file, con_file, fts_file = create_flame_model_files(dmatrix_df, column_names, contrasts_vectors, contrasts_list, custom_confile, ftest_list, group_config_obj.group_sep, grp_vector, group_config_obj.coding_scheme[0], model_name, resource_id, model_path) dmat_csv_path = os.path.join(model_path, "design_matrix.csv") - write_design_matrix_csv(dmatrix, model_df["participant_id"], column_names, + write_design_matrix_csv(dmatrix_df, model_df["participant_id"], column_names, dmat_csv_path) # workflow time @@ -988,19 +996,8 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, model_name, (r'_cluster(.)*[/]',''), (r'_slicer(.)*[/]',''), (r'_overlay(.)*[/]','')] - - ########datasink connections######### - #if fTest: - # wf.connect(gp_flow, 'outputspec.fts', - # ds, 'model_files.@0') - - #wf.connect(gp_flow, 'outputspec.mat', - # ds, 'model_files.@1' ) - #wf.connect(gp_flow, 'outputspec.con', - # ds, 'model_files.@2') - #wf.connect(gp_flow, 'outputspec.grp', - # ds, 'model_files.@3') + # datasink connections wf.connect(gpa_wf, 'outputspec.merged', ds, 'merged') wf.connect(gpa_wf, 'outputspec.zstats', @@ -1029,8 +1026,6 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, model_name, ds, 'rendered.@02') wf.connect(gpa_wf, 'outputspec.rendered_image', ds, 'rendered.@03') - - ###################################### # Run the actual group analysis workflow wf.run() @@ -1038,20 +1033,17 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, model_name, print "\n\nWorkflow finished for model %s\n\n" % wf_name - def run(config, subject_infos, resource): - import re import commands commands.getoutput('source ~/.bashrc') import os - import sys import pickle import yaml c = Configuration(yaml.load(open(os.path.realpath(config), 'r'))) - prep_group_analysis_workflow(c, pickle.load(open(resource, 'r') ), \ - pickle.load(open(subject_infos, 'r'))) + prep_group_analysis_workflow(c, pickle.load(open(resource, 'r')), + pickle.load(open(subject_infos, 'r'))) diff --git a/CPAC/pipeline/cpac_group_runner.py b/CPAC/pipeline/cpac_group_runner.py index 6828a621fc..2cdc8eb648 100644 --- a/CPAC/pipeline/cpac_group_runner.py +++ b/CPAC/pipeline/cpac_group_runner.py @@ -492,6 +492,24 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): import pandas as pd + # TODO + # make this a global list somewhere + derivatives = ['alff_to_standard_zstd', + 'alff_to_standard_smooth_zstd', + 'falff_to_standard_zstd', + 'falff_to_standard_smooth_zstd', + 'reho_to_standard_zstd', + 'reho_to_standard_smooth_zstd', + 'sca_roi_files_to_standard_fisher_zstd', + 'sca_roi_files_to_standard_smooth_fisher_zstd', + 'sca_tempreg_maps_zstat_files', + 'sca_tempreg_maps_zstat_files_smooth', + 'vmhc_fisher_zstd_zstat_map', + 'centrality_outputs_zstd', + 'centrality_outputs_smoothed_zstd', + 'dr_tempreg_maps_zstat_files_to_standard', + 'dr_tempreg_maps_zstat_files_to_standard_smooth'] + # Load the MAIN PIPELINE config file into 'c' as a CONFIGURATION OBJECT c = load_config_yml(config_file) @@ -516,7 +534,7 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): full_output_measure_list = [] get_motion = False get_raw_score = False - + for group_model_tuple in group_models: group_model = group_model_tuple[1] @@ -583,6 +601,22 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): else: group_model_names.append(model_name) + if len(group_model.derivative_list) == 0: + err = "\n\n[!] There are no derivatives listed in the " \ + "derivative_list field of your group analysis " \ + "configuration file.\n\nConfiguration file: " \ + "{0}\n".format(group_config_file) + raise Exception(err) + + for deriv_name in group_model.derivative_list: + if deriv_name not in derivatives: + err = "\n\n[!] One of the derivative names you provided " \ + "({0}) in the derivative_list field in your group " \ + "analysis configuration file is not a valid CPAC " \ + "output name.\n\nConfiguration file: {1}" \ + "\n".format(deriv_name, group_config_file) + raise Exception(err) + # load original phenotype CSV into a dataframe pheno_df = load_pheno_csv_into_df(group_model.pheno_file) @@ -738,11 +772,13 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # iterate over the Series/Scans for series_df_tuple in output_df.groupby("Series"): + print series_df_tuple series = series_df_tuple[0] # series_df = output_df but with only one of the Series series_df = series_df_tuple[1] # trim down the pheno DF to match the output DF and merge - newer_pheno_df = new_pheno_df[pheno_df["participant_id"].isin(series_df["participant_id"])] + newer_pheno_df = \ + new_pheno_df[pheno_df["participant_id"].isin(series_df["participant_id"])] newer_pheno_df = pd.merge(new_pheno_df, series_df, how="inner", on=["participant_id"]) @@ -769,7 +805,6 @@ def run(config_file, pipeline_output_folder): procss = [] for unique_resource_id in analysis_dict.keys(): - # unique_resource_id is a 5-long tuple: # ( model name, group model config file, output measure name, # preprocessing strategy string, @@ -784,15 +819,13 @@ def run(config_file, pipeline_output_folder): model_df = analysis_dict[unique_resource_id] if not c.runOnGrid: - from CPAC.pipeline.cpac_ga_model_generator import \ prep_group_analysis_workflow - procss.append(Process(target=prep_group_analysis_workflow, \ - args = (model_df, config_file, model_name, \ - group_config_file, resource_id, \ - preproc_strat, series_or_repeated))) - + procss.append(Process(target=prep_group_analysis_workflow, + args=(model_df, config_file, model_name, + group_config_file, resource_id, + preproc_strat, series_or_repeated))) else: print "\n\n[!] CPAC says: Group-level analysis has not yet been "\ "implemented to handle runs on a cluster or grid.\n\n" \ @@ -800,7 +833,7 @@ def run(config_file, pipeline_output_folder): "continue with group-level analysis. This will submit " \ "the job to only one node, however.\n\nWe will update " \ "users on when this feature will be available through " \ - "release note announcements.\n\n" + "release note announcements.\n\n" # start kicking it off pid = open(os.path.join(c.outputDirectory, 'pid_group.txt'), 'w') @@ -824,29 +857,21 @@ def run(config_file, pipeline_output_folder): the value of the parameter stated above """ idx = 0 - while(idx < len(procss)): - + while idx < len(procss): if len(jobQueue) == 0 and idx == 0: - idc = idx - for p in procss[idc: idc + c.numGPAModelsAtOnce]: - p.start() print >>pid,p.pid jobQueue.append(p) idx += 1 - else: - for job in jobQueue: - if not job.is_alive(): print 'found dead job ', job loc = jobQueue.index(job) del jobQueue[loc] procss[idx].start() - jobQueue.append(procss[idx]) idx += 1 diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_group_analysis_files.py index a34233e80a..b39a832963 100644 --- a/CPAC/utils/create_group_analysis_files.py +++ b/CPAC/utils/create_group_analysis_files.py @@ -302,9 +302,8 @@ def preset_single_group_avg(group_list, pheno_df=None, covariate=None, design_formula = "{0} + {1}".format(design_formula, covariate) group_config = {"pheno_file": design_mat_path, - "participant_id_label": pheno_sub_label, "ev_selections": {"demean": [covariate], - "categorical": ["Group Mean"]}, + "categorical": ["Group_Mean"]}, "design_formula": design_formula, "group_sep": "Off", "grouping_var": None, @@ -419,7 +418,6 @@ def preset_unpaired_two_group(group_list, pheno_df, groups, pheno_sub_label, design_formula = "{0} + {1}".format(groups[0], groups[1]) group_config = {"pheno_file": design_mat_path, - "participant_id_label": pheno_sub_label, "ev_selections": {"demean": [], "categorical": groups}, "design_formula": design_formula, @@ -476,6 +474,7 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, group_list = read_group_list_text_file(group_list_text_file) group_config = {"participant_list": group_list_text_file, + "participant_id_label": "participant_id", "mean_mask": ["Group Mask"], "custom_roi_mask": None, "derivative_list": derivative_list, diff --git a/CPAC/utils/create_group_analysis_info_files.py b/CPAC/utils/create_group_analysis_info_files.py index 5e1810cedb..900442b3ea 100644 --- a/CPAC/utils/create_group_analysis_info_files.py +++ b/CPAC/utils/create_group_analysis_info_files.py @@ -1,23 +1,27 @@ def write_design_matrix_csv(patsy_dmatrix, participant_column, column_names, - outfile_path): + outfile_path): - import os - import patsy import pandas as pd try: - group_model_dataframe = pd.DataFrame(data=patsy_dmatrix, \ - index=participant_column, \ - columns=column_names) - group_model_dataframe.to_csv(outfile_path) - except Exception as e: - err = "\n\n[!] Could not write the design matrix dataframe to the " \ - "CSV file: %s\n\nError details: %s\n\n" % (outfile_path, e) + # TODO + # patsy dmatrix might actually already be a dataframe- rename this + # variable please + patsy_dmatrix.to_csv(outfile_path) + except: + group_model_dataframe = pd.DataFrame(data=patsy_dmatrix, + index=participant_column, + columns=column_names) + group_model_dataframe.to_csv(outfile_path) + #except Exception as e: + # err = "\n\n[!] Could not write the design matrix dataframe to the " \ + # "CSV file: %s\n\nError details: %s\n\n" % (outfile_path, e) + # raise Exception(err) def write_custom_readme_file(): pass - \ No newline at end of file + From 1e22e16149ed0e0eb36ff201557b6f555091101d Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Thu, 15 Mar 2018 10:33:35 -0400 Subject: [PATCH 12/75] fixed montage_a/montage_s error numpy errors when 3D arrays are not float32 --- CPAC/qc/utils.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index 2f693bc384..efc608b102 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -2019,8 +2019,8 @@ def make_montage_axial(overlay, underlay, png_name, cbar_name): Y = nb.load(underlay).get_data() X = nb.load(overlay).get_data() - X = X.astype(np.float16) - Y = Y.astype(np.float16) + X = X.astype(np.float32) + Y = Y.astype(np.float32) if 'skull_vis' in png_name: X[X < 20.0] = 0.0 @@ -2186,8 +2186,8 @@ def make_montage_sagittal(overlay, underlay, png_name, cbar_name): Y = nb.load(underlay).get_data() X = nb.load(overlay).get_data() - X = X.astype(np.float16) - Y = Y.astype(np.float16) + X = X.astype(np.float32) + Y = Y.astype(np.float32) if 'skull_vis' in png_name: @@ -2309,10 +2309,10 @@ def montage_gm_wm_csf_axial(overlay_csf, overlay_wm, overlay_gm, underlay, png_n X_csf = nb.load(overlay_csf).get_data() X_wm = nb.load(overlay_wm).get_data() X_gm = nb.load(overlay_gm).get_data() - X_csf = X_csf.astype(np.float16) - X_wm = X_wm.astype(np.float16) - X_gm = X_gm.astype(np.float16) - Y = Y.astype(np.float16) + X_csf = X_csf.astype(np.float32) + X_wm = X_wm.astype(np.float32) + X_gm = X_gm.astype(np.float32) + Y = Y.astype(np.float32) max_csf = np.nanmax(np.abs(X_csf.flatten())) X_csf[X_csf != 0.0] = max_csf @@ -2420,10 +2420,10 @@ def montage_gm_wm_csf_sagittal(overlay_csf, overlay_wm, overlay_gm, underlay, pn X_csf = nb.load(overlay_csf).get_data() X_wm = nb.load(overlay_wm).get_data() X_gm = nb.load(overlay_gm).get_data() - X_csf = X_csf.astype(np.float16) - X_wm = X_wm.astype(np.float16) - X_gm = X_gm.astype(np.float16) - Y = Y.astype(np.float16) + X_csf = X_csf.astype(np.float32) + X_wm = X_wm.astype(np.float32) + X_gm = X_gm.astype(np.float32) + Y = Y.astype(np.float32) max_csf = np.nanmax(np.abs(X_csf.flatten())) X_csf[X_csf != 0.0] = max_csf From fe616dcdd8eb26cb86846e3a94aaddddc22e2b6a Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Thu, 15 Mar 2018 18:12:24 -0400 Subject: [PATCH 13/75] Began consolidating standard warping, z-scoring, smoothing, and averaging into succinct loops, and renamed several outputs. Also added the options to have smoothing and z-scoring both on/off. --- CPAC/GUI/interface/pages/smoothing.py | 43 +- CPAC/pipeline/cpac_pipeline.py | 1869 ++++------------- .../configs/pipeline_config_template.yml | 10 +- CPAC/utils/utils.py | 140 +- 4 files changed, 476 insertions(+), 1586 deletions(-) diff --git a/CPAC/GUI/interface/pages/smoothing.py b/CPAC/GUI/interface/pages/smoothing.py index e6afad116f..91a16db8e4 100644 --- a/CPAC/GUI/interface/pages/smoothing.py +++ b/CPAC/GUI/interface/pages/smoothing.py @@ -20,7 +20,6 @@ def get_counter(self): return self.counter - class AfterWarpingOptions(wx.ScrolledWindow): def __init__(self, parent, counter = 0): @@ -29,24 +28,40 @@ def __init__(self, parent, counter = 0): self.counter = counter self.page = GenericClass(self, "After Warping Options") - - self.page.add(label= "Smoothing Kernel FWHM (in mm) ", - control=control.TEXT_BOX, - name='fwhm', - type=dtype.LNUM, - values= "4", - validator = CharValidator("no-alpha"), - comment="Full Width at Half Maximum of the Gaussian kernel used during spatial smoothing.\n\nCan be a single value or multiple values separated by commas.\n\nNote that spatial smoothing is run as the last step in the individual-level analysis pipeline, such that all derivatives are output both smoothed and unsmoothed.") + + self.page.add(label="Run Smoothing ", + control=control.CHOICE_BOX, + name='run_smoothing', + type=dtype.LSTR, + comment="Smooth the derivative outputs or not, or " + "produce both unsmoothed and smoothed " + "versions, if preferred.", + values=["On", "Off", "On/Off"]) + + self.page.add(label="Smoothing Kernel FWHM (in mm) ", + control=control.TEXT_BOX, + name='fwhm', + type=dtype.LNUM, + values= "4", + validator = CharValidator("no-alpha"), + comment="Full Width at Half Maximum of the Gaussian " + "kernel used during spatial smoothing.\n\nCan " + "be a single value or multiple values " + "separated by commas.\n\nNote that spatial " + "smoothing is run as the last step in the " + "individual-level analysis pipeline, such " + "that all derivatives are output both " + "smoothed and unsmoothed.") self.page.add(label="Z-score Standardize Derivatives ", control=control.CHOICE_BOX, name='runZScoring', type=dtype.LSTR, - comment="Decides format of outputs. Off will produce" \ - " non-z-scored outputs, On will produce" \ - " z-scores of outputs and non-z-scored " \ - "outputs.", - values=["On", "Off"]) + comment="Decides format of outputs. Off will produce " + "non-z-scored outputs, On will produce " + "z-scores of outputs, and On/Off will " + "produce both.", + values=["On", "Off", "On/Off"]) self.page.set_sizer() parent.get_page_list().append(self) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 6b3455fa5c..26ec09e79e 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -157,6 +157,93 @@ def prep_workflow(sub_dict, c, strategies, run, pipeline_timing_info=None, # Import packages from CPAC.utils.utils import check_config_resources, check_system_deps + # Settle some things + debugging_outputs = ['despiked_fieldmap', + 'fmap_magnitude', + 'fmap_phase_diff', + 'raw_functional', + 'seg_mixeltype', + 'seg_partial_volume_files', + 'seg_partial_volume_map'] + + outputs_native_nonsmooth = ['alff_img', + 'falff_img', + 'reho_raw_map', + 'dr_tempreg_maps_files', + 'dr_tempreg_maps_zstat_files', + 'sca_roi_correlation_files'] + + outputs_template_nonsmooth = ['alff_to_standard', + 'alff_to_standard_zstd', + 'falff_to_standard', + 'falff_to_standard_zstd', + 'reho_to_standard', + 'reho_to_standard_zstd', + 'dr_tempreg_maps_files_to_standard', + 'dr_tempreg_maps_zstat_files_to_standard', + 'sca_roi_files_to_standard', + 'sca_roi_files_to_standard_fisher_zstd', + 'vmhc_raw_score', + 'vmhc_fisher_zstd', + 'vmhc_fisher_zstd_zstat_map', + 'sca_tempreg_maps_files', + 'sca_tempreg_zstat_files', + 'centrality_outputs', + 'centrality_outputs_zstd'] + + outputs_native_smooth = ['alff_smooth', + 'falff_smooth', + 'reho_smooth', + 'dr_tempreg_maps_files_smooth', + 'dr_tempreg_maps_zstat_files_smooth', + 'sca_roi_files_smooth'] + + outputs_template_smooth = ['alff_to_standard_smooth', + 'alff_to_standard_zstd_smooth', + 'falff_to_standard_smooth', + 'falff_to_standard_zstd_smooth', + 'reho_to_standard_smooth', + 'reho_to_standard_zstd_smooth', + 'dr_tempreg_maps_files_to_standard_smooth' + 'dr_tempreg_maps_zstat_files_to_standard_smooth', + 'sca_roi_files_to_standard_smooth', + 'sca_roi_files_to_standard_smooth_fisher_zstd', + 'sca_tempreg_maps_files_smooth', + 'sca_tempreg_maps_zstat_files_smooth', + 'centrality_outputs_smooth', + 'centrality_outputs_zstd_smooth'] + + outputs_template_raw = ['alff_to_standard', + 'alff_to_standard_smooth', + 'falff_to_standard', + 'falff_to_standard_smooth', + 'reho_to_standard', + 'reho_to_standard_smooth', + 'centrality_outputs', + 'centrality_outputs_smooth'] + + outputs_average = ['alff_img', + 'alff_to_standard', + 'falff_img', + 'falff_to_standard', + 'reho_to_standard', + 'dr_tempreg_maps_files', + 'dr_tempreg_maps_files_to_standard', + 'sca_roi_correlation_files', + 'sca_roi_files_to_standard', + 'sca_tempreg_maps_files', + 'alff_smooth', + 'alff_to_standard_smooth', + 'falff_smooth', + 'falff_to_standard_smooth', + 'reho_smooth', + 'reho_to_standard_smooth', + 'dr_tempreg_maps_files_smooth', + 'dr_tempreg_maps_files_to_standard_smooth' + 'sca_roi_files_smooth', + 'sca_roi_files_to_standard_smooth', + 'sca_tempreg_maps_files_smooth'] + # Start timing here pipeline_start_time = time.time() # at end of workflow, take timestamp again, take time elapsed and check @@ -368,6 +455,7 @@ def getNodeList(strategy): return nodes strat_list = [] + non_outputs = [] workflow_bit_id = {} workflow_counter = 0 @@ -539,17 +627,10 @@ def getNodeList(strategy): strat.set_leaf_properties(fnirt_reg_anat_mni, 'outputspec.output_brain') - strat.update_resource_pool({'anatomical_to_mni_linear_xfm': ( - fnirt_reg_anat_mni, 'outputspec.linear_xfm'), - 'anatomical_to_mni_nonlinear_xfm': ( - fnirt_reg_anat_mni, - 'outputspec.nonlinear_xfm'), - 'mni_to_anatomical_linear_xfm': ( - fnirt_reg_anat_mni, - 'outputspec.invlinear_xfm'), - 'mni_normalized_anatomical': ( - fnirt_reg_anat_mni, - 'outputspec.output_brain')}) + strat.update_resource_pool({'anatomical_to_mni_linear_xfm': (fnirt_reg_anat_mni, 'outputspec.linear_xfm'), + 'anatomical_to_mni_nonlinear_xfm': (fnirt_reg_anat_mni, 'outputspec.nonlinear_xfm'), + 'mni_to_anatomical_linear_xfm': (fnirt_reg_anat_mni, 'outputspec.invlinear_xfm'), + 'mni_normalized_anatomical': (fnirt_reg_anat_mni, 'outputspec.output_brain')}) create_log_node(fnirt_reg_anat_mni, 'outputspec.output_brain', num_strat) @@ -677,24 +758,13 @@ def getNodeList(strategy): strat.set_leaf_properties(ants_reg_anat_mni, 'outputspec.normalized_output_brain') - strat.update_resource_pool({'ants_initial_xfm': ( - ants_reg_anat_mni, 'outputspec.ants_initial_xfm'), - 'ants_rigid_xfm': (ants_reg_anat_mni, - 'outputspec.ants_rigid_xfm'), - 'ants_affine_xfm': (ants_reg_anat_mni, - 'outputspec.ants_affine_xfm'), - 'anatomical_to_mni_nonlinear_xfm': ( - ants_reg_anat_mni, - 'outputspec.warp_field'), - 'mni_to_anatomical_nonlinear_xfm': ( - ants_reg_anat_mni, - 'outputspec.inverse_warp_field'), - 'anat_to_mni_ants_composite_xfm': ( - ants_reg_anat_mni, - 'outputspec.composite_transform'), - 'mni_normalized_anatomical': ( - ants_reg_anat_mni, - 'outputspec.normalized_output_brain')}) + strat.update_resource_pool({'ants_initial_xfm': (ants_reg_anat_mni, 'outputspec.ants_initial_xfm'), + 'ants_rigid_xfm': (ants_reg_anat_mni, 'outputspec.ants_rigid_xfm'), + 'ants_affine_xfm': (ants_reg_anat_mni, 'outputspec.ants_affine_xfm'), + 'anatomical_to_mni_nonlinear_xfm': (ants_reg_anat_mni, 'outputspec.warp_field'), + 'mni_to_anatomical_nonlinear_xfm': (ants_reg_anat_mni, 'outputspec.inverse_warp_field'), + 'anat_to_mni_ants_composite_xfm': (ants_reg_anat_mni, 'outputspec.composite_transform'), + 'mni_normalized_anatomical': (ants_reg_anat_mni, 'outputspec.normalized_output_brain')}) create_log_node(ants_reg_anat_mni, 'outputspec.normalized_output_brain', num_strat) @@ -790,21 +860,10 @@ def getNodeList(strategy): strat.set_leaf_properties(fnirt_reg_anat_symm_mni, 'outputspec.output_brain') - strat.update_resource_pool({ - 'anatomical_to_symmetric_mni_linear_xfm': ( - fnirt_reg_anat_symm_mni, - 'outputspec.linear_xfm'), - 'anatomical_to_symmetric_mni_nonlinear_xfm': ( - fnirt_reg_anat_symm_mni, - 'outputspec.nonlinear_xfm'), - 'symmetric_mni_to_anatomical_linear_xfm': ( - fnirt_reg_anat_symm_mni, - 'outputspec.invlinear_xfm'), - 'symmetric_mni_normalized_anatomical': ( - fnirt_reg_anat_symm_mni, - 'outputspec.output_brain')}) # , - # 'mni_normalized_anatomical':(ants_reg_anat_symm_mni, 'outputspec.wait')}) - + strat.update_resource_pool({'anatomical_to_symmetric_mni_linear_xfm': (fnirt_reg_anat_symm_mni, 'outputspec.linear_xfm'), + 'anatomical_to_symmetric_mni_nonlinear_xfm': (fnirt_reg_anat_symm_mni, 'outputspec.nonlinear_xfm'), + 'symmetric_mni_to_anatomical_linear_xfm': (fnirt_reg_anat_symm_mni, 'outputspec.invlinear_xfm'), + 'symmetric_mni_normalized_anatomical': (fnirt_reg_anat_symm_mni, 'outputspec.output_brain')}) create_log_node(fnirt_reg_anat_symm_mni, 'outputspec.output_brain', num_strat) @@ -831,7 +890,6 @@ def getNodeList(strategy): num_threads=num_ants_cores) try: - # calculating the transform with the skullstripped is # reported to be better, but it requires very high # quality skullstripping. If skullstripping is imprecise @@ -928,11 +986,6 @@ def getNodeList(strategy): smoothing_sigmas = [[3, 2, 1, 0], [3, 2, 1, 0], [3, 2, 1, 0]] - # node, out_file = strat.get_node_from_resource_pool('mni_normalized_anatomical') - # workflow.connect(node, out_file, - # ants_reg_anat_symm_mni, 'inputspec.wait') - - except: logConnectionError( 'Symmetric Anatomical Registration (ANTS)', num_strat, @@ -943,27 +996,13 @@ def getNodeList(strategy): strat.set_leaf_properties(ants_reg_anat_symm_mni, 'outputspec.normalized_output_brain') - strat.update_resource_pool({'ants_symmetric_initial_xfm': ( - ants_reg_anat_symm_mni, 'outputspec.ants_initial_xfm'), - 'ants_symmetric_rigid_xfm': ( - ants_reg_anat_symm_mni, - 'outputspec.ants_rigid_xfm'), - 'ants_symmetric_affine_xfm': ( - ants_reg_anat_symm_mni, - 'outputspec.ants_affine_xfm'), - 'anatomical_to_symmetric_mni_nonlinear_xfm': ( - ants_reg_anat_symm_mni, - 'outputspec.warp_field'), - 'symmetric_mni_to_anatomical_nonlinear_xfm': ( - ants_reg_anat_symm_mni, - 'outputspec.inverse_warp_field'), - 'anat_to_symmetric_mni_ants_composite_xfm': ( - ants_reg_anat_symm_mni, - 'outputspec.composite_transform'), - 'symmetric_mni_normalized_anatomical': ( - ants_reg_anat_symm_mni, - 'outputspec.normalized_output_brain')}) # , - # 'mni_normalized_anatomical':(ants_reg_anat_symm_mni, 'outputspec.wait')}) + strat.update_resource_pool({'ants_symmetric_initial_xfm': (ants_reg_anat_symm_mni, 'outputspec.ants_initial_xfm'), + 'ants_symmetric_rigid_xfm': (ants_reg_anat_symm_mni, 'outputspec.ants_rigid_xfm'), + 'ants_symmetric_affine_xfm': (ants_reg_anat_symm_mni, 'outputspec.ants_affine_xfm'), + 'anatomical_to_symmetric_mni_nonlinear_xfm': (ants_reg_anat_symm_mni, 'outputspec.warp_field'), + 'symmetric_mni_to_anatomical_nonlinear_xfm': (ants_reg_anat_symm_mni, 'outputspec.inverse_warp_field'), + 'anat_to_symmetric_mni_ants_composite_xfm': (ants_reg_anat_symm_mni, 'outputspec.composite_transform'), + 'symmetric_mni_normalized_anatomical': (ants_reg_anat_symm_mni, 'outputspec.normalized_output_brain')}) create_log_node(ants_reg_anat_symm_mni, 'outputspec.normalized_output_brain', @@ -1577,6 +1616,8 @@ def getNodeList(strategy): strat.set_leaf_properties(func_preproc, 'outputspec.preprocessed') + # TODO redundant with above resource pool additions? + # add stuff to resource pool if we need it strat.update_resource_pool({'mean_functional': ( func_preproc, 'outputspec.example_func')}) @@ -1643,8 +1684,7 @@ def getNodeList(strategy): new_strat_list.append(strat) strat.append_name(fristons_model.name) - strat.update_resource_pool({'movement_parameters': ( - fristons_model, 'outputspec.movement_file')}) + strat.update_resource_pool({'movement_parameters': (fristons_model, 'outputspec.movement_file')}) create_log_node(fristons_model, 'outputspec.movement_file', num_strat) @@ -1937,8 +1977,8 @@ def pick_wm(seg_prob_list): c.spikeThreshold) try: - # #**special case where the workflow is not getting outputs from resource pool - # but is connected to functional datasource + # **special case where the workflow is not getting outputs from + # resource pool but is connected to functional datasource workflow.connect(funcFlow, 'outputspec.subject', gen_motion_stats, 'inputspec.subject_id') @@ -2128,12 +2168,8 @@ def pick_wm(seg_prob_list): strat.set_leaf_properties(nuisance, 'outputspec.subject') - strat.update_resource_pool({'functional_nuisance_residuals': ( - nuisance, 'outputspec.subject')}) - strat.update_resource_pool({ - 'functional_nuisance_regressors': ( - nuisance, - 'outputspec.regressors')}) + strat.update_resource_pool({'functional_nuisance_residuals': (nuisance, 'outputspec.subject')}) + strat.update_resource_pool({'functional_nuisance_regressors': (nuisance, 'outputspec.regressors')}) create_log_node(nuisance, 'outputspec.subject', num_strat) @@ -2179,8 +2215,7 @@ def pick_wm(seg_prob_list): strat.set_leaf_properties(median_angle_corr, 'outputspec.subject') - strat.update_resource_pool({'functional_median_angle_corrected': ( - median_angle_corr, 'outputspec.subject')}) + strat.update_resource_pool({'functional_median_angle_corrected': (median_angle_corr, 'outputspec.subject')}) create_log_node(median_angle_corr, 'outputspec.subject', num_strat) @@ -2220,15 +2255,16 @@ def pick_wm(seg_prob_list): logConnectionError('ALFF', num_strat, strat.get_resource_pool(), '0012') raise + strat.append_name(alff.name) - strat.update_resource_pool( - {'alff_img': (alff, 'outputspec.alff_img')}) - strat.update_resource_pool( - {'falff_img': (alff, 'outputspec.falff_img')}) + + strat.update_resource_pool({'alff_img': (alff, 'outputspec.alff_img')}) + strat.update_resource_pool({'falff_img': (alff, 'outputspec.falff_img')}) create_log_node(alff, 'outputspec.falff_img', num_strat) num_strat += 1 + strat_list += new_strat_list ''' @@ -2254,8 +2290,7 @@ def pick_wm(seg_prob_list): imports=filter_imports), name='frequency_filter_%d' % num_strat) - frequency_filter.iterables = ( - 'bandpass_freqs', c.nuisanceBandpassFreq) + frequency_filter.iterables = ('bandpass_freqs', c.nuisanceBandpassFreq) try: node, out_file = strat.get_leaf_properties() workflow.connect(node, out_file, @@ -2279,8 +2314,7 @@ def pick_wm(seg_prob_list): strat.set_leaf_properties(frequency_filter, 'bandpassed_file') - strat.update_resource_pool({'functional_freq_filtered': ( - frequency_filter, 'bandpassed_file')}) + strat.update_resource_pool({'functional_freq_filtered': (frequency_filter, 'bandpassed_file')}) create_log_node(frequency_filter, 'bandpassed_file', num_strat) @@ -2936,7 +2970,7 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ raise strat.update_resource_pool( - {'raw_reho_map': (reho, 'outputspec.raw_reho_map')}) + {'reho_raw_map': (reho, 'outputspec.raw_reho_map')}) strat.append_name(reho.name) create_log_node(reho, 'outputspec.raw_reho_map', num_strat) @@ -3115,18 +3149,13 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ if "SpatialReg" in ts_analysis_dict.keys(): strat.append_name(spatial_map_timeseries.name) - strat.update_resource_pool({'spatial_map_timeseries': ( - spatial_map_timeseries, 'outputspec.subject_timeseries')}) - create_log_node(spatial_map_timeseries, - 'outputspec.subject_timeseries', num_strat) + strat.update_resource_pool({'spatial_map_timeseries': (spatial_map_timeseries, 'outputspec.subject_timeseries')}) + create_log_node(spatial_map_timeseries, 'outputspec.subject_timeseries', num_strat) if "DualReg" in sca_analysis_dict.keys(): strat.append_name(spatial_map_timeseries_for_dr.name) - strat.update_resource_pool({'spatial_map_timeseries_for_DR': ( - spatial_map_timeseries_for_dr, - 'outputspec.subject_timeseries')}) - create_log_node(spatial_map_timeseries_for_dr, - 'outputspec.subject_timeseries', num_strat) + strat.update_resource_pool({'spatial_map_timeseries_for_DR': (spatial_map_timeseries_for_dr, 'outputspec.subject_timeseries')}) + create_log_node(spatial_map_timeseries_for_dr, 'outputspec.subject_timeseries', num_strat) if ("SpatialReg" in ts_analysis_dict.keys()) or \ ("DualReg" in sca_analysis_dict.keys()): @@ -3266,24 +3295,19 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ if "Avg" in ts_analysis_dict.keys(): strat.append_name(roi_timeseries.name) - strat.update_resource_pool({'roi_timeseries': ( - roi_timeseries, 'outputspec.roi_outputs')}) + strat.update_resource_pool({'roi_timeseries': (roi_timeseries, 'outputspec.roi_outputs')}) create_log_node(roi_timeseries, 'outputspec.roi_outputs', num_strat) if "Avg" in sca_analysis_dict.keys(): strat.append_name(roi_timeseries_for_sca.name) - strat.update_resource_pool({'roi_timeseries_for_SCA': ( - roi_timeseries_for_sca, 'outputspec.roi_outputs')}) + strat.update_resource_pool({'roi_timeseries_for_SCA': (roi_timeseries_for_sca, 'outputspec.roi_outputs')}) create_log_node(roi_timeseries_for_sca, 'outputspec.roi_outputs', num_strat) if "MultReg" in sca_analysis_dict.keys(): strat.append_name(roi_timeseries_for_multreg.name) - strat.update_resource_pool({ - 'roi_timeseries_for_SCA_multreg': ( - roi_timeseries_for_multreg, - 'outputspec.roi_outputs')}) + strat.update_resource_pool({'roi_timeseries_for_SCA_multreg': (roi_timeseries_for_multreg, 'outputspec.roi_outputs')}) create_log_node(roi_timeseries_for_multreg, 'outputspec.roi_outputs', num_strat) @@ -3344,8 +3368,7 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ raise strat.append_name(voxel_timeseries.name) - strat.update_resource_pool({'voxel_timeseries': ( - voxel_timeseries, 'outputspec.mask_outputs')}) + strat.update_resource_pool({'voxel_timeseries': (voxel_timeseries, 'outputspec.mask_outputs')}) create_log_node(voxel_timeseries, 'outputspec.mask_outputs', num_strat) num_strat += 1 @@ -3382,10 +3405,7 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ strat.get_resource_pool(), '0032') raise - strat.update_resource_pool({'sca_roi_correlation_stack': ( - sca_roi, 'outputspec.correlation_stack'), - 'sca_roi_correlation_files': (sca_roi, - 'outputspec.correlation_files')}) + strat.update_resource_pool({'sca_roi_correlation_files': (sca_roi, 'outputspec.correlation_files')}) create_log_node(sca_roi, 'outputspec.correlation_stack', num_strat) @@ -3395,7 +3415,6 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ strat_list += new_strat_list - ''' (Dual Regression) Temporal Regression for Dual Regression ''' @@ -3437,15 +3456,8 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ num_strat, strat.get_resource_pool(), '0033') raise - strat.update_resource_pool({'dr_tempreg_maps_stack': ( - dr_temp_reg, 'outputspec.temp_reg_map'), - 'dr_tempreg_maps_files': (dr_temp_reg, - 'outputspec.temp_reg_map_files')}) - strat.update_resource_pool({'dr_tempreg_maps_zstat_stack': ( - dr_temp_reg, 'outputspec.temp_reg_map_z'), - 'dr_tempreg_maps_zstat_files': ( - dr_temp_reg, - 'outputspec.temp_reg_map_z_files')}) + strat.update_resource_pool({'dr_tempreg_maps_files': (dr_temp_reg, 'outputspec.temp_reg_map_files')}) + strat.update_resource_pool({'dr_tempreg_maps_zstat_files': (dr_temp_reg, 'outputspec.temp_reg_map_z_files')}) strat.append_name(dr_temp_reg.name) @@ -3502,16 +3514,8 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ num_strat, strat.get_resource_pool(), '0037') raise - strat.update_resource_pool({'sca_tempreg_maps_stack': ( - sc_temp_reg, 'outputspec.temp_reg_map'), - 'sca_tempreg_maps_files': ( - sc_temp_reg, - 'outputspec.temp_reg_map_files')}) - strat.update_resource_pool({'sca_tempreg_maps_zstat_stack': ( - sc_temp_reg, 'outputspec.temp_reg_map_z'), - 'sca_tempreg_maps_zstat_files': ( - sc_temp_reg, - 'outputspec.temp_reg_map_z_files')}) + strat.update_resource_pool({'sca_tempreg_maps_files': (sc_temp_reg, 'outputspec.temp_reg_map_files')}) + strat.update_resource_pool({'sca_tempreg_maps_zstat_files': (sc_temp_reg, 'outputspec.temp_reg_map_z_files')}) create_log_node(sc_temp_reg, 'outputspec.temp_reg_map', num_strat) @@ -3520,103 +3524,6 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ strat_list += new_strat_list - ''' - Inserting Surface Registration - ''' - - ''' - new_strat_list = [] - num_strat = 0 - - workflow_counter += 1 - if 1 in c.runSurfaceRegistraion: - workflow_bit_id['surface_registration'] = workflow_counter - for strat in strat_list: - - surface_reg = create_surface_registration('surface_reg_%d' % num_strat) - surface_reg.inputs.inputspec.recon_subjects = c.reconSubjectsDirectory - surface_reg.inputs.inputspec.subject_id = subject_id - - - try: - - node, out_file = strat.get_leaf_properties() - workflow.connect(node, out_file, - surface_reg, 'inputspec.rest') - - node, out_file = strat.get_node_from_resource_pool('anatomical_brain') - workflow.connect(node, out_file, - surface_reg, 'inputspec.brain') - - except: - logConnectionError('Surface Registration Workflow', num_strat, strat.get_resource_pool(), '0048') - raise - - if 0 in c.runSurfaceRegistraion: - tmp = strategy() - tmp.resource_pool = dict(strat.resource_pool) - tmp.leaf_node = (strat.leaf_node) - tmp.leaf_out_file = str(strat.leaf_out_file) - tmp.name = list(strat.name) - strat = tmp - new_strat_list.append(strat) - - strat.append_name(surface_reg.name) - - strat.update_resource_pool({'bbregister_registration' : (surface_reg, 'outputspec.out_reg_file'), - 'left_hemisphere_surface' : (surface_reg, 'outputspec.lh_surface_file'), - 'right_hemisphere_surface' : (surface_reg, 'outputspec.rh_surface_file')}) - - num_strat += 1 - - strat_list += new_strat_list - ''' - - ''' - Inserting vertices based timeseries - ''' - - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runVerticesTimeSeries: - for strat in strat_list: - - vertices_timeseries = get_vertices_timeseries('vertices_timeseries_%d' % num_strat) - - try: - - node, out_file = strat.get_node_from_resource_pool('left_hemisphere_surface') - workflow.connect(node, out_file, - vertices_timeseries, 'inputspec.lh_surface_file') - - node, out_file = strat.get_node_from_resource_pool('right_hemisphere_surface') - workflow.connect(node, out_file, - vertices_timeseries, 'inputspec.rh_surface_file') - - except: - logConnectionError('Vertices Timeseries Extraction', num_strat, strat.get_resource_pool(), '0049') - raise - - if 0 in c.runVerticesTimeSeries: - tmp = strategy() - tmp.resource_pool = dict(strat.resource_pool) - tmp.leaf_node = (strat.leaf_node) - tmp.leaf_out_file = str(strat.leaf_out_file) - tmp.name = list(strat.name) - strat = tmp - new_strat_list.append(strat) - - strat.append_name(vertices_timeseries.name) - - strat.update_resource_pool({'vertices_timeseries' : (vertices_timeseries, 'outputspec.surface_outputs')}) - - num_strat += 1 - - strat_list += new_strat_list - ''' - ''' Inserting Network centrality ''' @@ -3734,10 +3641,8 @@ def connectCentralityWorkflow(methodOption, # into pipeline def connect_afni_centrality_wf(method_option, threshold_option, threshold): - ''' - ''' - # Import pacakges + # Import packages from CPAC.network_centrality.afni_network_centrality \ import create_afni_centrality_wf import CPAC.network_centrality.utils as cent_utils @@ -3756,7 +3661,8 @@ def connect_afni_centrality_wf(method_option, threshold_option, num_threads = c.maxCoresPerParticipant memory = c.memoryAllocatedForDegreeCentrality - # Format method and threshold options properly and check for errors + # Format method and threshold options properly and check for + # errors method_option, threshold_option = \ cent_utils.check_centrality_params(method_option, threshold_option, @@ -3834,8 +3740,9 @@ def connect_afni_centrality_wf(method_option, threshold_option, strat.update_resource_pool( {'centrality_outputs': (merge_node, 'merged_list')}) + ''' # if smoothing is required - if c.fwhm != None: + if 1 in c.run_smoothing: z_score = get_cent_zscore( 'centrality_zscore_%d' % num_strat) @@ -3874,15 +3781,13 @@ def connect_afni_centrality_wf(method_option, threshold_option, zstd_smoothing, 'op_string') strat.append_name(smoothing.name) - strat.update_resource_pool({'centrality_outputs_zstd': ( - z_score, 'outputspec.z_score_img'), - 'centrality_outputs_smoothed': ( - smoothing, 'out_file'), - 'centrality_outputs_smoothed_zstd': ( - zstd_smoothing, 'out_file')}) + strat.update_resource_pool({'centrality_outputs_zstd': (z_score, 'outputspec.z_score_img'), + 'centrality_outputs_smooth': (smoothing, 'out_file'), + 'centrality_outputs_zstd_smooth': (zstd_smoothing, 'out_file')}) strat.append_name(smoothing.name) create_log_node(smoothing, 'out_file', num_strat) + ''' except: logConnectionError('Network Centrality', num_strat, @@ -4077,115 +3982,92 @@ def output_to_standard(output_name, output_resource, strat, num_strat, num_strat += 1 ''' - OUTPUT TO SMOOTH + Transforming Dual Regression outputs to MNI ''' + new_strat_list = [] + num_strat = 0 - def output_smooth(output_name, output_resource, strat, num_strat, - map_node=0): - - output_to_standard_smooth = None - - if map_node == 0: - output_smooth = pe.Node(interface=fsl.MultiImageMaths(), - name='%s_smooth_%d' % ( - output_name, num_strat)) - - - elif map_node == 1: - output_smooth = pe.MapNode(interface=fsl.MultiImageMaths(), - name='%s_smooth_%d' % ( - output_name, num_strat), \ - iterfield=['in_file']) - - try: - - node, out_file = strat. \ - get_node_from_resource_pool(output_resource) - - workflow.connect(node, out_file, output_smooth, 'in_file') - - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - output_smooth, 'op_string') + if 1 in c.runRegisterFuncToMNI and "DualReg" in sca_analysis_dict.keys(): + for strat in strat_list: + output_to_standard('dr_tempreg_maps_stack', + 'dr_tempreg_maps_stack', strat, num_strat, + input_image_type=3) - node, out_file = strat. \ - get_node_from_resource_pool('functional_brain_mask') - workflow.connect(node, out_file, output_smooth, 'operand_files') + output_to_standard('dr_tempreg_maps_zstat_stack', + 'dr_tempreg_maps_zstat_stack', strat, + num_strat, input_image_type=3) + # dual reg 'files', too + output_to_standard('dr_tempreg_maps_files', + 'dr_tempreg_maps_files', strat, num_strat, 1) + output_to_standard('dr_tempreg_maps_zstat_files', + 'dr_tempreg_maps_zstat_files', strat, + num_strat, 1) - except: - logConnectionError('%s smooth' % output_name, num_strat, \ - strat.get_resource_pool(), '0027') - raise + num_strat += 1 - strat.append_name(output_smooth.name) - strat.update_resource_pool({'%s_smooth' % (output_name): \ - (output_smooth, 'out_file')}) + strat_list += new_strat_list - if 1 in c.runRegisterFuncToMNI: + ''' + Transforming alff/falff outputs to MNI + ''' + new_strat_list = [] + num_strat = 0 - if map_node == 0: - output_to_standard_smooth = pe.Node(interface= \ - fsl.MultiImageMaths(), - name='%s_to_standard_' \ - 'smooth_%d' % ( - output_name, - num_strat)) + if 1 in c.runRegisterFuncToMNI and (1 in c.runALFF): + for strat in strat_list: + output_to_standard('alff', 'alff_img', strat, num_strat) + output_to_standard('falff', 'falff_img', strat, num_strat) - elif map_node == 1: - output_to_standard_smooth = pe.MapNode(interface= \ - fsl.MultiImageMaths(), - name='%s_to_standard_' \ - 'smooth_%d' % ( - output_name, - num_strat), \ - iterfield=['in_file']) + num_strat += 1 - try: + strat_list += new_strat_list - node, out_file = strat.get_node_from_resource_pool('%s_to_' \ - 'standard' % output_name) + ''' + Transforming ReHo outputs to MNI + ''' + new_strat_list = [] + num_strat = 0 - workflow.connect(node, out_file, output_to_standard_smooth, - 'in_file') + if 1 in c.runRegisterFuncToMNI and (1 in c.runReHo): + for strat in strat_list: + output_to_standard('reho', 'reho_raw_map', strat, num_strat) - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - output_to_standard_smooth, 'op_string') + num_strat += 1 - node, out_file = strat.get_node_from_resource_pool('func' \ - 'tional_brain_mask_to_standard') - workflow.connect(node, out_file, output_to_standard_smooth, - 'operand_files') + strat_list += new_strat_list + ''' + Transforming SCA ROI outputs to MNI + ''' + new_strat_list = [] + num_strat = 0 - except: - logConnectionError('%s smooth in MNI' % output_name, \ - num_strat, strat.get_resource_pool(), - '0028') - raise Exception + if 1 in c.runRegisterFuncToMNI and (1 in c.runSCA) and ( + "Avg" in sca_analysis_dict.keys()): # in(1 in c.runROITimeseries): + for strat in strat_list: + output_to_standard('sca_roi_stack', 'sca_roi_correlation_stack', + strat, num_strat, input_image_type=3) + output_to_standard('sca_roi_files', 'sca_roi_correlation_files', + strat, num_strat, 1) - strat.append_name(output_to_standard_smooth.name) - strat.update_resource_pool({'%s_to_standard_smooth' % \ - (output_name): ( - output_to_standard_smooth, 'out_file')}) - create_log_node(output_to_standard_smooth, 'out_file', num_strat) + num_strat += 1 - num_strat += 1 + strat_list += new_strat_list - ''' - z-score standardization functions - ''' + '''''' + ''' Z-SCORING ''' - def z_score_standardize(output_name, output_resource, strat, num_strat, - map_node=0): + def z_score_standardize(output_name, strat, num_strat): - z_score_std = get_zscore(output_resource, 'z_score_std_%s_%d' % ( - output_name, num_strat)) + # call the z-scoring sub-workflow builder + z_score_std = get_zscore(output_name, 'z_score_std_%s_%d' % ( + output_name, num_strat)) try: - node, out_file = strat.get_node_from_resource_pool( - output_resource) + output_name) workflow.connect(node, out_file, z_score_std, 'inputspec.input_file') @@ -4200,29 +4082,26 @@ def z_score_standardize(output_name, output_resource, strat, num_strat, except: - logConnectionError('%s z-score standardize' % output_name, - num_strat, \ - strat.get_resource_pool(), '0127') + num_strat, strat.get_resource_pool(), '0127') raise strat.append_name(z_score_std.name) - strat.update_resource_pool({'%s_zstd' % (output_resource): \ + strat.update_resource_pool({'%s_zstd' % (output_name): \ (z_score_std, 'outputspec.z_score_img')}) - def fisher_z_score_standardize(output_name, output_resource, - timeseries_oned_file, strat, num_strat, - map_node=0): + def fisher_z_score_standardize(output_name, timeseries_oned_file, strat, + num_strat): - fisher_z_score_std = get_fisher_zscore(output_resource, map_node, \ + # call the fisher r-to-z sub-workflow builder + fisher_z_score_std = get_fisher_zscore(output_name, 'fisher_z_score_std_%s_%d' \ % (output_name, num_strat)) try: - node, out_file = strat. \ - get_node_from_resource_pool(output_resource) + get_node_from_resource_pool(output_name) workflow.connect(node, out_file, fisher_z_score_std, 'inputspec.correlation_file') @@ -4234,1156 +4113,181 @@ def fisher_z_score_standardize(output_name, output_resource, except: - logConnectionError('%s fisher z-score standardize' % output_name, - num_strat, \ - strat.get_resource_pool(), '0128') + num_strat, strat.get_resource_pool(), '0128') raise strat.append_name(fisher_z_score_std.name) - strat.update_resource_pool({'%s_fisher_zstd' % (output_resource): \ + strat.update_resource_pool({'%s_fisher_zstd' % (output_name): \ (fisher_z_score_std, 'outputspec.fisher_z_score_img')}) - # Calc average via 3dmaskave - def calc_avg(output_resource, strat, num_strat, map_node=0): - ''' - calculate output averages via individual-level mask - ''' + ''' Run the z-scoring nodes ''' + + if 1 in c.runZScoring: + num_strat = 0 + for strat in strat_list: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + if key in outputs_template_raw: + strat = z_score_standardize(key, strat, num_strat) + num_strat += 1 + + ''' + fisher-z-standardize SCA ROI MNI-standardized outputs + ''' + new_strat_list = [] + num_strat = 0 + + if 1 in c.runZScoring and (1 in c.runSCA) and \ + ("Avg" in sca_analysis_dict.keys()): + + for strat in strat_list: + + if c.run_smoothing: + fisher_z_score_standardize('sca_roi_files_to_standard_smooth', + 'sca_roi_files_to_standard_smooth', + 'roi_timeseries_for_SCA', + strat, num_strat, 1) + + fisher_z_score_standardize('sca_roi_files_to_standard', + 'sca_roi_files_to_standard', + 'roi_timeseries_for_SCA', + strat, num_strat, 1) + + num_strat += 1 + + strat_list += new_strat_list + + '''''' + ''' SMOOTHING ''' + + def output_smooth(output_name, mask_name, strat, num_strat): + + if map_node: + output_smooth = pe.MapNode(interface=fsl.MultiImageMaths(), + name='{0}_smooth_{1}'.format(output_name, + num_strat), + iterfield=['in_file']) + else: + output_smooth = pe.Node(interface=fsl.MultiImageMaths(), + name='{0}_smooth_{1}'.format(output_name, + num_strat)) + + try: + # get the resource to be smoothed + node, out_file = strat.get_node_from_resource_pool(output_name) + + workflow.connect(node, out_file, output_smooth, 'in_file') + + # get the parameters for fwhm + workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), + output_smooth, 'op_string') + + # get the mask + node, out_file = strat.get_node_from_resource_pool(mask_name) + workflow.connect(node, out_file, output_smooth, 'operand_files') + + except: + logConnectionError('{0} smooth'.format(output_name), num_strat, + strat.get_resource_pool(), '0027') + raise + + strat.append_name(output_smooth.name) + strat.update_resource_pool({'{0}_smooth'.format(output_name): (output_smooth, 'out_file')}) + + return strat + + ''' + Connect the smoothing nodes + ''' + + if 1 in c.run_smoothing: + num_strat = 0 + for strat in strat_list: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + if "centrality" in key: + strat = output_smooth(key, c.templateSpecificationFile, + strat, num_strat) + elif key in outputs_native_nonsmooth: + strat = output_smooth(key, "functional_brain_mask", + strat, num_strat) + elif key in outputs_template_nonsmooth: + strat = output_smooth(key, + "functional_brain_mask_to_standard", + strat, num_strat) + num_strat += 1 + + '''''' + ''' AVERAGING ''' + + def calc_avg(output_name, strat, num_strat): + """Calculate the average of an output using AFNI 3dmaskave.""" extract_imports = ['import os'] if map_node == 0: calc_average = pe.Node(interface=preprocess.Maskave(), - name='%s_mean_%d' % ( - output_resource, num_strat)) + name='{0}_mean_{1}'.format(output_name, + num_strat)) - mean_to_csv = pe.Node(util.Function(input_names= \ - ['in_file', - 'output_name'], + mean_to_csv = pe.Node(util.Function(input_names=['in_file', + 'output_name'], output_names=['output_mean'], function=extract_output_mean, imports=extract_imports), - name='%s_mean_to_txt_%d' % (output_resource, \ - num_strat)) + name='{0}_mean_to_txt_{1}'.format(output_name, + num_strat)) elif map_node == 1: calc_average = pe.MapNode(interface=preprocess.Maskave(), - name='%s_mean_%d' % ( - output_resource, num_strat), \ + name='{0}_mean_{1}'.format(output_name, + num_strat), iterfield=['in_file']) - mean_to_csv = pe.MapNode(util.Function(input_names= \ - ['in_file', - 'output_name'], - output_names=[ - 'output_mean'], + mean_to_csv = pe.MapNode(util.Function(input_names=['in_file', + 'output_name'], + output_names=['output_mean'], function=extract_output_mean, imports=extract_imports), - name='%s_mean_to_txt_%d' % ( - output_resource, num_strat), + name='{0}_mean_to_txt_{1}'.format(output_name, + num_strat), iterfield=['in_file']) - mean_to_csv.inputs.output_name = output_resource + mean_to_csv.inputs.output_name = output_name try: - node, out_file = strat. \ - get_node_from_resource_pool(output_resource) + node, out_file = strat.get_node_from_resource_pool(output_name) workflow.connect(node, out_file, calc_average, 'in_file') workflow.connect(calc_average, 'out_file', mean_to_csv, 'in_file') except: - logConnectionError('%s calc average' % \ - output_resource, num_strat, - strat.get_resource_pool(), '0128') + logConnectionError('{0} calc average'.format(output_name), + num_strat, strat.get_resource_pool(), '0128') raise strat.append_name(calc_average.name) - strat.update_resource_pool({'output_means.@%s_average' % ( - output_resource): (mean_to_csv, 'output_mean')}) + strat.update_resource_pool({'output_means.@{0}_average'.format(output_name): (mean_to_csv, 'output_mean')}) + + return strat ''' - Transforming Dual Regression outputs to MNI + Connect the averaging nodes ''' - new_strat_list = [] + num_strat = 0 + for strat in strat_list: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + if key in outputs_average: + strat = calc_avg(key, strat, num_strat) + num_strat += 1 - if (1 in c.runRegisterFuncToMNI) and ( - "DualReg" in sca_analysis_dict.keys()): # (1 in c.runDualReg) and (1 in c.runSpatialRegression): - for strat in strat_list: - output_to_standard('dr_tempreg_maps_stack', - 'dr_tempreg_maps_stack', strat, num_strat, - input_image_type=3) - - output_to_standard('dr_tempreg_maps_zstat_stack', - 'dr_tempreg_maps_zstat_stack', strat, - num_strat, input_image_type=3) - - # dual reg 'files', too - output_to_standard('dr_tempreg_maps_files', - 'dr_tempreg_maps_files', strat, num_strat, 1) - output_to_standard('dr_tempreg_maps_zstat_files', - 'dr_tempreg_maps_zstat_files', strat, - num_strat, 1) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Transforming alff/falff outputs to MNI - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runRegisterFuncToMNI and (1 in c.runALFF): - - for strat in strat_list: - output_to_standard('alff', 'alff_img', strat, num_strat) - output_to_standard('falff', 'falff_img', strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Transforming ReHo outputs to MNI - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runRegisterFuncToMNI and (1 in c.runReHo): - for strat in strat_list: - output_to_standard('reho', 'raw_reho_map', strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Transforming SCA ROI outputs to MNI - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runRegisterFuncToMNI and (1 in c.runSCA) and ( - "Avg" in sca_analysis_dict.keys()): # in(1 in c.runROITimeseries): - for strat in strat_list: - output_to_standard('sca_roi_stack', 'sca_roi_correlation_stack', - strat, num_strat, input_image_type=3) - output_to_standard('sca_roi_files', 'sca_roi_correlation_files', - strat, num_strat, 1) - - num_strat += 1 - - strat_list += new_strat_list - - """"""""""""""""""""""""""""""""""""""""""""""""""" - SMOOTHING NORMALIZED OUTPUTS - """"""""""""""""""""""""""""""""""""""""""""""""""" - - ''' - Smoothing Temporal Regression for SCA scores - ''' - new_strat_list = [] - num_strat = 0 - - # if (1 in c.runMultRegSCA) and (1 in c.runROITimeseries) and c.fwhm != None: - - if ("MultReg" in sca_analysis_dict.keys()) and (c.fwhm != None): - - for strat in strat_list: - - sc_temp_reg_maps_smooth = pe.MapNode( - interface=fsl.MultiImageMaths(), - name='sca_tempreg_maps_stack_smooth_%d' % num_strat, - iterfield=['in_file']) - sc_temp_reg_maps_files_smooth = pe.MapNode( - interface=fsl.MultiImageMaths(), - name='sca_tempreg_maps_files_smooth_%d' % num_strat, - iterfield=['in_file']) - sc_temp_reg_maps_Z_stack_smooth = pe.MapNode( - interface=fsl.MultiImageMaths(), - name='sca_tempreg_maps_zstat_stack_smooth_%d' % num_strat, - iterfield=['in_file']) - sc_temp_reg_maps_Z_files_smooth = pe.MapNode( - interface=fsl.MultiImageMaths(), - name='sca_tempreg_maps_zstat_files_smooth_%d' % num_strat, - iterfield=['in_file']) - - try: - node, out_file = strat.get_node_from_resource_pool( - 'sca_tempreg_maps_stack') - node5, out_file5 = strat.get_node_from_resource_pool( - 'sca_tempreg_maps_files') - node2, out_file2 = strat.get_node_from_resource_pool( - 'sca_tempreg_maps_zstat_stack') - node3, out_file3 = strat.get_node_from_resource_pool( - 'sca_tempreg_maps_zstat_files') - node4, out_file4 = strat.get_node_from_resource_pool( - 'functional_brain_mask_to_standard') - - # non-normalized stack - workflow.connect(node, out_file, - sc_temp_reg_maps_smooth, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - sc_temp_reg_maps_smooth, 'op_string') - - workflow.connect(node4, out_file4, - sc_temp_reg_maps_smooth, 'operand_files') - - # non-normalized files - workflow.connect(node5, out_file5, - sc_temp_reg_maps_files_smooth, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - sc_temp_reg_maps_files_smooth, 'op_string') - - workflow.connect(node4, out_file4, - sc_temp_reg_maps_files_smooth, - 'operand_files') - - # normalized stack - workflow.connect(node2, out_file2, - sc_temp_reg_maps_Z_stack_smooth, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - sc_temp_reg_maps_Z_stack_smooth, 'op_string') - - workflow.connect(node4, out_file4, - sc_temp_reg_maps_Z_stack_smooth, - 'operand_files') - - # normalized files - workflow.connect(node3, out_file3, - sc_temp_reg_maps_Z_files_smooth, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - sc_temp_reg_maps_Z_files_smooth, 'op_string') - - workflow.connect(node4, out_file4, - sc_temp_reg_maps_Z_files_smooth, - 'operand_files') - - except: - logConnectionError('SCA Temporal regression smooth', - num_strat, strat.get_resource_pool(), - '0038') - raise - strat.append_name(sc_temp_reg_maps_smooth.name) - strat.update_resource_pool({'sca_tempreg_maps_stack_smooth': ( - sc_temp_reg_maps_smooth, 'out_file'), - 'sca_tempreg_maps_files_smooth': ( - sc_temp_reg_maps_files_smooth, - 'out_file'), - 'sca_tempreg_maps_zstat_stack_smooth': ( - sc_temp_reg_maps_Z_stack_smooth, - 'out_file'), - 'sca_tempreg_maps_zstat_files_smooth': ( - sc_temp_reg_maps_Z_files_smooth, - 'out_file')}) - - create_log_node(sc_temp_reg_maps_smooth, 'out_file', num_strat) - num_strat += 1 - strat_list += new_strat_list - - ''' - calc averages of sca_tempreg outputs - ''' - new_strat_list = [] - num_strat = 0 - - # if (1 in c.runMultRegSCA) and (1 in c.runROITimeseries): - - if "MultReg" in sca_analysis_dict.keys(): - - for strat in strat_list: - - if 1 in c.runRegisterFuncToMNI: - - calc_avg("sca_tempreg_maps_files", strat, num_strat, 1) - - if c.fwhm != None: - calc_avg("sca_tempreg_maps_files_smooth", strat, - num_strat, 1) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Smoothing Temporal Regression for Dual Regression - ''' - new_strat_list = [] - num_strat = 0 - - # if (1 in c.runDualReg) and (1 in c.runSpatialRegression) and c.fwhm != None: - - if ("DualReg" in sca_analysis_dict.keys()) and (c.fwhm != None): - - for strat in strat_list: - - dr_temp_reg_maps_smooth = pe.Node(interface=fsl.MultiImageMaths(), - name='dr_tempreg_maps_stack_smooth_%d' % num_strat) - dr_temp_reg_maps_Z_stack_smooth = pe.Node( - interface=fsl.MultiImageMaths(), - name='dr_tempreg_maps_zstat_stack_smooth_%d' % num_strat) - dr_temp_reg_maps_files_smooth = pe.MapNode( - interface=fsl.MultiImageMaths(), - name='dr_tempreg_maps_files_smooth_%d' % num_strat, - iterfield=['in_file']) - dr_temp_reg_maps_Z_files_smooth = pe.MapNode( - interface=fsl.MultiImageMaths(), - name='dr_tempreg_maps_zstat_files_smooth_%d' % num_strat, - iterfield=['in_file']) - - try: - node, out_file = strat.get_node_from_resource_pool( - 'dr_tempreg_maps_stack_to_standard') - node2, out_file2 = strat.get_node_from_resource_pool( - 'dr_tempreg_maps_zstat_stack_to_standard') - node5, out_file5 = strat.get_node_from_resource_pool( - 'dr_tempreg_maps_files_to_standard') - node3, out_file3 = strat.get_node_from_resource_pool( - 'dr_tempreg_maps_zstat_files_to_standard') - node4, out_file4 = strat.get_node_from_resource_pool( - 'functional_brain_mask_to_standard') - - # non-normalized stack - workflow.connect(node, out_file, - dr_temp_reg_maps_smooth, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - dr_temp_reg_maps_smooth, 'op_string') - - workflow.connect(node4, out_file4, - dr_temp_reg_maps_smooth, 'operand_files') - - # normalized stack - workflow.connect(node2, out_file2, - dr_temp_reg_maps_Z_stack_smooth, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - dr_temp_reg_maps_Z_stack_smooth, 'op_string') - - workflow.connect(node4, out_file4, - dr_temp_reg_maps_Z_stack_smooth, - 'operand_files') - - # normalized files - workflow.connect(node5, out_file5, - dr_temp_reg_maps_files_smooth, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - dr_temp_reg_maps_files_smooth, 'op_string') - - workflow.connect(node4, out_file4, - dr_temp_reg_maps_files_smooth, - 'operand_files') - - # normalized z-stat files - workflow.connect(node3, out_file3, - dr_temp_reg_maps_Z_files_smooth, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - dr_temp_reg_maps_Z_files_smooth, 'op_string') - - workflow.connect(node4, out_file4, - dr_temp_reg_maps_Z_files_smooth, - 'operand_files') - - except: - logConnectionError('Dual regression temp reg smooth', - num_strat, strat.get_resource_pool(), - '0039') - raise - strat.append_name(dr_temp_reg_maps_smooth.name) - strat.update_resource_pool({ - 'dr_tempreg_maps_stack_to_standard_smooth': ( - dr_temp_reg_maps_smooth, - 'out_file'), - 'dr_tempreg_maps_zstat_stack_to_standard_smooth': ( - dr_temp_reg_maps_Z_stack_smooth, - 'out_file'), - 'dr_tempreg_maps_files_to_standard_smooth': ( - dr_temp_reg_maps_files_smooth, - 'out_file'), - 'dr_tempreg_maps_zstat_files_to_standard_smooth': ( - dr_temp_reg_maps_Z_files_smooth, - 'out_file')}) - create_log_node(dr_temp_reg_maps_smooth, 'out_file', num_strat) - num_strat += 1 - strat_list += new_strat_list - - ''' - calc averages of dr_tempreg outputs - ''' - new_strat_list = [] - num_strat = 0 - - if "DualReg" in sca_analysis_dict.keys(): - - for strat in strat_list: - - calc_avg("dr_tempreg_maps_files", strat, num_strat, 1) - - # if c.fwhm != None: - - if 1 in c.runRegisterFuncToMNI: - - calc_avg("dr_tempreg_maps_files_to_standard", strat, - num_strat, 1) - - if c.fwhm != None: - calc_avg("dr_tempreg_maps_files_to_standard_smooth", - strat, num_strat, 1) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Smoothing motion-corrected functional to MNI output - ''' - new_strat_list = [] - num_strat = 0 - if (1 in c.runRegisterFuncToMNI) and (c.fwhm != None): - for strat in strat_list: - output_smooth('motion_correct', 'motion_correct', strat, - num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Smoothing ALFF fALFF Z scores and or possibly Z scores in MNI - ''' - new_strat_list = [] - num_strat = 0 - if (1 in c.runALFF) and c.fwhm != None: - for strat in strat_list: - output_smooth('alff', 'alff_img', strat, num_strat) - output_smooth('falff', 'falff_img', strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - calc averages of alff/falff outputs - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runALFF: - - for strat in strat_list: - - calc_avg("alff_img", strat, num_strat) - calc_avg("falff_img", strat, num_strat) - - if c.fwhm != None: - calc_avg("alff_smooth", strat, num_strat) - calc_avg("falff_smooth", strat, num_strat) - - if 1 in c.runRegisterFuncToMNI: - - calc_avg("alff_to_standard", strat, num_strat) - calc_avg("falff_to_standard", strat, num_strat) - - if c.fwhm != None: - calc_avg("alff_to_standard_smooth", strat, num_strat) - calc_avg("falff_to_standard_smooth", strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - z-standardize alff/falff MNI-standardized outputs - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runZScoring and (1 in c.runALFF): - - for strat in strat_list: - - if c.fwhm != None: - z_score_standardize('alff_to_standard_smooth', - 'alff_to_standard_smooth', strat, - num_strat) - z_score_standardize('falff_to_standard_smooth', - 'falff_to_standard_smooth', strat, - num_strat) - - z_score_standardize('alff_to_standard', 'alff_to_standard', strat, - num_strat) - z_score_standardize('falff_to_standard', 'falff_to_standard', - strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Smoothing ReHo outputs and or possibly ReHo outputs in MNI - ''' - new_strat_list = [] - num_strat = 0 - - if (1 in c.runReHo) and c.fwhm != None: - for strat in strat_list: - output_smooth('reho', 'raw_reho_map', strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - calc averages of reho outputs - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runReHo: - - for strat in strat_list: - - calc_avg("raw_reho_map", strat, num_strat) - - if c.fwhm != None: - calc_avg("reho_smooth", strat, num_strat) - - if 1 in c.runRegisterFuncToMNI: - - calc_avg("reho_to_standard", strat, num_strat) - - if c.fwhm != None: - calc_avg("reho_to_standard_smooth", strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - z-standardize ReHo MNI-standardized outputs - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runZScoring and (1 in c.runReHo): - - for strat in strat_list: - - if c.fwhm != None: - z_score_standardize('reho_zstd', 'reho_to_standard_smooth', - strat, num_strat) - - z_score_standardize('reho', 'reho_to_standard', strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Smoothing SCA roi based Z scores and or possibly Z scores in MNI - ''' - if (1 in c.runSCA) and ( - "Avg" in sca_analysis_dict.keys()) and c.fwhm != None: - for strat in strat_list: - output_smooth('sca_roi_stack', 'sca_roi_correlation_stack', strat, - num_strat) - output_smooth('sca_roi_files', 'sca_roi_correlation_files', strat, - num_strat, 1) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - calc averages of SCA files outputs - ''' - new_strat_list = [] - num_strat = 0 - - if (1 in c.runSCA) and ("Avg" in sca_analysis_dict.keys()): - - for strat in strat_list: - - calc_avg("sca_roi_correlation_files", strat, num_strat, 1) - - if c.fwhm != None: - calc_avg("sca_roi_files_smooth", strat, num_strat, 1) - - if 1 in c.runRegisterFuncToMNI: - - calc_avg("sca_roi_files_to_standard", strat, num_strat, 1) - - if c.fwhm != None: - calc_avg("sca_roi_files_to_standard_smooth", strat, - num_strat, 1) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - fisher-z-standardize SCA ROI MNI-standardized outputs - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runZScoring and (1 in c.runSCA) and \ - ("Avg" in sca_analysis_dict.keys()): - - for strat in strat_list: - - if c.fwhm != None: - fisher_z_score_standardize('sca_roi_files_to_standard_smooth', \ - 'sca_roi_files_to_standard_smooth', \ - 'roi_timeseries_for_SCA', \ - strat, num_strat, 1) - - fisher_z_score_standardize('sca_roi_files_to_standard', \ - 'sca_roi_files_to_standard', \ - 'roi_timeseries_for_SCA', \ - strat, num_strat, 1) - - num_strat += 1 - - strat_list += new_strat_list - - """"""""""""""""""""""""""""""""""""""""""""""""""" - QUALITY CONTROL - to be re-implemented later - """"""""""""""""""""""""""""""""""""""""""""""""""" - - # TODO - QA pages: re-introduce - ''' - if 1 in c.generateQualityControlImages: - - #register color palettes - register_pallete(os.path.realpath( - os.path.join(CPAC.__path__[0], 'qc', 'red.py')), 'red') - register_pallete(os.path.realpath( - os.path.join(CPAC.__path__[0], 'qc', 'green.py')), 'green') - register_pallete(os.path.realpath( - os.path.join(CPAC.__path__[0], 'qc', 'blue.py')), 'blue') - register_pallete(os.path.realpath( - os.path.join(CPAC.__path__[0], 'qc', 'red_to_blue.py')), 'red_to_blue') - register_pallete(os.path.realpath( - os.path.join(CPAC.__path__[0], 'qc', 'cyan_to_yellow.py')), 'cyan_to_yellow') - - hist = pe.Node(util.Function(input_names=['measure_file', - 'measure'], - output_names=['hist_path'], - function=gen_histogram), - name='histogram') - - for strat in strat_list: - - nodes = getNodeList(strat) - - #make SNR plot - - try: - - hist_ = hist.clone('hist_snr_%d' % num_strat) - hist_.inputs.measure = 'snr' - - drop_percent = pe.Node(util.Function(input_names=['measure_file', - 'percent_'], - output_names=['modified_measure_file'], - function=drop_percent_), - name='dp_snr_%d' % num_strat) - drop_percent.inputs.percent_ = 99 - - preproc, out_file = strat.get_node_from_resource_pool('preprocessed') - brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') - func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') - anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') - mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') - - std_dev = pe.Node(util.Function(input_names=['mask_', 'func_'], - output_names=['new_fname'], - function=gen_std_dev), - name='std_dev_%d' % num_strat) - - std_dev_anat = pe.Node(util.Function(input_names=['func_', - 'ref_', - 'xfm_', - 'interp_'], - output_names=['new_fname'], - function=gen_func_anat_xfm), - name='std_dev_anat_%d' % num_strat) - - snr = pe.Node(util.Function(input_names=['std_dev', 'mean_func_anat'], - output_names=['new_fname'], - function=gen_snr), - name='snr_%d' % num_strat) - - ### - snr_val = pe.Node(util.Function(input_names=['measure_file'], - output_names=['snr_storefl'], - function=cal_snr_val), - name='snr_val%d' % num_strat) - - - std_dev_anat.inputs.interp_ = 'trilinear' - - montage_snr = create_montage('montage_snr_%d' % num_strat, - 'red_to_blue', 'snr') - - - workflow.connect(preproc, out_file, - std_dev, 'func_') - - workflow.connect(brain_mask, mask_file, - std_dev, 'mask_') - - workflow.connect(std_dev, 'new_fname', - std_dev_anat, 'func_') - - workflow.connect(func_to_anat_xfm, xfm_file, - std_dev_anat, 'xfm_') - - workflow.connect(anat_ref, ref_file, - std_dev_anat, 'ref_') - - workflow.connect(std_dev_anat, 'new_fname', - snr, 'std_dev') - - workflow.connect(mfa, mfa_file, - snr, 'mean_func_anat') - - workflow.connect(snr, 'new_fname', - hist_, 'measure_file') - - workflow.connect(snr, 'new_fname', - drop_percent, 'measure_file') - - workflow.connect(snr, 'new_fname', - snr_val, 'measure_file') ### - - - workflow.connect(drop_percent, 'modified_measure_file', - montage_snr, 'inputspec.overlay') - - workflow.connect(anat_ref, ref_file, - montage_snr, 'inputspec.underlay') - - - strat.update_resource_pool({'qc___snr_a': (montage_snr, 'outputspec.axial_png'), - 'qc___snr_s': (montage_snr, 'outputspec.sagittal_png'), - 'qc___snr_hist': (hist_, 'hist_path'), - 'qc___snr_val': (snr_val, 'snr_storefl')}) ### - if not 3 in qc_montage_id_a: - qc_montage_id_a[3] = 'snr_a' - qc_montage_id_s[3] = 'snr_s' - qc_hist_id[3] = 'snr_hist' - - except: - logStandardError('QC', 'unable to get resources for SNR plot', '0051') - raise - - - #make motion parameters plot - - try: - - mov_param, out_file = strat.get_node_from_resource_pool('movement_parameters') - mov_plot = pe.Node(util.Function(input_names=['motion_parameters'], - output_names=['translation_plot', - 'rotation_plot'], - function=gen_motion_plt), - name='motion_plt_%d' % num_strat) - - workflow.connect(mov_param, out_file, - mov_plot, 'motion_parameters') - strat.update_resource_pool({'qc___movement_trans_plot': (mov_plot, 'translation_plot'), - 'qc___movement_rot_plot': (mov_plot, 'rotation_plot')}) - - if not 6 in qc_plot_id: - qc_plot_id[6] = 'movement_trans_plot' - - if not 7 in qc_plot_id: - qc_plot_id[7] = 'movement_rot_plot' - - - except: - logStandardError('QC', 'unable to get resources for Motion Parameters plot', '0052') - raise - - - # make FD plot and volumes removed - if 'gen_motion_stats' in nodes: - - try: - - fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement') - excluded, out_file_ex = strat.get_node_from_resource_pool('scrubbing_frames_excluded') - - fd_plot = pe.Node(util.Function(input_names=['arr', - 'ex_vol', - 'measure'], - output_names=['hist_path'], - function=gen_plot_png), - name='fd_plot_%d' % num_strat) - fd_plot.inputs.measure = 'FD' - workflow.connect(fd, out_file, - fd_plot, 'arr') - workflow.connect(excluded, out_file_ex, - fd_plot, 'ex_vol') - strat.update_resource_pool({'qc___fd_plot': (fd_plot, 'hist_path')}) - if not 8 in qc_plot_id: - qc_plot_id[8] = 'fd_plot' - - - except Exception as e: - logStandardError('QC', 'unable to get resources for FD plot', '0053', e) - raise Exception - - # make QC montages for Skull Stripping Visualization - - try: - anat_underlay, out_file = strat.get_node_from_resource_pool('anatomical_brain') - skull, out_file_s = strat.get_node_from_resource_pool('anatomical_reorient') - - - montage_skull = create_montage('montage_skull_%d' % num_strat, - 'red', 'skull_vis') ### - - skull_edge = pe.Node(util.Function(input_names=['file_'], - output_names=['new_fname'], - function=make_edge), - name='skull_edge_%d' % num_strat) - - - workflow.connect(skull, out_file_s, - skull_edge, 'file_') - - workflow.connect(anat_underlay, out_file, - montage_skull, 'inputspec.underlay') - - workflow.connect(skull_edge, 'new_fname', - montage_skull, 'inputspec.overlay') - - strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, 'outputspec.axial_png'), - 'qc___skullstrip_vis_s': (montage_skull, 'outputspec.sagittal_png')}) - - if not 1 in qc_montage_id_a: - qc_montage_id_a[1] = 'skullstrip_vis_a' - qc_montage_id_s[1] = 'skullstrip_vis_s' - - except: - logStandardError('QC', 'Cannot generate QC montages for Skull Stripping: Resources Not Found', '0054') - raise - - - ### make QC montages for mni normalized anatomical image - - try: - mni_anat_underlay, out_file = strat.get_node_from_resource_pool('mni_normalized_anatomical') - - montage_mni_anat = create_montage('montage_mni_anat_%d' % num_strat, - 'red', 'mni_anat') - - workflow.connect(mni_anat_underlay, out_file, - montage_mni_anat, 'inputspec.underlay') - - montage_mni_anat.inputs.inputspec.overlay = p.resource_filename('CPAC','resources/templates/MNI152_Edge_AllTissues.nii.gz') - - strat.update_resource_pool({'qc___mni_normalized_anatomical_a': (montage_mni_anat, 'outputspec.axial_png'), - 'qc___mni_normalized_anatomical_s': (montage_mni_anat, 'outputspec.sagittal_png')}) - - if not 6 in qc_montage_id_a: - qc_montage_id_a[6] = 'mni_normalized_anatomical_a' - qc_montage_id_s[6] = 'mni_normalized_anatomical_s' - - except: - logStandardError('QC', 'Cannot generate QC montages for MNI normalized anatomical: Resources Not Found', '0054') - raise - # make QC montages for CSF WM GM + ''' QA PAGES GO HERE ''' - if 'seg_preproc' in nodes: - - try: - anat_underlay, out_file = strat.get_node_from_resource_pool('anatomical_brain') - csf_overlay, out_file_csf = strat.get_node_from_resource_pool('anatomical_csf_mask') - wm_overlay, out_file_wm = strat.get_node_from_resource_pool('anatomical_wm_mask') - gm_overlay, out_file_gm = strat.get_node_from_resource_pool('anatomical_gm_mask') - - montage_csf_gm_wm = create_montage_gm_wm_csf('montage_csf_gm_wm_%d' % num_strat, - 'montage_csf_gm_wm') - - workflow.connect(anat_underlay, out_file, - montage_csf_gm_wm, 'inputspec.underlay') - - workflow.connect(csf_overlay, out_file_csf, - montage_csf_gm_wm, 'inputspec.overlay_csf') - - workflow.connect(wm_overlay, out_file_wm, - montage_csf_gm_wm, 'inputspec.overlay_wm') - - workflow.connect(gm_overlay, out_file_gm, - montage_csf_gm_wm, 'inputspec.overlay_gm') - - strat.update_resource_pool({'qc___csf_gm_wm_a': (montage_csf_gm_wm, 'outputspec.axial_png'), - 'qc___csf_gm_wm_s': (montage_csf_gm_wm, 'outputspec.sagittal_png')}) - - if not 2 in qc_montage_id_a: - qc_montage_id_a[2] = 'csf_gm_wm_a' - qc_montage_id_s[2] = 'csf_gm_wm_s' - - except: - logStandardError('QC', 'Cannot generate QC montages for WM GM CSF masks: Resources Not Found', '0055') - raise - - - # make QC montage for Mean Functional in T1 with T1 edge - - try: - anat, out_file = strat.get_node_from_resource_pool('anatomical_brain') - m_f_a, out_file_mfa = strat.get_node_from_resource_pool('mean_functional_in_anat') - - montage_anat = create_montage('montage_anat_%d' % num_strat, - 'red', 't1_edge_on_mean_func_in_t1') ### - - anat_edge = pe.Node(util.Function(input_names=['file_'], - output_names=['new_fname'], - function=make_edge), - name='anat_edge_%d' % num_strat) - - workflow.connect(anat, out_file, - anat_edge, 'file_') - - - workflow.connect(m_f_a, out_file_mfa, - montage_anat, 'inputspec.underlay') - - workflow.connect(anat_edge, 'new_fname', - montage_anat, 'inputspec.overlay') - - strat.update_resource_pool({'qc___mean_func_with_t1_edge_a': (montage_anat, 'outputspec.axial_png'), - 'qc___mean_func_with_t1_edge_s': (montage_anat, 'outputspec.sagittal_png')}) - - if not 4 in qc_montage_id_a: - qc_montage_id_a[4] = 'mean_func_with_t1_edge_a' - qc_montage_id_s[4] = 'mean_func_with_t1_edge_s' - - - except: - logStandardError('QC', 'Cannot generate QC montages for Mean Functional in T1 with T1 edge: Resources Not Found', '0056') - raise - - # make QC montage for Mean Functional in MNI with MNI edge - - try: - m_f_i, out_file = strat.get_node_from_resource_pool('mean_functional_in_mni') - - montage_mfi = create_montage('montage_mfi_%d' % num_strat, - 'red', 'MNI_edge_on_mean_func_mni') ### - -# MNI_edge = pe.Node(util.Function(input_names=['file_'], -# output_names=['new_fname'], -# function=make_edge), -# name='MNI_edge_%d' % num_strat) -# #MNI_edge.inputs.file_ = c.template_brain_only_for_func -# workflow.connect(MNI_edge, 'new_fname', -# montage_mfi, 'inputspec.overlay') - - workflow.connect(m_f_i, out_file, - montage_mfi, 'inputspec.underlay') - - montage_mfi.inputs.inputspec.overlay = p.resource_filename('CPAC','resources/templates/MNI152_Edge_AllTissues.nii.gz') - - - strat.update_resource_pool({'qc___mean_func_with_mni_edge_a': (montage_mfi, 'outputspec.axial_png'), - 'qc___mean_func_with_mni_edge_s': (montage_mfi, 'outputspec.sagittal_png')}) - - if not 5 in qc_montage_id_a: - qc_montage_id_a[5] = 'mean_func_with_mni_edge_a' - qc_montage_id_s[5] = 'mean_func_with_mni_edge_s' - - - except: - logStandardError('QC', 'Cannot generate QC montages for Mean Functional in MNI with MNI edge: Resources Not Found', '0057') - raise - - - - - - # QA pages function - def QA_montages(measure, idx): - - try: - - histogram = hist.clone('hist_%s_%d' % (measure, num_strat)) - histogram.inputs.measure = measure - - drop_percent = pe.MapNode(util.Function(input_names=['measure_file', - 'percent_'], - output_names=['modified_measure_file'], - function=drop_percent_), - name='dp_%s_%d' % (measure, num_strat), iterfield=['measure_file']) - drop_percent.inputs.percent_ = 99.999 - - overlay, out_file = strat.get_node_from_resource_pool(measure) - - montage = create_montage('montage_%s_%d' % (measure, num_strat), - 'cyan_to_yellow', measure) - montage.inputs.inputspec.underlay = c.template_brain_only_for_func - - workflow.connect(overlay, out_file, - drop_percent, 'measure_file') - - workflow.connect(drop_percent, 'modified_measure_file', - montage, 'inputspec.overlay') - - workflow.connect(overlay, out_file, - histogram, 'measure_file') - - strat.update_resource_pool({'qc___%s_a' % measure: (montage, 'outputspec.axial_png'), - 'qc___%s_s' % measure: (montage, 'outputspec.sagittal_png'), - 'qc___%s_hist' % measure: (histogram, 'hist_path')}) - - if not idx in qc_montage_id_a: - qc_montage_id_a[idx] = '%s_a' % measure - qc_montage_id_s[idx] = '%s_s' % measure - qc_hist_id[idx] = '%s_hist' % measure - - except Exception as e: - print "[!] Creation of QA montages for %s has failed.\n" % measure - print "Error: %s" % e - pass - - - - # ALFF and f/ALFF QA montages - if 1 in c.runALFF: - - if 1 in c.runRegisterFuncToMNI: - QA_montages('alff_to_standard', 7) - QA_montages('falff_to_standard', 8) - - if c.fwhm != None: - QA_montages('alff_to_standard_smooth', 9) - QA_montages('falff_to_standard_smooth', 10) - - if 1 in c.runZScoring: - - if c.fwhm != None: - QA_montages('alff_to_standard_smooth_zstd', 11) - QA_montages('falff_to_standard_smooth_zstd', 12) - - else: - QA_montages('alff_to_standard_zstd', 13) - QA_montages('falff_to_standard_zstd', 14) - - - # ReHo QA montages - if 1 in c.runReHo: - - if 1 in c.runRegisterFuncToMNI: - QA_montages('reho_to_standard', 15) - - if c.fwhm != None: - QA_montages('reho_to_standard_smooth', 16) - - if 1 in c.runZScoring: - - if c.fwhm != None: - QA_montages('reho_to_standard_smooth_fisher_zstd', 17) - - else: - QA_montages('reho_to_standard_fisher_zstd', 18) - - - - # SCA ROI QA montages - if (1 in c.runSCA) and (1 in c.runROITimeseries): - - if 1 in c.runRegisterFuncToMNI: - QA_montages('sca_roi_to_standard', 19) - - if c.fwhm != None: - QA_montages('sca_roi_to_standard_smooth', 20) - - if 1 in c.runZScoring: - - if c.fwhm != None: - QA_montages('sca_roi_to_standard_smooth_fisher_zstd', 22) - - else: - QA_montages('sca_roi_to_standard_fisher_zstd', 21) - - - - # SCA Seed QA montages - if (1 in c.runSCA) and ("Voxel" in ts_analysis_dict.keys()): #(1 in c.runVoxelTimeseries): - - if 1 in c.runRegisterFuncToMNI: - QA_montages('sca_seed_to_standard', 23) - - if c.fwhm != None: - QA_montages('sca_seed_to_standard_smooth', 24) - - if 1 in c.runZScoring: - - if c.fwhm != None: - QA_montages('sca_seed_to_standard_smooth_fisher_zstd', 26) - - else: - QA_montages('sca_seed_to_standard_fisher_zstd', 25) - - - # SCA Multiple Regression - if "MultReg" in sca_analysis_dict.keys(): #(1 in c.runMultRegSCA) and (1 in c.runROITimeseries): - - if 1 in c.runRegisterFuncToMNI: - QA_montages('sca_tempreg_maps_files', 27) - QA_montages('sca_tempreg_maps_zstat_files', 28) - - if c.fwhm != None: - QA_montages('sca_tempreg_maps_files_smooth', 29) - QA_montages('sca_tempreg_maps_zstat_files_smooth', 30) - - - - # Dual Regression QA montages - if (1 in c.runDualReg) and (1 in c.runSpatialRegression): - - QA_montages('dr_tempreg_maps_files', 31) - QA_montages('dr_tempreg_maps_zstat_files', 32) - - if 1 in c.runRegisterFuncToMNI: - QA_montages('dr_tempreg_maps_files_to_standard', 33) - QA_montages('dr_tempreg_maps_zstat_files_to_standard', 34) - - if c.fwhm != None: - QA_montages('dr_tempreg_maps_files_to_standard_smooth', 35) - QA_montages('dr_tempreg_maps_zstat_files_to_standard_smooth', 36) - - - - # VMHC QA montages - if 1 in c.runVMHC: - - QA_montages('vmhc_raw_score', 37) - QA_montages('vmhc_fisher_zstd', 38) - QA_montages('vmhc_fisher_zstd_zstat_map', 39) - - - # Network Centrality QA montages - if 1 in c.runNetworkCentrality: - - QA_montages('centrality_outputs', 40) - QA_montages('centrality_outputs_zstd', 41) - - if c.fwhm != None: - QA_montages('centrality_outputs_smoothed', 42) - QA_montages('centrality_outputs_smoothed_zstd', 43) - - - num_strat += 1 - ''' logger.info('\n\n' + 'Pipeline building completed.' + '\n\n') @@ -5596,9 +4500,6 @@ def is_number(s): # this in future p_name = None - logger.info('strat_tag, ---- , hash_val, ---- , pipeline_id: ' - '%s, ---- %s, ---- %s' - % (strat_tag, hash_val, pipeline_id)) pip_ids.append(pipeline_id) wf_names.append(strat.get_name()) @@ -5625,72 +4526,53 @@ def is_number(s): except Exception as exc: encrypt_data = False - debugging_outputs = ['despiked_fieldmap', - 'dr_tempreg_maps_stack', - 'dr_tempreg_maps_stack_to_standard', - 'dr_tempreg_maps_stack_to_standard_smooth', - 'dr_tempreg_maps_zstat_stack', - 'dr_tempreg_maps_zstat_stack_to_standard', - 'dr_tempreg_maps_zstat_stack_to_standard_smooth', - 'fmap_magnitude', - 'fmap_phase_diff', - 'raw_functional', - 'sca_roi_correlation_stack', - 'sca_roi_stack_smooth', - 'sca_roi_stack_to_standard', - 'sca_roi_stack_to_standard_smooth', - 'sca_tempreg_maps_stack', - 'sca_tempreg_maps_stack_smooth', - 'sca_tempreg_maps_zstat_stack', - 'sca_tempreg_maps_zstat_stack_smooth', - 'seg_mixeltype', - 'seg_partial_volume_files', - 'seg_partial_volume_map'] - for key in sorted(rp.keys()): if key in debugging_outputs: continue + if key in non_outputs: + continue + + if 0 not in c.run_smoothing: + # write out only the smoothed outputs + if key in outputs_nonsmooth: + continue + ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) # Write QC outputs to log directory if 'qc' in key.lower(): ds.inputs.base_directory = c.logDirectory else: ds.inputs.base_directory = c.outputDirectory + ds.inputs.creds_path = creds_path ds.inputs.encrypt_bucket_keys = encrypt_data - ds.inputs.container = os.path.join( - 'pipeline_%s' % pipeline_id, subject_id) + ds.inputs.container = os.path.join('pipeline_%s' % pipeline_id, subject_id) ds.inputs.regexp_substitutions = [(r"/_sca_roi(.)*[/]", '/'), - ( - r"/_smooth_centrality_(\d)+[/]", - '/'), + (r"/_smooth_centrality_(\d)+[/]", '/'), (r"/_z_score(\d)+[/]", "/"), - ( - r"/_dr_tempreg_maps_zstat_files_smooth_(\d)+[/]", - "/"), - ( - r"/_sca_tempreg_maps_zstat_files_smooth_(\d)+[/]", - "/"), + (r"/_dr_tempreg_maps_zstat_files_smooth_(\d)+[/]", "/"), + (r"/_sca_tempreg_maps_zstat_files_smooth_(\d)+[/]", "/"), (r"/qc___", '/qc/')] + node, out_file = rp[key] - workflow.connect(node, out_file, - ds, key) - logger.info( - 'node, out_file, key: %s, %s, %s' % (node, out_file, key)) + workflow.connect(node, out_file, ds, key) + + logger.info('node, out_file, key: ' + '%s, %s, %s' % (node, out_file, key)) link_node = pe.Node(interface=util.Function( - input_names=['in_file', 'strategies', - 'subject_id', 'pipeline_id', 'helper', - 'create_sym_links'], - output_names=[], - function=process_outputs), + input_names=['in_file', 'strategies', + 'subject_id', 'pipeline_id', 'helper', + 'create_sym_links'], + output_names=[], + function=process_outputs), name='process_outputs_%d' % sink_idx) link_node.inputs.strategies = strategies link_node.inputs.subject_id = subject_id - link_node.inputs.pipeline_id = 'pipeline_%s' % (pipeline_id) + link_node.inputs.pipeline_id = 'pipeline_%s' % pipeline_id link_node.inputs.helper = dict(strategy_tag_helper_symlinks) if 1 in c.runSymbolicLinks: @@ -5704,6 +4586,7 @@ def is_number(s): logger.info('sink index: %s' % sink_idx) d_name = os.path.join(c.logDirectory, ds.inputs.container) + if not os.path.exists(d_name): os.makedirs(d_name) @@ -5717,7 +4600,9 @@ def is_number(s): format_dot(dotfilename, 'png') except: logStandardWarning('Datasink', - 'Cannot Create the strategy and pipeline graph, dot or/and pygraphviz is not installed') + 'Cannot Create the strategy and pipeline ' + 'graph, dot or/and pygraphviz is not ' + 'installed') pass logger.info('%s*' % d_name) diff --git a/CPAC/resources/configs/pipeline_config_template.yml b/CPAC/resources/configs/pipeline_config_template.yml index 57ce35ecc1..5f76fcefd3 100644 --- a/CPAC/resources/configs/pipeline_config_template.yml +++ b/CPAC/resources/configs/pipeline_config_template.yml @@ -492,14 +492,20 @@ lfcdCorrelationThreshold : 0.001 memoryAllocatedForDegreeCentrality : 3.0 +# Smooth the derivative outputs or not, or produce both unsmoothed and smoothed versions, if preferred. +# Options: [1], [0], or [1,0] +run_smoothing: [1] + + # Full Width at Half Maximum of the Gaussian kernel used during spatial smoothing. # Can be a single value or multiple values separated by commas. # Note that spatial smoothing is run as the last step in the individual-level analysis pipeline, such that all derivatives are output both smoothed and unsmoothed. fwhm : [4] -# Decides format of outputs. Off will produce non-z-scored outputs, On will produce z-scores of outputs and non-z-scored outputs. -runZScoring : [1] +# Decides format of outputs. Off will produce non-z-scored outputs, On will produce z-scores of outputs, and On/Off will produce both. +# Options: [1] +runZScoring : [1], [0], or [1,0] # This number depends on computing resources. diff --git a/CPAC/utils/utils.py b/CPAC/utils/utils.py index 34c7bd51ad..27b447db32 100644 --- a/CPAC/utils/utils.py +++ b/CPAC/utils/utils.py @@ -96,30 +96,24 @@ 'falff_to_standard_smooth': 'alff', 'alff_to_standard_zstd': 'alff', 'falff_to_standard_zstd': 'alff', - 'alff_to_standard_smooth_zstd': 'alff', - 'falff_to_standard_smooth_zstd': 'alff', + 'alff_to_standard_zstd_smooth': 'alff', + 'falff_to_standard_zstd_smooth': 'alff', 'raw_reho_map': 'reho', 'reho_smooth': 'reho', 'reho_to_standard': 'reho', 'reho_to_standard_smooth': 'reho', 'reho_to_standard_zstd': 'reho', - 'reho_to_standard_smooth_zstd': 'reho', + 'reho_to_standard_zstd_smooth': 'reho', 'voxel_timeseries': 'timeseries', 'roi_timeseries': 'timeseries', 'roi_timeseries_for_SCA': 'timeseries', 'roi_timeseries_for_SCA_multreg': 'timeseries', - 'sca_roi_correlation_stack': 'sca_roi', 'sca_roi_correlation_files': 'sca_roi', - 'sca_roi_stack_smooth': 'sca_roi', 'sca_roi_files_smooth': 'sca_roi', - 'sca_roi_stack_to_standard': 'sca_roi', 'sca_roi_files_to_standard': 'sca_roi', - 'sca_roi_stack_to_standard_smooth': 'sca_roi', 'sca_roi_files_to_standard_smooth': 'sca_roi', - 'sca_roi_stack_to_standard_fisher_zstd': 'sca_roi', - 'sca_roi_stack_to_standard_smooth_fisher_zstd': 'sca_roi', 'sca_roi_files_to_standard_fisher_zstd': 'sca_roi', - 'sca_roi_files_to_standard_smooth_fisher_zstd': 'sca_roi', + 'sca_roi_files_to_standard_fisher_zstd_smooth': 'sca_roi', 'bbregister_registration': 'surface_registration', 'left_hemisphere_surface': 'surface_registration', 'right_hemisphere_surface': 'surface_registration', @@ -127,7 +121,7 @@ 'centrality_outputs': 'centrality', 'centrality_outputs_smoothed': 'centrality', 'centrality_outputs_zstd': 'centrality', - 'centrality_outputs_smoothed_zstd': 'centrality', + 'centrality_outputs_zstd_smoothed': 'centrality', 'centrality_graphs': 'centrality', 'seg_probability_maps': 'anat', 'seg_mixeltype': 'anat', @@ -135,25 +129,15 @@ 'seg_partial_volume_files': 'anat', 'spatial_map_timeseries': 'timeseries', 'spatial_map_timeseries_for_DR': 'timeseries', - 'dr_tempreg_maps_stack': 'spatial_regression', 'dr_tempreg_maps_files': 'spatial_regression', - 'dr_tempreg_maps_zstat_stack': 'spatial_regression', 'dr_tempreg_maps_zstat_files': 'spatial_regression', - 'dr_tempreg_maps_stack_to_standard': 'spatial_regression', 'dr_tempreg_maps_files_to_standard': 'spatial_regression', - 'dr_tempreg_maps_zstat_stack_to_standard': 'spatial_regression', 'dr_tempreg_maps_zstat_files_to_standard': 'spatial_regression', - 'dr_tempreg_maps_stack_to_standard_smooth': 'spatial_regression', 'dr_tempreg_maps_files_to_standard_smooth': 'spatial_regression', - 'dr_tempreg_maps_zstat_stack_to_standard_smooth': 'spatial_regression', 'dr_tempreg_maps_zstat_files_to_standard_smooth': 'spatial_regression', - 'sca_tempreg_maps_stack': 'sca_roi', 'sca_tempreg_maps_files': 'sca_roi', 'sca_tempreg_maps_files_smooth': 'sca_roi', - 'sca_tempreg_maps_zstat_stack': 'sca_roi', 'sca_tempreg_maps_zstat_files': 'sca_roi', - 'sca_tempreg_maps_stack_smooth': 'sca_roi', - 'sca_tempreg_maps_zstat_stack_smooth': 'sca_roi', 'sca_tempreg_maps_zstat_files_smooth': 'sca_roi', } @@ -264,63 +248,7 @@ def get_zscore(input_name, wf_name='z_score'): return wflow -def get_operand_string(mean, std_dev): - """ - Method to get operand string for Fsl Maths - - Parameters - ---------- - mean : string - path to img containing mean - std_dev : string - path to img containing standard deviation - - Returns - ------ - op_string : string - operand string - """ - - str1 = "-sub %f -div %f" % (float(mean), float(std_dev)) - op_string = str1 + " -mas %s" - return op_string - - -def get_roi_num_list(timeseries_file, prefix=None): - # extracts the ROI labels from the 3dROIstats output CSV file - with open(timeseries_file, "r") as f: - roi_file_lines = f.read().splitlines() - - roi_err = "\n\n[!] The output of 3dROIstats, used in extracting the " \ - "timeseries, was not in the expected format.\n\nROI output " \ - "file: %s\n\n" % timeseries_file - - for line in roi_file_lines: - if "Mean_" in line: - try: - roi_list = line.split("\t") - # clear out any blank strings/non ROI labels in the list - roi_list = [x for x in roi_list if "Mean" in x] - # rename labels - roi_list = [x.replace("Mean", "ROI").replace(" ", "") \ - for x in roi_list] - except: - raise Exception(roi_err) - break - else: - raise Exception(roi_err) - - if prefix: - temp_rois = [] - for roi in roi_list: - roi = prefix + "_" + str(roi) - temp_rois.append(roi) - roi_list = temp_rois - - return roi_list - - -def get_fisher_zscore(input_name, map_node, wf_name='fisher_z_score'): +def get_fisher_zscore(input_name, wf_name='fisher_z_score'): """ Runs the compute_fisher_z_score function as part of a one-node workflow. """ @@ -472,6 +400,62 @@ def compute_fisher_z_score(correlation_file, timeseries_one_d, input_name): return out_file +def get_operand_string(mean, std_dev): + """ + Method to get operand string for Fsl Maths + + Parameters + ---------- + mean : string + path to img containing mean + std_dev : string + path to img containing standard deviation + + Returns + ------ + op_string : string + operand string + """ + + str1 = "-sub %f -div %f" % (float(mean), float(std_dev)) + op_string = str1 + " -mas %s" + return op_string + + +def get_roi_num_list(timeseries_file, prefix=None): + # extracts the ROI labels from the 3dROIstats output CSV file + with open(timeseries_file, "r") as f: + roi_file_lines = f.read().splitlines() + + roi_err = "\n\n[!] The output of 3dROIstats, used in extracting the " \ + "timeseries, was not in the expected format.\n\nROI output " \ + "file: %s\n\n" % timeseries_file + + for line in roi_file_lines: + if "Mean_" in line: + try: + roi_list = line.split("\t") + # clear out any blank strings/non ROI labels in the list + roi_list = [x for x in roi_list if "Mean" in x] + # rename labels + roi_list = [x.replace("Mean", "ROI").replace(" ", "") \ + for x in roi_list] + except: + raise Exception(roi_err) + break + else: + raise Exception(roi_err) + + if prefix: + temp_rois = [] + for roi in roi_list: + roi = prefix + "_" + str(roi) + temp_rois.append(roi) + roi_list = temp_rois + + return roi_list + + def safe_shape(*vol_data): """ Checks if the volume (first three dimensions) of multiple ndarrays From 71decae2f0e94c722496f77f750df3532198cc44 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 16 Mar 2018 18:30:41 -0400 Subject: [PATCH 14/75] Fixed some data config nesting parsing, and also condensed template apply warps into a loop. --- CPAC/pipeline/cpac_pipeline.py | 641 +++++++++---------------- CPAC/registration/registration.py | 97 ++-- CPAC/timeseries/timeseries_analysis.py | 22 +- CPAC/utils/datasource.py | 70 ++- CPAC/utils/utils.py | 94 ++-- 5 files changed, 381 insertions(+), 543 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 26ec09e79e..b565088c0b 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -168,10 +168,11 @@ def prep_workflow(sub_dict, c, strategies, run, pipeline_timing_info=None, outputs_native_nonsmooth = ['alff_img', 'falff_img', - 'reho_raw_map', - 'dr_tempreg_maps_files', - 'dr_tempreg_maps_zstat_files', - 'sca_roi_correlation_files'] + 'reho_raw_map'] + + outputs_native_nonsmooth_mult = ['dr_tempreg_maps_files', + 'dr_tempreg_maps_zstat_files', + 'sca_roi_correlation_files'] outputs_template_nonsmooth = ['alff_to_standard', 'alff_to_standard_zstd', @@ -179,70 +180,73 @@ def prep_workflow(sub_dict, c, strategies, run, pipeline_timing_info=None, 'falff_to_standard_zstd', 'reho_to_standard', 'reho_to_standard_zstd', - 'dr_tempreg_maps_files_to_standard', - 'dr_tempreg_maps_zstat_files_to_standard', - 'sca_roi_files_to_standard', - 'sca_roi_files_to_standard_fisher_zstd', 'vmhc_raw_score', 'vmhc_fisher_zstd', - 'vmhc_fisher_zstd_zstat_map', - 'sca_tempreg_maps_files', - 'sca_tempreg_zstat_files', - 'centrality_outputs', - 'centrality_outputs_zstd'] - - outputs_native_smooth = ['alff_smooth', - 'falff_smooth', - 'reho_smooth', - 'dr_tempreg_maps_files_smooth', - 'dr_tempreg_maps_zstat_files_smooth', - 'sca_roi_files_smooth'] - - outputs_template_smooth = ['alff_to_standard_smooth', - 'alff_to_standard_zstd_smooth', - 'falff_to_standard_smooth', - 'falff_to_standard_zstd_smooth', - 'reho_to_standard_smooth', - 'reho_to_standard_zstd_smooth', - 'dr_tempreg_maps_files_to_standard_smooth' - 'dr_tempreg_maps_zstat_files_to_standard_smooth', - 'sca_roi_files_to_standard_smooth', - 'sca_roi_files_to_standard_smooth_fisher_zstd', - 'sca_tempreg_maps_files_smooth', - 'sca_tempreg_maps_zstat_files_smooth', - 'centrality_outputs_smooth', - 'centrality_outputs_zstd_smooth'] + 'vmhc_fisher_zstd_zstat_map',] + + outputs_template_nonsmooth_mult = ['dr_tempreg_maps_files_to_standard', + 'dr_tempreg_maps_zstat_files_to_standard', + 'sca_roi_files_to_standard', + 'sca_roi_files_to_standard_fisher_zstd', + 'sca_tempreg_maps_files', + 'sca_tempreg_zstat_files', + 'centrality_outputs', + 'centrality_outputs_zstd'] + + # outputs_native_smooth = ['alff_smooth', + # 'falff_smooth', + # 'reho_smooth', + # 'dr_tempreg_maps_files_smooth', + # 'dr_tempreg_maps_zstat_files_smooth', + # 'sca_roi_files_smooth'] + # + # outputs_template_smooth = ['alff_to_standard_smooth', + # 'alff_to_standard_zstd_smooth', + # 'falff_to_standard_smooth', + # 'falff_to_standard_zstd_smooth', + # 'reho_to_standard_smooth', + # 'reho_to_standard_zstd_smooth', + # 'dr_tempreg_maps_files_to_standard_smooth' + # 'dr_tempreg_maps_zstat_files_to_standard_smooth', + # 'sca_roi_files_to_standard_smooth', + # 'sca_roi_files_to_standard_smooth_fisher_zstd', + # 'sca_tempreg_maps_files_smooth', + # 'sca_tempreg_maps_zstat_files_smooth', + # 'centrality_outputs_smooth', + # 'centrality_outputs_zstd_smooth'] outputs_template_raw = ['alff_to_standard', 'alff_to_standard_smooth', 'falff_to_standard', 'falff_to_standard_smooth', 'reho_to_standard', - 'reho_to_standard_smooth', - 'centrality_outputs', - 'centrality_outputs_smooth'] + 'reho_to_standard_smooth'] + + outputs_template_raw_mult = ['centrality_outputs', + 'centrality_outputs_smooth'] outputs_average = ['alff_img', 'alff_to_standard', 'falff_img', 'falff_to_standard', 'reho_to_standard', - 'dr_tempreg_maps_files', - 'dr_tempreg_maps_files_to_standard', - 'sca_roi_correlation_files', - 'sca_roi_files_to_standard', - 'sca_tempreg_maps_files', 'alff_smooth', 'alff_to_standard_smooth', 'falff_smooth', 'falff_to_standard_smooth', 'reho_smooth', - 'reho_to_standard_smooth', - 'dr_tempreg_maps_files_smooth', - 'dr_tempreg_maps_files_to_standard_smooth' - 'sca_roi_files_smooth', - 'sca_roi_files_to_standard_smooth', - 'sca_tempreg_maps_files_smooth'] + 'reho_to_standard_smooth'] + + outputs_average_mult = ['dr_tempreg_maps_files', + 'dr_tempreg_maps_files_to_standard', + 'sca_roi_correlation_files', + 'sca_roi_files_to_standard', + 'sca_tempreg_maps_files', + 'dr_tempreg_maps_files_smooth', + 'dr_tempreg_maps_files_to_standard_smooth' + 'sca_roi_files_smooth', + 'sca_roi_files_to_standard_smooth', + 'sca_tempreg_maps_files_smooth'] # Start timing here pipeline_start_time = time.time() @@ -494,6 +498,7 @@ def getNodeList(strategy): flow.inputs.inputnode.subject = subject_id flow.inputs.inputnode.anat = sub_dict['anat'] flow.inputs.inputnode.creds_path = input_creds_path + flow.inputs.inputnode.dl_dir = c.workingDirectory anat_flow = flow.clone('anat_gather_%d' % num_strat) @@ -1126,6 +1131,7 @@ def getNodeList(strategy): 'func_gather_%d' % num_strat) funcFlow.inputs.inputnode.subject = subject_id funcFlow.inputs.inputnode.creds_path = input_creds_path + funcFlow.inputs.inputnode.dl_dir = c.workingDirectory except Exception as xxx: logger.info("Error create_func_datasource failed. " "(%s:%d)" % dbg_file_lineno()) @@ -1239,10 +1245,18 @@ def getNodeList(strategy): strat.update_resource_pool( {'raw_functional': (funcFlow, 'outputspec.rest')}) - if fmap_phasediff and fmap_mag: - strat.update_resource_pool( - {"fmap_phase_diff": (funcFlow, 'outputspec.phase_diff'), - "fmap_magnitude": (funcFlow, 'outputspec.magnitude')}) + if 1 in c.runEPI_DistCorr: + try: + strat.update_resource_pool( + {"fmap_phase_diff": (funcFlow, 'outputspec.phase_diff'), + "fmap_magnitude": (funcFlow, 'outputspec.magnitude')}) + except: + err = "\n\n[!] You have selected to run field map " \ + "distortion correction, but at least one of your " \ + "scans listed in your data configuration file is " \ + "missing either a field map phase difference file " \ + "or a field map magnitude file, or both.\n\n" + raise Exception(err) if "Selected Functional Volume" in c.func_reg_input: strat.update_resource_pool( @@ -1315,19 +1329,19 @@ def getNodeList(strategy): if 1 in c.runEPI_DistCorr: - if not fmap_phasediff: - err = "\n\n[!] Field-map distortion correction is enabled, but " \ - "there is no field map phase difference file set in the " \ - "data configuration YAML file for participant {0}." \ - "\n\n".format(sub_dict['subject_id']) - raise Exception(err) - - if not fmap_mag: - err = "\n\n[!] Field-map distortion correction is enabled, but " \ - "there is no field map magnitude file set in the " \ - "data configuration YAML file for participant {0}." \ - "\n\n".format(sub_dict['subject_id']) - raise Exception(err) + # if not fmap_phasediff: + # err = "\n\n[!] Field-map distortion correction is enabled, but " \ + # "there is no field map phase difference file set in the " \ + # "data configuration YAML file for participant {0}." \ + # "\n\n".format(sub_dict['subject_id']) + # raise Exception(err) + # + # if not fmap_mag: + # err = "\n\n[!] Field-map distortion correction is enabled, but " \ + # "there is no field map magnitude file set in the " \ + # "data configuration YAML file for participant {0}." \ + # "\n\n".format(sub_dict['subject_id']) + # raise Exception(err) workflow_bit_id['epi_distcorr'] = workflow_counter @@ -2513,9 +2527,7 @@ def fsl_to_itk_conversion(source_file, reference, func_name): # converts FSL-format .mat affine xfm into ANTS-format # .txt; .mat affine comes from Func->Anat registration - fsl_to_itk_func_mni = create_wf_c3d_fsl_to_itk(0, name= \ - 'fsl_to_itk_%s_%d' % (func_name, \ - num_strat)) + fsl_to_itk_func_mni = create_wf_c3d_fsl_to_itk(name='fsl_to_itk_%s_%d' % (func_name, num_strat)) try: @@ -2543,22 +2555,16 @@ def fsl_to_itk_conversion(source_file, reference, func_name): strat.get_resource_pool(), '0016') raise - strat.update_resource_pool({'itk_func_anat_affine_%s' % \ - (func_name): ( - fsl_to_itk_func_mni, \ - 'outputspec.itk_transform')}) + strat.update_resource_pool({'itk_func_anat_affine_%s' % (func_name): (fsl_to_itk_func_mni, 'outputspec.itk_transform')}) strat.append_name(fsl_to_itk_func_mni.name) - create_log_node(fsl_to_itk_func_mni, \ - 'outputspec.itk_transform', num_strat) + create_log_node(fsl_to_itk_func_mni, 'outputspec.itk_transform', num_strat) def collect_transforms_func_mni(func_name): # collects series of warps to be applied collect_transforms_func_mni = \ - create_wf_collect_transforms(0, name= \ - 'collect_transforms_%s_%d' % \ - (func_name, num_strat)) + create_wf_collect_transforms(name='collect_transforms_%s_%d' % (func_name, num_strat)) try: @@ -2615,35 +2621,27 @@ def collect_transforms_func_mni(func_name): 'outputspec.transformation_series', num_strat) - def ants_apply_warps_func_mni(input_node, input_outfile, \ + def ants_apply_warps_func_mni(input_node, input_outfile, ref_node, ref_outfile, standard, - func_name, interp, \ + func_name, interp, input_image_type): # converts FSL-format .mat affine xfm into ANTS-format # .txt; .mat affine comes from Func->Anat registration - fsl_to_itk_func_mni = create_wf_c3d_fsl_to_itk(0, name= \ - 'fsl_to_itk_%s_%d' % (func_name, \ - num_strat)) + fsl_to_itk_func_mni = create_wf_c3d_fsl_to_itk(name= 'fsl_to_itk_%s_%d' % (func_name, num_strat)) # collects series of warps to be applied collect_transforms_func_mni = \ - create_wf_collect_transforms(0, name= \ - 'collect_transforms_%s_%d' % \ - (func_name, num_strat)) + create_wf_collect_transforms(name= 'collect_transforms_%s_%d' % (func_name, num_strat)) # apply ants warps apply_ants_warp_func_mni = \ - create_wf_apply_ants_warp(0, - name='apply_ants_warp_%s_%d' % \ - (func_name, num_strat), - ants_threads=int( - num_ants_cores)) + create_wf_apply_ants_warp(name='apply_ants_warp_%s_%d' % (func_name, num_strat), + ants_threads=int(num_ants_cores)) apply_ants_warp_func_mni.inputs.inputspec.reference_image = standard apply_ants_warp_func_mni.inputs.inputspec.dimension = 3 - apply_ants_warp_func_mni.inputs.inputspec. \ - interpolation = interp + apply_ants_warp_func_mni.inputs.inputspec.interpolation = interp # input_image_type: # (0 or 1 or 2 or 3) # Option specifying the input image type of scalar @@ -2770,59 +2768,6 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ "mean_functional_to_standard", "Linear", 0) - """ THEN GET RID OF THESE """ - """ - # 4D FUNCTIONAL apply warp - fsl_to_itk_conversion('mean_functional', - 'anatomical_brain', - 'functional_mni') - collect_transforms_func_mni('functional_mni') - - node, out_file = strat.get_leaf_properties() - ants_apply_warps_func_mni(node, out_file, - c.template_brain_only_for_func, - 'Linear', 3, - 'functional_mni') - - # 4D FUNCTIONAL MOTION-CORRECTED apply warp - fsl_to_itk_conversion('mean_functional', - 'anatomical_brain', - 'motion_correct_to_standard') - collect_transforms_func_mni('motion_correct_to_standard') - - node, out_file = strat.get_node_from_resource_pool('motion_correct') - ants_apply_warps_func_mni(node, out_file, - c.template_brain_only_for_func, - 'Linear', 3, - 'motion_correct_to_standard') - - # FUNCTIONAL MASK apply warp - fsl_to_itk_conversion('functional_brain_mask', - 'anatomical_brain', - 'functional_brain_mask_to_standard') - collect_transforms_func_mni('functional_brain_mask_to_standard') - - node, out_file = strat.get_node_from_resource_pool('func' \ - 'tional_brain_mask') - ants_apply_warps_func_mni(node, out_file, - c.template_brain_only_for_func, - 'NearestNeighbor', 0, - 'functional_brain_mask_to_standard') - - # FUNCTIONAL MEAN apply warp - fsl_to_itk_conversion('mean_functional', - 'anatomical_brain', - 'mean_functional_in_mni') - collect_transforms_func_mni('mean_functional_in_mni') - - node, out_file = strat.get_node_from_resource_pool('mean' \ - '_functional') - ants_apply_warps_func_mni(node, out_file, - c.template_brain_only_for_func, - 'Linear', 0, - 'mean_functional_in_mni') - """ - num_strat += 1 strat_list += new_strat_list @@ -3090,7 +3035,8 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ node2, out_file2 = strat.get_node_from_resource_pool( 'functional_brain_mask_to_standard') - # resample the input functional file and functional mask to spatial map + # resample the input functional file and functional mask + # to spatial map workflow.connect(node, out_file, resample_spatial_map_to_native_space, 'reference') @@ -3117,7 +3063,8 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ node2, out_file2 = strat.get_node_from_resource_pool( 'functional_brain_mask_to_standard') - # resample the input functional file and functional mask to spatial map + # resample the input functional file and functional mask + # to spatial map workflow.connect(node, out_file, resample_spatial_map_to_native_space_for_dr, 'reference') @@ -3139,7 +3086,6 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ spatial_map_timeseries_for_dr, 'inputspec.subject_rest') - except Exception as e: logConnectionError('Spatial map timeseries extraction', num_strat, strat.get_resource_pool(), @@ -3170,8 +3116,6 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ new_strat_list = [] num_strat = 0 - # if 1 in c.runROITimeseries: - if ("Avg" in ts_analysis_dict.keys()) or \ ("Avg" in sca_analysis_dict.keys()) or \ ("MultReg" in sca_analysis_dict.keys()): @@ -3226,7 +3170,6 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ 'roi_timeseries_for_mult_reg_%d' % num_strat) try: - if "Avg" in ts_analysis_dict.keys(): node, out_file = strat.get_node_from_resource_pool( 'functional_to_standard') @@ -3243,7 +3186,7 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ workflow.connect(resample_functional_to_roi, 'out_file', roi_timeseries, 'inputspec.rest') - if ("Avg" in sca_analysis_dict.keys()): + if "Avg" in sca_analysis_dict.keys(): node, out_file = strat.get_node_from_resource_pool( 'functional_to_standard') @@ -3264,7 +3207,7 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ 'out_file', roi_timeseries_for_sca, 'inputspec.rest') - if ("MultReg" in sca_analysis_dict.keys()): + if "MultReg" in sca_analysis_dict.keys(): node, out_file = strat.get_node_from_resource_pool( 'functional_to_standard') @@ -3287,7 +3230,6 @@ def ants_apply_warps_func_mni(input_node, input_outfile, \ roi_timeseries_for_multreg, 'inputspec.rest') - except: logConnectionError('ROI Timeseries analysis', num_strat, strat.get_resource_pool(), '0031') @@ -3739,56 +3681,6 @@ def connect_afni_centrality_wf(method_option, threshold_option, try: strat.update_resource_pool( {'centrality_outputs': (merge_node, 'merged_list')}) - - ''' - # if smoothing is required - if 1 in c.run_smoothing: - z_score = get_cent_zscore( - 'centrality_zscore_%d' % num_strat) - - z_score.inputs.inputspec.mask_file = \ - c.templateSpecificationFile - - smoothing = pe.MapNode(interface=fsl.MultiImageMaths(), - name='network_centrality_smooth_%d' % num_strat, - iterfield=['in_file']) - - smoothing.inputs.operand_files = \ - c.templateSpecificationFile - - zstd_smoothing = pe.MapNode( - interface=fsl.MultiImageMaths(), - name='network_centrality_zstd_smooth_%d' % num_strat, - iterfield=['in_file']) - - zstd_smoothing.inputs.operand_files = \ - c.templateSpecificationFile - - # calculate zscores - workflow.connect(merge_node, 'merged_list', - z_score, 'inputspec.input_file') - - # connecting raw centrality outputs to smoothing - workflow.connect(merge_node, 'merged_list', - smoothing, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - smoothing, 'op_string') - - # connecting zscores to smoothing - workflow.connect(z_score, 'outputspec.z_score_img', - zstd_smoothing, 'in_file') - workflow.connect(inputnode_fwhm, ('fwhm', set_gauss), - zstd_smoothing, 'op_string') - - strat.append_name(smoothing.name) - strat.update_resource_pool({'centrality_outputs_zstd': (z_score, 'outputspec.z_score_img'), - 'centrality_outputs_smooth': (smoothing, 'out_file'), - 'centrality_outputs_zstd_smooth': (zstd_smoothing, 'out_file')}) - - strat.append_name(smoothing.name) - create_log_node(smoothing, 'out_file', num_strat) - ''' - except: logConnectionError('Network Centrality', num_strat, strat.get_resource_pool(), '0050') @@ -3813,12 +3705,11 @@ def connect_afni_centrality_wf(method_option, threshold_option, WARP OUTPUTS TO TEMPLATE """"""""""""""""""""""""""""""""""""""""""""""""""" - ''' - OUTPUT TO STANDARD - ''' + '''''' + ''' APPLY WARPS ''' - def output_to_standard(output_name, output_resource, strat, num_strat, - map_node=0, input_image_type=0): + def output_to_standard(output_name, strat, num_strat, pipeline_config_obj, + map_node=False, input_image_type=0): nodes = getNodeList(strat) @@ -3826,75 +3717,64 @@ def output_to_standard(output_name, output_resource, strat, num_strat, # ANTS WARP APPLICATION - fsl_to_itk_convert = create_wf_c3d_fsl_to_itk(map_node, - input_image_type, - name= \ - '%s_fsl_to_itk_%d' % ( - output_name, - num_strat)) + # convert the func-to-anat linear warp from FSL FLIRT to + # ITK (ANTS) format + fsl_to_itk_convert = create_wf_c3d_fsl_to_itk(input_image_type, + map_node, + name='{0}_fsl_to_itk_{1}'.format(output_name, num_strat)) - collect_transforms = create_wf_collect_transforms(map_node, \ - name='%s_collect_transforms_%d' \ - % ( - output_name, - num_strat)) + # collect the list of warps into a single stack to feed into the + # ANTS warp apply tool + collect_transforms = create_wf_collect_transforms(map_node, + name='{0}_collect_transforms_{1}'.format(output_name, num_strat)) + # ANTS apply warp apply_ants_warp = create_wf_apply_ants_warp(map_node, - name='%s_to_standard_%d' % ( - output_name, - num_strat), - ants_threads=int( - num_ants_cores)) + name='{0}_to_standard_{1}'.format(output_name, num_strat), + ants_threads=int(pipeline_config_obj.num_ants_threads)) apply_ants_warp.inputs.inputspec.dimension = 3 apply_ants_warp.inputs.inputspec.interpolation = 'Linear' - apply_ants_warp.inputs.inputspec. \ - reference_image = c.template_brain_only_for_func + apply_ants_warp.inputs.inputspec.reference_image = \ + pipeline_config_obj.template_brain_only_for_func - apply_ants_warp.inputs.inputspec. \ - input_image_type = input_image_type + apply_ants_warp.inputs.inputspec.input_image_type = \ + input_image_type try: - # affine from FLIRT func->anat linear registration - node, out_file = strat.get_node_from_resource_pool('func' \ - 'tional_to_anat_linear_xfm') + node, out_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') workflow.connect(node, out_file, fsl_to_itk_convert, 'inputspec.affine_file') # reference used in FLIRT func->anat linear registration - node, out_file = strat.get_node_from_resource_pool('anat' \ - 'omical_brain') + node, out_file = strat.get_node_from_resource_pool('anatomical_brain') workflow.connect(node, out_file, fsl_to_itk_convert, 'inputspec.reference_file') # output file to be converted - node, out_file = strat. \ - get_node_from_resource_pool(output_resource) + node, out_file = \ + strat.get_node_from_resource_pool(output_name) workflow.connect(node, out_file, fsl_to_itk_convert, 'inputspec.source_file') # nonlinear warp from anatomical->template ANTS registration - node, out_file = strat.get_node_from_resource_pool('anat' \ - 'omical_to_mni_nonlinear_xfm') + node, out_file = strat.get_node_from_resource_pool('anatomical_to_mni_nonlinear_xfm') workflow.connect(node, out_file, collect_transforms, 'inputspec.warp_file') # linear initial from anatomical->template ANTS registration - node, out_file = strat.get_node_from_resource_pool('ants' \ - '_initial_xfm') + node, out_file = strat.get_node_from_resource_pool('ants_initial_xfm') workflow.connect(node, out_file, collect_transforms, 'inputspec.linear_initial') # linear affine from anatomical->template ANTS registration - node, out_file = strat.get_node_from_resource_pool('ants' \ - '_affine_xfm') + node, out_file = strat.get_node_from_resource_pool('ants_affine_xfm') workflow.connect(node, out_file, collect_transforms, 'inputspec.linear_affine') # rigid affine from anatomical->template ANTS registration - node, out_file = strat.get_node_from_resource_pool('ants' \ - '_rigid_xfm') + node, out_file = strat.get_node_from_resource_pool('ants_rigid_xfm') workflow.connect(node, out_file, collect_transforms, 'inputspec.linear_rigid') @@ -3905,8 +3785,7 @@ def output_to_standard(output_name, output_resource, strat, num_strat, 'inputspec.fsl_to_itk_affine') # output file to be converted - node, out_file = strat. \ - get_node_from_resource_pool(output_resource) + node, out_file = strat.get_node_from_resource_pool(output_name) workflow.connect(node, out_file, apply_ants_warp, 'inputspec.input_image') @@ -3916,170 +3795,89 @@ def output_to_standard(output_name, output_resource, strat, num_strat, apply_ants_warp, 'inputspec.transforms') - - except: - logConnectionError('%s to MNI (ANTS)' % (output_name), + logConnectionError('{0} to MNI (ANTS)'.format(output_name), num_strat, strat.get_resource_pool(), '0022') raise - strat.update_resource_pool({'%s_to_standard' % (output_name): \ - (apply_ants_warp, - 'outputspec.output_image')}) + strat.update_resource_pool({'{0}_to_standard'.format(output_name): (apply_ants_warp, 'outputspec.output_image')}) strat.append_name(apply_ants_warp.name) num_strat += 1 - else: - # FSL WARP APPLICATION - if map_node == 0: - - apply_fsl_warp = pe.Node(interface=fsl.ApplyWarp(), - name='%s_to_standard_%d' % ( - output_name, num_strat)) - - - elif map_node == 1: - + if map_node: apply_fsl_warp = pe.MapNode(interface=fsl.ApplyWarp(), - name='%s_to_standard_%d' % ( - output_name, num_strat), \ + name='{0}_to_standard_{1}'.format(output_name, num_strat), iterfield=['in_file']) + else: + apply_fsl_warp = pe.Node(interface=fsl.ApplyWarp(), + name='{0}_to_standard_{1}'.format(output_name, + num_strat)) - apply_fsl_warp.inputs.ref_file = c.template_skull_for_func + apply_fsl_warp.inputs.ref_file = \ + pipeline_config_obj.template_skull_for_func try: - # output file to be warped - node, out_file = strat. \ - get_node_from_resource_pool(output_resource) + node, out_file = strat.get_node_from_resource_pool(output_name) workflow.connect(node, out_file, apply_fsl_warp, 'in_file') # linear affine from func->anat linear FLIRT registration - node, out_file = strat.get_node_from_resource_pool('func' \ - 'tional_to_anat_linear_xfm') + node, out_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') workflow.connect(node, out_file, apply_fsl_warp, 'premat') # nonlinear warp from anatomical->template FNIRT registration - node, out_file = strat.get_node_from_resource_pool('anat' \ - 'omical_to_mni_nonlinear_xfm') + node, out_file = strat.get_node_from_resource_pool('anatomical_to_mni_nonlinear_xfm') workflow.connect(node, out_file, apply_fsl_warp, 'field_file') - except: - logConnectionError('%s to MNI (FSL)' % (output_name), \ + logConnectionError('{0} to MNI (FSL)'.format(output_name), num_strat, strat.get_resource_pool(), '0021') raise Exception - strat.update_resource_pool({'%s_to_standard' % (output_name): \ - (apply_fsl_warp, 'out_file')}) + strat.update_resource_pool({'{0}_to_standard'.format(output_name): (apply_fsl_warp, 'out_file')}) strat.append_name(apply_fsl_warp.name) - num_strat += 1 - - ''' - Transforming Dual Regression outputs to MNI - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runRegisterFuncToMNI and "DualReg" in sca_analysis_dict.keys(): - for strat in strat_list: - output_to_standard('dr_tempreg_maps_stack', - 'dr_tempreg_maps_stack', strat, num_strat, - input_image_type=3) - - output_to_standard('dr_tempreg_maps_zstat_stack', - 'dr_tempreg_maps_zstat_stack', strat, - num_strat, input_image_type=3) - - # dual reg 'files', too - output_to_standard('dr_tempreg_maps_files', - 'dr_tempreg_maps_files', strat, num_strat, 1) - output_to_standard('dr_tempreg_maps_zstat_files', - 'dr_tempreg_maps_zstat_files', strat, - num_strat, 1) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Transforming alff/falff outputs to MNI - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runRegisterFuncToMNI and (1 in c.runALFF): - - for strat in strat_list: - output_to_standard('alff', 'alff_img', strat, num_strat) - output_to_standard('falff', 'falff_img', strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list - - ''' - Transforming ReHo outputs to MNI - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runRegisterFuncToMNI and (1 in c.runReHo): - for strat in strat_list: - output_to_standard('reho', 'reho_raw_map', strat, num_strat) - - num_strat += 1 - - strat_list += new_strat_list + return strat ''' - Transforming SCA ROI outputs to MNI + Apply the warps ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runRegisterFuncToMNI and (1 in c.runSCA) and ( - "Avg" in sca_analysis_dict.keys()): # in(1 in c.runROITimeseries): + if 1 in c.runRegisterFuncToMNI: + num_strat = 0 for strat in strat_list: - output_to_standard('sca_roi_stack', 'sca_roi_correlation_stack', - strat, num_strat, input_image_type=3) - output_to_standard('sca_roi_files', 'sca_roi_correlation_files', - strat, num_strat, 1) - + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + if key in outputs_native_nonsmooth: + # smoothing happens at the end, so only the non-smooth + # named output labels for the native-space outputs + strat = output_to_standard(key, strat, num_strat, c) num_strat += 1 - strat_list += new_strat_list - '''''' ''' Z-SCORING ''' - def z_score_standardize(output_name, strat, num_strat): + def z_score_standardize(output_name, strat, num_strat, map_node=False): # call the z-scoring sub-workflow builder - z_score_std = get_zscore(output_name, 'z_score_std_%s_%d' % ( - output_name, num_strat)) + z_score_std = get_zscore(output_name, map_node, + 'z_score_std_%s_%d' % (output_name, num_strat)) try: - node, out_file = strat.get_node_from_resource_pool( - output_name) + node, out_file = strat.get_node_from_resource_pool(output_name) workflow.connect(node, out_file, z_score_std, 'inputspec.input_file') # needs the template-space functional mask because we are z-score # standardizing outputs that have already been warped to template - node, out_file = strat. \ - get_node_from_resource_pool( - 'functional_brain_mask_to_standard') - workflow.connect(node, out_file, z_score_std, - 'inputspec.mask_file') - + node, out_file = strat.get_node_from_resource_pool('functional_brain_mask_to_standard') + workflow.connect(node, out_file, + z_score_std, 'inputspec.mask_file') except: logConnectionError('%s z-score standardize' % output_name, @@ -4087,15 +3885,15 @@ def z_score_standardize(output_name, strat, num_strat): raise strat.append_name(z_score_std.name) - strat.update_resource_pool({'%s_zstd' % (output_name): \ - (z_score_std, - 'outputspec.z_score_img')}) + strat.update_resource_pool({'{0}_zstd'.format(output_name): (z_score_std, 'outputspec.z_score_img')}) + + return strat def fisher_z_score_standardize(output_name, timeseries_oned_file, strat, - num_strat): + num_strat, map_node=False): # call the fisher r-to-z sub-workflow builder - fisher_z_score_std = get_fisher_zscore(output_name, + fisher_z_score_std = get_fisher_zscore(output_name, map_node, 'fisher_z_score_std_%s_%d' \ % (output_name, num_strat)) @@ -4111,16 +3909,15 @@ def fisher_z_score_standardize(output_name, timeseries_oned_file, strat, workflow.connect(node, out_file, fisher_z_score_std, 'inputspec.timeseries_one_d') - except: logConnectionError('%s fisher z-score standardize' % output_name, num_strat, strat.get_resource_pool(), '0128') raise strat.append_name(fisher_z_score_std.name) - strat.update_resource_pool({'%s_fisher_zstd' % (output_name): \ - (fisher_z_score_std, - 'outputspec.fisher_z_score_img')}) + strat.update_resource_pool({'{0}_fisher_zstd'.format(output_name): (fisher_z_score_std, 'outputspec.fisher_z_score_img')}) + + return strat ''' Run the z-scoring nodes ''' @@ -4129,40 +3926,26 @@ def fisher_z_score_standardize(output_name, timeseries_oned_file, strat, for strat in strat_list: rp = strat.get_resource_pool() for key in sorted(rp.keys()): - if key in outputs_template_raw: + if "sca_roi_files_to_standard" in key: + # correlation files need the r-to-z + strat = fisher_z_score_standardize(key, + "roi_timeseries_for_SCA", + strat, num_strat, + map_node=True) + elif key in outputs_template_raw: + # raw score, in template space strat = z_score_standardize(key, strat, num_strat) + elif key in outputs_template_raw_mult: + # same as above but multiple files so mapnode required + strat = z_score_standardize(key, strat, num_strat, + map_node=True) num_strat += 1 - ''' - fisher-z-standardize SCA ROI MNI-standardized outputs - ''' - new_strat_list = [] - num_strat = 0 - - if 1 in c.runZScoring and (1 in c.runSCA) and \ - ("Avg" in sca_analysis_dict.keys()): - - for strat in strat_list: - - if c.run_smoothing: - fisher_z_score_standardize('sca_roi_files_to_standard_smooth', - 'sca_roi_files_to_standard_smooth', - 'roi_timeseries_for_SCA', - strat, num_strat, 1) - - fisher_z_score_standardize('sca_roi_files_to_standard', - 'sca_roi_files_to_standard', - 'roi_timeseries_for_SCA', - strat, num_strat, 1) - - num_strat += 1 - - strat_list += new_strat_list - '''''' ''' SMOOTHING ''' - def output_smooth(output_name, mask_name, strat, num_strat): + def output_smooth(output_name, mask_name, strat, num_strat, + map_node=False): if map_node: output_smooth = pe.MapNode(interface=fsl.MultiImageMaths(), @@ -4172,7 +3955,7 @@ def output_smooth(output_name, mask_name, strat, num_strat): else: output_smooth = pe.Node(interface=fsl.MultiImageMaths(), name='{0}_smooth_{1}'.format(output_name, - num_strat)) + num_strat)) try: # get the resource to be smoothed @@ -4185,8 +3968,14 @@ def output_smooth(output_name, mask_name, strat, num_strat): output_smooth, 'op_string') # get the mask - node, out_file = strat.get_node_from_resource_pool(mask_name) - workflow.connect(node, out_file, output_smooth, 'operand_files') + if "/" in mask_name: + # mask_name is a direct file path and not the name of a + # resource pool key + output_smooth.inputs.operand_files = mask_name + else: + node, out_file = strat.get_node_from_resource_pool(mask_name) + workflow.connect(node, out_file, + output_smooth, 'operand_files') except: logConnectionError('{0} smooth'.format(output_name), num_strat, @@ -4208,38 +3997,38 @@ def output_smooth(output_name, mask_name, strat, num_strat): rp = strat.get_resource_pool() for key in sorted(rp.keys()): if "centrality" in key: + # centrality needs its own mask strat = output_smooth(key, c.templateSpecificationFile, - strat, num_strat) + strat, num_strat, map_node=True) elif key in outputs_native_nonsmooth: + # native space strat = output_smooth(key, "functional_brain_mask", strat, num_strat) + elif key in outputs_native_nonsmooth_mult: + # native space with multiple files (map nodes) + strat = output_smooth(key, "functional_brain_mask", + strat, num_strat, map_node=True) elif key in outputs_template_nonsmooth: + # template space strat = output_smooth(key, "functional_brain_mask_to_standard", strat, num_strat) + elif key in outputs_template_nonsmooth_mult: + # template space with multiple files (map nodes) + strat = output_smooth(key, + "functional_brain_mask_to_standard", + strat, num_strat, map_node=True) num_strat += 1 '''''' ''' AVERAGING ''' - def calc_avg(output_name, strat, num_strat): + def calc_avg(output_name, strat, num_strat, map_node=False): """Calculate the average of an output using AFNI 3dmaskave.""" extract_imports = ['import os'] - if map_node == 0: - calc_average = pe.Node(interface=preprocess.Maskave(), - name='{0}_mean_{1}'.format(output_name, - num_strat)) - - mean_to_csv = pe.Node(util.Function(input_names=['in_file', - 'output_name'], - output_names=['output_mean'], - function=extract_output_mean, - imports=extract_imports), - name='{0}_mean_to_txt_{1}'.format(output_name, - num_strat)) - elif map_node == 1: + if map_node: calc_average = pe.MapNode(interface=preprocess.Maskave(), name='{0}_mean_{1}'.format(output_name, num_strat), @@ -4253,6 +4042,18 @@ def calc_avg(output_name, strat, num_strat): name='{0}_mean_to_txt_{1}'.format(output_name, num_strat), iterfield=['in_file']) + else: + calc_average = pe.Node(interface=preprocess.Maskave(), + name='{0}_mean_{1}'.format(output_name, + num_strat)) + + mean_to_csv = pe.Node(util.Function(input_names=['in_file', + 'output_name'], + output_names=['output_mean'], + function=extract_output_mean, + imports=extract_imports), + name='{0}_mean_to_txt_{1}'.format(output_name, + num_strat)) mean_to_csv.inputs.output_name = output_name @@ -4260,7 +4061,6 @@ def calc_avg(output_name, strat, num_strat): node, out_file = strat.get_node_from_resource_pool(output_name) workflow.connect(node, out_file, calc_average, 'in_file') workflow.connect(calc_average, 'out_file', mean_to_csv, 'in_file') - except: logConnectionError('{0} calc average'.format(output_name), num_strat, strat.get_resource_pool(), '0128') @@ -4280,12 +4080,14 @@ def calc_avg(output_name, strat, num_strat): rp = strat.get_resource_pool() for key in sorted(rp.keys()): if key in outputs_average: + # the outputs we need the averages for strat = calc_avg(key, strat, num_strat) + elif key in outputs_average_mult: + # those outputs, but the ones with multiple files (map nodes) + strat = calc_avg(key, strat, num_strat, map_node=True) num_strat += 1 - - ''' QA PAGES GO HERE ''' @@ -4536,7 +4338,8 @@ def is_number(s): if 0 not in c.run_smoothing: # write out only the smoothed outputs - if key in outputs_nonsmooth: + if key in outputs_native_nonsmooth or \ + key in outputs_template_nonsmooth: continue ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) diff --git a/CPAC/registration/registration.py b/CPAC/registration/registration.py index 533c2d3869..04d46d79aa 100644 --- a/CPAC/registration/registration.py +++ b/CPAC/registration/registration.py @@ -144,7 +144,6 @@ def create_nonlinear_register(name='nonlinear_register'): outputspec, 'linear_xfm') return nonlinear_register - def create_register_func_to_mni(name='register_func_to_mni'): @@ -836,7 +835,8 @@ def create_wf_calculate_ants_warp(name='create_wf_calculate_ants_warp', mult_inp return calc_ants_warp_wf -def create_wf_apply_ants_warp(map_node, name='create_wf_apply_ants_warp', +def create_wf_apply_ants_warp(map_node=False, + name='create_wf_apply_ants_warp', ants_threads=1): """ @@ -895,55 +895,55 @@ def create_wf_apply_ants_warp(map_node, name='create_wf_apply_ants_warp', apply_ants_warp_wf = pe.Workflow(name=name) inputspec = pe.Node(util.IdentityInterface(fields=['input_image', - 'reference_image', 'transforms', 'dimension', 'input_image_type', - 'interpolation']), name='inputspec') - - if map_node == 0: - apply_ants_warp = pe.Node(interface=ants.ApplyTransforms(), - name='apply_ants_warp') + 'reference_image', + 'transforms', + 'dimension', + 'input_image_type', + 'interpolation']), + name='inputspec') - elif map_node == 1: + if map_node: apply_ants_warp = pe.MapNode(interface=ants.ApplyTransforms(), - name='apply_ants_warp_mapnode', iterfield=['input_image', \ - 'transforms']) + name='apply_ants_warp_mapnode', + iterfield=['input_image', 'transforms']) + else: + apply_ants_warp = pe.Node(interface=ants.ApplyTransforms(), + name='apply_ants_warp') apply_ants_warp.inputs.out_postfix = '_antswarp' apply_ants_warp.interface.num_threads = ants_threads apply_ants_warp.interface.estimated_memory_gb = 1.5 outputspec = pe.Node(util.IdentityInterface(fields=['output_image']), - name='outputspec') - + name='outputspec') # connections from inputspec - apply_ants_warp_wf.connect(inputspec, 'input_image', apply_ants_warp, - 'input_image') + 'input_image') apply_ants_warp_wf.connect(inputspec, 'reference_image', apply_ants_warp, - 'reference_image') + 'reference_image') apply_ants_warp_wf.connect(inputspec, 'transforms', apply_ants_warp, - 'transforms') + 'transforms') apply_ants_warp_wf.connect(inputspec, 'dimension', apply_ants_warp, - 'dimension') + 'dimension') apply_ants_warp_wf.connect(inputspec, 'input_image_type', apply_ants_warp, - 'input_image_type') + 'input_image_type') apply_ants_warp_wf.connect(inputspec, 'interpolation', apply_ants_warp, - 'interpolation') + 'interpolation') # connections to outputspec - apply_ants_warp_wf.connect(apply_ants_warp, 'output_image', - outputspec, 'output_image') + outputspec, 'output_image') return apply_ants_warp_wf -def create_wf_c3d_fsl_to_itk(map_node, input_image_type=0, +def create_wf_c3d_fsl_to_itk(input_image_type=0, map_node=False, name='create_wf_c3d_fsl_to_itk'): """ @@ -988,42 +988,43 @@ def create_wf_c3d_fsl_to_itk(map_node, input_image_type=0, fsl_to_itk_conversion = pe.Workflow(name=name) + itk_imports = ['import os'] + inputspec = pe.Node(util.IdentityInterface(fields=['affine_file', - 'reference_file', 'source_file']), name='inputspec') + 'reference_file', + 'source_file']), + name='inputspec') # converts FSL-format .mat affine xfm into ANTS-format .txt # .mat affine comes from Func->Anat registration - if map_node == 0: - fsl_reg_2_itk = pe.Node(c3.C3dAffineTool(), name='fsl_reg_2_itk') - - elif map_node == 1: + if map_node: fsl_reg_2_itk = pe.MapNode(c3.C3dAffineTool(), - name='fsl_reg_2_itk_mapnode', iterfield=['source_file']) - - fsl_reg_2_itk.inputs.itk_transform = True - fsl_reg_2_itk.inputs.fsl2ras = True - - itk_imports = ['import os'] + name='fsl_reg_2_itk_mapnode', + iterfield=['source_file']) - if map_node == 0: - change_transform = pe.Node(util.Function( + change_transform = pe.MapNode(util.Function( input_names=['input_affine_file'], - output_names=['updated_affine_file'], + output_names=['updated_affine_file'], function=change_itk_transform_type, imports=itk_imports), - name='change_transform_type') + name='change_transform_type', + iterfield=['input_affine_file']) + else: + fsl_reg_2_itk = pe.Node(c3.C3dAffineTool(), name='fsl_reg_2_itk') - elif map_node == 1: - change_transform = pe.MapNode(util.Function( + change_transform = pe.Node(util.Function( input_names=['input_affine_file'], - output_names=['updated_affine_file'], + output_names=['updated_affine_file'], function=change_itk_transform_type, imports=itk_imports), - name='change_transform_type', iterfield=['input_affine_file']) + name='change_transform_type') + + fsl_reg_2_itk.inputs.itk_transform = True + fsl_reg_2_itk.inputs.fsl2ras = True outputspec = pe.Node(util.IdentityInterface(fields=['itk_transform']), - name='outputspec') + name='outputspec') fsl_to_itk_conversion.connect(inputspec, 'affine_file', fsl_reg_2_itk, 'transform_file') @@ -1066,7 +1067,8 @@ def create_wf_c3d_fsl_to_itk(map_node, input_image_type=0, return fsl_to_itk_conversion -def create_wf_collect_transforms(map_node, name='create_wf_collect_transforms'): +def create_wf_collect_transforms(map_node=False, + name='create_wf_collect_transforms'): """ DOCSTRINGS @@ -1082,12 +1084,11 @@ def create_wf_collect_transforms(map_node, name='create_wf_collect_transforms'): # converts FSL-format .mat affine xfm into ANTS-format .txt # .mat affine comes from Func->Anat registration - if map_node == 0: - collect_transforms = pe.Node(util.Merge(5), name='collect_transforms') - - elif map_node == 1: + if map_node: collect_transforms = pe.MapNode(util.Merge(5), name='collect_transforms_mapnode', iterfield=['in5']) + else: + collect_transforms = pe.Node(util.Merge(5), name='collect_transforms') outputspec = pe.Node(util.IdentityInterface( fields=['transformation_series']), name='outputspec') diff --git a/CPAC/timeseries/timeseries_analysis.py b/CPAC/timeseries/timeseries_analysis.py index 80cba8eb97..a5dc93a7ef 100644 --- a/CPAC/timeseries/timeseries_analysis.py +++ b/CPAC/timeseries/timeseries_analysis.py @@ -396,26 +396,20 @@ def get_spatial_map_timeseries(wf_name='spatial_map_timeseries'): 'demean']), name='inputspec') - outputNode = pe.Node(util.IdentityInterface - (fields=['subject_timeseries']), - name='outputspec') + outputNode = pe.Node(util.IdentityInterface( + fields=['subject_timeseries']), + name='outputspec') spatialReg = pe.Node(interface=fsl.GLM(), name='spatial_regression') spatialReg.inputs.out_file = 'spatial_map_timeseries.txt' - wflow.connect(inputNode, 'subject_rest', - spatialReg, 'in_file') - wflow.connect(inputNode, 'subject_mask', - spatialReg, 'mask') - wflow.connect(inputNode, 'spatial_map', - spatialReg, 'design') - wflow.connect(inputNode, 'demean', - spatialReg, 'demean') - - wflow.connect(spatialReg, 'out_file', - outputNode, 'subject_timeseries') + wflow.connect(inputNode, 'subject_rest', spatialReg, 'in_file') + wflow.connect(inputNode, 'subject_mask', spatialReg, 'mask') + wflow.connect(inputNode, 'spatial_map', spatialReg, 'design') + wflow.connect(inputNode, 'demean', spatialReg, 'demean') + wflow.connect(spatialReg, 'out_file', outputNode, 'subject_timeseries') return wflow diff --git a/CPAC/utils/datasource.py b/CPAC/utils/datasource.py index bf0295a11a..d79301d365 100644 --- a/CPAC/utils/datasource.py +++ b/CPAC/utils/datasource.py @@ -11,6 +11,11 @@ def extract_scan_params_dct(scan_params_dct): return scan_params_dct +def get_map(map, map_dct): + # return the spatial map required + return map_dct[map] + + def create_func_datasource(rest_dict, wf_name='func_datasource'): import nipype.pipeline.engine as pe @@ -19,7 +24,8 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): wf = pe.Workflow(name=wf_name) inputnode = pe.Node(util.IdentityInterface( - fields=['subject', 'scan', 'creds_path'], + fields=['subject', 'scan', 'creds_path', + 'dl_dir'], mandatory_inputs=True), name='inputnode') @@ -36,6 +42,20 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): scan_resources = rest_dict[scan] + # have this here for now because of the big change in the data + # configuration format + try: + keys = scan_resources.keys() + except AttributeError: + err = "\n[!] The data configuration file you provided is " \ + "missing a level under the 'func:' key. CPAC versions " \ + "1.0.4 and later use data configurations with an " \ + "additional level of nesting.\n\nExample\nfunc:\n " \ + "rest01:\n scan: /path/to/rest01_func.nii.gz\n" \ + " scan parameters: /path/to/scan_params.json\n\n" \ + "See the User Guide for more information.\n\n" + raise Exception(err) + # actual 4D time series file if "scan" not in scan_resources.keys(): err = "\n\n[!] The {0} scan is missing its actual time-series " \ @@ -43,13 +63,12 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): "'scan' key.\n\n".format(scan) raise Exception(err) - scan_file = scan_resources["scan"] - - if '.' in scan_file or '+' in scan_file or '*' in scan_file: + # Nipype restriction (may have changed) + if '.' in scan or '+' in scan or '*' in scan: raise Exception('\n\n[!] Scan names cannot contain any special ' 'characters (., +, *, etc.). Please update this ' 'and try again.\n\nScan: {0}' - '\n\n'.format(scan_file)) + '\n\n'.format(scan)) # scan parameters CSV if "scan_parameters" in scan_resources.keys(): @@ -59,6 +78,7 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): s3_scan_params = \ pe.Node(util.Function(input_names=['file_path', 'creds_path', + 'dl_dir', 'img_type'], output_names=['local_path'], function=check_for_s3), @@ -66,11 +86,13 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): s3_scan_params.inputs.file_path = \ scan_resources["scan_parameters"] + s3_scan_params.inputs.dl_dir = dl_dir wf.connect(inputnode, 'creds_path', s3_scan_params, 'creds_path') wf.connect(s3_scan_params, 'local_path', outputnode, 'scan_params') + wf.connect(inputnode, 'dl_dir', s3_scan_params, 'dl_dir') elif isinstance(scan_resources["scan_parameters"], dict): get_scan_params_dct = \ @@ -97,6 +119,7 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): s3_fmap_phase = pe.Node(util.Function(input_names=['file_path', 'creds_path', + 'dl_dir', 'img_type'], output_names=['local_path'], function=check_for_s3), @@ -104,6 +127,7 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): s3_fmap_phase.inputs.file_path = fmap_phase s3_fmap_phase.inputs.img_type = "other" wf.connect(inputnode, 'creds_path', s3_fmap_phase, 'creds_path') + wf.connect(inputnode, 'dl_dir', s3_fmap_phase, 'dl_dir') wf.connect(s3_fmap_phase, 'local_path', outputnode, 'phase_diff') if "fmap_mag" in scan_resources.keys(): @@ -119,6 +143,7 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): s3_fmap_mag = pe.Node(util.Function(input_names=['file_path', 'creds_path', + 'dl_dir', 'img_type'], output_names=['local_path'], function=check_for_s3), @@ -126,6 +151,7 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): s3_fmap_mag.inputs.file_path = fmap_mag s3_fmap_mag.inputs.img_type = "other" wf.connect(inputnode, 'creds_path', s3_fmap_mag, 'creds_path') + wf.connect(inputnode, 'dl_dir', s3_fmap_mag, 'dl_dir') wf.connect(s3_fmap_mag, 'local_path', outputnode, 'magnitude') selectrest = pe.Node(util.Function(input_names=['scan', 'rest_dict'], @@ -136,6 +162,7 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): check_s3_node = pe.Node(util.Function(input_names=['file_path', 'creds_path', + 'dl_dir', 'img_type'], output_names=['local_path'], function=check_for_s3), @@ -143,10 +170,10 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): wf.connect(selectrest, 'rest', check_s3_node, 'file_path') wf.connect(inputnode, 'creds_path', check_s3_node, 'creds_path') + wf.connect(inputnode, 'dl_dir', check_s3_node, 'dl_dir') check_s3_node.inputs.img_type = 'func' wf.connect(inputnode, 'scan', selectrest, 'scan') - wf.connect(inputnode, 'subject', outputnode, 'subject') wf.connect(check_s3_node, 'local_path', outputnode, 'rest') wf.connect(inputnode, 'scan', outputnode, 'scan') @@ -184,7 +211,7 @@ def check_for_s3(file_path, creds_path, dl_dir=None, img_type='anat'): # Extract relative key path from bucket and local path s3_prefix = os.path.join(s3_str, bucket_name) s3_key = file_path.replace(s3_prefix, '').lstrip('/') - local_path = os.path.join(dl_dir, os.path.basename(s3_key)) + local_path = os.path.join(dl_dir, s3_key) # Get local directory and create folders if they dont exist local_dir = os.path.dirname(local_path) @@ -253,12 +280,14 @@ def create_anat_datasource(wf_name='anat_datasource'): wf = pe.Workflow(name=wf_name) inputnode = pe.Node(util.IdentityInterface( - fields=['subject', 'anat', 'creds_path'], + fields=['subject', 'anat', 'creds_path', + 'dl_dir'], mandatory_inputs=True), name='inputnode') check_s3_node = pe.Node(util.Function(input_names=['file_path', 'creds_path', + 'dl_dir', 'img_type'], output_names=['local_path'], function=check_for_s3), @@ -266,6 +295,7 @@ def create_anat_datasource(wf_name='anat_datasource'): wf.connect(inputnode, 'anat', check_s3_node, 'file_path') wf.connect(inputnode, 'creds_path', check_s3_node, 'creds_path') + wf.connect(inputnode, 'dl_dir', check_s3_node, 'dl_dir') check_s3_node.inputs.img_type = 'anat' outputnode = pe.Node(util.IdentityInterface(fields=['subject', @@ -330,20 +360,17 @@ def create_roi_mask_dataflow(masks, wf_name='datasource_roi_mask'): inputnode.iterables = [('mask', mask_dict.keys())] - selectmask = pe.Node(util.Function(input_names=['scan', 'rest_dict'], + selectmask = pe.Node(util.Function(input_names=['map', 'map_dct'], output_names=['out_file'], - function=get_rest), + function=get_map), name='select_mask') - selectmask.inputs.rest_dict = mask_dict + selectmask.inputs.map_dct = mask_dict outputnode = pe.Node(util.IdentityInterface(fields=['out_file']), name='outputspec') - wf.connect(inputnode, 'mask', - selectmask, 'scan') - - wf.connect(selectmask, 'out_file', - outputnode, 'out_file') + wf.connect(inputnode, 'mask', selectmask, 'map') + wf.connect(selectmask, 'out_file', outputnode, 'out_file') return wf @@ -396,15 +423,14 @@ def create_spatial_map_dataflow(spatial_maps, wf_name='datasource_maps'): inputnode.iterables = [('spatial_map', spatial_map_dict.keys())] - select_spatial_map = pe.Node(util.Function(input_names=['scan', - 'rest_dict'], + select_spatial_map = pe.Node(util.Function(input_names=['map', + 'map_dct'], output_names=['out_file'], - function=get_rest), + function=get_map), name='select_spatial_map') - select_spatial_map.inputs.rest_dict = spatial_map_dict + select_spatial_map.inputs.map_dct = spatial_map_dict - wf.connect(inputnode, 'spatial_map', - select_spatial_map, 'scan') + wf.connect(inputnode, 'spatial_map', select_spatial_map, 'map') return wf diff --git a/CPAC/utils/utils.py b/CPAC/utils/utils.py index 27b447db32..637f32c022 100644 --- a/CPAC/utils/utils.py +++ b/CPAC/utils/utils.py @@ -142,7 +142,7 @@ } -def get_zscore(input_name, wf_name='z_score'): +def get_zscore(input_name, map_node=False, wf_name='z_score'): """ Workflow to calculate z-scores @@ -206,49 +206,64 @@ def get_zscore(input_name, wf_name='z_score'): outputNode = pe.Node(util.IdentityInterface(fields=['z_score_img']), name='outputspec') - mean = pe.Node(interface=fsl.ImageStats(), - name='mean') + if map_node: + mean = pe.MapNode(interface=fsl.ImageStats(), + name='mean', + iterfield=['in_file']) + + standard_deviation = pe.MapNode(interface=fsl.ImageStats(), + name='standard_deviation', + iterfield=['in_file']) + + op_string = pe.MapNode(util.Function(input_names=['mean', 'std_dev'], + output_names=['op_string'], + function=get_operand_string), + name='op_string', + iterfield=['mean', 'std_dev']) + + z_score = pe.MapNode(interface=fsl.MultiImageMaths(), + name='z_score', + iterfield=['in_file', 'op_string']) + + else: + mean = pe.Node(interface=fsl.ImageStats(), name='mean') + + standard_deviation = pe.Node(interface=fsl.ImageStats(), + name='standard_deviation') + + op_string = pe.Node(util.Function(input_names=['mean', 'std_dev'], + output_names=['op_string'], + function=get_operand_string), + name='op_string') + + z_score = pe.Node(interface=fsl.MultiImageMaths(), name='z_score') + + # calculate the mean mean.inputs.op_string = '-k %s -m' - wflow.connect(inputNode, 'input_file', - mean, 'in_file') - wflow.connect(inputNode, 'mask_file', - mean, 'mask_file') + wflow.connect(inputNode, 'input_file', mean, 'in_file') + wflow.connect(inputNode, 'mask_file', mean, 'mask_file') - standard_deviation = pe.Node(interface=fsl.ImageStats(), - name='standard_deviation') + # calculate the standard deviation standard_deviation.inputs.op_string = '-k %s -s' - wflow.connect(inputNode, 'input_file', - standard_deviation, 'in_file') - wflow.connect(inputNode, 'mask_file', - standard_deviation, 'mask_file') - - op_string = pe.Node(util.Function(input_names=['mean', 'std_dev'], - output_names=['op_string'], - function=get_operand_string), - name='op_string') - wflow.connect(mean, 'out_stat', - op_string, 'mean') - wflow.connect(standard_deviation, 'out_stat', - op_string, 'std_dev') - - z_score = pe.Node(interface=fsl.MultiImageMaths(), - name='z_score') + wflow.connect(inputNode, 'input_file', standard_deviation, 'in_file') + wflow.connect(inputNode, 'mask_file', standard_deviation, 'mask_file') + + # calculate the z-score + wflow.connect(mean, 'out_stat', op_string, 'mean') + wflow.connect(standard_deviation, 'out_stat', op_string, 'std_dev') z_score.inputs.out_file = input_name + '_zstd.nii.gz' - wflow.connect(op_string, 'op_string', - z_score, 'op_string') - wflow.connect(inputNode, 'input_file', - z_score, 'in_file') - wflow.connect(inputNode, 'mask_file', - z_score, 'operand_files') + wflow.connect(op_string, 'op_string', z_score, 'op_string') + wflow.connect(inputNode, 'input_file', z_score, 'in_file') + wflow.connect(inputNode, 'mask_file', z_score, 'operand_files') wflow.connect(z_score, 'out_file', outputNode, 'z_score_img') return wflow -def get_fisher_zscore(input_name, wf_name='fisher_z_score'): +def get_fisher_zscore(input_name, map_node=False, wf_name='fisher_z_score'): """ Runs the compute_fisher_z_score function as part of a one-node workflow. """ @@ -267,25 +282,24 @@ def get_fisher_zscore(input_name, wf_name='fisher_z_score'): util.IdentityInterface(fields=['fisher_z_score_img']), name='outputspec') - if map_node == 0: - fisher_z_score = pe.Node( + if map_node: + # node to separate out + fisher_z_score = pe.MapNode( util.Function(input_names=['correlation_file', 'timeseries_one_d', 'input_name'], output_names=['out_file'], function=compute_fisher_z_score), - name='fisher_z_score') - + name='fisher_z_score', + iterfield=['correlation_file']) else: - # node to separate out - fisher_z_score = pe.MapNode( + fisher_z_score = pe.Node( util.Function(input_names=['correlation_file', 'timeseries_one_d', 'input_name'], output_names=['out_file'], function=compute_fisher_z_score), - name='fisher_z_score', - iterfield=['correlation_file']) + name='fisher_z_score') fisher_z_score.inputs.input_name = input_name From d70227edc4e1ec1ecf6e1b155d6159119c90f4e9 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Tue, 20 Mar 2018 08:37:21 -0400 Subject: [PATCH 15/75] html files for QA pages + fixed reho --- CPAC/pipeline/cpac_pipeline.py | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 8ef8ef2d5a..8fbeda1117 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -5272,20 +5272,20 @@ def QA_montages(measure, idx): # ReHo QA montages -# if 1 in c.runReHo: -# if 1 in c.runRegisterFuncToMNI: -# QA_montages('reho_to_standard', 15) -# -# if c.fwhm != None: -# QA_montages('reho_to_standard_smooth', 16) -# -# if 1 in c.runZScoring: -# -# if c.fwhm != None: -# QA_montages('reho_to_standard_smooth_fisher_zstd', 17) -# -# else: -# QA_montages('reho_to_standard_fisher_zstd', 18) + if 1 in c.runReHo: + if 1 in c.runRegisterFuncToMNI: + QA_montages('reho_to_standard', 15) + + if c.fwhm != None: + QA_montages('reho_to_standard_smooth', 16) + + if 1 in c.runZScoring: + + if c.fwhm != None: + QA_montages('reho_to_standard_smooth_fisher_zstd', 17) + + else: + QA_montages('reho_to_standard_fisher_zstd', 18) @@ -5646,9 +5646,9 @@ def is_number(s): ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) # Write QC outputs to log directory if 'qc' in key.lower(): - ds.inputs.base_directory = c.logDirectory - else: ds.inputs.base_directory = c.outputDirectory + else: + ds.inputs.base_directory = c.logDirectory ds.inputs.creds_path = creds_path ds.inputs.encrypt_bucket_keys = encrypt_data ds.inputs.container = os.path.join( @@ -5843,7 +5843,7 @@ def is_number(s): # If QC is enabled # TODO - QA pages: re-introduce - ''' + if 1 in c.generateQualityControlImages: # For each pipeline ID, generate the QC pages for pip_id in pip_ids: @@ -5855,7 +5855,7 @@ def is_number(s): qc_montage_id_s, qc_plot_id, qc_hist_id) # Automatically generate QC index page create_all_qc.run(pipeline_out_base) - ''' + # pipeline timing code starts here From 102f6e5c40819d80a4c3cede8461bb8fa477cb8c Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Tue, 20 Mar 2018 08:38:20 -0400 Subject: [PATCH 16/75] fixed reho+html pages error From c670b487050933cc83a473745c0c0fa93c62fa0f Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Tue, 20 Mar 2018 18:44:11 -0400 Subject: [PATCH 17/75] Fixed a couple of problems with the new condensed loops, and also updated some of the output directory write-outs. --- CPAC/GUI/interface/pages/functional_tab.py | 16 +- CPAC/pipeline/cpac_pipeline.py | 248 ++++++++++-------- .../configs/pipeline_config_template.yml | 3 +- CPAC/utils/utils.py | 6 +- 4 files changed, 160 insertions(+), 113 deletions(-) diff --git a/CPAC/GUI/interface/pages/functional_tab.py b/CPAC/GUI/interface/pages/functional_tab.py index 74ebedbb8f..96aea4838d 100644 --- a/CPAC/GUI/interface/pages/functional_tab.py +++ b/CPAC/GUI/interface/pages/functional_tab.py @@ -52,8 +52,8 @@ def __init__(self, parent, counter =0): control=control.TEXT_BOX, name='TR', type=dtype.NUM, - values= "None", - validator = CharValidator("no-alpha"), + values="None", + validator=CharValidator("no-alpha"), comment="Specify the TR (in seconds) at which images " "were acquired." "\n\nDefault is None- TR information is then " @@ -282,9 +282,15 @@ def __init__(self, parent, counter = 0): control=control.CHOICE_BOX, name='runRegisterFuncToMNI', type=dtype.LSTR, - comment="Register functional images to a standard MNI152 template.\n\nThis option must be enabled if you wish to calculate any derivatives.", - values=["On","Off"], - wkf_switch = True) + comment="Register functional images to a standard " + "MNI152 template.\n\nThis option must be " + "enabled if you wish to calculate any " + "derivatives. If set to On [1], only the " + "template-space files will be output. If set " + "to On/Off [1,0], both template-space and " + "native-space files will be output.", + values=["On", "On/Off", "Off"], + wkf_switch=True) self.page.add(label="Functional-to-Template Resolution ", control=control.CHOICE_BOX, diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index b565088c0b..882d25c712 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -161,14 +161,29 @@ def prep_workflow(sub_dict, c, strategies, run, pipeline_timing_info=None, debugging_outputs = ['despiked_fieldmap', 'fmap_magnitude', 'fmap_phase_diff', - 'raw_functional', 'seg_mixeltype', 'seg_partial_volume_files', - 'seg_partial_volume_map'] - - outputs_native_nonsmooth = ['alff_img', - 'falff_img', - 'reho_raw_map'] + 'seg_partial_volume_map', + 'seg_probability_maps', + 'coordinate_transformation', + 'max_displacement', + 'power_params', + 'movement_parameters'] + + extra_functional_outputs = ['raw_functional', + 'mean_functional', + 'functional_nuisance_residuals', + 'functional_preprocessed_mask', + 'mean_functional_in_anat', + 'motion_correct_smooth', + 'motion_correct_to_standard', + 'motion_correct_to_standard_smooth', + 'preprocessed', + 'slice_time_corrected'] + + outputs_native_nonsmooth = ['alff', + 'falff', + 'reho'] outputs_native_nonsmooth_mult = ['dr_tempreg_maps_files', 'dr_tempreg_maps_zstat_files', @@ -193,13 +208,13 @@ def prep_workflow(sub_dict, c, strategies, run, pipeline_timing_info=None, 'centrality_outputs', 'centrality_outputs_zstd'] - # outputs_native_smooth = ['alff_smooth', - # 'falff_smooth', - # 'reho_smooth', - # 'dr_tempreg_maps_files_smooth', - # 'dr_tempreg_maps_zstat_files_smooth', - # 'sca_roi_files_smooth'] - # + outputs_native_smooth = ['alff_smooth', + 'falff_smooth', + 'reho_smooth', + 'dr_tempreg_maps_files_smooth', + 'dr_tempreg_maps_zstat_files_smooth', + 'sca_roi_files_smooth'] + # outputs_template_smooth = ['alff_to_standard_smooth', # 'alff_to_standard_zstd_smooth', # 'falff_to_standard_smooth', @@ -225,15 +240,16 @@ def prep_workflow(sub_dict, c, strategies, run, pipeline_timing_info=None, outputs_template_raw_mult = ['centrality_outputs', 'centrality_outputs_smooth'] - outputs_average = ['alff_img', + outputs_average = ['alff', 'alff_to_standard', - 'falff_img', + 'falff', 'falff_to_standard', 'reho_to_standard', 'alff_smooth', 'alff_to_standard_smooth', 'falff_smooth', 'falff_to_standard_smooth', + 'reho', 'reho_smooth', 'reho_to_standard_smooth'] @@ -435,7 +451,8 @@ def logStandardError(sectionName, errLine, errNum, errInfo=None): if errInfo: logger.info("Error details: {0}\n\n".format(errInfo)) - def logConnectionError(workflow_name, numStrat, resourcePool, errNum): + def logConnectionError(workflow_name, numStrat, resourcePool, errNum, + errInfo=None): logger.info( "\n\n" + 'ERROR: Invalid Connection: %s: %s, resource_pool: %s' \ @@ -445,6 +462,9 @@ def logConnectionError(workflow_name, numStrat, resourcePool, errNum): "\n\n" + "This is a pipeline creation error - the workflows " "have not started yet." + "\n\n") + if errInfo: + logger.info(str(errInfo)) + def logStandardWarning(sectionName, warnLine): logger.info( @@ -459,7 +479,6 @@ def getNodeList(strategy): return nodes strat_list = [] - non_outputs = [] workflow_bit_id = {} workflow_counter = 0 @@ -2272,8 +2291,8 @@ def pick_wm(seg_prob_list): strat.append_name(alff.name) - strat.update_resource_pool({'alff_img': (alff, 'outputspec.alff_img')}) - strat.update_resource_pool({'falff_img': (alff, 'outputspec.falff_img')}) + strat.update_resource_pool({'alff': (alff, 'outputspec.alff_img')}) + strat.update_resource_pool({'falff': (alff, 'outputspec.falff_img')}) create_log_node(alff, 'outputspec.falff_img', num_strat) @@ -2915,7 +2934,7 @@ def ants_apply_warps_func_mni(input_node, input_outfile, raise strat.update_resource_pool( - {'reho_raw_map': (reho, 'outputspec.raw_reho_map')}) + {'reho': (reho, 'outputspec.raw_reho_map')}) strat.append_name(reho.name) create_log_node(reho, 'outputspec.raw_reho_map', num_strat) @@ -3702,7 +3721,7 @@ def connect_afni_centrality_wf(method_option, threshold_option, num_strat = 0 """"""""""""""""""""""""""""""""""""""""""""""""""" - WARP OUTPUTS TO TEMPLATE + Apply warps, Z-scoring, Smoothing, Averages """"""""""""""""""""""""""""""""""""""""""""""""""" '''''' @@ -3844,24 +3863,11 @@ def output_to_standard(output_name, strat, num_strat, pipeline_config_obj, return strat - ''' - Apply the warps - ''' - if 1 in c.runRegisterFuncToMNI: - num_strat = 0 - for strat in strat_list: - rp = strat.get_resource_pool() - for key in sorted(rp.keys()): - if key in outputs_native_nonsmooth: - # smoothing happens at the end, so only the non-smooth - # named output labels for the native-space outputs - strat = output_to_standard(key, strat, num_strat, c) - num_strat += 1 - '''''' ''' Z-SCORING ''' - def z_score_standardize(output_name, strat, num_strat, map_node=False): + def z_score_standardize(output_name, mask_name, strat, num_strat, + map_node=False): # call the z-scoring sub-workflow builder z_score_std = get_zscore(output_name, map_node, @@ -3869,19 +3875,23 @@ def z_score_standardize(output_name, strat, num_strat, map_node=False): try: node, out_file = strat.get_node_from_resource_pool(output_name) - workflow.connect(node, out_file, z_score_std, 'inputspec.input_file') - # needs the template-space functional mask because we are z-score - # standardizing outputs that have already been warped to template - node, out_file = strat.get_node_from_resource_pool('functional_brain_mask_to_standard') - workflow.connect(node, out_file, - z_score_std, 'inputspec.mask_file') + # get the mask + if "/" in mask_name: + # mask_name is a direct file path and not the name of a + # resource pool key + z_score_std.inputs.inputspec.mask_file = mask_name + else: + node, out_file = strat.get_node_from_resource_pool(mask_name) + workflow.connect(node, out_file, + z_score_std, 'inputspec.mask_file') - except: + except Exception as e: logConnectionError('%s z-score standardize' % output_name, - num_strat, strat.get_resource_pool(), '0127') + num_strat, strat.get_resource_pool(), + '0127', e) raise strat.append_name(z_score_std.name) @@ -3919,28 +3929,6 @@ def fisher_z_score_standardize(output_name, timeseries_oned_file, strat, return strat - ''' Run the z-scoring nodes ''' - - if 1 in c.runZScoring: - num_strat = 0 - for strat in strat_list: - rp = strat.get_resource_pool() - for key in sorted(rp.keys()): - if "sca_roi_files_to_standard" in key: - # correlation files need the r-to-z - strat = fisher_z_score_standardize(key, - "roi_timeseries_for_SCA", - strat, num_strat, - map_node=True) - elif key in outputs_template_raw: - # raw score, in template space - strat = z_score_standardize(key, strat, num_strat) - elif key in outputs_template_raw_mult: - # same as above but multiple files so mapnode required - strat = z_score_standardize(key, strat, num_strat, - map_node=True) - num_strat += 1 - '''''' ''' SMOOTHING ''' @@ -3987,39 +3975,6 @@ def output_smooth(output_name, mask_name, strat, num_strat, return strat - ''' - Connect the smoothing nodes - ''' - - if 1 in c.run_smoothing: - num_strat = 0 - for strat in strat_list: - rp = strat.get_resource_pool() - for key in sorted(rp.keys()): - if "centrality" in key: - # centrality needs its own mask - strat = output_smooth(key, c.templateSpecificationFile, - strat, num_strat, map_node=True) - elif key in outputs_native_nonsmooth: - # native space - strat = output_smooth(key, "functional_brain_mask", - strat, num_strat) - elif key in outputs_native_nonsmooth_mult: - # native space with multiple files (map nodes) - strat = output_smooth(key, "functional_brain_mask", - strat, num_strat, map_node=True) - elif key in outputs_template_nonsmooth: - # template space - strat = output_smooth(key, - "functional_brain_mask_to_standard", - strat, num_strat) - elif key in outputs_template_nonsmooth_mult: - # template space with multiple files (map nodes) - strat = output_smooth(key, - "functional_brain_mask_to_standard", - strat, num_strat, map_node=True) - num_strat += 1 - '''''' ''' AVERAGING ''' @@ -4072,19 +4027,91 @@ def calc_avg(output_name, strat, num_strat, map_node=False): return strat ''' - Connect the averaging nodes + Loop through the resource pool and connect the nodes for: + - applying warps to standard + - z-score standardization + - smoothing + - calculating output averages ''' num_strat = 0 for strat in strat_list: + + if 1 in c.runRegisterFuncToMNI: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + # connect nodes to apply warps to template + if key in outputs_native_nonsmooth: + # smoothing happens at the end, so only the non-smooth + # named output labels for the native-space outputs + strat = output_to_standard(key, strat, num_strat, c) + + if 1 in c.runZScoring: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + # connect nodes for z-score standardization + if "sca_roi_files_to_standard" in key: + # correlation files need the r-to-z + strat = fisher_z_score_standardize(key, + "roi_timeseries_for_SCA", + strat, num_strat, + map_node=True) + elif "centrality" in key: + # specific mask + strat = z_score_standardize(key, + c.templateSpecificationFile, + strat, num_strat, + map_node=True) + elif key in outputs_template_raw: + # raw score, in template space + strat = z_score_standardize(key, + "functional_brain_mask_to_standard", + strat, num_strat) + elif key in outputs_template_raw_mult: + # same as above but multiple files so mapnode required + strat = z_score_standardize(key, + "functional_brain_mask_to_standard", + strat, num_strat, + map_node=True) + + if 1 in c.run_smoothing: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + # connect nodes for smoothing + if "centrality" in key: + # centrality needs its own mask + strat = output_smooth(key, + c.templateSpecificationFile, + strat, num_strat, map_node=True) + elif key in outputs_native_nonsmooth: + # native space + strat = output_smooth(key, "functional_brain_mask", + strat, num_strat) + elif key in outputs_native_nonsmooth_mult: + # native space with multiple files (map nodes) + strat = output_smooth(key, "functional_brain_mask", + strat, num_strat, map_node=True) + elif key in outputs_template_nonsmooth: + # template space + strat = output_smooth(key, + "functional_brain_mask_to_standard", + strat, num_strat) + elif key in outputs_template_nonsmooth_mult: + # template space with multiple files (map nodes) + strat = output_smooth(key, + "functional_brain_mask_to_standard", + strat, num_strat, map_node=True) + rp = strat.get_resource_pool() for key in sorted(rp.keys()): + # connect nodes to calculate averages if key in outputs_average: # the outputs we need the averages for strat = calc_avg(key, strat, num_strat) elif key in outputs_average_mult: # those outputs, but the ones with multiple files (map nodes) strat = calc_avg(key, strat, num_strat, map_node=True) + num_strat += 1 @@ -4274,7 +4301,6 @@ def is_number(s): for name in strat.get_name(): import re - print (name) extra_string = re.search('_\d+', name).group(0) if extra_string: @@ -4330,16 +4356,28 @@ def is_number(s): for key in sorted(rp.keys()): - if key in debugging_outputs: + if key in debugging_outputs or \ + key in extra_functional_outputs: continue - if key in non_outputs: - continue + if 0 not in c.runRegisterFuncToMNI: + if key in outputs_native_nonsmooth or \ + key in outputs_native_nonsmooth_mult or \ + key in outputs_native_smooth: + continue + + if 0 not in c.runZScoring: + # write out only the z-scored outputs + if key in outputs_template_raw or \ + key in outputs_template_raw_mult: + continue if 0 not in c.run_smoothing: # write out only the smoothed outputs if key in outputs_native_nonsmooth or \ - key in outputs_template_nonsmooth: + key in outputs_template_nonsmooth or \ + key in outputs_native_nonsmooth_mult or \ + key in outputs_template_nonsmooth_mult: continue ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) diff --git a/CPAC/resources/configs/pipeline_config_template.yml b/CPAC/resources/configs/pipeline_config_template.yml index 5f76fcefd3..2f6c4dbd15 100644 --- a/CPAC/resources/configs/pipeline_config_template.yml +++ b/CPAC/resources/configs/pipeline_config_template.yml @@ -248,7 +248,8 @@ functionalMasking : ['3dAutoMask'] # Register functional images to a standard MNI152 template. -# This option must be enabled if you wish to calculate any derivatives. +# This option must be enabled if you wish to calculate any derivatives. If set to On [1], only the template-space files will be output. If set to On/Off [1,0], both template-space and native-space files will be output. +# Options: [1], [0], or [1,0] runRegisterFuncToMNI : [1] diff --git a/CPAC/utils/utils.py b/CPAC/utils/utils.py index 637f32c022..bbd237e239 100644 --- a/CPAC/utils/utils.py +++ b/CPAC/utils/utils.py @@ -119,9 +119,9 @@ 'right_hemisphere_surface': 'surface_registration', 'vertices_timeseries': 'timeseries', 'centrality_outputs': 'centrality', - 'centrality_outputs_smoothed': 'centrality', + 'centrality_outputs_smooth': 'centrality', 'centrality_outputs_zstd': 'centrality', - 'centrality_outputs_zstd_smoothed': 'centrality', + 'centrality_outputs_zstd_smooth': 'centrality', 'centrality_graphs': 'centrality', 'seg_probability_maps': 'anat', 'seg_mixeltype': 'anat', @@ -130,7 +130,9 @@ 'spatial_map_timeseries': 'timeseries', 'spatial_map_timeseries_for_DR': 'timeseries', 'dr_tempreg_maps_files': 'spatial_regression', + 'dr_tempreg_maps_files_smooth': 'spatial_regression', 'dr_tempreg_maps_zstat_files': 'spatial_regression', + 'dr_tempreg_maps_zstat_files_smooth': 'spatial_regression', 'dr_tempreg_maps_files_to_standard': 'spatial_regression', 'dr_tempreg_maps_zstat_files_to_standard': 'spatial_regression', 'dr_tempreg_maps_files_to_standard_smooth': 'spatial_regression', From 17d27a1b0943030775799923bf4fa4a43a8d0359 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Wed, 21 Mar 2018 20:21:45 -0400 Subject: [PATCH 18/75] Created a CSV matrix of CPAC outputs and how to handle them regarding post-processing and writing to the output directory, and threaded it into cpac_pipeline. --- CPAC/GUI/interface/pages/smoothing.py | 25 +++-- CPAC/pipeline/cpac_pipeline.py | 155 ++++++++------------------ CPAC/resources/cpac_outputs.csv | 1 + 3 files changed, 62 insertions(+), 119 deletions(-) create mode 100644 CPAC/resources/cpac_outputs.csv diff --git a/CPAC/GUI/interface/pages/smoothing.py b/CPAC/GUI/interface/pages/smoothing.py index 91a16db8e4..2f52788a71 100644 --- a/CPAC/GUI/interface/pages/smoothing.py +++ b/CPAC/GUI/interface/pages/smoothing.py @@ -33,10 +33,12 @@ def __init__(self, parent, counter = 0): control=control.CHOICE_BOX, name='run_smoothing', type=dtype.LSTR, - comment="Smooth the derivative outputs or not, or " - "produce both unsmoothed and smoothed " - "versions, if preferred.", - values=["On", "Off", "On/Off"]) + comment="Smooth the derivative outputs.\n\nOn - Run " + "smoothing and output only the smoothed " + "outputs.\nOn/Off - Run smoothing and output " + "both the smoothed and non-smoothed outputs.\n" + "Off - Don't run smoothing.", + values=["On", "On/Off", "Off"]) self.page.add(label="Smoothing Kernel FWHM (in mm) ", control=control.TEXT_BOX, @@ -53,15 +55,18 @@ def __init__(self, parent, counter = 0): "that all derivatives are output both " "smoothed and unsmoothed.") - self.page.add(label="Z-score Standardize Derivatives ", + self.page.add(label="z-score Standardize Derivatives ", control=control.CHOICE_BOX, name='runZScoring', type=dtype.LSTR, - comment="Decides format of outputs. Off will produce " - "non-z-scored outputs, On will produce " - "z-scores of outputs, and On/Off will " - "produce both.", - values=["On", "Off", "On/Off"]) + comment="z-score standardize the derivatives. This is " + "required for group-level analysis.\n\n" + "On - Run z-scoring and output only the " + "z-scored outputs.\nOn/Off - Run z-scoring and " + "output both the z-scored and raw score " + "versions of the outputs.\nOff - Don't run " + "z-scoring.", + values=["On", "On/Off", "Off"]) self.page.set_sizer() parent.get_page_list().append(self) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 882d25c712..f39e8b5602 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -12,6 +12,7 @@ import linecache import csv import pickle +import pandas as pd import pkg_resources as p # Nipype packages @@ -157,112 +158,48 @@ def prep_workflow(sub_dict, c, strategies, run, pipeline_timing_info=None, # Import packages from CPAC.utils.utils import check_config_resources, check_system_deps - # Settle some things - debugging_outputs = ['despiked_fieldmap', - 'fmap_magnitude', - 'fmap_phase_diff', - 'seg_mixeltype', - 'seg_partial_volume_files', - 'seg_partial_volume_map', - 'seg_probability_maps', - 'coordinate_transformation', - 'max_displacement', - 'power_params', - 'movement_parameters'] - - extra_functional_outputs = ['raw_functional', - 'mean_functional', - 'functional_nuisance_residuals', - 'functional_preprocessed_mask', - 'mean_functional_in_anat', - 'motion_correct_smooth', - 'motion_correct_to_standard', - 'motion_correct_to_standard_smooth', - 'preprocessed', - 'slice_time_corrected'] - - outputs_native_nonsmooth = ['alff', - 'falff', - 'reho'] - - outputs_native_nonsmooth_mult = ['dr_tempreg_maps_files', - 'dr_tempreg_maps_zstat_files', - 'sca_roi_correlation_files'] - - outputs_template_nonsmooth = ['alff_to_standard', - 'alff_to_standard_zstd', - 'falff_to_standard', - 'falff_to_standard_zstd', - 'reho_to_standard', - 'reho_to_standard_zstd', - 'vmhc_raw_score', - 'vmhc_fisher_zstd', - 'vmhc_fisher_zstd_zstat_map',] - - outputs_template_nonsmooth_mult = ['dr_tempreg_maps_files_to_standard', - 'dr_tempreg_maps_zstat_files_to_standard', - 'sca_roi_files_to_standard', - 'sca_roi_files_to_standard_fisher_zstd', - 'sca_tempreg_maps_files', - 'sca_tempreg_zstat_files', - 'centrality_outputs', - 'centrality_outputs_zstd'] - - outputs_native_smooth = ['alff_smooth', - 'falff_smooth', - 'reho_smooth', - 'dr_tempreg_maps_files_smooth', - 'dr_tempreg_maps_zstat_files_smooth', - 'sca_roi_files_smooth'] - - # outputs_template_smooth = ['alff_to_standard_smooth', - # 'alff_to_standard_zstd_smooth', - # 'falff_to_standard_smooth', - # 'falff_to_standard_zstd_smooth', - # 'reho_to_standard_smooth', - # 'reho_to_standard_zstd_smooth', - # 'dr_tempreg_maps_files_to_standard_smooth' - # 'dr_tempreg_maps_zstat_files_to_standard_smooth', - # 'sca_roi_files_to_standard_smooth', - # 'sca_roi_files_to_standard_smooth_fisher_zstd', - # 'sca_tempreg_maps_files_smooth', - # 'sca_tempreg_maps_zstat_files_smooth', - # 'centrality_outputs_smooth', - # 'centrality_outputs_zstd_smooth'] - - outputs_template_raw = ['alff_to_standard', - 'alff_to_standard_smooth', - 'falff_to_standard', - 'falff_to_standard_smooth', - 'reho_to_standard', - 'reho_to_standard_smooth'] - - outputs_template_raw_mult = ['centrality_outputs', - 'centrality_outputs_smooth'] - - outputs_average = ['alff', - 'alff_to_standard', - 'falff', - 'falff_to_standard', - 'reho_to_standard', - 'alff_smooth', - 'alff_to_standard_smooth', - 'falff_smooth', - 'falff_to_standard_smooth', - 'reho', - 'reho_smooth', - 'reho_to_standard_smooth'] - - outputs_average_mult = ['dr_tempreg_maps_files', - 'dr_tempreg_maps_files_to_standard', - 'sca_roi_correlation_files', - 'sca_roi_files_to_standard', - 'sca_tempreg_maps_files', - 'dr_tempreg_maps_files_smooth', - 'dr_tempreg_maps_files_to_standard_smooth' - 'sca_roi_files_smooth', - 'sca_roi_files_to_standard_smooth', - 'sca_tempreg_maps_files_smooth'] + # Settle some things about the resource pool keys and the output directory + keys_csv = p.resource_filename('CPAC', 'resources/cpac_outputs.csv') + + try: + keys = pd.read_csv(keys_csv) + except Exception as e: + err = "\n[!] Could not access or read the cpac_outputs.csv " \ + "resource file:\n{0}\n\nError details {1}\n".format(keys_csv, e) + raise Exception(err) + + # extra outputs that we don't write to the output directory, unless the + # user selects to do so + debugging_outputs = list(keys[keys['Optional outputs: Debugging outputs'] == 'yes']['Resource']) + + # outputs to write out if the user selects to write all the functional + # resources and files CPAC generates + extra_functional_outputs = list(keys[keys['Optional outputs: Extra functionals'] == 'yes']['Resource']) + + # outputs to send into smoothing, if smoothing is enabled, and + # outputs to write out if the user selects to write non-smoothed outputs + # "_mult" is for items requiring mapnodes + outputs_native_nonsmooth = list(keys[keys['Optional outputs: Native space'] == 'yes'][keys['Optional outputs: Non-smoothed'] == 'yes'][keys['Multiple outputs'] != 'yes']['Resource']) + outputs_native_nonsmooth_mult = list(keys[keys['Optional outputs: Native space'] == 'yes'][keys['Optional outputs: Non-smoothed'] == 'yes'][keys['Multiple outputs'] == 'yes']['Resource']) + outputs_template_nonsmooth = list(keys[keys['Space'] == 'template'][keys['Optional outputs: Non-smoothed'] == 'yes'][keys['Multiple outputs'] != 'yes']['Resource']) + outputs_template_nonsmooth_mult = list(keys[keys['Space'] == 'template'][keys['Optional outputs: Non-smoothed'] == 'yes'][keys['Multiple outputs'] == 'yes']['Resource']) + + # don't write these, unless the user selects to write native-space outputs + outputs_native_smooth = list(keys[keys['Space'] != 'template'][keys['Derivative'] == 'yes'][keys['Optional outputs: Non-smoothed'] != 'yes']['Resource']) + + # ever used??? contains template-space, smoothed, both raw and z-scored + outputs_template_smooth = list(keys[keys['Space'] == 'template'][keys['Derivative'] == 'yes'][keys['Optional outputs: Non-smoothed'] != 'yes']['Resource']) + + # outputs to send into z-scoring, if z-scoring is enabled, and + # outputs to write out if user selects to write non-z-scored outputs + # "_mult" is for items requiring mapnodes + outputs_template_raw = list(keys[keys['Space'] == 'template'][keys['Multiple outputs'] != 'yes'][keys['Optional outputs: Raw scores'] == 'yes']['Resource']) + outputs_template_raw_mult = list(keys[keys['Space'] == 'template'][keys['Multiple outputs'] == 'yes'][keys['Optional outputs: Raw scores'] == 'yes']['Resource']) + + # outputs to send into the average calculation nodes + # "_mult" is for items requiring mapnodes + outputs_average = list(keys[keys['Calculate averages'] == 'yes'][keys['Multiple outputs'] != 'yes']['Resource']) + outputs_average_mult = list(keys[keys['Calculate averages'] == 'yes'][keys['Multiple outputs'] == 'yes']['Resource']) # Start timing here pipeline_start_time = time.time() @@ -3366,7 +3303,7 @@ def ants_apply_warps_func_mni(input_node, input_outfile, strat.get_resource_pool(), '0032') raise - strat.update_resource_pool({'sca_roi_correlation_files': (sca_roi, 'outputspec.correlation_files')}) + strat.update_resource_pool({'sca_roi_files': (sca_roi, 'outputspec.correlation_files')}) create_log_node(sca_roi, 'outputspec.correlation_stack', num_strat) @@ -4400,8 +4337,8 @@ def is_number(s): node, out_file = rp[key] workflow.connect(node, out_file, ds, key) - logger.info('node, out_file, key: ' - '%s, %s, %s' % (node, out_file, key)) + #logger.info('node, out_file, key: ' + # '%s, %s, %s' % (node, out_file, key)) link_node = pe.Node(interface=util.Function( input_names=['in_file', 'strategies', diff --git a/CPAC/resources/cpac_outputs.csv b/CPAC/resources/cpac_outputs.csv new file mode 100644 index 0000000000..862c33d21d --- /dev/null +++ b/CPAC/resources/cpac_outputs.csv @@ -0,0 +1 @@ +Resource,Space,Values,Multiple outputs,Functional timeseries,Derivative,Warp to template,Calculate z-scores,Calculate averages,Optional outputs: Extra functionals,Optional outputs: Native space,Optional outputs: Raw scores,Optional outputs: Non-smoothed,Optional outputs: Debugging outputs alff,functional,raw,,,yes,yes,,yes,,yes,yes,yes, alff_smooth,functional,raw,,,yes,,,yes,,yes,yes,, alff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes, alff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,, alff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes, alff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,, anatomical_brain,anatomical,raw,,,,,,,,,,, anatomical_csf_mask,anatomical,binary,,,,,,,,,,, anatomical_gm_mask,anatomical,binary,,,,,,,,,,, anatomical_reorient,anatomical,raw,,,,,,,,,,, anatomical_to_mni_nonlinear_xfm,,transform,,,,,,,,,,, anatomical_to_symmetric_mni_nonlinear_xfm,,transform,,,,,,,,,,, anatomical_wm_mask,anatomical,binary,,,,,,,,,,, ants_affine_xfm,,transform,,,,,,,,,,, ants_initial_xfm,,transform,,,,,,,,,,, ants_rigid_xfm,,transform,,,,,,,,,,, ants_symmetric_affine_xfm,,transform,,,,,,,,,,, ants_symmetric_initial_xfm,,transform,,,,,,,,,,, ants_symmetric_rigid_xfm,,transform,,,,,,,,,,, centrality_outputs,template,raw,yes,,yes,,yes,,,,yes,yes, centrality_outputs_smooth,template,raw,yes,,yes,,,,,,yes,, centrality_outputs_zstd,template,z-score,yes,,yes,,,,,,,yes, centrality_outputs_zstd_smooth,template,z-score,yes,,yes,,,,,,,, coordinate_transformation,,,,,,,,,,,,,yes despiked_fieldmap,,,,,,,,,,,,,yes dr_tempreg_maps_files,functional,GLM betas,yes,,yes,yes,,yes,,yes,,yes, dr_tempreg_maps_files_smooth,functional,GLM betas,yes,,yes,,,yes,,yes,,, dr_tempreg_maps_files_to_standard,template,GLM betas,yes,,yes,,,yes,,,,yes, dr_tempreg_maps_files_to_standard_smooth,template,GLM betas,yes,,yes,,,yes,,,,, dr_tempreg_maps_zstat_files,functional,z-stat,yes,,yes,yes,,,,yes,,yes,yes dr_tempreg_maps_zstat_files_smooth,functional,z-stat,yes,,yes,,,,,yes,,,yes dr_tempreg_maps_zstat_files_to_standard,template,z-stat,yes,,yes,,,,,,,yes,yes dr_tempreg_maps_zstat_files_to_standard_smooth,template,z-stat,yes,,yes,,,,,,,,yes falff,functional,raw,,,yes,yes,,yes,,yes,yes,yes, falff_smooth,functional,raw,,,yes,,,yes,,yes,yes,, falff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes, falff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,, falff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes, falff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,, fmap_magnitude,,,,,,,,,,,,,yes fmap_phase_diff,,,,,,,,,,,,,yes frame_wise_displacement_jenkinson,,,,,,,,,,,,, frame_wise_displacement_power,,,,,,,,,,,,, functional_brain_mask,functional,binary,,,,,,,,,,, functional_brain_mask_to_standard,template,binary,,,,,,,,,,, functional_freq_filtered,functional,raw,,yes,,,,,,,,, functional_nuisance_regressors,,,,,,,,,,,,, functional_nuisance_residuals,functional,,,yes,,,,,yes,,,,yes functional_preprocessed_mask,functional,binary,,,,,,,yes,,,, functional_to_anat_linear_xfm,,transform,,,,,,,,,,, functional_to_standard,template,raw,,yes,,,,,,,,, max_displacement,,,,,,,,,,,,,yes mean_functional,functional,raw,,,,,,,yes,,,, mean_functional_in_anat,anatomical,raw,,,,,,,yes,,,, mean_functional_to_standard,template,raw,,,,,,,,,,, mni_normalized_anatomical,template,raw,,,,,,,,,,, mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,, motion_correct,functional,raw,,yes,,yes,,,,,,yes, motion_correct_smooth,functional,raw,,yes,,,,,yes,,,, motion_correct_to_standard,template,raw,,yes,,,,,yes,,,yes, motion_correct_to_standard_smooth,template,raw,,yes,,,,,yes,,,, motion_params,,,,,,,,,,,,, movement_parameters,,,,,,,,,,,,,yes output_means,,,,,,,,,,,,, power_params,,,,,,,,,,,,,yes preprocessed,functional,raw,,yes,,,,,yes,,,, raw_functional,functional,raw,,yes,,,,,yes,,,, reho,functional,raw,,,yes,yes,,yes,,yes,yes,yes, reho_smooth,functional,raw,,,yes,,,yes,,yes,yes,, reho_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes, reho_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,, reho_to_standard_zstd,template,z-score,,,yes,,,,,,,yes, reho_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,, roi_timeseries,,,,,,,,,,,,, roi_timeseries_for_SCA,,,,,,,,,,,,, roi_timeseries_for_SCA_multreg,,,,,,,,,,,,, sca_roi_files,functional,Pearson's r,yes,,yes,yes,,yes,,yes,,yes, sca_roi_files_smooth,functional,Pearson's r,yes,,yes,,,yes,,,,, sca_roi_files_to_standard,template,Pearson's r,yes,,yes,,yes,yes,,,,yes, sca_roi_files_to_standard_fisher_zstd,template,r-to-z,yes,,yes,,,,,,,yes, sca_roi_files_to_standard_smooth,template,Pearson's r,yes,,yes,,,yes,,,,, sca_roi_files_to_standard_fisher_zstd_smooth,template,r-to-z,yes,,yes,,,,,,,, sca_tempreg_maps_files,template,GLM betas,yes,,yes,,,yes,,,,yes, sca_tempreg_maps_files_smooth,template,GLM betas,yes,,yes,,,yes,,,,, sca_tempreg_maps_zstat_files,template,z-stat,yes,,yes,,,,,,,yes, sca_tempreg_maps_zstat_files_smooth,template,z-stat,yes,,yes,,,,,,,, seg_mixeltype,,,,,,,,,,,,,yes seg_partial_volume_files,???,,,,,,,,,,,,yes seg_partial_volume_map,???,,,,,,,,,,,,yes seg_probability_maps,???,,,,,,,,,,,,yes slice_time_corrected,functional,raw,,yes,,,,,yes,,,, spatial_map_timeseries,,,,,,,,,,,,, spatial_map_timeseries_for_DR,,,,,,,,,,,,, symmetric_mni_normalized_anatomical,template,raw,,,,,,,,,,, symmetric_mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,, vmhc_fisher_zstd,template,r-to-z,,,yes,,,,,,,,yes vmhc_fisher_zstd_zstat_map,template,z-stat,,,yes,,,,,,,, vmhc_raw_score,template,Pearson's r,,,yes,,yes,,,,yes,,yes \ No newline at end of file From c696fe1aa63e9f57fb2bbf36913afaf1b3f91779 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Thu, 22 Mar 2018 11:43:37 -0400 Subject: [PATCH 19/75] Added new stuff for getting text+html op From bd7afb6febae2209328d1549c80338999872ba60 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Thu, 22 Mar 2018 11:45:11 -0400 Subject: [PATCH 20/75] fixed html+text op for QA Outputs: text file of movement parameters, paths html files of QA outputs --- CPAC/utils/create_all_qc.py | 166 +++++++++++++++++++++++++----------- 1 file changed, 117 insertions(+), 49 deletions(-) diff --git a/CPAC/utils/create_all_qc.py b/CPAC/utils/create_all_qc.py index 3046a95ca6..4e214e26fa 100644 --- a/CPAC/utils/create_all_qc.py +++ b/CPAC/utils/create_all_qc.py @@ -1,8 +1,74 @@ import os + +def append_to_files_in_dict_way(list_files, file_): + + """ + Combine files so at each resource in file appears exactly once + + Parameters + ---------- + + list_files : list + + file_ : string + + Returns + ------- + + None + + Notes + ----- + + Writes contents of file_ into list_files, ensuring list_files finally has + each resource appearing exactly once + + """ + + f_1 = open(file_, 'r') + + lines = f_1.readlines() + + lines = [line.rstrip('\r\n') for line in lines] + + one_dict = {} + + for line in lines: + + if not line in one_dict: + one_dict[line] = 1 + + + + f_1.close() + + for f_ in list_files: + + two_dict = {} + f_2 = open(f_, 'r') + lines = f_2.readlines() + f_2.close() + f_2 = open(f_, 'w') + lines = [line.rstrip('\r\n') for line in lines] + for line in lines: + + if not line in one_dict: + two_dict[line] = 1 + + for key in one_dict: + if not key in two_dict: + two_dict[key] = 1 + + for key in two_dict: + + print >> f_2, key + + f_2.close + def first_pass_organizing_files(qc_path): - from CPAC.qc.utils import append_to_files_in_dict_way + from CPAC.utils.create_all_qc import append_to_files_in_dict_way qc_files = os.listdir(qc_path) strat_dict = {} @@ -146,6 +212,7 @@ def populate_htmls(gp_html, sub_html, subj, f_ = None + if not os.path.isfile(gp_html): @@ -169,13 +236,13 @@ def populate_htmls(gp_html, sub_html, subj, # print >> f_, " Include in Group Analysis" # print >> f_, " Comments" print >> f_, " " - + else: f_ = open(gp_html, 'a') print >> f_, " %s" % (sub_html, subj) - print >> f_, " %s" % (meanFD) + print >> f_, " %s" % (meanFD) #each cell in the table# print >> f_, " %s" % (meanDVARS) print >> f_, " %s" % (mean_rms) print >> f_, " %s" % (max_rms) @@ -189,6 +256,7 @@ def populate_htmls(gp_html, sub_html, subj, f_.close() + def prep_resources(pip_path): #for pipeline in sorted(pipelines): @@ -332,51 +400,51 @@ def get_resources_for_strategy(files_, resources, special_res, dict_): -# def organize_resources(output_path, pipelines): -# -# from CPAC.utils.create_all_qc import get_path_files_in_dict -# from CPAC.utils.create_all_qc import get_resources_for_strategy -# -# resources = ['/alff_Z_to_standard/', '/falff_Z_to_standard/', \ -# '/reho_Z_to_standard/', \ -# '/vmhc_z_score_stat_map/'] -# -# special_res = ['/sca_roi_Z_to_standard/', '/sca_seed_Z_to_standard/', \ -# '/dr_tempreg_maps_z_files/', '/sca_tempreg_maps_z_files/', \ -# '/centrality_outputs_zscore/'] -# -# -# resource_dict = {} -# for pipeline in sorted(pipelines): -# -# pip_path = os.path.join(output_path, pipeline) -# subjects = os.listdir(pip_path) -# subjects = [subj for subj in subjects if os.path.isdir(os.path.join(pip_path, subj))] -# -# for subj in sorted(subjects): -# -# subj_path = os.path.join(pip_path, subj) -# qc_path = os.path.join(subj_path, 'qc_files_here') -# html_files = os.listdir(qc_path) -# html_files = [html for html in html_files if not (html.endswith('_0.html') or html.endswith('_1.html') or html.endswith('.txt'))] -# dict_ = get_path_files_in_dict(qc_path, html_files) -# -# for strat, resource_files in dict_.items(): -# -# if not strat in resource_dict: -# -# per_strat_dict = {} -# per_strat_dict = get_resources_for_strategy(resource_files, resources, special_res, per_strat_dict) -# resource_dict[strat] = dict(per_strat_dict) -# -# else: -# -# per_strat_dict = dict(resource_dict[strat]) -# per_strat_dict = get_resources_for_strategy(resource_files, resources, special_res, per_strat_dict) -# resource_dict[strat] = dict(per_strat_dict) -# -# -# return resource_dict +def organize_resources(output_path, pipelines): + + from CPAC.utils.create_all_qc import get_path_files_in_dict + from CPAC.utils.create_all_qc import get_resources_for_strategy + + resources = ['/alff_Z_to_standard/', '/falff_Z_to_standard/', \ + '/reho_Z_to_standard/', \ + '/vmhc_z_score_stat_map/'] + + special_res = ['/sca_roi_Z_to_standard/', '/sca_seed_Z_to_standard/', \ + '/dr_tempreg_maps_z_files/', '/sca_tempreg_maps_z_files/', \ + '/centrality_outputs_zscore/'] + + + resource_dict = {} + for pipeline in sorted(pipelines): + + pip_path = os.path.join(output_path, pipeline) + subjects = os.listdir(pip_path) + subjects = [subj for subj in subjects if os.path.isdir(os.path.join(pip_path, subj))] + + for subj in sorted(subjects): + + subj_path = os.path.join(pip_path, subj) + qc_path = os.path.join(subj_path, 'qc_files_here') + html_files = os.listdir(qc_path) + html_files = [html for html in html_files if not (html.endswith('_0.html') or html.endswith('_1.html') or html.endswith('.txt'))] + dict_ = get_path_files_in_dict(qc_path, html_files) + + for strat, resource_files in dict_.items(): + + if not strat in resource_dict: + + per_strat_dict = {} + per_strat_dict = get_resources_for_strategy(resource_files, resources, special_res, per_strat_dict) + resource_dict[strat] = dict(per_strat_dict) + + else: + + per_strat_dict = dict(resource_dict[strat]) + per_strat_dict = get_resources_for_strategy(resource_files, resources, special_res, per_strat_dict) + resource_dict[strat] = dict(per_strat_dict) + + + return resource_dict def get_power_params(qc_path, file_): @@ -479,7 +547,7 @@ def make_group_htmls(pip_path): #pipelines = [pipeline for pipeline in pipelines if os.path.isdir(os.path.join(output_path, pipeline))] ### prep_resources(pip_path) -# organize_resources(output_path, pipelines) + organize_resources(output_path, pipelines) #for pipeline in sorted(pipelines): From 6c965e39d2ee8b88c83dad3513c091086ef6da79 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Mon, 26 Mar 2018 15:06:19 -0400 Subject: [PATCH 21/75] Fixed how Fisher r-to-z writes out its files into the working directory. --- CPAC/pipeline/cpac_pipeline.py | 54 ++++++++++++++++++++------------- CPAC/resources/cpac_outputs.csv | 2 +- CPAC/utils/utils.py | 50 +++--------------------------- 3 files changed, 38 insertions(+), 68 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index f39e8b5602..80668d7452 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -168,6 +168,13 @@ def prep_workflow(sub_dict, c, strategies, run, pipeline_timing_info=None, "resource file:\n{0}\n\nError details {1}\n".format(keys_csv, e) raise Exception(err) + # TODO: this way of pulling from the dataframe produces a warning + # TODO: also may want to put these into a function of some sort + + # outputs marked as optional in the matrix file, but we want them to be + # written out no matter what for a specific reason + override_optional = list(keys[keys['Override optional'] == 'yes']['Resource']) + # extra outputs that we don't write to the output directory, unless the # user selects to do so debugging_outputs = list(keys[keys['Optional outputs: Debugging outputs'] == 'yes']['Resource']) @@ -691,7 +698,7 @@ def getNodeList(strategy): ants_reg_anat_mni.inputs.inputspec. \ sampling_percentage = [0.25, 0.25, None] ants_reg_anat_mni.inputs.inputspec. \ - number_of_iterations = [[1000, 500, 250, 100], \ + number_of_iterations = [[1000, 500, 250, 100], [1000, 500, 250, 100], [100, 100, 70, 20]] ants_reg_anat_mni.inputs.inputspec. \ @@ -3982,6 +3989,9 @@ def calc_avg(output_name, strat, num_strat, map_node=False): # smoothing happens at the end, so only the non-smooth # named output labels for the native-space outputs strat = output_to_standard(key, strat, num_strat, c) + elif key in outputs_native_nonsmooth_mult: + strat = output_to_standard(key, strat, num_strat, c, + map_node=True) if 1 in c.runZScoring: rp = strat.get_resource_pool() @@ -4293,29 +4303,31 @@ def is_number(s): for key in sorted(rp.keys()): - if key in debugging_outputs or \ - key in extra_functional_outputs: - continue + if key not in override_optional: - if 0 not in c.runRegisterFuncToMNI: - if key in outputs_native_nonsmooth or \ - key in outputs_native_nonsmooth_mult or \ - key in outputs_native_smooth: + if key in debugging_outputs or \ + key in extra_functional_outputs: continue - if 0 not in c.runZScoring: - # write out only the z-scored outputs - if key in outputs_template_raw or \ - key in outputs_template_raw_mult: - continue - - if 0 not in c.run_smoothing: - # write out only the smoothed outputs - if key in outputs_native_nonsmooth or \ - key in outputs_template_nonsmooth or \ - key in outputs_native_nonsmooth_mult or \ - key in outputs_template_nonsmooth_mult: - continue + if 0 not in c.runRegisterFuncToMNI: + if key in outputs_native_nonsmooth or \ + key in outputs_native_nonsmooth_mult or \ + key in outputs_native_smooth: + continue + + if 0 not in c.runZScoring: + # write out only the z-scored outputs + if key in outputs_template_raw or \ + key in outputs_template_raw_mult: + continue + + if 0 not in c.run_smoothing: + # write out only the smoothed outputs + if key in outputs_native_nonsmooth or \ + key in outputs_template_nonsmooth or \ + key in outputs_native_nonsmooth_mult or \ + key in outputs_template_nonsmooth_mult: + continue ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) # Write QC outputs to log directory diff --git a/CPAC/resources/cpac_outputs.csv b/CPAC/resources/cpac_outputs.csv index 862c33d21d..c7a17ac73b 100644 --- a/CPAC/resources/cpac_outputs.csv +++ b/CPAC/resources/cpac_outputs.csv @@ -1 +1 @@ -Resource,Space,Values,Multiple outputs,Functional timeseries,Derivative,Warp to template,Calculate z-scores,Calculate averages,Optional outputs: Extra functionals,Optional outputs: Native space,Optional outputs: Raw scores,Optional outputs: Non-smoothed,Optional outputs: Debugging outputs alff,functional,raw,,,yes,yes,,yes,,yes,yes,yes, alff_smooth,functional,raw,,,yes,,,yes,,yes,yes,, alff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes, alff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,, alff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes, alff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,, anatomical_brain,anatomical,raw,,,,,,,,,,, anatomical_csf_mask,anatomical,binary,,,,,,,,,,, anatomical_gm_mask,anatomical,binary,,,,,,,,,,, anatomical_reorient,anatomical,raw,,,,,,,,,,, anatomical_to_mni_nonlinear_xfm,,transform,,,,,,,,,,, anatomical_to_symmetric_mni_nonlinear_xfm,,transform,,,,,,,,,,, anatomical_wm_mask,anatomical,binary,,,,,,,,,,, ants_affine_xfm,,transform,,,,,,,,,,, ants_initial_xfm,,transform,,,,,,,,,,, ants_rigid_xfm,,transform,,,,,,,,,,, ants_symmetric_affine_xfm,,transform,,,,,,,,,,, ants_symmetric_initial_xfm,,transform,,,,,,,,,,, ants_symmetric_rigid_xfm,,transform,,,,,,,,,,, centrality_outputs,template,raw,yes,,yes,,yes,,,,yes,yes, centrality_outputs_smooth,template,raw,yes,,yes,,,,,,yes,, centrality_outputs_zstd,template,z-score,yes,,yes,,,,,,,yes, centrality_outputs_zstd_smooth,template,z-score,yes,,yes,,,,,,,, coordinate_transformation,,,,,,,,,,,,,yes despiked_fieldmap,,,,,,,,,,,,,yes dr_tempreg_maps_files,functional,GLM betas,yes,,yes,yes,,yes,,yes,,yes, dr_tempreg_maps_files_smooth,functional,GLM betas,yes,,yes,,,yes,,yes,,, dr_tempreg_maps_files_to_standard,template,GLM betas,yes,,yes,,,yes,,,,yes, dr_tempreg_maps_files_to_standard_smooth,template,GLM betas,yes,,yes,,,yes,,,,, dr_tempreg_maps_zstat_files,functional,z-stat,yes,,yes,yes,,,,yes,,yes,yes dr_tempreg_maps_zstat_files_smooth,functional,z-stat,yes,,yes,,,,,yes,,,yes dr_tempreg_maps_zstat_files_to_standard,template,z-stat,yes,,yes,,,,,,,yes,yes dr_tempreg_maps_zstat_files_to_standard_smooth,template,z-stat,yes,,yes,,,,,,,,yes falff,functional,raw,,,yes,yes,,yes,,yes,yes,yes, falff_smooth,functional,raw,,,yes,,,yes,,yes,yes,, falff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes, falff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,, falff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes, falff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,, fmap_magnitude,,,,,,,,,,,,,yes fmap_phase_diff,,,,,,,,,,,,,yes frame_wise_displacement_jenkinson,,,,,,,,,,,,, frame_wise_displacement_power,,,,,,,,,,,,, functional_brain_mask,functional,binary,,,,,,,,,,, functional_brain_mask_to_standard,template,binary,,,,,,,,,,, functional_freq_filtered,functional,raw,,yes,,,,,,,,, functional_nuisance_regressors,,,,,,,,,,,,, functional_nuisance_residuals,functional,,,yes,,,,,yes,,,,yes functional_preprocessed_mask,functional,binary,,,,,,,yes,,,, functional_to_anat_linear_xfm,,transform,,,,,,,,,,, functional_to_standard,template,raw,,yes,,,,,,,,, max_displacement,,,,,,,,,,,,,yes mean_functional,functional,raw,,,,,,,yes,,,, mean_functional_in_anat,anatomical,raw,,,,,,,yes,,,, mean_functional_to_standard,template,raw,,,,,,,,,,, mni_normalized_anatomical,template,raw,,,,,,,,,,, mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,, motion_correct,functional,raw,,yes,,yes,,,,,,yes, motion_correct_smooth,functional,raw,,yes,,,,,yes,,,, motion_correct_to_standard,template,raw,,yes,,,,,yes,,,yes, motion_correct_to_standard_smooth,template,raw,,yes,,,,,yes,,,, motion_params,,,,,,,,,,,,, movement_parameters,,,,,,,,,,,,,yes output_means,,,,,,,,,,,,, power_params,,,,,,,,,,,,,yes preprocessed,functional,raw,,yes,,,,,yes,,,, raw_functional,functional,raw,,yes,,,,,yes,,,, reho,functional,raw,,,yes,yes,,yes,,yes,yes,yes, reho_smooth,functional,raw,,,yes,,,yes,,yes,yes,, reho_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes, reho_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,, reho_to_standard_zstd,template,z-score,,,yes,,,,,,,yes, reho_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,, roi_timeseries,,,,,,,,,,,,, roi_timeseries_for_SCA,,,,,,,,,,,,, roi_timeseries_for_SCA_multreg,,,,,,,,,,,,, sca_roi_files,functional,Pearson's r,yes,,yes,yes,,yes,,yes,,yes, sca_roi_files_smooth,functional,Pearson's r,yes,,yes,,,yes,,,,, sca_roi_files_to_standard,template,Pearson's r,yes,,yes,,yes,yes,,,,yes, sca_roi_files_to_standard_fisher_zstd,template,r-to-z,yes,,yes,,,,,,,yes, sca_roi_files_to_standard_smooth,template,Pearson's r,yes,,yes,,,yes,,,,, sca_roi_files_to_standard_fisher_zstd_smooth,template,r-to-z,yes,,yes,,,,,,,, sca_tempreg_maps_files,template,GLM betas,yes,,yes,,,yes,,,,yes, sca_tempreg_maps_files_smooth,template,GLM betas,yes,,yes,,,yes,,,,, sca_tempreg_maps_zstat_files,template,z-stat,yes,,yes,,,,,,,yes, sca_tempreg_maps_zstat_files_smooth,template,z-stat,yes,,yes,,,,,,,, seg_mixeltype,,,,,,,,,,,,,yes seg_partial_volume_files,???,,,,,,,,,,,,yes seg_partial_volume_map,???,,,,,,,,,,,,yes seg_probability_maps,???,,,,,,,,,,,,yes slice_time_corrected,functional,raw,,yes,,,,,yes,,,, spatial_map_timeseries,,,,,,,,,,,,, spatial_map_timeseries_for_DR,,,,,,,,,,,,, symmetric_mni_normalized_anatomical,template,raw,,,,,,,,,,, symmetric_mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,, vmhc_fisher_zstd,template,r-to-z,,,yes,,,,,,,,yes vmhc_fisher_zstd_zstat_map,template,z-stat,,,yes,,,,,,,, vmhc_raw_score,template,Pearson's r,,,yes,,yes,,,,yes,,yes \ No newline at end of file +Resource,Space,Values,Multiple outputs,Functional timeseries,Derivative,Warp to template,Calculate z-scores,Calculate averages,Optional outputs: Extra functionals,Optional outputs: Native space,Optional outputs: Raw scores,Optional outputs: Non-smoothed,Optional outputs: Debugging outputs,Override optional alff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, alff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, alff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, alff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, alff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, alff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, anatomical_brain,anatomical,raw,,,,,,,,,,,, anatomical_csf_mask,anatomical,binary,,,,,,,,,,,, anatomical_gm_mask,anatomical,binary,,,,,,,,,,,, anatomical_reorient,anatomical,raw,,,,,,,,,,,, anatomical_to_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_to_symmetric_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_wm_mask,anatomical,binary,,,,,,,,,,,, ants_affine_xfm,,transform,,,,,,,,,,,, ants_initial_xfm,,transform,,,,,,,,,,,, ants_rigid_xfm,,transform,,,,,,,,,,,, ants_symmetric_affine_xfm,,transform,,,,,,,,,,,, ants_symmetric_initial_xfm,,transform,,,,,,,,,,,, ants_symmetric_rigid_xfm,,transform,,,,,,,,,,,, centrality_outputs,template,raw,yes,,yes,,yes,,,,yes,yes,, centrality_outputs_smooth,template,raw,yes,,yes,,,,,,yes,,, centrality_outputs_zstd,template,z-score,yes,,yes,,,,,,,yes,, centrality_outputs_zstd_smooth,template,z-score,yes,,yes,,,,,,,,, coordinate_transformation,,,,,,,,,,,,,yes, despiked_fieldmap,,,,,,,,,,,,,yes, dr_tempreg_maps_files,functional,GLM betas,yes,,yes,yes,,yes,,yes,,yes,, dr_tempreg_maps_files_smooth,functional,GLM betas,yes,,yes,,,yes,,yes,,,, dr_tempreg_maps_files_to_standard,template,GLM betas,yes,,yes,,,yes,,,,yes,, dr_tempreg_maps_files_to_standard_smooth,template,GLM betas,yes,,yes,,,yes,,,,,, dr_tempreg_maps_zstat_files,functional,z-stat,yes,,yes,yes,,,,yes,,yes,yes, dr_tempreg_maps_zstat_files_smooth,functional,z-stat,yes,,yes,,,,,yes,,,yes, dr_tempreg_maps_zstat_files_to_standard,template,z-stat,yes,,yes,,,,,,,yes,yes, dr_tempreg_maps_zstat_files_to_standard_smooth,template,z-stat,yes,,yes,,,,,,,,yes, falff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, falff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, falff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, falff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, falff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, falff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, fmap_magnitude,,,,,,,,,,,,,yes, fmap_phase_diff,,,,,,,,,,,,,yes, frame_wise_displacement_jenkinson,,,,,,,,,,,,,, frame_wise_displacement_power,,,,,,,,,,,,,, functional_brain_mask,functional,binary,,,,,,,,,,,, functional_brain_mask_to_standard,template,binary,,,,,,,,,,,, functional_freq_filtered,functional,raw,,yes,,,,,,,,,, functional_nuisance_regressors,,,,,,,,,,,,,, functional_nuisance_residuals,functional,,,yes,,,,,yes,,,,yes, functional_preprocessed_mask,functional,binary,,,,,,,yes,,,,, functional_to_anat_linear_xfm,,transform,,,,,,,,,,,, functional_to_standard,template,raw,,yes,,,,,,,,,, max_displacement,,,,,,,,,,,,,yes, mean_functional,functional,raw,,,,,,,yes,,,,, mean_functional_in_anat,anatomical,raw,,,,,,,yes,,,,, mean_functional_to_standard,template,raw,,,,,,,,,,,, mni_normalized_anatomical,template,raw,,,,,,,,,,,, mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, motion_correct,functional,raw,,yes,,yes,,,,,,yes,, motion_correct_smooth,functional,raw,,yes,,,,,yes,,,,, motion_correct_to_standard,template,raw,,yes,,,,,yes,,,yes,, motion_correct_to_standard_smooth,template,raw,,yes,,,,,yes,,,,, motion_params,,,,,,,,,,,,,, movement_parameters,,,,,,,,,,,,,yes, output_means,,,,,,,,,,,,,, power_params,,,,,,,,,,,,,yes, preprocessed,functional,raw,,yes,,,,,yes,,,,, raw_functional,functional,raw,,yes,,,,,yes,,,,, reho,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, reho_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, reho_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, reho_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, reho_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, reho_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, roi_timeseries,,,,,,,,,,,,,, roi_timeseries_for_SCA,,,,,,,,,,,,,, roi_timeseries_for_SCA_multreg,,,,,,,,,,,,,, sca_roi_files,functional,Pearson's r,yes,,yes,yes,,yes,,yes,yes,yes,,yes sca_roi_files_smooth,functional,Pearson's r,yes,,yes,,,yes,,yes,yes,,,yes sca_roi_files_to_standard,template,Pearson's r,yes,,yes,,yes,yes,,,yes,yes,,yes sca_roi_files_to_standard_smooth,template,Pearson's r,yes,,yes,,,yes,,,yes,,,yes sca_roi_files_to_standard_fisher_zstd,template,r-to-z,yes,,yes,,,,,,,yes,,yes sca_roi_files_to_standard_fisher_zstd_smooth,template,r-to-z,yes,,yes,,,,,,,,,yes sca_tempreg_maps_files,template,GLM betas,yes,,yes,,,yes,,,,yes,,yes sca_tempreg_maps_files_smooth,template,GLM betas,yes,,yes,,,yes,,,,,,yes sca_tempreg_maps_zstat_files,template,z-stat,yes,,yes,,,,,,,yes,,yes sca_tempreg_maps_zstat_files_smooth,template,z-stat,yes,,yes,,,,,,,,,yes seg_mixeltype,,,,,,,,,,,,,yes, seg_partial_volume_files,anatomical,,,,,,,,,,,,yes, seg_partial_volume_map,anatomical,,,,,,,,,,,,yes, seg_probability_maps,anatomical,,,,,,,,,,,,yes, slice_time_corrected,functional,raw,,yes,,,,,yes,,,,, spatial_map_timeseries,,,,,,,,,,,,,, spatial_map_timeseries_for_DR,,,,,,,,,,,,,, symmetric_mni_normalized_anatomical,template,raw,,,,,,,,,,,, symmetric_mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, vmhc_fisher_zstd,template,r-to-z,,,yes,,,,,,,,yes, vmhc_fisher_zstd_zstat_map,template,z-stat,,,yes,,,,,,,,, vmhc_raw_score,template,Pearson's r,,,yes,,yes,,,,,,yes, \ No newline at end of file diff --git a/CPAC/utils/utils.py b/CPAC/utils/utils.py index bbd237e239..ca83331e05 100644 --- a/CPAC/utils/utils.py +++ b/CPAC/utils/utils.py @@ -108,7 +108,7 @@ 'roi_timeseries': 'timeseries', 'roi_timeseries_for_SCA': 'timeseries', 'roi_timeseries_for_SCA_multreg': 'timeseries', - 'sca_roi_correlation_files': 'sca_roi', + 'sca_roi_files': 'sca_roi', 'sca_roi_files_smooth': 'sca_roi', 'sca_roi_files_to_standard': 'sca_roi', 'sca_roi_files_to_standard_smooth': 'sca_roi', @@ -350,11 +350,6 @@ def compute_fisher_z_score(correlation_file, timeseries_one_d, input_name): if '.1D' in timeseries or '.csv' in timeseries: timeseries_file = timeseries - roi_numbers = [] - - with open(timeseries_file, "r") as f: - roi_list = f.read().splitlines()[0].replace("#", "").split("\t") - # get the specific roi number filename = correlation_file.split("/")[-1] filename = filename.replace(".nii", "") @@ -369,49 +364,12 @@ def compute_fisher_z_score(correlation_file, timeseries_one_d, input_name): # calculate the Fisher r-to-z transformation corr_data = np.log((1 + corr_data) / (1 - corr_data)) / 2.0 - dims = corr_data.shape - - out_file = [] - - # dims = tuple of dimensions of correlation NIFTI file - # roi_numbers = list of label numbers for each ROI; length of this will be - # how many ROIs you have - - # I think the point of this check is to see if there are multiple volumes - # in the correlation file (i.e. is a stack), or is a file with ROIs, and - # if so, to deal with it appropriately - if len(dims) == 5 or len(roi_numbers) > 0: - - if len(dims) == 5: - x, y, z, one, roi_number = dims - - corr_data = np.reshape(corr_data, (x * y * z, roi_number), - order='F') - - sub_data = corr_data - - sub_img = nb.Nifti1Image(sub_data, header=corr_img.get_header(), + z_score_img = nb.Nifti1Image(corr_data, header=hdr, affine=corr_img.get_affine()) - sub_z_score_file = os.path.join(os.getcwd(), - (filename + '_fisher_zstd.nii.gz')) - - sub_img.to_filename(sub_z_score_file) - - out_file.append(sub_z_score_file) - - # if the correlation file is a single volume image - else: - - z_score_img = nb.Nifti1Image(corr_data, header=hdr, - affine=corr_img.get_affine()) - - z_score_file = os.path.join(os.getcwd(), - filename + '_fisher_zstd.nii.gz') - - z_score_img.to_filename(z_score_file) + out_file = os.path.join(os.getcwd(), filename + '_fisher_zstd.nii.gz') - out_file.append(z_score_file) + z_score_img.to_filename(out_file) return out_file From 64be583e09810afb6d408fae44db079c249463e1 Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Thu, 29 Mar 2018 14:36:14 -0400 Subject: [PATCH 22/75] pin wxpython, prov and nypipe versions --- scripts/cpac_install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/cpac_install.sh b/scripts/cpac_install.sh index 51b85b4c23..3c51910664 100755 --- a/scripts/cpac_install.sh +++ b/scripts/cpac_install.sh @@ -78,9 +78,9 @@ ubuntu1604_packages=("libmotif-dev" "xutils-dev" "libtool" "libx11-dev" "x11prot # configuration options that are specific to Ubuntu 16.10 ubuntu1610_packages=("libmotif-dev" "xutils-dev" "libtool" "libx11-dev" "x11proto-xext-dev" "x11proto-print-dev" "dh-autoreconf" "libxext-dev" "libgsl-dev") -conda_packages=("pandas" "cython" "numpy==1.11" "scipy" "matplotlib" "networkx==1.11" "traits" "pyyaml" "jinja2==2.7.2" "nose" "ipython" "pip" "wxpython") +conda_packages=("pandas" "cython" "numpy==1.11" "scipy" "matplotlib" "networkx==1.11" "traits" "pyyaml" "jinja2==2.7.2" "nose" "ipython" "pip" "wxpython==3.0.0.0") -pip_packages=("future==0.15.2" "prov" "simplejson" "lockfile" "pygraphviz" "nibabel" "nipype" "patsy" "memory_profiler" "psutil" "configparser" "INDI-Tools" "fs==0.5.4" "boto3") +pip_packages=("future==0.15.2" "prov==1.5.0" "simplejson" "lockfile" "pygraphviz" "nibabel" "nipype==0.13.1" "patsy" "memory_profiler" "psutil" "configparser" "INDI-Tools" "fs==0.5.4" "boto3") ##### Helper functions for installing system dependencies. From 8b5a8b57464d9c676de657b9c56f12cfc9bfca58 Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Thu, 29 Mar 2018 14:37:15 -0400 Subject: [PATCH 23/75] retrieve ants from apt when on ubuntu 16.04 --- scripts/cpac_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cpac_install.sh b/scripts/cpac_install.sh index 3c51910664..b5681f832e 100755 --- a/scripts/cpac_install.sh +++ b/scripts/cpac_install.sh @@ -788,7 +788,7 @@ function install_ants { apt-get -y install ants ;; 16.04) - compile_ants + apt-get -y install ants ;; 16.10) apt-get -y install ants From bd5bf1f3da0491a4d4657298e4a1000030547287 Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Thu, 29 Mar 2018 15:54:56 -0400 Subject: [PATCH 24/75] Update to new drawing api --- CPAC/pipeline/cpac_pipeline.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 80d6d04c29..1332bd9feb 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -5723,12 +5723,14 @@ def is_number(s): os.makedirs(d_name) try: + from networkx.drawing.nx_pydot import write_dot + G = nx.DiGraph() strat_name = strat.get_name() G.add_edges_from([(strat_name[s], strat_name[s + 1]) for s in range(len(strat_name) - 1)]) dotfilename = os.path.join(d_name, 'strategy.dot') - nx.write_dot(G, dotfilename) + write_dot(G, dotfilename) format_dot(dotfilename, 'png') except: logStandardWarning('Datasink', From 832d4fdefa22eaf4afa1715d9f07525ee924c68e Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Thu, 29 Mar 2018 16:43:36 -0400 Subject: [PATCH 25/75] avoid reimport --- CPAC/pipeline/cpac_pipeline.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 1332bd9feb..11b7b2cd45 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -5723,14 +5723,12 @@ def is_number(s): os.makedirs(d_name) try: - from networkx.drawing.nx_pydot import write_dot - G = nx.DiGraph() strat_name = strat.get_name() G.add_edges_from([(strat_name[s], strat_name[s + 1]) for s in range(len(strat_name) - 1)]) dotfilename = os.path.join(d_name, 'strategy.dot') - write_dot(G, dotfilename) + nx.drawing.nx_pydot.write_dot(G, dotfilename) format_dot(dotfilename, 'png') except: logStandardWarning('Datasink', From 3e7d3d7fd3f1bc4b3e4fdb275aaad392a1e52bb1 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Thu, 29 Mar 2018 22:24:44 -0400 Subject: [PATCH 26/75] added HTML pages for QA fixed another reho error fixed float32 error --- CPAC/pipeline/cpac_pipeline.py | 40 +++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 8fbeda1117..6ed466aeb3 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -5216,7 +5216,7 @@ def QA_montages(measure, idx): drop_percent = pe.MapNode(util.Function(input_names=['measure_file', 'percent_'], - output_names=['modified_measure_file'], function=drop_percent_), + output_names=['modified_measure_file'], function=drop_percent_), name='dp_%s_%d' % (measure, num_strat), iterfield=['measure_file']) drop_percent.inputs.percent_ = 99.999 @@ -5646,9 +5646,16 @@ def is_number(s): ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) # Write QC outputs to log directory if 'qc' in key.lower(): - ds.inputs.base_directory = c.outputDirectory - else: ds.inputs.base_directory = c.logDirectory + else: + ds.inputs.base_directory = c.outputDirectory + # For each pipeline ID, generate the QC pages + # for pip_id in pip_ids: + # Define pipeline-level logging for QC + # pipeline_out_base = os.path.join(c.logDirectory, 'pipeline_%s' % pip_id) + #qc_output_folder = os.path.join(pipeline_out_base, subject_id, 'qc_files_here') + #For each subject, create a QC index.html page + #make_QC_html_pages(qc_output_folder) ds.inputs.creds_path = creds_path ds.inputs.encrypt_bucket_keys = encrypt_data ds.inputs.container = os.path.join( @@ -5840,21 +5847,28 @@ def is_number(s): for count, scanID in enumerate(pip_ids): for scan in scan_ids: create_log_node(None, None, count, scan).run() - + print i # If QC is enabled # TODO - QA pages: re-introduce - if 1 in c.generateQualityControlImages: - # For each pipeline ID, generate the QC pages + print "NECTAR" for pip_id in pip_ids: - # Define pipeline-level logging for QC - pipeline_out_base = os.path.join(c.logDirectory, 'pipeline_%s' % pip_id) - qc_output_folder = os.path.join(pipeline_out_base, subject_id, 'qc_files_here') - # Generate the QC pages - generateQCPages(qc_output_folder, qc_montage_id_a, - qc_montage_id_s, qc_plot_id, qc_hist_id) + + f_path = os.path.join(os.path.join(c.logDirectory, 'pipeline_%s' %pip_id)) + + qc_output_folder = os.path.join(f_path, 'qc_files_here') + + generateQCPages(qc_output_folder) + create_all_qc.run(f_path) + + # Generate the QC pages -- this function isn't even running, because there is noparameter for qc_montage_id_a/qc_montage_id_s/qc_plot_id,qc_hist_id + #two methods can be done here: + #i) group all the qc_montage_ids in the resource pool or + #add a loop for all the files in the qc output folder, generate the html pages using the same functions, but with different parameters + # generateQCPages(qc_output_folder, qc_montage_id_a, + # qc_montage_id_s, qc_plot_id, qc_hist_id) # Automatically generate QC index page - create_all_qc.run(pipeline_out_base) + #create_all_qc.run(pipeline_out_base) # pipeline timing code starts here From 09c91f95c147a1c7aa754b235a9c0b4783c1bc39 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Thu, 29 Mar 2018 22:25:47 -0400 Subject: [PATCH 27/75] QA HTML pages added --- CPAC/qc/utils.py | 149 +++++++++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 71 deletions(-) diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index efc608b102..d18665d701 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -157,7 +157,6 @@ def first_pass_organizing_files(qc_path): strat_dict[str_] = [file_] - def second_pass_organizing_files(qc_path): @@ -290,8 +289,8 @@ def organize(dict_, all_ids, png_, new_dict): """ for id_no, png_type in dict_.items(): - # Check if folder name is png type - if png_type + '/' in png_: + + if png_type in png_: if not id_no in new_dict.keys(): new_dict[id_no] = [png_] else: @@ -355,25 +354,23 @@ def grp_pngs_by_id(pngs_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_ list of png id nos """ - # Import packages from CPAC.qc.utils import organize - # Init variables dict_a = {} dict_s = {} dict_hist = {} dict_plot = {} - all_ids = [] - # For each png, organize the ids into respective dictoinaries + all_ids = [] for png_ in pngs_: + + all_ids = organize(qc_montage_id_a, all_ids, png_, dict_a) all_ids = organize(qc_montage_id_s, all_ids, png_, dict_s) all_ids = organize(qc_plot_id, all_ids, png_, dict_plot) all_ids = organize(qc_hist_id, all_ids, png_, dict_hist) - return dict(dict_a), dict(dict_s), dict(dict_hist),\ - dict(dict_plot), list(all_ids) + return dict(dict_a), dict(dict_s), dict(dict_hist), dict(dict_plot), list(all_ids) def add_head(f_html_, f_html_0, f_html_1): @@ -433,7 +430,6 @@ def add_head(f_html_, f_html_0, f_html_1): print >>f_html_1, "" - def add_tail(f_html_, f_html_0, f_html_1): @@ -490,7 +486,6 @@ def add_tail(f_html_, f_html_0, f_html_1): print >>f_html_1, "" - def feed_line_nav(id_, image_name, anchor, @@ -761,15 +756,7 @@ def get_map_and_measure(png_a): if 'centrality' in png_a: - try: - map_name = get_map_id(str_, 'centrality_') - except: - pass - - try: - map_name = get_map_id(str_, 'lFCD_') - except: - pass + map_name = get_map_id(str_, 'centrality_') return map_name, measure_name @@ -842,6 +829,7 @@ def feed_lines_html(id_, from CPAC.qc.utils import feed_line_body from CPAC.qc.utils import get_map_and_measure + #print 'id_ :', id_ if id_ in dict_a: dict_a[id_] = sorted(dict_a[id_]) @@ -891,8 +879,10 @@ def feed_lines_html(id_, id_a = '_'.join([id_a, str(idx), 'a']) id_s = '_'.join([id_s, str(idx), 's']) id_h = '_'.join([id_h, str(idx), 'h' ]) + if idx == 0: + # add general user readable link names for QC navigation bar if image_name_a_nav == 'skullstrip_vis': image_readable = 'Visual Result of Skull Strip' @@ -1034,7 +1024,7 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): feed_lines_html - + f_ = open(file_, 'r') pngs_ = [line.rstrip('\r\n') for line in f_.readlines()] f_.close() @@ -1047,47 +1037,39 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): html_f_name_1 = html_f_name + '_1.html' html_f_name = html_f_name + '.html' - - try: + f_html_ = open(html_f_name, 'wb') + f_html_0 = open(html_f_name_0, 'wb') + f_html_1 = open(html_f_name_1, 'wb') - f_html_ = open(html_f_name, 'wb') - f_html_0 = open(html_f_name_0, 'wb') - f_html_1 = open(html_f_name_1, 'wb') + dict_a, dict_s, dict_hist, dict_plot, all_ids = grp_pngs_by_id(pngs_, qc_montage_id_a, \ + qc_montage_id_s, qc_plot_id, qc_hist_id) - dict_a, dict_s, dict_hist, dict_plot, all_ids = grp_pngs_by_id(pngs_, qc_montage_id_a, \ - qc_montage_id_s, qc_plot_id, qc_hist_id) + #for k, v in dict_plot.items(): + # print '_a~~~> ', k, v - #for k, v in dict_plot.items(): - # print '_a~~~> ', k, v + add_head(f_html_, f_html_0, f_html_1) - add_head(f_html_, f_html_0, f_html_1) + for id_ in sorted(all_ids): + feed_lines_html(id_, + dict_a, + dict_s, + dict_hist, + dict_plot, + qc_montage_id_a, + qc_montage_id_s, + qc_plot_id, + qc_hist_id, + f_html_0, + f_html_1) - for id_ in sorted(all_ids): - print 'id: ', id_ - feed_lines_html(id_, - dict_a, - dict_s, - dict_hist, - dict_plot, - qc_montage_id_a, - qc_montage_id_s, - qc_plot_id, - qc_hist_id, - f_html_0, - f_html_1) - - add_tail(f_html_, f_html_0, f_html_1) - f_html_.close() - f_html_0.close() - f_html_1.close() + add_tail(f_html_, f_html_0, f_html_1) - except Exception as exc: - print '\n\nWarning: QC HTML pages could not be generated. Error: %s' \ - % exc - raise Exception + f_html_.close() + f_html_0.close() + f_html_1.close() def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): @@ -1127,6 +1109,8 @@ def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist """ import os from CPAC.qc.utils import make_page + + qc_files = os.listdir(qc_path) @@ -1140,7 +1124,7 @@ def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist -def generateQCPages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): +def generateQCPages(qc_path): """ parafile = open('QC_input_para.txt', 'w') @@ -1208,8 +1192,37 @@ def generateQCPages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hi second_pass_organizing_files(qc_path) #generate pages from qc files - make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) - + text_file_list = os.listdir(qc_path) + print text_file_list + image_path_list = [] + for text_file in text_file_list: + if text_file[0] != '.': + f = open(os.path.join(path, text_file),'r') + eof = 0 + while (not eof): + temp_line = f.readline() + if temp_line == '': + eof=1 #This is to check if reading the file is complete + else: + if temp_line[-2:] == "\n": + temp_line = temp_line[:-1] + image_path_list.append(temp_line) + f.close() + f = open(os.path.join(qc_output_folder, "QC_index.html"),'w') + header = """ + QA Images +

""" + footer = """

+ """ + + f.write(header) + for image in image_path_list: + image_html = "" + f.write(image_html) + + f.write(footer) + f.close() + print f def make_edge(file_): @@ -1368,6 +1381,9 @@ def cal_snr_val(measure_file): return avg_snr_file + + + def gen_std_dev(mask_, func_): """ @@ -1640,28 +1656,18 @@ def gen_histogram(measure_file, measure): if 'centrality' in measure.lower(): fname = os.path.basename(os.path.splitext(os.path.splitext(file_)[0])[0]) - - if 'centrality_' in fname: - type_, fname = fname.split('centrality_') - fname = type_ + 'centrality_' + fname.split('_')[0] - measure = fname - elif 'lfcd_' in fname.lower(): - fname = 'lFCD_' + fname.split('_')[1] - measure = fname + type_, fname = fname.split('centrality_') + fname = type_ + 'centrality_' + fname.split('_')[0] + measure = fname hist_path.append(make_histogram(file_, measure)) else: - - print "measure_file: ", measure_file - print "measure: ", measure - hist_path = make_histogram(measure_file, measure) return hist_path - def make_histogram(measure_file, measure): """ @@ -1931,7 +1937,7 @@ def montage_axial(overlay, underlay, png_name, cbar_name): """ Draws Montage using overlay on Anatomical brain in Axial Direction - calls make_montage_axial + calls make_montage_axial Parameters ---------- @@ -2100,7 +2106,7 @@ def montage_sagittal(overlay, underlay, png_name, cbar_name): """ Draws Montage using overlay on Anatomical brain in Sagittal Direction - calls make_montage_sagittal + calls make_montage_sagittal Parameters ---------- @@ -2589,3 +2595,4 @@ def make_resample_1mm(file_): commands.getoutput(cmd) return new_fname + From 639ea82222ee133939cdccbf16fc31373fb2d691 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 30 Mar 2018 17:48:56 -0400 Subject: [PATCH 28/75] Fixed nuisance regression forking- can now run nuisance on/off, and despiking, and scrubbing, all simultaneously but each in a separate strategy. --- CPAC/GUI/interface/pages/functional_tab.py | 4 +- CPAC/GUI/interface/pages/nuisance.py | 15 +- CPAC/GUI/interface/pages/settings.py | 32 +- CPAC/GUI/interface/utils/constants.py | 1 - CPAC/GUI/interface/windows/config_window.py | 21 +- CPAC/GUI/interface/windows/main_window.py | 2 +- CPAC/GUI/resources/config_parameters.txt | 2 + CPAC/pipeline/cpac_pipeline.py | 327 ++++++++++++++---- .../configs/pipeline_config_template.yml | 8 + CPAC/resources/cpac_outputs.csv | 2 +- CPAC/sca/sca.py | 4 +- CPAC/utils/utils.py | 22 +- CPAC/vmhc/vmhc.py | 4 +- 13 files changed, 340 insertions(+), 104 deletions(-) diff --git a/CPAC/GUI/interface/pages/functional_tab.py b/CPAC/GUI/interface/pages/functional_tab.py index 96aea4838d..fe062de351 100644 --- a/CPAC/GUI/interface/pages/functional_tab.py +++ b/CPAC/GUI/interface/pages/functional_tab.py @@ -96,14 +96,14 @@ def __init__(self, parent, counter =0): control=control.TEXT_BOX, name='stopIdx', type=dtype.NUM, - values="End", + values="None", validator=CharValidator("no-alpha"), comment="Last timepoint to include in analysis.\n\n" "Default is None or End (end of timeseries).\n" "\nLast timepoint selection in the scan " "parameters in the data configuration file, if " "present, will over-ride this selection.\n\n" - "NOte: the selection here applies to all scans " + "Note: the selection here applies to all scans " "of all participants.") self.page.set_sizer() diff --git a/CPAC/GUI/interface/pages/nuisance.py b/CPAC/GUI/interface/pages/nuisance.py index bc66615950..36bf4b6020 100644 --- a/CPAC/GUI/interface/pages/nuisance.py +++ b/CPAC/GUI/interface/pages/nuisance.py @@ -40,7 +40,7 @@ def __init__(self, parent, counter = 0): name='runNuisance', type=dtype.LSTR, comment="Run Nuisance Signal Regression", - values=["Off","On","On/Off"], + values=["Off", "On", "On/Off"], wkf_switch = True) self.page.add(label="Lateral Ventricles Mask (Standard Space) ", @@ -89,12 +89,17 @@ def __init__(self, parent, counter = 0): values=["On", "Off", "On/Off"]) self.page.add(label="Motion Spike De-Noising ", - control=control.CHOICE_BOX, + control=control.CHECKLIST_BOX, name='runMotionSpike', type=dtype.LSTR, comment="Remove or regress out volumes exhibiting " - "excessive motion.", - values=["Off", "De-Spiking", "Scrubbing"], + "excessive motion.\n\nEach of these options " + "are mutually exclusive, and selecting more " + "than one will create a new pipeline fork " + "for each option. For example, de-spiking and " + "scrubbing will not run within the same " + "pipeline strategy.", + values=["None", "De-Spiking", "Scrubbing"], wkf_switch=True) self.page.add(label="Framewise Displacement (FD) Calculation ", @@ -111,7 +116,7 @@ def __init__(self, parent, counter = 0): control=control.TEXT_BOX, name='spikeThreshold', type=dtype.LNUM, - values="5%", + values="0.2", validator=CharValidator("no-alpha"), comment="(Motion Spike De-Noising only) Specify the " "maximum acceptable Framewise Displacement " diff --git a/CPAC/GUI/interface/pages/settings.py b/CPAC/GUI/interface/pages/settings.py index 8e6811f86f..53ed1f2b44 100644 --- a/CPAC/GUI/interface/pages/settings.py +++ b/CPAC/GUI/interface/pages/settings.py @@ -216,12 +216,30 @@ def __init__(self, parent, counter=0): "to the S3 bucket", values=["On", "Off"]) + self.page.add(label="Write Extra Functional Outputs ", + control=control.CHOICE_BOX, + name='write_func_outputs', + type=dtype.LSTR, + comment="Include extra versions and intermediate steps " + "of functional preprocessing in the output " + "directory.", + values=["Off", "On"]) + + self.page.add(label="Write Debugging Outputs ", + control=control.CHOICE_BOX, + name='write_debugging_outputs', + type=dtype.LSTR, + comment="Include extra outputs in the output " + "directory that may be of interest when more " + "information is needed.", + values=["Off", "On"]) + self.page.add(label="Create Symbolic Links ", control=control.CHOICE_BOX, name='runSymbolicLinks', type=dtype.LSTR, - comment="Create a user-friendly, well organized version of the output directory.\n\n" - "We recommend all users enable this option.", + comment="Create a user-friendly, well organized " + "version of the output directory.", values=["On", "Off"]) #self.page.add(label="Enable Quality Control Interface ", @@ -236,7 +254,10 @@ def __init__(self, parent, counter=0): name='removeWorkingDir', type=dtype.BOOL, values=["False", "True"], - comment="Deletes the contents of the Working Directory after running.\n\nThis saves disk space, but any additional preprocessing or analysis will have to be completely re-run.") + comment="Deletes the contents of the Working " + "Directory after running.\n\nThis saves disk " + "space, but any additional preprocessing or " + "analysis will have to be completely re-run.") self.page.add(label="Run Logging ", control=control.CHOICE_BOX, @@ -251,7 +272,10 @@ def __init__(self, parent, counter=0): name='reGenerateOutputs', type=dtype.BOOL, values=["False", "True"], - comment="Uses the contents of the Working Directory to regenerate all outputs and their symbolic links.\n\nRequires an intact Working Directory from a previous CPAC run.") + comment="Uses the contents of the Working Directory " + "to regenerate all outputs and their " + "symbolic links.\n\nRequires an intact " + "Working Directory from a previous CPAC run.") self.page.set_sizer() parent.get_page_list().append(self) diff --git a/CPAC/GUI/interface/utils/constants.py b/CPAC/GUI/interface/utils/constants.py index 4ae1cfa415..913ec340f4 100644 --- a/CPAC/GUI/interface/utils/constants.py +++ b/CPAC/GUI/interface/utils/constants.py @@ -45,7 +45,6 @@ def enum(**enums): 'Network Centrality (smoothed)': 'centrality_outputs_smoothed_zstd', 'Dual Regression':'dr_tempreg_maps_zstat_files_to_standard', 'Dual Regression (smoothed)':'dr_tempreg_maps_zstat_files_to_standard_smooth', - 'End': 'None', 'ROI Average Time Series Extraction': 'roi_average', 'ROI Voxelwise Time Series Extraction': 'roi_voxelwise', } diff --git a/CPAC/GUI/interface/windows/config_window.py b/CPAC/GUI/interface/windows/config_window.py index c2df890162..24292ab209 100644 --- a/CPAC/GUI/interface/windows/config_window.py +++ b/CPAC/GUI/interface/windows/config_window.py @@ -302,8 +302,7 @@ def load(self): for item in val if s_map.get(item) != None] if not value: value = [ str(item) for item in val] - - + elif ctrl.get_datatype() == 5 and \ ctrl.get_type() == 6: value = [sample_list[v] for v in val] @@ -695,11 +694,9 @@ def display(win, msg, changeBg=True): raise Exception paramInfo = params_file.read().split('\n') - paramList = [] for param in paramInfo: - if param != '': paramList.append(param.split(',')) @@ -710,7 +707,6 @@ def testFile(filepath, paramName, switch): fileTest = open(filepath) fileTest.close() except: - testDlg1.Destroy() for param in paramList: @@ -795,7 +791,6 @@ def testFile(filepath, paramName, switch): prep_workflow(sublist[0], c, strategies, 0) except Exception as xxx: - print xxx print "an exception occurred" @@ -813,7 +808,6 @@ def testFile(filepath, paramName, switch): errDlg1.Destroy() else: - testDlg1.Destroy() okDlg1 = wx.MessageDialog( @@ -1071,7 +1065,12 @@ def write(self, path, config_list): if substitution_map.get(value) != None: value = substitution_map.get(value) elif value != 'None': - value = ast.literal_eval(str(value)) + try: + value = ast.literal_eval(str(value)) + except Exception as e: + raise Exception("could not parse value: " + "{0}\n\n{1}" + "\n".format(str(value), e)) print >>f, label, ": ", value print >>f,"\n" @@ -1116,12 +1115,13 @@ def write(self, path, config_list): values = ['3dAutoMask','BET'] print>>f, label, ": ", values - print>>f,"\n" + print>>f, "\n" # parameters that are bracketed numbers (int or float) elif dtype == 5: - ### parse user input ### can't use internal function type() here??? + ### parse user input ### can't use internal function + # type() here??? if value.find(',') != -1: lvalue = value.split(',') elif value.find(';') != -1: @@ -1143,7 +1143,6 @@ def write(self, path, config_list): lvalue = new_lvalue else: lvalue = 0 - print>>f, label, ":", lvalue ### print>>f, "\n" diff --git a/CPAC/GUI/interface/windows/main_window.py b/CPAC/GUI/interface/windows/main_window.py index 1bc253e75c..953574aca2 100644 --- a/CPAC/GUI/interface/windows/main_window.py +++ b/CPAC/GUI/interface/windows/main_window.py @@ -624,7 +624,7 @@ def display(win, msg, changeBg=True): # If any missing parameters, notify user if missingParams: message = 'The following parameters are missing from your pipeline configuration file:\n\n' - print missingParams + for param in missingParams: message = message + "\"" + str(param[1]) + "\"" + "\n" + "which can be found in tab:" + "\n" + "\"" + str(param[2]) + "\"\n\n" diff --git a/CPAC/GUI/resources/config_parameters.txt b/CPAC/GUI/resources/config_parameters.txt index e596926673..93338abde2 100644 --- a/CPAC/GUI/resources/config_parameters.txt +++ b/CPAC/GUI/resources/config_parameters.txt @@ -14,6 +14,8 @@ logDirectory,Log Directory,Output Settings outputDirectory,Output Directory,Output Settings awsOutputBucketCredentials,AWS Output Bucket Credentials,Output Settings s3Encryption,S3 Encryption,Output Settings +write_func_outputs,Write Extra Functional Outputs,Output Settings +write_debugging_outputs,Write Debugging Outputs,Output Settings runSymbolicLinks,Create Symbolic Links,Output Settings removeWorkingDir,Remove Working Directory,Output Settings run_logging,Run Logging,Output Settings diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 80668d7452..6b659389bd 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -118,10 +118,23 @@ def update_resource_pool(self, resources): if key in self.resource_pool: logger.info('Warning key %s already exists in resource' \ ' pool, replacing with %s ' % (key, value)) - self.resource_pool[key] = value +def create_new_fork(strat): + """Create a new strategy fork in the pipeline by producing a copy of the + current strategy (with resource pool).""" + + tmp = strategy() + tmp.resource_pool = dict(strat.resource_pool) + tmp.leaf_node = strat.leaf_node + tmp.leaf_out_file = str(strat.leaf_out_file) + tmp.name = list(strat.name) + strat = tmp + + return strat + + # Create and prepare C-PAC pipeline workflow def prep_workflow(sub_dict, c, strategies, run, pipeline_timing_info=None, p_name=None, plugin='MultiProc', plugin_args=None): @@ -389,7 +402,6 @@ def create_log_node(wflow, output, indx, scan_id=None): return log_wf def logStandardError(sectionName, errLine, errNum, errInfo=None): - logger.info("\n\nERROR: {0} - {1}\n\nError name: cpac_pipeline" "_{2}\n\n".format(sectionName, errLine, errNum)) if errInfo: @@ -397,7 +409,6 @@ def logStandardError(sectionName, errLine, errNum, errInfo=None): def logConnectionError(workflow_name, numStrat, resourcePool, errNum, errInfo=None): - logger.info( "\n\n" + 'ERROR: Invalid Connection: %s: %s, resource_pool: %s' \ % (workflow_name, numStrat, @@ -405,21 +416,17 @@ def logConnectionError(workflow_name, numStrat, resourcePool, errNum, errNum) + \ "\n\n" + "This is a pipeline creation error - the workflows " "have not started yet." + "\n\n") - if errInfo: logger.info(str(errInfo)) def logStandardWarning(sectionName, warnLine): - logger.info( "\n\n" + 'WARNING: %s - %s' % (sectionName, warnLine) + "\n\n") def getNodeList(strategy): - nodes = [] for node in strategy.name: nodes.append(node[:-2]) - return nodes strat_list = [] @@ -598,7 +605,7 @@ def getNodeList(strategy): strat.update_resource_pool({'anatomical_to_mni_linear_xfm': (fnirt_reg_anat_mni, 'outputspec.linear_xfm'), 'anatomical_to_mni_nonlinear_xfm': (fnirt_reg_anat_mni, 'outputspec.nonlinear_xfm'), 'mni_to_anatomical_linear_xfm': (fnirt_reg_anat_mni, 'outputspec.invlinear_xfm'), - 'mni_normalized_anatomical': (fnirt_reg_anat_mni, 'outputspec.output_brain')}) + 'anatomical_to_standard': (fnirt_reg_anat_mni, 'outputspec.output_brain')}) create_log_node(fnirt_reg_anat_mni, 'outputspec.output_brain', num_strat) @@ -628,7 +635,7 @@ def getNodeList(strategy): # reported to be better, but it requires very high # quality skullstripping. If skullstripping is imprecise # registration with skull is preferred - if (1 in c.regWithSkull): + if 1 in c.regWithSkull: if already_skullstripped == 1: err_msg = '\n\n[!] CPAC says: You selected ' \ @@ -732,7 +739,7 @@ def getNodeList(strategy): 'anatomical_to_mni_nonlinear_xfm': (ants_reg_anat_mni, 'outputspec.warp_field'), 'mni_to_anatomical_nonlinear_xfm': (ants_reg_anat_mni, 'outputspec.inverse_warp_field'), 'anat_to_mni_ants_composite_xfm': (ants_reg_anat_mni, 'outputspec.composite_transform'), - 'mni_normalized_anatomical': (ants_reg_anat_mni, 'outputspec.normalized_output_brain')}) + 'anatomical_to_standard': (ants_reg_anat_mni, 'outputspec.normalized_output_brain')}) create_log_node(ants_reg_anat_mni, 'outputspec.normalized_output_brain', num_strat) @@ -831,7 +838,7 @@ def getNodeList(strategy): strat.update_resource_pool({'anatomical_to_symmetric_mni_linear_xfm': (fnirt_reg_anat_symm_mni, 'outputspec.linear_xfm'), 'anatomical_to_symmetric_mni_nonlinear_xfm': (fnirt_reg_anat_symm_mni, 'outputspec.nonlinear_xfm'), 'symmetric_mni_to_anatomical_linear_xfm': (fnirt_reg_anat_symm_mni, 'outputspec.invlinear_xfm'), - 'symmetric_mni_normalized_anatomical': (fnirt_reg_anat_symm_mni, 'outputspec.output_brain')}) + 'symmetric_anatomical_to_standard': (fnirt_reg_anat_symm_mni, 'outputspec.output_brain')}) create_log_node(fnirt_reg_anat_symm_mni, 'outputspec.output_brain', num_strat) @@ -970,7 +977,7 @@ def getNodeList(strategy): 'anatomical_to_symmetric_mni_nonlinear_xfm': (ants_reg_anat_symm_mni, 'outputspec.warp_field'), 'symmetric_mni_to_anatomical_nonlinear_xfm': (ants_reg_anat_symm_mni, 'outputspec.inverse_warp_field'), 'anat_to_symmetric_mni_ants_composite_xfm': (ants_reg_anat_symm_mni, 'outputspec.composite_transform'), - 'symmetric_mni_normalized_anatomical': (ants_reg_anat_symm_mni, 'outputspec.normalized_output_brain')}) + 'symmetric_anatomical_to_standard': (ants_reg_anat_symm_mni, 'outputspec.normalized_output_brain')}) create_log_node(ants_reg_anat_symm_mni, 'outputspec.normalized_output_brain', @@ -1507,11 +1514,8 @@ def getNodeList(strategy): " (%s:%d)" % dbg_file_lineno()) raise - node = None - out_file = None try: node, out_file = strat.get_leaf_properties() - logger.info("%s::%s==>%s" % (node, out_file, func_preproc)) try: workflow.connect(node, out_file, func_preproc, 'inputspec.func') @@ -1543,22 +1547,22 @@ def getNodeList(strategy): strat.set_leaf_properties(func_preproc, 'outputspec.preprocessed') # add stuff to resource pool if we need it - strat.update_resource_pool({'mean_functional': ( - func_preproc, 'outputspec.example_func')}) - strat.update_resource_pool({'functional_preprocessed_mask': ( - func_preproc, 'outputspec.preprocessed_mask')}) - strat.update_resource_pool({'movement_parameters': ( - func_preproc, 'outputspec.movement_parameters')}) - strat.update_resource_pool({'max_displacement': ( - func_preproc, 'outputspec.max_displacement')}) strat.update_resource_pool( - {'preprocessed': (func_preproc, 'outputspec.preprocessed')}) + {'mean_functional': (func_preproc, 'outputspec.example_func')}) + strat.update_resource_pool( + {'functional_preprocessed_mask': (func_preproc, 'outputspec.preprocessed_mask')}) + strat.update_resource_pool( + {'movement_parameters': (func_preproc, 'outputspec.movement_parameters')}) + strat.update_resource_pool( + {'max_displacement': (func_preproc, 'outputspec.max_displacement')}) + strat.update_resource_pool( + {'functional_preprocessed': (func_preproc, 'outputspec.preprocessed')}) strat.update_resource_pool( {'functional_brain_mask': (func_preproc, 'outputspec.mask')}) - strat.update_resource_pool({'motion_correct': ( - func_preproc, 'outputspec.motion_correct')}) - strat.update_resource_pool({'coordinate_transformation': ( - func_preproc, 'outputspec.oned_matrix_save')}) + strat.update_resource_pool( + {'motion_correct': (func_preproc, 'outputspec.motion_correct')}) + strat.update_resource_pool( + {'coordinate_transformation': (func_preproc, 'outputspec.oned_matrix_save')}) create_log_node(func_preproc, 'outputspec.preprocessed', num_strat) @@ -1605,7 +1609,7 @@ def getNodeList(strategy): strat.update_resource_pool({'max_displacement': ( func_preproc, 'outputspec.max_displacement')}) strat.update_resource_pool( - {'preprocessed': (func_preproc, 'outputspec.preprocessed')}) + {'functional_preprocessed': (func_preproc, 'outputspec.preprocessed')}) strat.update_resource_pool( {'functional_brain_mask': (func_preproc, 'outputspec.mask')}) strat.update_resource_pool({'motion_correct': ( @@ -1929,7 +1933,6 @@ def pick_wm(seg_prob_list): "run again.\n\n" raise Exception(err) - strat_list += new_strat_list ''' @@ -2009,12 +2012,35 @@ def pick_wm(seg_prob_list): gen_motion_stats, 'outputspec.frames_ex_1D'), 'despiking_frames_included': ( gen_motion_stats, 'outputspec.frames_in_1D')}) - elif "Scrubbing" in c.runMotionSpike and 1 in c.runNuisance: + + ''' + if "Scrubbing" in c.runMotionSpike: + tmp = strategy() + tmp.resource_pool = dict(strat.resource_pool) + tmp.leaf_node = (strat.leaf_node) + tmp.out_file = str(strat.leaf_out_file) + tmp.name = list(strat.name) + strat = tmp + new_strat_list.append(strat) + ''' + + if "Scrubbing" in c.runMotionSpike and 1 in c.runNuisance: strat.update_resource_pool({'scrubbing_frames_excluded': ( gen_motion_stats, 'outputspec.frames_ex_1D'), 'scrubbing_frames_included': ( gen_motion_stats, 'outputspec.frames_in_1D')}) + ''' + if "De-Spiking" in c.runMotionSpike: + tmp = strategy() + tmp.resource_pool = dict(strat.resource_pool) + tmp.leaf_node = (strat.leaf_node) + tmp.out_file = str(strat.leaf_out_file) + tmp.name = list(strat.name) + strat = tmp + new_strat_list.append(strat) + ''' + create_log_node(gen_motion_stats, 'outputspec.motion_params', num_strat) num_strat += 1 @@ -2044,16 +2070,22 @@ def pick_wm(seg_prob_list): # avoiding a crash) on the strat without segmentation if 'seg_preproc' in nodes: + subwf_name = "nuisance" + if "De-Spiking" in c.runMotionSpike: + subwf_name = "nuisance_with_despiking" + if 'anat_mni_fnirt_register' in nodes: nuisance = create_nuisance(False, - 'nuisance_%d' % num_strat) + '{0}_{1}'.format(subwf_name, + num_strat)) else: nuisance = create_nuisance(True, - 'nuisance_%d' % num_strat) + '{0}_{1}'.format(subwf_name, + num_strat)) nuisance.get_node('residuals').iterables = ( - [('selector', c.Regressors), - ('compcor_ncomponents', c.nComponents)]) + [('selector', c.Regressors), + ('compcor_ncomponents', c.nComponents)]) nuisance.inputs.inputspec.lat_ventricles_mask = c.lateral_ventricles_mask @@ -2082,7 +2114,6 @@ def pick_wm(seg_prob_list): workflow.connect(node, out_file, nuisance, 'inputspec.motion_components') - #TODO: DE-SPIKING OPTIONS if "De-Spiking" in c.runMotionSpike: node, out_file = strat.get_node_from_resource_pool( 'despiking_frames_excluded') @@ -2126,20 +2157,25 @@ def pick_wm(seg_prob_list): nuisance, 'inputspec.anat_to_mni_affine_xfm') - - except: + except Exception as e: logConnectionError('Nuisance', num_strat, - strat.get_resource_pool(), '0010') + strat.get_resource_pool(), '0010', e) raise if 0 in c.runNuisance: - tmp = strategy() - tmp.resource_pool = dict(strat.resource_pool) - tmp.leaf_node = (strat.leaf_node) - tmp.leaf_out_file = str(strat.leaf_out_file) - tmp.name = list(strat.name) - strat = tmp - new_strat_list.append(strat) + new_strat_list.append(create_new_fork(strat)) + + if 1 in c.runNuisance and "De-Spiking" in c.runMotionSpike and \ + "Scrubbing" in c.runMotionSpike: + # create a new fork that will run nuisance like above but + # without the de-spiking + new_strat_list.append(create_new_fork(strat)) + + if 1 in c.runNuisance and "De-Spiking" in c.runMotionSpike and \ + "None" in c.runMotionSpike: + # create a new fork that will run nuisance like above but + # without the de-spiking + new_strat_list.append(create_new_fork(strat)) strat.append_name(nuisance.name) @@ -2154,6 +2190,133 @@ def pick_wm(seg_prob_list): strat_list += new_strat_list + # set a flag in case we're doing nuisance on/off + non_nuisance_strat = False + + for strat in strat_list: + + nodes = getNodeList(strat) + + if 0 in c.runNuisance and \ + ("nuisance" not in nodes and "nuisance_with_despiking" not in nodes): + if not non_nuisance_strat: + # save one of the strats so that it won't have any nuisance + # at all - this only fires if nuisance is on/off + non_nuisance_strat = True + continue + + if 1 in c.runNuisance and "De-Spiking" in c.runMotionSpike and \ + "nuisance_with_despiking" not in nodes and \ + ("Scrubbing" in c.runMotionSpike or "None" in c.runMotionSpike): + # run nuisance in the new fork (if created), without de-spiking, + # so that we can have nuisance and then scrubbing, or a nuisance + # strat without de-spiking if doing de-spiking on/off + # this only runs if we have ["De-Spiking", "Scrubbing"] or + # ["De-Spiking", "Scrubbing", "Off"] in c.runMotionSpike + + # this is needed here in case tissue segmentation is set on/off + # and you have nuisance enabled- this will ensure nuisance will + # run for the strat that has segmentation but will not run (thus + # avoiding a crash) on the strat without segmentation + if 'seg_preproc' in nodes: + + if 'anat_mni_fnirt_register' in nodes: + nuisance = create_nuisance(False, + 'nuisance_no_despiking_%d' % num_strat) + else: + nuisance = create_nuisance(True, + 'nuisance_no_despiking_%d' % num_strat) + + nuisance.get_node('residuals').iterables = ( + [('selector', c.Regressors), + ('compcor_ncomponents', c.nComponents)]) + + nuisance.inputs.inputspec.lat_ventricles_mask = c.lateral_ventricles_mask + + try: + # enforcing no de-spiking here! + # TODO: when condensing these sub-wf builders, pass + # TODO: something so that the check in the nuisance strat + # TODO: above can be modified for this version down here + nuisance.inputs.inputspec.frames_ex = None + + node, out_file = strat.get_leaf_properties() + workflow.connect(node, out_file, + nuisance, 'inputspec.subject') + + node, out_file = strat.get_node_from_resource_pool( + 'anatomical_gm_mask') + workflow.connect(node, out_file, + nuisance, 'inputspec.gm_mask') + + node, out_file = strat.get_node_from_resource_pool( + 'anatomical_wm_mask') + workflow.connect(node, out_file, + nuisance, 'inputspec.wm_mask') + + node, out_file = strat.get_node_from_resource_pool( + 'anatomical_csf_mask') + workflow.connect(node, out_file, + nuisance, 'inputspec.csf_mask') + + node, out_file = strat.get_node_from_resource_pool( + 'movement_parameters') + workflow.connect(node, out_file, + nuisance, 'inputspec.motion_components') + + node, out_file = strat.get_node_from_resource_pool( + 'functional_to_anat_linear_xfm') + workflow.connect(node, out_file, + nuisance, + 'inputspec.func_to_anat_linear_xfm') + + if 'anat_mni_fnirt_register' in nodes: + node, out_file = strat.get_node_from_resource_pool( + 'mni_to_anatomical_linear_xfm') + workflow.connect(node, out_file, + nuisance, + 'inputspec.mni_to_anat_linear_xfm') + else: + # pass the ants_affine_xfm to the input for the + # INVERSE transform, but ants_affine_xfm gets inverted + # within the workflow + + node, out_file = strat.get_node_from_resource_pool( + 'ants_initial_xfm') + workflow.connect(node, out_file, + nuisance, + 'inputspec.anat_to_mni_initial_xfm') + + node, out_file = strat.get_node_from_resource_pool( + 'ants_rigid_xfm') + workflow.connect(node, out_file, + nuisance, + 'inputspec.anat_to_mni_rigid_xfm') + + node, out_file = strat.get_node_from_resource_pool( + 'ants_affine_xfm') + workflow.connect(node, out_file, + nuisance, + 'inputspec.anat_to_mni_affine_xfm') + + except Exception as e: + logConnectionError('Nuisance', num_strat, + strat.get_resource_pool(), '0010b', e) + raise + + strat.append_name(nuisance.name) + + strat.set_leaf_properties(nuisance, 'outputspec.subject') + + strat.update_resource_pool( + {'functional_nuisance_residuals': (nuisance, 'outputspec.subject')}) + strat.update_resource_pool( + {'functional_nuisance_regressors': (nuisance, 'outputspec.regressors')}) + + create_log_node(nuisance, 'outputspec.subject', num_strat) + + num_strat += 1 + ''' Inserting Median Angle Correction Workflow ''' @@ -2312,17 +2475,32 @@ def pick_wm(seg_prob_list): workflow_bit_id['scrubbing'] = workflow_counter + # set a flag in case we're doing nuisance on/off + non_nuisance_strat = False + for strat in strat_list: nodes = getNodeList(strat) - if 'gen_motion_stats' in nodes: + if 0 in c.runNuisance and \ + "nuisance" not in nodes and \ + "nuisance_with_despiking" not in nodes and \ + "nuisance_no_despiking" not in nodes: + if not non_nuisance_strat: + # save one of the strats so that it won't have any + # nuisance at all - this only fires if nuisance is on/off + non_nuisance_strat = True + continue - scrubbing = create_scrubbing_preproc( - 'scrubbing_%d' % num_strat) + # skip if this strat had de-spiking (mutually exclusive) + if "nuisance_with_despiking" in nodes: + continue - try: + if 'gen_motion_stats' in nodes: + scrubbing = \ + create_scrubbing_preproc('scrubbing_%d' % num_strat) + try: node, out_file = strat.get_leaf_properties() workflow.connect(node, out_file, scrubbing, 'inputspec.preprocessed') @@ -2343,14 +2521,8 @@ def pick_wm(seg_prob_list): strat.get_resource_pool(), '0014') raise - if 0 in c.runNuisance: - tmp = strategy() - tmp.resource_pool = dict(strat.resource_pool) - tmp.leaf_node = (strat.leaf_node) - tmp.leaf_out_file = str(strat.leaf_out_file) - tmp.name = list(strat.name) - strat = tmp - new_strat_list.append(strat) + if "None" in c.runMotionSpike: + new_strat_list.append(create_new_fork(strat)) strat.append_name(scrubbing.name) @@ -2481,8 +2653,8 @@ def pick_wm(seg_prob_list): nodes = getNodeList(strat) - if ('ANTS' in c.regOption) and ( - 'anat_mni_fnirt_register' not in nodes): + if 'ANTS' in c.regOption and \ + 'anat_mni_fnirt_register' not in nodes: # ANTS warp application @@ -4166,7 +4338,11 @@ def is_number(s): forklabel = 'bbreg' if 'frequency' in fork: forklabel = 'freq-filter' - if 'nuisance' in fork: + if 'nuisance_with_despiking' in fork: + forklabel = 'nuisance_with_despiking' + elif 'nuisance_no_despiking' in fork: + forklabel = 'nuisance_no_despiking' + elif 'nuisance' in fork: forklabel = 'nuisance' if 'median' in fork: forklabel = 'median' @@ -4216,7 +4392,6 @@ def is_number(s): # build helper dictionary to assist with a clean strategy label # for symlinks - strategy_tag_helper_symlinks = {} if any('scrubbing' in name for name in strat.get_name()): @@ -4301,13 +4476,33 @@ def is_number(s): except Exception as exc: encrypt_data = False + # TODO: remove this once forking for despiking/scrubbing is + # TODO: modified at the gen motion params level + # ensure X_frames_included/excluded only gets sent to output dir + # for appropriate strats + nodes = getNodeList(strat) + if "nuisance_with_despiking" not in nodes: + if "despiking_frames_included" in rp.keys(): + del rp["despiking_frames_included"] + if "despiking_frames_excluded" in rp.keys(): + del rp["despiking_frames_excluded"] + if "scrubbing" not in nodes: + if "scrubbing_frames_included" in rp.keys(): + del rp["scrubbing_frames_included"] + if "scrubbing_frames_excluded" in rp.keys(): + del rp["scrubbing_frames_excluded"] + for key in sorted(rp.keys()): if key not in override_optional: - if key in debugging_outputs or \ - key in extra_functional_outputs: - continue + if 1 not in c.write_func_outputs: + if key in extra_functional_outputs: + continue + + if 1 not in c.write_debugging_outputs: + if key in debugging_outputs: + continue if 0 not in c.runRegisterFuncToMNI: if key in outputs_native_nonsmooth or \ @@ -4403,7 +4598,7 @@ def is_number(s): # creates the HTML files used to represent the logging-based status create_log_template(pip_ids, wf_names, scan_ids, subject_id, log_dir) - logger.info('\n\n' + ('Strategy forks: %s' % pipes) + '\n\n') + logger.info("\n\nStrategy forks: {0}\n\n".format(str(set(pipes)))) pipeline_start_date = strftime("%Y-%m-%d") pipeline_start_datetime = strftime("%Y-%m-%d %H:%M:%S") diff --git a/CPAC/resources/configs/pipeline_config_template.yml b/CPAC/resources/configs/pipeline_config_template.yml index 2f6c4dbd15..421a962188 100644 --- a/CPAC/resources/configs/pipeline_config_template.yml +++ b/CPAC/resources/configs/pipeline_config_template.yml @@ -75,6 +75,14 @@ awsOutputBucketCredentials : s3Encryption : [1] +# Include extra versions and intermediate steps of functional preprocessing in the output directory. +write_func_outputs: [0] + + +# Include extra outputs in the output directory that may be of interest when more information is needed. +write_debugging_outputs: [0] + + # Create a user-friendly, well organized version of the output directory. # We recommend all users enable this option. runSymbolicLinks : [1] diff --git a/CPAC/resources/cpac_outputs.csv b/CPAC/resources/cpac_outputs.csv index c7a17ac73b..0eb1c07e8c 100644 --- a/CPAC/resources/cpac_outputs.csv +++ b/CPAC/resources/cpac_outputs.csv @@ -1 +1 @@ -Resource,Space,Values,Multiple outputs,Functional timeseries,Derivative,Warp to template,Calculate z-scores,Calculate averages,Optional outputs: Extra functionals,Optional outputs: Native space,Optional outputs: Raw scores,Optional outputs: Non-smoothed,Optional outputs: Debugging outputs,Override optional alff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, alff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, alff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, alff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, alff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, alff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, anatomical_brain,anatomical,raw,,,,,,,,,,,, anatomical_csf_mask,anatomical,binary,,,,,,,,,,,, anatomical_gm_mask,anatomical,binary,,,,,,,,,,,, anatomical_reorient,anatomical,raw,,,,,,,,,,,, anatomical_to_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_to_symmetric_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_wm_mask,anatomical,binary,,,,,,,,,,,, ants_affine_xfm,,transform,,,,,,,,,,,, ants_initial_xfm,,transform,,,,,,,,,,,, ants_rigid_xfm,,transform,,,,,,,,,,,, ants_symmetric_affine_xfm,,transform,,,,,,,,,,,, ants_symmetric_initial_xfm,,transform,,,,,,,,,,,, ants_symmetric_rigid_xfm,,transform,,,,,,,,,,,, centrality_outputs,template,raw,yes,,yes,,yes,,,,yes,yes,, centrality_outputs_smooth,template,raw,yes,,yes,,,,,,yes,,, centrality_outputs_zstd,template,z-score,yes,,yes,,,,,,,yes,, centrality_outputs_zstd_smooth,template,z-score,yes,,yes,,,,,,,,, coordinate_transformation,,,,,,,,,,,,,yes, despiked_fieldmap,,,,,,,,,,,,,yes, dr_tempreg_maps_files,functional,GLM betas,yes,,yes,yes,,yes,,yes,,yes,, dr_tempreg_maps_files_smooth,functional,GLM betas,yes,,yes,,,yes,,yes,,,, dr_tempreg_maps_files_to_standard,template,GLM betas,yes,,yes,,,yes,,,,yes,, dr_tempreg_maps_files_to_standard_smooth,template,GLM betas,yes,,yes,,,yes,,,,,, dr_tempreg_maps_zstat_files,functional,z-stat,yes,,yes,yes,,,,yes,,yes,yes, dr_tempreg_maps_zstat_files_smooth,functional,z-stat,yes,,yes,,,,,yes,,,yes, dr_tempreg_maps_zstat_files_to_standard,template,z-stat,yes,,yes,,,,,,,yes,yes, dr_tempreg_maps_zstat_files_to_standard_smooth,template,z-stat,yes,,yes,,,,,,,,yes, falff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, falff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, falff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, falff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, falff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, falff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, fmap_magnitude,,,,,,,,,,,,,yes, fmap_phase_diff,,,,,,,,,,,,,yes, frame_wise_displacement_jenkinson,,,,,,,,,,,,,, frame_wise_displacement_power,,,,,,,,,,,,,, functional_brain_mask,functional,binary,,,,,,,,,,,, functional_brain_mask_to_standard,template,binary,,,,,,,,,,,, functional_freq_filtered,functional,raw,,yes,,,,,,,,,, functional_nuisance_regressors,,,,,,,,,,,,,, functional_nuisance_residuals,functional,,,yes,,,,,yes,,,,yes, functional_preprocessed_mask,functional,binary,,,,,,,yes,,,,, functional_to_anat_linear_xfm,,transform,,,,,,,,,,,, functional_to_standard,template,raw,,yes,,,,,,,,,, max_displacement,,,,,,,,,,,,,yes, mean_functional,functional,raw,,,,,,,yes,,,,, mean_functional_in_anat,anatomical,raw,,,,,,,yes,,,,, mean_functional_to_standard,template,raw,,,,,,,,,,,, mni_normalized_anatomical,template,raw,,,,,,,,,,,, mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, motion_correct,functional,raw,,yes,,yes,,,,,,yes,, motion_correct_smooth,functional,raw,,yes,,,,,yes,,,,, motion_correct_to_standard,template,raw,,yes,,,,,yes,,,yes,, motion_correct_to_standard_smooth,template,raw,,yes,,,,,yes,,,,, motion_params,,,,,,,,,,,,,, movement_parameters,,,,,,,,,,,,,yes, output_means,,,,,,,,,,,,,, power_params,,,,,,,,,,,,,yes, preprocessed,functional,raw,,yes,,,,,yes,,,,, raw_functional,functional,raw,,yes,,,,,yes,,,,, reho,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, reho_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, reho_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, reho_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, reho_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, reho_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, roi_timeseries,,,,,,,,,,,,,, roi_timeseries_for_SCA,,,,,,,,,,,,,, roi_timeseries_for_SCA_multreg,,,,,,,,,,,,,, sca_roi_files,functional,Pearson's r,yes,,yes,yes,,yes,,yes,yes,yes,,yes sca_roi_files_smooth,functional,Pearson's r,yes,,yes,,,yes,,yes,yes,,,yes sca_roi_files_to_standard,template,Pearson's r,yes,,yes,,yes,yes,,,yes,yes,,yes sca_roi_files_to_standard_smooth,template,Pearson's r,yes,,yes,,,yes,,,yes,,,yes sca_roi_files_to_standard_fisher_zstd,template,r-to-z,yes,,yes,,,,,,,yes,,yes sca_roi_files_to_standard_fisher_zstd_smooth,template,r-to-z,yes,,yes,,,,,,,,,yes sca_tempreg_maps_files,template,GLM betas,yes,,yes,,,yes,,,,yes,,yes sca_tempreg_maps_files_smooth,template,GLM betas,yes,,yes,,,yes,,,,,,yes sca_tempreg_maps_zstat_files,template,z-stat,yes,,yes,,,,,,,yes,,yes sca_tempreg_maps_zstat_files_smooth,template,z-stat,yes,,yes,,,,,,,,,yes seg_mixeltype,,,,,,,,,,,,,yes, seg_partial_volume_files,anatomical,,,,,,,,,,,,yes, seg_partial_volume_map,anatomical,,,,,,,,,,,,yes, seg_probability_maps,anatomical,,,,,,,,,,,,yes, slice_time_corrected,functional,raw,,yes,,,,,yes,,,,, spatial_map_timeseries,,,,,,,,,,,,,, spatial_map_timeseries_for_DR,,,,,,,,,,,,,, symmetric_mni_normalized_anatomical,template,raw,,,,,,,,,,,, symmetric_mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, vmhc_fisher_zstd,template,r-to-z,,,yes,,,,,,,,yes, vmhc_fisher_zstd_zstat_map,template,z-stat,,,yes,,,,,,,,, vmhc_raw_score,template,Pearson's r,,,yes,,yes,,,,,,yes, \ No newline at end of file +Resource,Space,Values,Multiple outputs,Functional timeseries,Derivative,Warp to template,Calculate z-scores,Calculate averages,Optional outputs: Extra functionals,Optional outputs: Native space,Optional outputs: Raw scores,Optional outputs: Non-smoothed,Optional outputs: Debugging outputs,Override optional alff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, alff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, alff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, alff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, alff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, alff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, anatomical_brain,anatomical,raw,,,,,,,,,,,, anatomical_csf_mask,anatomical,binary,,,,,,,,,,,, anatomical_gm_mask,anatomical,binary,,,,,,,,,,,, anatomical_reorient,anatomical,raw,,,,,,,,,,,, anatomical_to_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_to_standard,template,raw,,,,,,,,,,,, anatomical_to_symmetric_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_wm_mask,anatomical,binary,,,,,,,,,,,, ants_affine_xfm,,transform,,,,,,,,,,,, ants_initial_xfm,,transform,,,,,,,,,,,, ants_rigid_xfm,,transform,,,,,,,,,,,, ants_symmetric_affine_xfm,,transform,,,,,,,,,,,, ants_symmetric_initial_xfm,,transform,,,,,,,,,,,, ants_symmetric_rigid_xfm,,transform,,,,,,,,,,,, centrality_outputs,template,raw,yes,,yes,,yes,,,,yes,yes,, centrality_outputs_smooth,template,raw,yes,,yes,,,,,,yes,,, centrality_outputs_zstd,template,z-score,yes,,yes,,,,,,,yes,, centrality_outputs_zstd_smooth,template,z-score,yes,,yes,,,,,,,,, coordinate_transformation,,,,,,,,,,,,,yes, despiked_fieldmap,,,,,,,,,,,,,yes, dr_tempreg_maps_files,functional,GLM betas,yes,,yes,yes,,yes,,yes,,yes,, dr_tempreg_maps_files_smooth,functional,GLM betas,yes,,yes,,,yes,,yes,,,, dr_tempreg_maps_files_to_standard,template,GLM betas,yes,,yes,,,yes,,,,yes,, dr_tempreg_maps_files_to_standard_smooth,template,GLM betas,yes,,yes,,,yes,,,,,, dr_tempreg_maps_zstat_files,functional,z-stat,yes,,yes,yes,,,,yes,,yes,yes, dr_tempreg_maps_zstat_files_smooth,functional,z-stat,yes,,yes,,,,,yes,,,yes, dr_tempreg_maps_zstat_files_to_standard,template,z-stat,yes,,yes,,,,,,,yes,yes, dr_tempreg_maps_zstat_files_to_standard_smooth,template,z-stat,yes,,yes,,,,,,,,yes, falff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, falff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, falff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, falff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, falff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, falff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, fmap_magnitude,,,,,,,,,,,,,yes, fmap_phase_diff,,,,,,,,,,,,,yes, frame_wise_displacement_jenkinson,,,,,,,,,,,,,, frame_wise_displacement_power,,,,,,,,,,,,,, functional_brain_mask,functional,binary,,,,,,,,,,,, functional_brain_mask_to_standard,template,binary,,,,,,,,,,,, functional_freq_filtered,functional,raw,,yes,,,,,,,,,, functional_nuisance_regressors,,,,,,,,,,,,,, functional_nuisance_residuals,functional,,,yes,,,,,yes,,,,yes, functional_preprocessed,functional,raw,,yes,,,,,yes,,,,, functional_preprocessed_mask,functional,binary,,,,,,,yes,,,,, functional_to_anat_linear_xfm,,transform,,,,,,,,,,,, functional_to_standard,template,raw,,yes,,,,,,,,,, max_displacement,,,,,,,,,,,,,yes, mean_functional,functional,raw,,,,,,,yes,,,,, mean_functional_in_anat,anatomical,raw,,,,,,,yes,,,,, mean_functional_to_standard,template,raw,,,,,,,,,,,, mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, motion_correct,functional,raw,,yes,,yes,,,,,,yes,, motion_correct_smooth,functional,raw,,yes,,,,,yes,,,,, motion_correct_to_standard,template,raw,,yes,,,,,yes,,,yes,, motion_correct_to_standard_smooth,template,raw,,yes,,,,,yes,,,,, motion_params,,,,,,,,,,,,,, movement_parameters,,,,,,,,,,,,,yes, output_means,,,,,,,,,,,,,, power_params,,,,,,,,,,,,,yes, raw_functional,functional,raw,,yes,,,,,yes,,,,, reho,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, reho_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, reho_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, reho_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, reho_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, reho_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, roi_timeseries,,,,,,,,,,,,,, roi_timeseries_for_SCA,,,,,,,,,,,,,, roi_timeseries_for_SCA_multreg,,,,,,,,,,,,,, sca_roi_files,functional,Pearson's r,yes,,yes,yes,,yes,,yes,yes,yes,,yes sca_roi_files_smooth,functional,Pearson's r,yes,,yes,,,yes,,yes,yes,,,yes sca_roi_files_to_standard,template,Pearson's r,yes,,yes,,yes,yes,,,yes,yes,,yes sca_roi_files_to_standard_smooth,template,Pearson's r,yes,,yes,,,yes,,,yes,,,yes sca_roi_files_to_standard_fisher_zstd,template,r-to-z,yes,,yes,,,,,,,yes,,yes sca_roi_files_to_standard_fisher_zstd_smooth,template,r-to-z,yes,,yes,,,,,,,,,yes sca_tempreg_maps_files,template,GLM betas,yes,,yes,,,yes,,,,yes,,yes sca_tempreg_maps_files_smooth,template,GLM betas,yes,,yes,,,yes,,,,,,yes sca_tempreg_maps_zstat_files,template,z-stat,yes,,yes,,,,,,,yes,,yes sca_tempreg_maps_zstat_files_smooth,template,z-stat,yes,,yes,,,,,,,,,yes seg_mixeltype,,,,,,,,,,,,,yes, seg_partial_volume_files,anatomical,,,,,,,,,,,,yes, seg_partial_volume_map,anatomical,,,,,,,,,,,,yes, seg_probability_maps,anatomical,,,,,,,,,,,,yes, slice_time_corrected,functional,raw,,yes,,,,,yes,,,,, spatial_map_timeseries,,,,,,,,,,,,,, spatial_map_timeseries_for_DR,,,,,,,,,,,,,, symmetric_anatomical_to_standard,template,raw,,,,,,,,,,,, symmetric_mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, vmhc_fisher_zstd,template,r-to-z,,,yes,,,,,,,,yes, vmhc_fisher_zstd_zstat_map,template,z-stat,,,yes,,,,,,,,, vmhc_raw_score,template,Pearson's r,,,yes,,yes,,,,,,yes, \ No newline at end of file diff --git a/CPAC/sca/sca.py b/CPAC/sca/sca.py index b28f310e7e..82b558e65d 100644 --- a/CPAC/sca/sca.py +++ b/CPAC/sca/sca.py @@ -290,7 +290,6 @@ def create_temporal_reg(wflow_name='temporal_reg', which='SR'): check_timeseries, 'in_file') temporalReg = pe.Node(interface=fsl.GLM(), name='temporal_regression') - temporalReg.inputs.out_file = 'temp_reg_map.nii.gz' temporalReg.inputs.out_z_name = 'temp_reg_map_z.nii.gz' @@ -307,8 +306,7 @@ def create_temporal_reg(wflow_name='temporal_reg', which='SR'): split.inputs.dimension = 't' split.inputs.out_base_name = 'temp_reg_map_' - wflow.connect(temporalReg, 'out_file', - split, 'in_file') + wflow.connect(temporalReg, 'out_file', split, 'in_file') split_zstat = pe.Node(interface=fsl.Split(), name='split_zstat_volumes') split_zstat.inputs.dimension = 't' diff --git a/CPAC/utils/utils.py b/CPAC/utils/utils.py index ca83331e05..d501e0eeb2 100644 --- a/CPAC/utils/utils.py +++ b/CPAC/utils/utils.py @@ -34,7 +34,7 @@ 'max_displacement': 'parameters', 'xform_matrix': 'parameters', 'output_means': 'parameters', - 'preprocessed': 'func', + 'functional_preprocessed': 'func', 'functional_brain_mask': 'func', 'motion_correct': 'func', 'motion_correct_smooth': 'func', @@ -80,14 +80,14 @@ 'symmetric_mni_to_anatomical_nonlinear_xfm': 'registration', 'symmetric_mni_to_anatomical_linear_xfm': 'registration', 'anat_to_symmetric_mni_ants_composite_xfm': 'registration', - 'symmetric_mni_normalized_anatomical': 'registration', + 'symmetric_anatomical_to_standard': 'registration', 'anatomical_to_symmetric_mni_linear_xfm': 'registration', - 'mni_normalized_anatomical': 'anat', + 'anatomical_to_standard': 'anat', 'vmhc_raw_score': 'vmhc', 'vmhc_fisher_zstd': 'vmhc', 'vmhc_fisher_zstd_zstat_map': 'vmhc', - 'alff_img': 'alff', - 'falff_img': 'alff', + 'alff': 'alff', + 'falff': 'alff', 'alff_smooth': 'alff', 'falff_smooth': 'alff', 'alff_to_standard': 'alff', @@ -98,7 +98,7 @@ 'falff_to_standard_zstd': 'alff', 'alff_to_standard_zstd_smooth': 'alff', 'falff_to_standard_zstd_smooth': 'alff', - 'raw_reho_map': 'reho', + 'reho': 'reho', 'reho_smooth': 'reho', 'reho_to_standard': 'reho', 'reho_to_standard_smooth': 'reho', @@ -402,8 +402,10 @@ def get_roi_num_list(timeseries_file, prefix=None): roi_file_lines = f.read().splitlines() roi_err = "\n\n[!] The output of 3dROIstats, used in extracting the " \ - "timeseries, was not in the expected format.\n\nROI output " \ - "file: %s\n\n" % timeseries_file + "timeseries, is either empty, or not in the expected " \ + "format.\n\nROI output file: {0}\n\nIf there are no rows " \ + "in the output file, double-check your ROI/mask selection." \ + "\n\n".format(str(timeseries_file)) for line in roi_file_lines: if "Mean_" in line: @@ -1451,6 +1453,10 @@ def get_scan_params(subject_id, scan, pipeconfig_tr, pipeconfig_tpattern, if "None" in pipeconfig_tr or "none" in pipeconfig_tr: pipeconfig_tr = None + if isinstance(pipeconfig_stop_indx, str): + if "End" in pipeconfig_stop_indx or "end" in pipeconfig_stop_indx: + pipeconfig_stop_indx = None + if data_config_scan_params: if ".json" in data_config_scan_params: diff --git a/CPAC/vmhc/vmhc.py b/CPAC/vmhc/vmhc.py index aea181d624..f887460c24 100644 --- a/CPAC/vmhc/vmhc.py +++ b/CPAC/vmhc/vmhc.py @@ -371,8 +371,8 @@ def create_vmhc(use_ants, name='vmhc_workflow', ants_threads=1): vmhc.connect(inputNode, 'standard_for_func', apply_ants_xfm_vmhc, 'inputspec.reference_image') - vmhc.connect(collect_transforms_vmhc, \ - 'outputspec.transformation_series', \ + vmhc.connect(collect_transforms_vmhc, + 'outputspec.transformation_series', apply_ants_xfm_vmhc, 'inputspec.transforms') vmhc.connect(apply_ants_xfm_vmhc, 'outputspec.output_image', From d3d77504ef8ad2be3ef111d92f2fcef31906ea15 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Mon, 2 Apr 2018 05:42:19 -0400 Subject: [PATCH 29/75] final iteration of the qa html pages --- CPAC/qc/utils.py | 63 ++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index d18665d701..e68096d7bb 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -3,7 +3,7 @@ import matplotlib import pkg_resources as p matplotlib.use('Agg') - +import os def append_to_files_in_dict_way(list_files, file_): @@ -1124,7 +1124,7 @@ def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist -def generateQCPages(qc_path): +def generateQCPages(qc_path,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): """ parafile = open('QC_input_para.txt', 'w') @@ -1190,39 +1190,40 @@ def generateQCPages(qc_path): first_pass_organizing_files(qc_path) #according to bandpass and hp_lp and smoothing iterables combines the files second_pass_organizing_files(qc_path) + make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) #generate pages from qc files - text_file_list = os.listdir(qc_path) - print text_file_list - image_path_list = [] - for text_file in text_file_list: - if text_file[0] != '.': - f = open(os.path.join(path, text_file),'r') - eof = 0 - while (not eof): - temp_line = f.readline() - if temp_line == '': - eof=1 #This is to check if reading the file is complete - else: - if temp_line[-2:] == "\n": - temp_line = temp_line[:-1] - image_path_list.append(temp_line) - f.close() - f = open(os.path.join(qc_output_folder, "QC_index.html"),'w') - header = """ - QA Images -

""" - footer = """

- """ +# text_file_list = os.listdir(qc_path) +# print text_file_list +# image_path_list = [] +# for text_file in text_file_list: +# if text_file[0] != '.': +# f = open(os.path.join(qc_path, text_file),'r') +# eof = 0 +# while (not eof): +# temp_line = f.readline() +# if temp_line == '': +# eof=1 #This is to check if reading the file is complete +# else: +# if temp_line[-2:] == "\n": +# temp_line = temp_line[:-1] +# image_path_list.append(temp_line) +# f.close() +# f = open(os.path.join(qc_path, "QC_index.html"),'w') +# header = """ +# QA Images +#

""" +# footer = """

+# """ - f.write(header) - for image in image_path_list: - image_html = "" - f.write(image_html) +# f.write(header) +# for image in image_path_list: +# image_html = "" +# f.write(image_html) - f.write(footer) - f.close() - print f +# f.write(footer) +# f.close() +# print f def make_edge(file_): From 96b63910b88a129524704576d10420292d1f73b7 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Mon, 2 Apr 2018 05:43:10 -0400 Subject: [PATCH 30/75] final changes for qc html pages --- CPAC/pipeline/cpac_pipeline.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 6ed466aeb3..514d4b9b2d 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -5793,6 +5793,7 @@ def is_number(s): # Actually run the pipeline now, for the current subject workflow.run(plugin=plugin, plugin_args=plugin_args) + # Dump subject info pickle file to subject log dir subject_info['status'] = 'Completed' subject_info_pickle = open( @@ -5847,20 +5848,21 @@ def is_number(s): for count, scanID in enumerate(pip_ids): for scan in scan_ids: create_log_node(None, None, count, scan).run() - print i - # If QC is enabled - # TODO - QA pages: re-introduce - if 1 in c.generateQualityControlImages: - print "NECTAR" - for pip_id in pip_ids: - - f_path = os.path.join(os.path.join(c.logDirectory, 'pipeline_%s' %pip_id)) - - qc_output_folder = os.path.join(f_path, 'qc_files_here') - - generateQCPages(qc_output_folder) - create_all_qc.run(f_path) + for pip_id in pip_ids: + try: + pipeline_base = os.path.join(c.logDirectory, 'pipeline_%s' % pip_id) + qc_output_folder = os.path.join(pipeline_base, subject_id, 'qc_files_here') + generateQCPages(qc_output_folder,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) + #create_all_qc.run(pipeline_base) + except Exception as e: + print "Error: this function is not running" + print "" + print e + print type(e) + raise Exception + + # Generate the QC pages -- this function isn't even running, because there is noparameter for qc_montage_id_a/qc_montage_id_s/qc_plot_id,qc_hist_id #two methods can be done here: #i) group all the qc_montage_ids in the resource pool or From 4a816194b9897c4cda400b9255834e2edda27dcc Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Tue, 3 Apr 2018 18:35:07 -0400 Subject: [PATCH 31/75] Fixed several issues with the data config builder: parsing fieldmaps properly, and also scan parameters. Code cleanup as well. --- CPAC/GUI/interface/pages/functional_tab.py | 2 +- CPAC/utils/build_data_config.py | 1239 +++++++------------- 2 files changed, 446 insertions(+), 795 deletions(-) diff --git a/CPAC/GUI/interface/pages/functional_tab.py b/CPAC/GUI/interface/pages/functional_tab.py index fe062de351..cf631097be 100644 --- a/CPAC/GUI/interface/pages/functional_tab.py +++ b/CPAC/GUI/interface/pages/functional_tab.py @@ -123,7 +123,7 @@ def __init__(self, parent, counter =0): self.counter = counter fsl = os.environ.get('FSLDIR') if fsl == None: - fsl = "$FSLDIR" + fsl = "$FSLDIR" self.page.add(label="Perform Field Map Distortion Correction ", control=control.CHOICE_BOX, diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index 6af4e9c648..bc41c676dc 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -12,8 +12,6 @@ def gather_file_paths(base_directory, verbose=False): # - proper data directory # - empty directory - # TODO: this is not being used by the data config builder- do we need? - import os path_list = [] @@ -30,6 +28,8 @@ def gather_file_paths(base_directory, verbose=False): def pull_s3_sublist(data_folder, creds_path=None, keep_prefix=True): + """Return a list of input data file paths that are available on an AWS S3 + bucket on the cloud.""" import os from indi_aws import fetch_creds @@ -73,6 +73,8 @@ def pull_s3_sublist(data_folder, creds_path=None, keep_prefix=True): def get_file_list(base_directory, creds_path=None, write_txt=None, write_pkl=None, write_info=False): + """Return a list of input and data file paths either stored locally or on + an AWS S3 bucket on the cloud.""" import os @@ -307,51 +309,20 @@ def extract_scan_params_csv(scan_params_csv): return site_dict -def extract_scan_params_json(scan_params_json): - - # TODO: this is never used in data config builder- do we/will we need it? - - import json - - with open(scan_params_json, "r") as f: - params_dct = json.load(f) - - return params_dct - - -def format_incl_excl_dct(site_incl_list=None, participant_incl_list=None, - session_incl_list=None, scan_incl_list=None): +def format_incl_excl_dct(incl_list=None, info_type='participants'): + """Create either an inclusion or exclusion dictionary to determine which + input files to include or not include in the data configuration file.""" incl_dct = {} - if isinstance(site_incl_list, str): - if '.txt' in site_incl_list: - with open(site_incl_list, 'r') as f: - incl_dct['sites'] = [x.rstrip("\n").replace(" ", "") for x in f.readlines() if x != ''] - elif isinstance(site_incl_list, list): - incl_dct['sites'] = site_incl_list - - if isinstance(participant_incl_list, str): - if '.txt' in participant_incl_list: - with open(participant_incl_list, 'r') as f: - incl_dct['participants'] = \ - [x.rstrip('\n').replace(" ", "") for x in f.readlines() if x != ''] - elif isinstance(participant_incl_list, list): - incl_dct['participants'] = participant_incl_list - - if isinstance(session_incl_list, str): - if '.txt' in session_incl_list: - with open(session_incl_list, 'r') as f: - incl_dct['sessions'] = [x.rstrip("\n").replace(" ", "") for x in f.readlines() if x != ''] - elif isinstance(session_incl_list, list): - incl_dct['sessions'] = session_incl_list - - if isinstance(scan_incl_list, str): - if '.txt' in scan_incl_list: - with open(scan_incl_list, 'r') as f: - incl_dct['scans'] = [x.rstrip("\n").replace(" ", "") for x in f.readlines() if x != ''] - elif isinstance(scan_incl_list, list): - incl_dct['scans'] = scan_incl_list + if isinstance(incl_list, str): + if '.txt' in incl_list: + with open(incl_list, 'r') as f: + incl_dct[info_type] = [x.rstrip("\n").replace(" ", "") for x in f.readlines() if x != ''] + elif ',' in incl_list: + incl_dct[info_type] = [x.replace(" ", "") for x in incl_list.split(",")] + elif isinstance(incl_list, list): + incl_dct[info_type] = incl_list return incl_dct @@ -359,6 +330,13 @@ def format_incl_excl_dct(site_incl_list=None, participant_incl_list=None, def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, aws_creds_path=None, inclusion_dct=None, exclusion_dct=None, config_dir=None): + """Return a data dictionary mapping input file paths to participant, + session, scan, and site IDs (where applicable) for a BIDS-formatted data + directory. + + This function prepares a file path template, then uses the + already-existing custom template function for producing a data dictionary. + """ import os import re @@ -388,7 +366,7 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, fmap_phase_sess = os.path.join(bids_base_dir, "sub-{participant}/ses-{session}/fmap/" - "sub-{participant}_ses-{session}_phase" + "sub-{participant}_ses-{session}_*_phase" "diff.nii.gz") fmap_phase = os.path.join(bids_base_dir, "sub-{participant}/fmap/sub-{participant}" @@ -396,11 +374,11 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, fmap_mag_sess = os.path.join(bids_base_dir, "sub-{participant}/ses-{session}/fmap/" - "sub-{participant}_ses-{session}_" - "magnitude1.nii.gz") + "sub-{participant}_ses-{session}_*_" + "magnitud*.nii.gz") fmap_mag = os.path.join(bids_base_dir, "sub-{participant}/fmap/sub-{participant}" - "_magnitude1.nii.gz") + "_magnitud*.nii.gz") sess_glob = os.path.join(bids_base_dir, "sub-*/ses-*/*") @@ -706,22 +684,393 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, return data_dct +def find_unique_scan_params(scan_params_dct, site_id, sub_id, ses_id, + scan_id): + """Return the scan parameters information stored in the provided scan + parameters dictionary for the IDs of a specific functional input scan.""" + + scan_params = None + + if site_id not in scan_params_dct.keys(): + site_id = "All" + if sub_id not in scan_params_dct[site_id].keys(): + sub_id = "All" + if ses_id not in scan_params_dct[site_id][sub_id].keys(): + ses_id = "All" + if scan_id not in scan_params_dct[site_id][sub_id][ses_id].keys(): + for key in scan_params_dct[site_id][sub_id][ses_id].keys(): + # scan_id (incoming file path) might have run- or acq-, if + # this is a BIDS dataset, such as "acq-inv1_run-1_BOLD" + # however, the scan ID keys here in scan_params_dct might + # have something like "run-1_BOLD", meaning this also + # applies to the "acq-inv1_run-1_BOLD" scan! + # + # "key" will always be the smaller/equal of the scan tags + if key in scan_id: + scan_id = key + break + else: + scan_id = "All" + + print scan_params_dct.keys() + print "{0} {1} {2} {3}".format(site_id, sub_id, ses_id, scan_id) + + try: + scan_params = scan_params_dct[site_id][sub_id][ses_id][scan_id] + except TypeError: + # this ideally should never fire + err = "\n[!] The scan parameters dictionary supplied to the data " \ + "configuration builder is not in the proper format.\n\n The " \ + "key combination that caused this error:\n{0}, {1}, {2}, {3}" \ + "\n\n".format(site_id, sub_id, ses_id, scan_id) + raise Exception(err) + except KeyError: + pass + + if not scan_params: + warn = "\n[!] No scan parameter information found in your scan " \ + "parameter configuration for the functional input file:\n" \ + "site: {0}, participant: {1}, session: {2}, series: {3}\n\n" \ + "".format(site_id, sub_id, ses_id, scan_id) + print warn + + return scan_params + + +def update_data_dct(file_path, file_template, data_dct=None, data_type="anat", + anat_scan=None, sites_dct=None, scan_params_dct=None, + inclusion_dct=None, exclusion_dct=None, + aws_creds_path=None, verbose=True): + """Return a data dictionary with a new file path parsed and added in, + keyed with its appropriate ID labels.""" + + import os + import glob + + if not data_dct: + data_dct = {} + + # NIFTIs only + if '.nii' not in file_path: + return data_dct + + if data_type == "anat": + # pick the right anatomical scan + if anat_scan: + if anat_scan not in os.path.basename(file_path): + return data_dct + + # reduce the template down to only the substrings that do not have + # these tags or IDs + + # Example + # file_template = + # /path/to/{site}/sub-{participant}/ses-{session}/func/ + # sub-{participant}_ses-{session}_task-{scan}_bold.nii.gz + site_parts = file_template.split('{site}') + + # Example, cont. + # site_parts = + # ['/path/to/', '/sub-{participant}/ses-{session}/..'] + + partic_parts = [] + for part in site_parts: + partic_parts = partic_parts + part.split('{participant}') + + # Example, cont. + # partic_parts = + # ['/path/to/', '/sub-', '/ses-{session}/func/sub-', '_ses-{session}..'] + + ses_parts = [] + for part in partic_parts: + ses_parts = ses_parts + part.split('{session}') + + # Example, cont. + # ses_parts = + # ['/path/to/', '/sub-', '/ses-', '/func/sub-', '_ses-', + # '_task-{scan}_bold.nii.gz'] + + if data_type == "anat": + parts = ses_parts + else: + # if functional, or field map files + parts = [] + for part in ses_parts: + parts = parts + part.split('{scan}') + + # Example, cont. + # parts = ['/path/to/', '/sub-', '/ses-', '/func/sub-', '_ses-', + # '_task-', '_bold.nii.gz'] + + if "*" in file_template: + wild_parts = [] + for part in parts: + wild_parts = wild_parts + part.split('*') + parts = wild_parts + + new_template = file_template + new_path = file_path + + # go through the non-label/non-ID substrings and parse them out, + # going from left to right and chopping out both sides of the whole + # string until only the tag, and the ID, are left + path_dct = {} + for idx in range(0, len(parts)): + part1 = parts[idx] + try: + part2 = parts[idx + 1] + except IndexError: + break + + label = new_template.split(part1, 1)[1] + label = label.split(part2, 1)[0] + + if label == "*": + # if current key is a wildcard + continue + + id = new_path.split(part1, 1)[1] + id = id.split(part2, 1)[0] + + # example, ideally at this point, something like this: + # template: /path/to/sub-{participant}/etc. + # filepath: /path/to/sub-200/etc. + # label = {participant} + # id = '200' + + if label not in path_dct.keys(): + path_dct[label] = id + skip = False + else: + if path_dct[label] != id: + warn = "\n\n[!] WARNING: While parsing your input data " \ + "files, a file path was found with conflicting " \ + "IDs for the same data level.\n\n" \ + "File path: {0}\n" \ + "Level: {1}\n" \ + "Conflicting IDs: {2}, {3}\n\n" \ + "Thus, we can't tell which {4} it belongs to, and " \ + "whether this file should be included or excluded! " \ + "Therefore, this file has not been added to the " \ + "data configuration.".format(file_path, label, + path_dct[label], id, + label.replace("{", "").replace("}", "")) + print warn + skip = True + break + + new_template = new_template.replace(part1, '', 1) + new_template = new_template.replace(label, '', 1) + + new_path = new_path.replace(part1, '', 1) + new_path = new_path.replace(id, '', 1) + + if skip: + return data_dct + + sub_id = path_dct['{participant}'] + + if '{site}' in path_dct.keys(): + site_id = path_dct['{site}'] + elif sites_dct: + # mainly if we're pulling site info from a participants.tsv file + # for a BIDS data set + try: + site_id = sites_dct[sub_id] + except KeyError: + site_id = "site-1" + else: + site_id = 'site-1' + + if '{session}' in path_dct.keys(): + ses_id = path_dct['{session}'] + else: + ses_id = 'ses-1' + + if data_type != "anat": + if '{scan}' in path_dct.keys(): + scan_id = path_dct['{scan}'] + else: + if data_type == "func": + scan_id = 'func-1' + else: + # field map files - keep these open as "None" so that they + # can be applied to all scans, if there isn't one specified + scan_id = None + + if inclusion_dct: + if 'sites' in inclusion_dct.keys(): + if site_id not in inclusion_dct['sites']: + return data_dct + if 'sessions' in inclusion_dct.keys(): + if ses_id not in inclusion_dct['sessions']: + return data_dct + if 'participants' in inclusion_dct.keys(): + if sub_id not in inclusion_dct['participants']: + return data_dct + if data_type == "func": + if 'scans' in inclusion_dct.keys(): + if scan_id not in inclusion_dct['scans']: + return data_dct + + if exclusion_dct: + if 'sites' in exclusion_dct.keys(): + if site_id in exclusion_dct['sites']: + return data_dct + if 'sessions' in exclusion_dct.keys(): + if ses_id in exclusion_dct['sessions']: + return data_dct + if 'participants' in exclusion_dct.keys(): + if sub_id in exclusion_dct['participants']: + return data_dct + if data_type == "func": + if 'scans' in exclusion_dct.keys(): + if scan_id in exclusion_dct['scans']: + return data_dct + + # start the data dictionary updating + if data_type == "anat": + if "*" in file_path: + if "s3://" in file_path: + err = "\n\n[!] Cannot use wildcards (*) in AWS S3 bucket " \ + "(s3://) paths!" + raise Exception(err) + paths = glob.glob(file_path) + + temp_sub_dct = {'subject_id': sub_id, + 'unique_id': ses_id, + 'site': site_id, + 'anat': file_path, + 'creds_path': aws_creds_path} + + if site_id not in data_dct.keys(): + data_dct[site_id] = {} + if sub_id not in data_dct[site_id].keys(): + data_dct[site_id][sub_id] = {} + if ses_id not in data_dct[site_id][sub_id].keys(): + data_dct[site_id][sub_id][ses_id] = temp_sub_dct + else: + # doubt this ever happens, but just be safe + warn = "\n\n[!] WARNING: Duplicate site-participant-session " \ + "entry found in your input data directory!\n\nDuplicate " \ + "sets:\n\n{0}\n\n{1}\n\nOnly adding the first one to " \ + "the data configuration file." \ + "\n\n".format(str(data_dct[site_id][sub_id][ses_id]), + str(temp_sub_dct)) + print warn + + elif data_type == "func": + + temp_func_dct = {scan_id: {"scan": file_path}} + + # scan parameters time + scan_params = None + if scan_params_dct: + scan_params = find_unique_scan_params(scan_params_dct, site_id, + sub_id, ses_id, scan_id) + if scan_params: + temp_func_dct[scan_id].update( + {'scan_parameters': scan_params}) + + if site_id not in data_dct.keys(): + if verbose: + print "No anatomical found for functional for site {0}:" \ + "\n{1}\n".format(site_id, file_path) + return data_dct + if sub_id not in data_dct[site_id].keys(): + if verbose: + print "No anatomical found for functional for participant " \ + "{0}:\n{1}\n".format(sub_id, file_path) + return data_dct + if ses_id not in data_dct[site_id][sub_id].keys(): + if verbose: + print "No anatomical found for functional for session {0}:" \ + "\n{1}\n".format(ses_id, file_path) + return data_dct + + if 'func' not in data_dct[site_id][sub_id][ses_id].keys(): + data_dct[site_id][sub_id][ses_id]['func'] = temp_func_dct + else: + data_dct[site_id][sub_id][ses_id]['func'].update(temp_func_dct) + + else: + # field map files! + if "run-" in file_path or "acq-" in file_path: + # check for run- and acq- tags, for BIDS datasets + run_id = None + acq_id = None + tags = file_path.split("/")[-1].split("_") + for tag in tags: + if "run-" in tag: + run_id = tag + if "acq-" in tag: + acq_id = tag + + # TODO: re-visit in the future + # this is a little iffy, because we're checking the contents of + # the data_dct for already-existing functional entries - this is + # assuming that all of the functionals have already been populated + # into the data_dct - this is always the case, but only because of + # how we use the function in "get_nonBIDS_data" below + # but what if we want to use this manually as part of an API? + try: + scan_ids = data_dct[site_id][sub_id][ses_id]['func'] + for func_scan_id in scan_ids: + if run_id and acq_id: + if run_id and acq_id in func_scan_id: + scan_id = func_scan_id + elif run_id: + if run_id in func_scan_id and \ + "acq-" not in func_scan_id: + scan_id = func_scan_id + elif acq_id: + if acq_id in func_scan_id and \ + "run-" not in func_scan_id: + scan_id = func_scan_id + except KeyError: + # TODO: error msg + pass + + temp_fmap_dct = {data_type: file_path} + + if site_id not in data_dct.keys(): + if verbose: + print "No anatomical found for field map file for site {0}:" \ + "\n{1}\n".format(site_id, file_path) + return data_dct + if sub_id not in data_dct[site_id].keys(): + if verbose: + print "No anatomical found for field map file for " \ + "participant {0}:\n{1}\n".format(sub_id, file_path) + return data_dct + if ses_id not in data_dct[site_id][sub_id].keys(): + if verbose: + print "No anatomical found for field map file for session " \ + "{0}:\n{1}\n".format(ses_id, file_path) + return data_dct + + if 'func' not in data_dct[site_id][sub_id][ses_id].keys(): + # if no scan ID specified, nest it under the "func" key in the + # dictionary, so that it will apply to all scans that do not have + # their own assigned field map files (nested under the scan ID + # key in the dictionary) + data_dct[site_id][sub_id][ses_id]['func'] = temp_fmap_dct + elif scan_id not in data_dct[site_id][sub_id][ses_id]['func'].keys(): + data_dct[site_id][sub_id][ses_id]['func'][scan_id] = temp_fmap_dct + else: + data_dct[site_id][sub_id][ses_id]['func'][scan_id].update(temp_fmap_dct) + + return data_dct + + def get_nonBIDS_data(anat_template, func_template, file_list=None, anat_scan=None, scan_params_dct=None, fmap_phase_template=None, fmap_mag_template=None, aws_creds_path=None, inclusion_dct=None, exclusion_dct=None, sites_dct=None, verbose=False): + """Prepare a data dictionary for the data configuration file when given + file path templates describing the input data directories.""" - # go over the file paths, validate for nifti's? - # work with the template - - # test cases: - # - no anats, no funcs - # - combination of .nii and .nii.gz? - # - throw error/warning if anat and func templates are identical - # - all permutations of scan parameters json/csv's at different levels - - import os import glob import fnmatch @@ -758,6 +1107,7 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, "(anatomical_scan) setting for more information.\n\n" raise Exception(err) + # replace the keywords with wildcards for keyword in keywords: if keyword in anat_glob: anat_glob = anat_glob.replace(keyword, '*') @@ -782,476 +1132,23 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, anat_local_pool = glob.glob(anat_glob) func_local_pool = glob.glob(func_glob) + # anat_pool and func_pool are now lists with (presumably) all of the file + # paths that match the templates entered anat_pool = anat_pool + anat_local_pool func_pool = func_pool + func_local_pool - data_dct = {} - # pull out the site/participant/etc. IDs from each path and connect them + data_dct = {} for anat_path in anat_pool: + data_dct = update_data_dct(anat_path, anat_template, data_dct, "anat", + anat_scan, sites_dct, None, inclusion_dct, + exclusion_dct, aws_creds_path) - # NIFTIs only - if '.nii' not in anat_path: - continue - - # pick the right anatomical scan - if anat_scan: - if anat_scan not in os.path.basename(anat_path): - continue - - path_dct = {} - - # reduce the template down to only the substrings that do not have - # these tags or IDs - site_parts = anat_template.split('{site}') - - partic_parts = [] - for part in site_parts: - partic_parts = partic_parts + part.split('{participant}') - ses_parts = [] - for part in partic_parts: - ses_parts = ses_parts + part.split('{session}') - if "*" in anat_template: - wild_parts = [] - for part in ses_parts: - wild_parts = wild_parts + part.split('*') - ses_parts = wild_parts - - new_template = anat_template - new_path = anat_path - - # go through the non-label/non-ID substrings and parse them out, - # going from left to right and chopping out both sides of the whole - # string until only the tag, and the ID, are left - for idx in range(0, len(ses_parts)): - part1 = ses_parts[idx] - try: - part2 = ses_parts[idx+1] - except IndexError: - break - - # example: /home/{site}/ses-{session}/.. - # part1 = /home/, part2 = /ses- - # first split -> ['', '{site}/ses-{session}/..'] - # (pick second item) - # second split -> ['{site}', '{session}/..'] - # (pick first item) - label = new_template.split(part1, 1)[1] - label = label.split(part2, 1)[0] - - if label == "*": - continue - - id = new_path.split(part1, 1)[1] - id = id.split(part2, 1)[0] - - if label not in path_dct.keys(): - path_dct[label] = id - skip = False - else: - warn = None - if path_dct[label] != id: - if str(path_dct[label]) in id: - warn = "\n\n[!] WARNING: Multiple anatomical " \ - "scans found for a single participant. " \ - "One anatomical scan must be selected " \ - "for the pipeline run. You can use the " \ - "'Which Anatomical Scan?' (anatomical_" \ - "scan) setting in the data config" \ - "uration builder to select which one to " \ - "use. Otherwise, review the completed " \ - "data configuration file to ensure the " \ - "proper scans are included together.\n\n" \ - "Scan not included:\n{0}" \ - "\n\n".format(anat_path) - else: - warn = "\n\n[!] WARNING: While parsing your input " \ - "data files, a file path was found with " \ - "conflicting IDs for the same data level." \ - "\n\nFile path: {0}\nLevel: {1}\n" \ - "Conflicting IDs: {2}, {3}\n\n" \ - "This file has not been added to the data " \ - "configuration.".format(anat_path, label, - path_dct[label], id) - if warn: - print warn - skip = True - break - else: - skip = False - pass - - new_template = new_template.replace(part1, '', 1) - new_template = new_template.replace(label, '', 1) - - new_path = new_path.replace(part1, '', 1) - new_path = new_path.replace(id, '', 1) - - if skip: - continue - - sub_id = path_dct['{participant}'] - - if '{site}' in path_dct.keys(): - site_id = path_dct['{site}'] - elif sites_dct: - # mainly if we're pulling site info from a participants.tsv file - # for a BIDS data set - try: - site_id = sites_dct[sub_id] - except KeyError: - site_id = "site-1" - else: - site_id = 'site-1' - - if '{session}' in path_dct.keys(): - ses_id = path_dct['{session}'] - else: - ses_id = 'ses-1' - - if inclusion_dct: - if 'sites' in inclusion_dct.keys(): - if site_id not in inclusion_dct['sites']: - continue - if 'sessions' in inclusion_dct.keys(): - if ses_id not in inclusion_dct['sessions']: - continue - if 'participants' in inclusion_dct.keys(): - if sub_id not in inclusion_dct['participants']: - continue - - if exclusion_dct: - if 'sites' in exclusion_dct.keys(): - if site_id in exclusion_dct['sites']: - continue - if 'sessions' in exclusion_dct.keys(): - if ses_id in exclusion_dct['sessions']: - continue - if 'participants' in exclusion_dct.keys(): - if sub_id in exclusion_dct['participants']: - continue - - if "*" in anat_path: - if "s3://" in anat_path: - err = "\n\n[!] Cannot use wildcards (*) in AWS S3 bucket " \ - "(s3://) paths!" - paths = glob.glob(anat_path) - - temp_sub_dct = {'subject_id': sub_id, - 'unique_id': ses_id, - 'site': site_id, - 'anat': anat_path, - 'creds_path': aws_creds_path} - - if site_id not in data_dct.keys(): - data_dct[site_id] = {} - if sub_id not in data_dct[site_id].keys(): - data_dct[site_id][sub_id] = {} - if ses_id not in data_dct[site_id][sub_id].keys(): - data_dct[site_id][sub_id][ses_id] = temp_sub_dct - else: - # doubt this ever happens, but just be safe - warn = "\n\n[!] WARNING: Duplicate site-participant-session " \ - "entry found in your input data directory!\n\nDuplicate " \ - "sets:\n\n{0}\n\n{1}\n\nOnly adding the first one to " \ - "the data configuration file." \ - "\n\n".format(str(data_dct[site_id][sub_id][ses_id]), - str(temp_sub_dct)) - print warn - - # functional time for func_path in func_pool: - - # NIFTIs only - if '.nii' not in func_path: - continue - - path_dct = {} - - site_parts = func_template.split('{site}') - - partic_parts = [] - for part in site_parts: - partic_parts = partic_parts + part.split('{participant}') - ses_parts = [] - for part in partic_parts: - ses_parts = ses_parts + part.split('{session}') - scan_parts = [] - for part in ses_parts: - scan_parts = scan_parts + part.split('{scan}') - if "*" in func_template: - wild_parts = [] - for part in scan_parts: - wild_parts = wild_parts + part.split('*') - scan_parts = wild_parts - - new_template = func_template - new_path = func_path - - for idx in range(0, len(scan_parts)): - part1 = scan_parts[idx] - try: - part2 = scan_parts[idx+1] - except IndexError: - break - - label = new_template.split(part1, 1)[1] - label = label.split(part2, 1)[0] - - if label == "*": - continue - - id = new_path.split(part1, 1)[1] - id = id.split(part2, 1)[0] - - if label not in path_dct.keys(): - path_dct[label] = id - skip = False - else: - if path_dct[label] != id: - warn = "\n\n[!] WARNING: While parsing your input data " \ - "files, a file path was found with conflicting " \ - "IDs for the same data level.\n\n" \ - "File path: {0}\n" \ - "Level: {1}\n" \ - "Conflicting IDs: {2}, {3}\n\n" \ - "This file has not been added to the data " \ - "configuration.".format(func_path, label, - path_dct[label], id) - print warn - skip = True - break - - new_template = new_template.replace(part1, '', 1) - new_template = new_template.replace(label, '', 1) - - new_path = new_path.replace(part1, '', 1) - new_path = new_path.replace(id, '', 1) - - if skip: - continue - - sub_id = path_dct['{participant}'] - - if '{site}' in path_dct.keys(): - site_id = path_dct['{site}'] - elif sites_dct: - # mainly if we're pulling site info from a participants.tsv file - # for a BIDS data set - try: - site_id = sites_dct[sub_id] - except KeyError: - site_id = "site-1" - else: - site_id = 'site-1' - - if '{session}' in path_dct.keys(): - ses_id = path_dct['{session}'] - else: - ses_id = 'ses-1' - - if '{scan}' in path_dct.keys(): - scan_id = path_dct['{scan}'] - else: - scan_id = 'func-1' - - if inclusion_dct: - if 'sites' in inclusion_dct.keys(): - if site_id not in inclusion_dct['sites']: - continue - if 'sessions' in inclusion_dct.keys(): - if ses_id not in inclusion_dct['sessions']: - continue - if 'participants' in inclusion_dct.keys(): - if sub_id not in inclusion_dct['participants']: - continue - if 'scans' in inclusion_dct.keys(): - if scan_id not in inclusion_dct['scans']: - continue - - if exclusion_dct: - if 'sites' in exclusion_dct.keys(): - if site_id in exclusion_dct['sites']: - continue - if 'sessions' in exclusion_dct.keys(): - if ses_id in exclusion_dct['sessions']: - continue - if 'participants' in exclusion_dct.keys(): - if sub_id in exclusion_dct['participants']: - continue - if 'scans' in exclusion_dct.keys(): - if scan_id in exclusion_dct['scans']: - continue - - temp_func_dct = {scan_id: {"scan": func_path}} - - # scan parameters time - scan_params = None - - if scan_params_dct: - - if site_id in scan_params_dct.keys(): - # all site-specific - if sub_id in scan_params_dct[site_id].keys(): - # all site-specific and sub-specific - if ses_id in scan_params_dct[site_id][sub_id].keys(): - if scan_id in scan_params_dct[site_id][sub_id][ses_id].keys(): - # site, sub, session, scan specific scan params - scan_params = scan_params_dct[site_id][sub_id][ses_id][scan_id] - elif "run-" in scan_id or "acq-" in scan_id: - # not scan specific, but run and/or acquisition - # specific - run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) - if run_acq_id in scan_params_dct[site_id][sub_id][ses_id].keys(): - scan_params = scan_params_dct[site_id][sub_id][ses_id][run_acq_id] - elif 'All' in scan_params_dct[site_id][sub_id][ses_id].keys(): - # site, sub, session specific scan params - scan_params = scan_params_dct[site_id][sub_id][ses_id]['All'] - elif 'All' in scan_params_dct[site_id][sub_id].keys(): - if scan_id in scan_params_dct[site_id][sub_id]['All'].keys(): - # site, sub, scan specific scan params - scan_params = scan_params_dct[site_id][sub_id]['All'][scan_id] - elif "run-" in scan_id or "acq-" in scan_id: - # not scan specific, but run and/or acquisition - # specific - run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) - if run_acq_id in scan_params_dct[site_id][sub_id]['All'].keys(): - scan_params = scan_params_dct[site_id][sub_id]['All'][run_acq_id] - elif 'All' in scan_params_dct[site_id][sub_id]['All'].keys(): - # site and sub specific scan params, same across - # all sessions and scans - scan_params = scan_params_dct[site_id][sub_id]['All']['All'] - elif "All" in scan_params_dct[site_id].keys(): - # same across all subs - if ses_id in scan_params_dct[site_id]["All"].keys(): - if scan_id in scan_params_dct[site_id]["All"][ses_id].keys(): - # site, session, scan specific, same across all - # subs - scan_params = scan_params_dct[site_id]["All"][ses_id][scan_id] - elif "run-" in scan_id or "acq-" in scan_id: - # not scan specific, but run and/or acquisition - # specific - run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) - if run_acq_id in scan_params_dct[site_id]["All"][ses_id].keys(): - scan_params = scan_params_dct[site_id]["All"][ses_id][run_acq_id] - elif "All" in scan_params_dct[site_id]["All"][ses_id].keys(): - # site and session specific scan params, same - # across all subs and scans - scan_params = scan_params_dct[site_id]["All"][ses_id]["All"] - elif "All" in scan_params_dct[site_id]["All"].keys(): - # TODO: but what about task-rest_acq-1 but for all runs??? - # TODO: scan_id would be rest_acq-1_run-1, but scan_params_dct tag would be rest_acq-1 - if scan_id in scan_params_dct[site_id]["All"]["All"].keys(): - # site and scan specific, same across all subs and - # all sessions - scan_params = scan_params_dct[site_id]["All"]["All"][scan_id] - elif "run-" in scan_id and "acq-" in scan_id: - # not scan specific, but run and acquisition - # specific - run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) - if run_acq_id in scan_params_dct[site_id]["All"]["All"].keys(): - scan_params = scan_params_dct[site_id]["All"]["All"][run_acq_id] - elif "acq-" in scan_id: - # not scan or run specific, but acquisition - # specific - acq_id = "[All]_{0}".format(scan_id.split("acq-")[1].split("_")[0]) - if site_id == "NKI_TRT": - print acq_id - print scan_params_dct[site_id]["All"]["All"].keys() - if acq_id in scan_params_dct[site_id]["All"]["All"].keys(): - scan_params = scan_params_dct[site_id]["All"]["All"][acq_id] - elif "All" in scan_params_dct[site_id]["All"]["All"].keys(): - # site specific scan params, same across all subs, - # sessions, and scans - scan_params = scan_params_dct[site_id]["All"]["All"]["All"] - - elif "All" in scan_params_dct.keys(): - # not site specific - if sub_id in scan_params_dct["All"].keys(): - # sub-specific - if ses_id in scan_params_dct["All"][sub_id].keys(): - if scan_id in scan_params_dct["All"][sub_id][ses_id].keys(): - # sub, session, scan specific, same across all - # sites (or no site specified) - scan_params = scan_params_dct["All"][sub_id][ses_id][scan_id] - elif "run-" in scan_id or "acq-" in scan_id: - # not scan specific, but run and/or acquisition - # specific - run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) - if run_acq_id in scan_params_dct["All"][sub_id][ses_id].keys(): - scan_params = scan_params_dct["All"][sub_id][ses_id][run_acq_id] - elif "All" in scan_params_dct["All"][sub_id][ses_id].keys(): - # sub, session specific, same across all scans and - # sites (or site and scan not specified) - scan_params = scan_params_dct["All"][sub_id][ses_id][scan_id]["All"] - elif "All" in scan_params_dct["All"][sub_id].keys(): - if scan_id in scan_params_dct["All"][sub_id]["All"].keys(): - # sub and scan specific, same across all sites and - # sessions (or site and session not specified) - scan_params = scan_params_dct["All"][sub_id]["All"][scan_id] - elif "run-" in scan_id or "acq-" in scan_id: - # not scan specific, but run and/or acquisition - # specific - run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) - if run_acq_id in scan_params_dct["All"][sub_id]["All"].keys(): - scan_params = scan_params_dct["All"][sub_id]["All"][run_acq_id] - elif "All" in scan_params_dct["All"][sub_id]["All"].keys(): - # sub specific, same across all sites, sessions, - # and scans (or site, session, and scan not - # specified) - scan_params = scan_params_dct["All"][sub_id]["All"]["All"] - elif "All" in scan_params_dct["All"].keys(): - if ses_id in scan_params_dct["All"]["All"].keys(): - if scan_id in scan_params_dct["All"]["All"][ses_id].keys(): - # session and scan specific only - scan_params = scan_params_dct["All"]["All"][ses_id][scan_id] - elif "run-" in scan_id or "acq-" in scan_id: - # not scan specific, but run and/or acquisition - # specific - run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) - if run_acq_id in scan_params_dct["All"]["All"][ses_id].keys(): - scan_params = scan_params_dct["All"]["All"][ses_id][run_acq_id] - elif "All" in scan_params_dct["All"]["All"][ses_id].keys(): - # session specific only - scan_params = scan_params_dct["All"]["All"][ses_id]["All"] - elif "All" in scan_params_dct["All"]["All"].keys(): - if scan_id in scan_params_dct["All"]["All"]["All"].keys(): - # scan specific only - scan_params = scan_params_dct["All"]["All"]["All"][scan_id] - elif "run-" in scan_id or "acq-" in scan_id: - # not scan specific, but run and/or acquisition - # specific - run_acq_id = "[All]_{0}".format(scan_id.split("_", 1)[1]) - if run_acq_id in scan_params_dct["All"]["All"]["All"].keys(): - scan_params = scan_params_dct["All"]["All"]["All"][run_acq_id] - elif "All" in scan_params_dct["All"]["All"]["All"].keys(): - # same scan params across all sites, subs, - # sessions, and scans - scan_params = scan_params_dct["All"]["All"]["All"]["All"] - - if scan_params: - temp_func_dct[scan_id].update({'scan_parameters': scan_params}) - - if site_id not in data_dct.keys(): - if verbose: - print "No anatomical for functional for site:" \ - "\n{0}\n{1}\n".format(func_path, site_id) - continue - if sub_id not in data_dct[site_id].keys(): - if verbose: - print "No anatomical for functional for participant:" \ - "\n{0}\n{1}\n".format(func_path, sub_id) - continue - if ses_id not in data_dct[site_id][sub_id].keys(): - if verbose: - print "No anatomical for functional for session:" \ - "\n{0}\n{1}\n".format(func_path, ses_id) - continue - - if 'func' not in data_dct[site_id][sub_id][ses_id].keys(): - data_dct[site_id][sub_id][ses_id]['func'] = temp_func_dct - else: - data_dct[site_id][sub_id][ses_id]['func'].update(temp_func_dct) + data_dct = update_data_dct(func_path, func_template, data_dct, "func", + None, sites_dct, scan_params_dct, + inclusion_dct, exclusion_dct, + aws_creds_path) if fmap_phase_template and fmap_mag_template: # if we're doing the whole field map distortion correction thing @@ -1291,279 +1188,25 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, fmap_mag_pool = glob.glob(fmap_mag_glob) for fmap_phase in fmap_phase_pool: - - path_dct = {} - - site_parts = fmap_phase_template.split('{site}') - - partic_parts = [] - for part in site_parts: - partic_parts = partic_parts + part.split('{participant}') - ses_parts = [] - for part in partic_parts: - ses_parts = ses_parts + part.split('{session}') - scan_parts = [] - for part in ses_parts: - scan_parts = scan_parts + part.split('{scan}') - if "*" in fmap_phase_template: - wild_parts = [] - for part in scan_parts: - wild_parts = wild_parts + part.split('*') - scan_parts = wild_parts - - new_template = fmap_phase_template - new_path = fmap_phase - - for idx in range(0, len(scan_parts)): - part1 = scan_parts[idx] - try: - part2 = scan_parts[idx+1] - except IndexError: - break - - label = new_template.split(part1, 1)[1] - label = label.split(part2, 1)[0] - - if label == "*": - continue - - id = new_path.split(part1, 1)[1] - id = id.split(part2, 1)[0] - - if label not in path_dct.keys(): - path_dct[label] = id - skip = False - else: - warn = None - if path_dct[label] != id: - ''' - if str(path_dct[label]) in id and "run-" in id: - # TODO: this is here only because we do not - # TODO: support scan-level nesting yet!!! - warn = "\n\n[!] WARNING: Multiple field map " \ - "phase or magnitude files were found " \ - "for a single session. Multiple-run " \ - "field map files are not supported yet. " \ - "Review the completed data " \ - "configuration file to ensure the " \ - "proper scans are included together.\n\n" \ - "File not included:\n{0}" \ - "\n\n".format(fmap_phase) - else: - ''' - warn = "\n\n[!] WARNING: While parsing your input data " \ - "files, a file path was found with conflicting " \ - "IDs for the same data level.\n\n" \ - "File path: {0}\n" \ - "Level: {1}\n" \ - "Conflicting IDs: {2}, {3}\n\n" \ - "This file has not been added to the data " \ - "configuration.".format(fmap_phase, label, - path_dct[label], id) - if warn: - print warn - skip = True - break - else: - skip = False - - new_template = new_template.replace(part1, '', 1) - new_template = new_template.replace(label, '', 1) - - new_path = new_path.replace(part1, '', 1) - new_path = new_path.replace(id, '', 1) - - if skip: - continue - - sub_id = path_dct['{participant}'] - - if '{site}' in path_dct.keys(): - site_id = path_dct['{site}'] - elif sites_dct: - # mainly if we're pulling site info from a participants.tsv - # file for a BIDS data set - try: - site_id = sites_dct[sub_id] - except KeyError: - site_id = "site-1" - else: - site_id = 'site-1' - - if '{session}' in path_dct.keys(): - ses_id = path_dct['{session}'] - else: - ses_id = 'ses-1' - - if '{scan}' in path_dct.keys(): - scan_id = path_dct['{scan}'] - else: - scan_id = None - - temp_fmap_dct = {"fmap_phase": fmap_phase} - - if site_id not in data_dct.keys(): - if verbose: - print "Missing inputs (no anat/func) for field map for " \ - "site:\n{0}\n{1}\n".format(fmap_phase, site_id) - continue - if sub_id not in data_dct[site_id].keys(): - if verbose: - print "Missing inputs (no anat/func) for field map for " \ - "participant:" \ - "\n{0}\n{1}\n".format(fmap_phase, sub_id) - continue - if ses_id not in data_dct[site_id][sub_id].keys(): - if verbose: - print "Missing inputs (no anat/func) for field map for " \ - "session:\n{0}\n{1}\n".format(fmap_phase, ses_id) - continue - - if scan_id: - data_dct[site_id][sub_id][ses_id]['func'][scan_id].update(temp_fmap_dct) - else: - for scan in data_dct[site_id][sub_id][ses_id]['func'].keys(): - data_dct[site_id][sub_id][ses_id]['func'][scan].update(temp_fmap_dct) + data_dct = update_data_dct(fmap_phase, fmap_phase_template, + data_dct, "fmap_phase", None, + sites_dct, scan_params_dct, + inclusion_dct, exclusion_dct, + aws_creds_path) for fmap_mag in fmap_mag_pool: - - path_dct = {} - - site_parts = fmap_mag_template.split('{site}') - - partic_parts = [] - for part in site_parts: - partic_parts = partic_parts + part.split('{participant}') - ses_parts = [] - for part in partic_parts: - ses_parts = ses_parts + part.split('{session}') - scan_parts = [] - for part in ses_parts: - scan_parts = scan_parts + part.split('{scan}') - if "*" in fmap_mag_template: - wild_parts = [] - for part in scan_parts: - wild_parts = wild_parts + part.split('*') - scan_parts = wild_parts - - new_template = fmap_mag_template - new_path = fmap_mag - - for idx in range(0, len(scan_parts)): - part1 = scan_parts[idx] - try: - part2 = scan_parts[idx + 1] - except IndexError: - break - - label = new_template.split(part1, 1)[1] - label = label.split(part2, 1)[0] - - if label == "*": - continue - - id = new_path.split(part1, 1)[1] - id = id.split(part2, 1)[0] - - if label not in path_dct.keys(): - path_dct[label] = id - skip = False - else: - warn = None - if path_dct[label] != id: - ''' - if str(path_dct[label]) in id and "run-" in id: - # TODO: this is here only because we do not - # TODO: support scan-level nesting yet!!! - warn = "\n\n[!] WARNING: Multiple field map " \ - "phase or magnitude files were found " \ - "for a single session. Multiple-run " \ - "field map files are not supported yet. " \ - "Review the completed data " \ - "configuration file to ensure the " \ - "proper scans are included together.\n\n" \ - "File not included:\n{0}" \ - "\n\n".format(fmap_phase) - else: - ''' - warn = "\n\n[!] WARNING: While parsing your input data " \ - "files, a file path was found with conflicting " \ - "IDs for the same data level.\n\n" \ - "File path: {0}\n" \ - "Level: {1}\n" \ - "Conflicting IDs: {2}, {3}\n\n" \ - "This file has not been added to the data " \ - "configuration.".format(fmap_mag, label, - path_dct[label], id) - if warn: - print warn - skip = True - break - else: - skip = False - - new_template = new_template.replace(part1, '', 1) - new_template = new_template.replace(label, '', 1) - - new_path = new_path.replace(part1, '', 1) - new_path = new_path.replace(id, '', 1) - - if skip: - continue - - sub_id = path_dct['{participant}'] - - if '{site}' in path_dct.keys(): - site_id = path_dct['{site}'] - elif sites_dct: - # mainly if we're pulling site info from a participants.tsv - # file for a BIDS data set - try: - site_id = sites_dct[sub_id] - except KeyError: - site_id = "site-1" - else: - site_id = 'site-1' - - if '{session}' in path_dct.keys(): - ses_id = path_dct['{session}'] - else: - ses_id = 'ses-1' - - if '{scan}' in path_dct.keys(): - scan_id = path_dct['{scan}'] - else: - scan_id = None - - temp_fmap_dct = {"fmap_mag": fmap_mag} - - if site_id not in data_dct.keys(): - if verbose: - print "Missing inputs (no anat/func) for field map for " \ - "site:\n{0}\n{1}\n".format(fmap_mag, site_id) - continue - if sub_id not in data_dct[site_id].keys(): - if verbose: - print "Missing inputs (no anat/func) for field map for " \ - "participant:" \ - "\n{0}\n{1}\n".format(fmap_mag, sub_id) - continue - if ses_id not in data_dct[site_id][sub_id].keys(): - if verbose: - print "Missing inputs (no anat/func) for field map for " \ - "session:\n{0}\n{1}\n".format(fmap_mag, ses_id) - continue - - if scan_id: - data_dct[site_id][sub_id][ses_id]['func'][scan_id].update(temp_fmap_dct) - else: - for scan in data_dct[site_id][sub_id][ses_id]['func'].keys(): - data_dct[site_id][sub_id][ses_id]['func'][scan].update(temp_fmap_dct) + data_dct = update_data_dct(fmap_mag, fmap_mag_template, + data_dct, "fmap_mag", None, + sites_dct, scan_params_dct, + inclusion_dct, exclusion_dct, + aws_creds_path) return data_dct def run(data_settings_yml): + """Generate and write out a CPAC data configuration (participant list) + YAML file.""" import os import yaml @@ -1581,15 +1224,23 @@ def run(data_settings_yml): "none" in settings_dct["anatomical_scan"]: settings_dct["anatomical_scan"] = None - incl_dct = format_incl_excl_dct(settings_dct['siteList'], - settings_dct['subjectList'], - settings_dct['sessionList'], - settings_dct['scanList']) + # inclusion lists + incl_dct = format_incl_excl_dct(settings_dct['siteList'], 'sites') + incl_dct.update(format_incl_excl_dct(settings_dct['subjectList'], + 'participants')) + incl_dct.update(format_incl_excl_dct(settings_dct['sessionList'], + 'sessions')) + incl_dct.update(format_incl_excl_dct(settings_dct['scanList'], 'scans')) + # exclusion lists excl_dct = format_incl_excl_dct(settings_dct['exclusionSiteList'], - settings_dct['exclusionSubjectList'], - settings_dct['exclusionSessionList'], - settings_dct['exclusionScanList']) + 'sites') + excl_dct.update(format_incl_excl_dct(settings_dct['exclusionSubjectList'], + 'participants')) + excl_dct.update(format_incl_excl_dct(settings_dct['exclusionSessionList'], + 'sessions')) + excl_dct.update(format_incl_excl_dct(settings_dct['exclusionScanList'], + 'scans')) if 'BIDS' in settings_dct['dataFormat'] or \ 'bids' in settings_dct['dataFormat']: From 911e6b8d60f85de245569fdd0f9ead12a24f162c Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Thu, 5 Apr 2018 16:33:17 -0400 Subject: [PATCH 32/75] Fixed the scan inclusion/exclusion and field map granularity in the data config builder, and fixed some errors. --- CPAC/pipeline/cpac_pipeline.py | 2 - CPAC/utils/build_data_config.py | 91 ++++++++++++++++++++++++++------- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 6b659389bd..61f9a98b8f 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -675,9 +675,7 @@ def getNodeList(strategy): ants_reg_anat_mni.inputs.inputspec.reference_skull = \ c.template_skull_for_anat - else: - node, out_file = strat.get_node_from_resource_pool( 'anatomical_brain') diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index bc41c676dc..bc73dd402d 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -309,7 +309,7 @@ def extract_scan_params_csv(scan_params_csv): return site_dict -def format_incl_excl_dct(incl_list=None, info_type='participants'): +def format_incl_excl_dct(incl_list, info_type='participants'): """Create either an inclusion or exclusion dictionary to determine which input files to include or not include in the data configuration file.""" @@ -320,7 +320,13 @@ def format_incl_excl_dct(incl_list=None, info_type='participants'): with open(incl_list, 'r') as f: incl_dct[info_type] = [x.rstrip("\n").replace(" ", "") for x in f.readlines() if x != ''] elif ',' in incl_list: - incl_dct[info_type] = [x.replace(" ", "") for x in incl_list.split(",")] + incl_dct[info_type] = \ + [x.replace(" ", "") for x in incl_list.split(",")] + elif incl_list: + # if there's only one item in the box, most common probably + if "None" in incl_list or "none" in incl_list: + return incl_dct + incl_dct[info_type] = incl_list elif isinstance(incl_list, list): incl_dct[info_type] = incl_list @@ -352,8 +358,8 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, "sub-{participant}/anat/sub-{participant}_T1w.nii.gz") if anat_scan: - anat_sess = anat_sess.replace("_T1w", "_*_T1w") - anat = anat.replace("_T1w", "_*_T1w") + anat_sess = anat_sess.replace("_T1w", "_*T1w") #"_*_T1w") + anat = anat.replace("_T1w", "_*T1w") #"_*_T1w") func_sess = os.path.join(bids_base_dir, "sub-{participant}" @@ -417,9 +423,14 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, import fnmatch for filepath in file_list: - if fnmatch.fnmatch(filepath, site_dir_glob) and \ "derivatives" not in filepath: + print filepath + + ''' + HERE -- why adding site glob to NKI-RS??? + ''' + # check if there is a directory level encoding site ID, even # though that is not BIDS format site_dir = True @@ -433,8 +444,8 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, part_tsv = filepath for glob_str in site_json_globs: - site_dir = True if fnmatch.fnmatch(filepath, glob_str): + site_dir = True site_jsons.append(filepath) for glob_str in json_globs: @@ -663,6 +674,7 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, if ses: # if there is a session level in the BIDS dataset data_dct = get_nonBIDS_data(anat_sess, func_sess, file_list=file_list, + anat_scan=anat_scan, scan_params_dct=scan_params_dct, fmap_phase_template=fmap_phase_sess, fmap_mag_template=fmap_mag_sess, @@ -673,6 +685,7 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, else: # no session level data_dct = get_nonBIDS_data(anat, func, file_list=file_list, + anat_scan=anat_scan, scan_params_dct=scan_params_dct, fmap_phase_template=fmap_phase, fmap_mag_template=fmap_mag, @@ -712,9 +725,6 @@ def find_unique_scan_params(scan_params_dct, site_id, sub_id, ses_id, else: scan_id = "All" - print scan_params_dct.keys() - print "{0} {1} {2} {3}".format(site_id, sub_id, ses_id, scan_id) - try: scan_params = scan_params_dct[site_id][sub_id][ses_id][scan_id] except TypeError: @@ -908,7 +918,7 @@ def update_data_dct(file_path, file_template, data_dct=None, data_type="anat", if 'participants' in inclusion_dct.keys(): if sub_id not in inclusion_dct['participants']: return data_dct - if data_type == "func": + if data_type != "anat": if 'scans' in inclusion_dct.keys(): if scan_id not in inclusion_dct['scans']: return data_dct @@ -923,7 +933,7 @@ def update_data_dct(file_path, file_template, data_dct=None, data_type="anat", if 'participants' in exclusion_dct.keys(): if sub_id in exclusion_dct['participants']: return data_dct - if data_type == "func": + if data_type != "anat": if 'scans' in exclusion_dct.keys(): if scan_id in exclusion_dct['scans']: return data_dct @@ -941,7 +951,7 @@ def update_data_dct(file_path, file_template, data_dct=None, data_type="anat", 'unique_id': ses_id, 'site': site_id, 'anat': file_path, - 'creds_path': aws_creds_path} + 'creds_path': str(aws_creds_path)} if site_id not in data_dct.keys(): data_dct[site_id] = {} @@ -969,8 +979,7 @@ def update_data_dct(file_path, file_template, data_dct=None, data_type="anat", scan_params = find_unique_scan_params(scan_params_dct, site_id, sub_id, ses_id, scan_id) if scan_params: - temp_func_dct[scan_id].update( - {'scan_parameters': scan_params}) + temp_func_dct[scan_id].update({'scan_parameters': str(scan_params)}) if site_id not in data_dct.keys(): if verbose: @@ -1137,19 +1146,53 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, anat_pool = anat_pool + anat_local_pool func_pool = func_pool + func_local_pool + if not anat_pool: + err = "\n\n[!] No anatomical input file paths found given the data " \ + "settings provided.\n\nAnatomical file template being used: " \ + "{0}\n".format(anat_glob) + if anat_scan: + err = "{0}Anatomical scan identifier provided: {1}" \ + "\n\n".format(err, anat_scan) + raise Exception(err) + # pull out the site/participant/etc. IDs from each path and connect them + # for the anatomicals data_dct = {} for anat_path in anat_pool: data_dct = update_data_dct(anat_path, anat_template, data_dct, "anat", anat_scan, sites_dct, None, inclusion_dct, exclusion_dct, aws_creds_path) + if not data_dct: + # this fires if no anatomicals were found + # collect some possible examples of anat files that got missed + possible_anats = [] + for anat_path in anat_pool: + if "T1w" in anat_path or "mprage" in anat_path: + possible_anats.append(anat_path) + + err = "\n\n[!] No anatomical input files were found given the " \ + "data settings provided.\n\n" + if possible_anats: + err = "{0}There are some file paths found in the directories " \ + "described in the data settings that may be anatomicals " \ + "that were missed. Here are a few examples:\n".format(err) + for anat in possible_anats[0:5]: + err = "{0}{1}\n".format(err, anat) + err = "{0}\nIf you are using the 'anatomical_scan' option in " \ + "the data settings, check the setting to make sure " \ + "you are properly selecting which anatomical scan to " \ + "use for your analysis.\n\n".format(err) + raise Exception(err) + + # now gather the functionals for func_path in func_pool: data_dct = update_data_dct(func_path, func_template, data_dct, "func", None, sites_dct, scan_params_dct, inclusion_dct, exclusion_dct, aws_creds_path) + # do the same for the fieldmap files, if applicable if fmap_phase_template and fmap_mag_template: # if we're doing the whole field map distortion correction thing @@ -1310,6 +1353,10 @@ def run(data_settings_yml): if len(data_dct) > 0: + # TODO: make this a toggle option later for when we want anat-only + # TODO: data configs, i.e. for preprocessing only + anats_only = False + # get some data num_sites = len(data_dct.keys()) num_subs = num_sess = num_scan = 0 @@ -1329,10 +1376,22 @@ def run(data_settings_yml): # put data_dct contents in an ordered list for the YAML dump data_list = [] + included = {'site': [], 'sub': [], 'ses': [], 'scan': []} for site in sorted(data_dct.keys()): for sub in sorted(data_dct[site].keys()): for ses in sorted(data_dct[site][sub].keys()): + if not anats_only: + # avoiding including anatomicals if there are no + # functionals associated with it (i.e. if we're using + # scan inclusion/exclusion and only some participants + # have the scans included) + if 'func' in data_dct[site][sub][ses]: + # TODO: put the numbers here instead, using + # TODO: "included" + pass + else: + continue data_list.append(data_dct[site][sub][ses]) with open(data_config_outfile, "wt") as f: @@ -1363,7 +1422,3 @@ def run(data_settings_yml): err = "\n\n[!] No anatomical input files were found given the data " \ "settings provided.\n\n" raise Exception(err) - - - - From 2fec1337670f7255d753da7be86c9f0158f2e497 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Thu, 5 Apr 2018 16:52:07 -0400 Subject: [PATCH 33/75] Changing the scan counting. --- CPAC/utils/build_data_config.py | 34 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index bc73dd402d..1874736376 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -1357,17 +1357,17 @@ def run(data_settings_yml): # TODO: data configs, i.e. for preprocessing only anats_only = False - # get some data - num_sites = len(data_dct.keys()) - num_subs = num_sess = num_scan = 0 - for site in data_dct.keys(): - num_subs += len(data_dct[site]) - for sub in data_dct[site].keys(): - num_sess += len(data_dct[site][sub]) - for session in data_dct[site][sub].keys(): - if 'func' in data_dct[site][sub][session].keys(): - for scan in data_dct[site][sub][session]['func'].keys(): - num_scan += 1 + # # get some data + # num_sites = len(data_dct.keys()) + # num_subs = num_sess = num_scan = 0 + # for site in data_dct.keys(): + # num_subs += len(data_dct[site]) + # for sub in data_dct[site].keys(): + # num_sess += len(data_dct[site][sub]) + # for session in data_dct[site][sub].keys(): + # if 'func' in data_dct[site][sub][session].keys(): + # for scan in data_dct[site][sub][session]['func'].keys(): + # num_scan += 1 data_config_outfile = \ os.path.join(settings_dct['outputSubjectListLocation'], @@ -1387,13 +1387,19 @@ def run(data_settings_yml): # scan inclusion/exclusion and only some participants # have the scans included) if 'func' in data_dct[site][sub][ses]: - # TODO: put the numbers here instead, using - # TODO: "included" - pass + # get some numbers + included['site'].append(site) + included['sub'].append(sub) + included['ses'].append(ses) + for scan in data_dct[site][sub][ses]['func'].keys(): + included['scan'].append(scan) else: continue data_list.append(data_dct[site][sub][ses]) + # calculate numbers + # TODO: parse included dct + with open(data_config_outfile, "wt") as f: # Make sure YAML doesn't dump aliases (so it's more human # read-able) From c82ca214653331dac0970add227f8d2fca7080a2 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Thu, 5 Apr 2018 19:05:01 -0400 Subject: [PATCH 34/75] Got all field maps showing up in the data config builder now. --- CPAC/utils/build_data_config.py | 100 +++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index 1874736376..617ed1023c 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -372,7 +372,7 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, fmap_phase_sess = os.path.join(bids_base_dir, "sub-{participant}/ses-{session}/fmap/" - "sub-{participant}_ses-{session}_*_phase" + "sub-{participant}_ses-{session}_*phase" "diff.nii.gz") fmap_phase = os.path.join(bids_base_dir, "sub-{participant}/fmap/sub-{participant}" @@ -380,14 +380,23 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, fmap_mag_sess = os.path.join(bids_base_dir, "sub-{participant}/ses-{session}/fmap/" - "sub-{participant}_ses-{session}_*_" + "sub-{participant}_ses-{session}_*" "magnitud*.nii.gz") + fmap_mag = os.path.join(bids_base_dir, "sub-{participant}/fmap/sub-{participant}" "_magnitud*.nii.gz") sess_glob = os.path.join(bids_base_dir, "sub-*/ses-*/*") + fmap_phase_scan_glob = os.path.join(bids_base_dir, + "sub-*fmap/" + "sub-*_task-*_phasediff.nii.gz") + + fmap_mag_scan_glob = os.path.join(bids_base_dir, + "sub-*fmap/" + "sub-*_task-*_magnitud*.nii.gz") + part_tsv_glob = os.path.join(bids_base_dir, "*participants.tsv") ''' @@ -425,12 +434,6 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, for filepath in file_list: if fnmatch.fnmatch(filepath, site_dir_glob) and \ "derivatives" not in filepath: - print filepath - - ''' - HERE -- why adding site glob to NKI-RS??? - ''' - # check if there is a directory level encoding site ID, even # though that is not BIDS format site_dir = True @@ -439,6 +442,26 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, if fnmatch.fnmatch(filepath, sess_glob): # check if there is a session level ses = True + + if fnmatch.fnmatch(filepath, fmap_phase_scan_glob): + # check if there is a scan level for the fmap phase files + fmap_phase_sess = os.path.join(bids_base_dir, + "sub-{participant}/ses-{session}/fmap/" + "sub-{participant}_ses-{session}_task-{scan}_phase" + "diff.nii.gz") + fmap_phase = os.path.join(bids_base_dir, + "sub-{participant}/fmap/sub-{participant}" + "task-{scan}_phasediff.nii.gz") + + if fnmatch.fnmatch(filepath, fmap_mag_scan_glob): + # check if there is a scan level for the fmap magnitude files + fmap_mag_sess = os.path.join(bids_base_dir, + "sub-{participant}/ses-{session}/fmap/" + "sub-{participant}_ses-{session}_task-{scan}_magnitud*.nii.gz") + fmap_mag = os.path.join(bids_base_dir, + "sub-{participant}/fmap/sub-{participant}" + "task-{scan}_magnitud*.nii.gz") + if fnmatch.fnmatch(filepath, part_tsv_glob): # check if there is a participants.tsv file part_tsv = filepath @@ -769,8 +792,18 @@ def update_data_dct(file_path, file_template, data_dct=None, data_type="anat", if anat_scan: if anat_scan not in os.path.basename(file_path): return data_dct + else: + # what if it is in the filename, but there are other things + # as well? + # for example, anat_scan = 'run-1', and we have: + # sub-*_run-1_T1w.nii.gz + # sub-*_acq-inv1_run-1_T1w.nii.gz + # TODO: HERE + # TODO: try hard-coding "and not acq-inv1" or something, and + # TODO: see how this influences the behavior! + pass - # reduce the template down to only the substrings that do not have + # reduce the template down to only the sub-strings that do not have # these tags or IDs # Example @@ -1059,12 +1092,15 @@ def update_data_dct(file_path, file_template, data_dct=None, data_type="anat", return data_dct if 'func' not in data_dct[site_id][sub_id][ses_id].keys(): - # if no scan ID specified, nest it under the "func" key in the - # dictionary, so that it will apply to all scans that do not have - # their own assigned field map files (nested under the scan ID - # key in the dictionary) + # would this ever fire? the way we're using this function now data_dct[site_id][sub_id][ses_id]['func'] = temp_fmap_dct + elif not scan_id: + # TODO: re-visit in the future (same reason above) + # if no scan ID specified, add it to all scans for that session + for scan in data_dct[site_id][sub_id][ses_id]['func'].keys(): + data_dct[site_id][sub_id][ses_id]['func'][scan].update(temp_fmap_dct) elif scan_id not in data_dct[site_id][sub_id][ses_id]['func'].keys(): + # same- would this ever fire? data_dct[site_id][sub_id][ses_id]['func'][scan_id] = temp_fmap_dct else: data_dct[site_id][sub_id][ses_id]['func'][scan_id].update(temp_fmap_dct) @@ -1357,18 +1393,6 @@ def run(data_settings_yml): # TODO: data configs, i.e. for preprocessing only anats_only = False - # # get some data - # num_sites = len(data_dct.keys()) - # num_subs = num_sess = num_scan = 0 - # for site in data_dct.keys(): - # num_subs += len(data_dct[site]) - # for sub in data_dct[site].keys(): - # num_sess += len(data_dct[site][sub]) - # for session in data_dct[site][sub].keys(): - # if 'func' in data_dct[site][sub][session].keys(): - # for scan in data_dct[site][sub][session]['func'].keys(): - # num_scan += 1 - data_config_outfile = \ os.path.join(settings_dct['outputSubjectListLocation'], "data_config_{0}.yml" @@ -1376,29 +1400,37 @@ def run(data_settings_yml): # put data_dct contents in an ordered list for the YAML dump data_list = [] - included = {'site': [], 'sub': [], 'ses': [], 'scan': []} + included = {'site': [], 'sub': []} + num_sess = num_scan = 0 for site in sorted(data_dct.keys()): for sub in sorted(data_dct[site].keys()): for ses in sorted(data_dct[site][sub].keys()): if not anats_only: - # avoiding including anatomicals if there are no - # functionals associated with it (i.e. if we're using - # scan inclusion/exclusion and only some participants - # have the scans included) if 'func' in data_dct[site][sub][ses]: - # get some numbers + # if there are scans, get some numbers included['site'].append(site) included['sub'].append(sub) - included['ses'].append(ses) + num_sess += 1 for scan in data_dct[site][sub][ses]['func'].keys(): - included['scan'].append(scan) + num_scan += 1 else: + # avoiding including anatomicals if there are no + # functionals associated with it (i.e. if we're + # using scan inclusion/exclusion and only some + # participants have the scans included) continue + else: + # preprocessing for anats only, so count all subs + included['site'].append(site) + included['sub'].append(sub) + num_sess += 1 + data_list.append(data_dct[site][sub][ses]) # calculate numbers - # TODO: parse included dct + num_sites = len(set(included['site'])) + num_subs = len(set(included['sub'])) with open(data_config_outfile, "wt") as f: # Make sure YAML doesn't dump aliases (so it's more human From 5aedecd36539be75908a0ec46ba38588c1223c99 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 6 Apr 2018 18:05:37 -0400 Subject: [PATCH 35/75] Got the "anatomical_scan" data settings parameter working for all cases. --- CPAC/utils/build_data_config.py | 41 ++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index 617ed1023c..b1ffb64955 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -788,20 +788,39 @@ def update_data_dct(file_path, file_template, data_dct=None, data_type="anat", return data_dct if data_type == "anat": - # pick the right anatomical scan + # pick the right anatomical scan, if "anatomical_scan" has been + # provided if anat_scan: - if anat_scan not in os.path.basename(file_path): + file_name = os.path.basename(file_path) + if anat_scan not in file_name: return data_dct else: - # what if it is in the filename, but there are other things - # as well? - # for example, anat_scan = 'run-1', and we have: - # sub-*_run-1_T1w.nii.gz - # sub-*_acq-inv1_run-1_T1w.nii.gz - # TODO: HERE - # TODO: try hard-coding "and not acq-inv1" or something, and - # TODO: see how this influences the behavior! - pass + # if we're dealing with BIDS here + if "sub-" in file_name and "T1w." in file_name: + anat_scan_identifier = False + # BIDS tags are delineated with underscores + bids_tags = [] + for tag in file_name.split("_"): + if "sub-" not in tag and "ses-" not in tag and \ + "T1w" not in tag: + bids_tags.append(tag) + if anat_scan in tag: + # the "anatomical_scan" substring provided was + # found in one of the BIDS tags + anat_scan_identifier = True + if anat_scan_identifier: + if len(bids_tags) > 1: + # if this fires, then there are other tags as well + # in addition to what was defined in the + # "anatomical_scan" field in the data settings, + # for example, we might be looking for only + # run-1, but we found acq-inv_run-1 instead + return data_dct + + # if we're dealing with a custom data directory format + else: + # TODO: more involved processing here? or not necessary? + pass # reduce the template down to only the sub-strings that do not have # these tags or IDs From 7ed7da6568acaf055629d4260049cc078beaccdd Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 6 Apr 2018 18:33:50 -0400 Subject: [PATCH 36/75] Better checks for blank data settings parameters. --- CPAC/pipeline/cpac_pipeline.py | 5 +---- CPAC/utils/build_data_config.py | 9 +++++++-- CPAC/utils/datasource.py | 3 +-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 61f9a98b8f..f815845042 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -797,7 +797,6 @@ def getNodeList(strategy): 'anat_symmetric_mni_fnirt_register_%d' % num_strat) try: - node, out_file = strat.get_node_from_resource_pool( 'anatomical_brain') workflow.connect(node, out_file, @@ -858,7 +857,7 @@ def getNodeList(strategy): ants_reg_anat_symm_mni = \ create_wf_calculate_ants_warp( - 'anat_symmetric_mni_ants_register_%d' % num_strat, \ + 'anat_symmetric_mni_ants_register_%d' % num_strat, c.regWithSkull[0], num_threads=num_ants_cores) @@ -908,9 +907,7 @@ def getNodeList(strategy): ants_reg_anat_symm_mni.inputs.inputspec.reference_skull = \ c.template_symmetric_skull - else: - # get the skullstripped anatomical from resource pool node, out_file = strat.get_node_from_resource_pool( 'anatomical_brain') diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index b1ffb64955..140f20b213 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -1315,10 +1315,15 @@ def run(data_settings_yml): with open(data_settings_yml, "r") as f: settings_dct = yaml.load(f) - if "None" in settings_dct["awsCredentialsFile"]: + if not settings_dct["awsCredentialsFile"]: + settings_dct["awsCredentialsFile"] = None + elif "None" in settings_dct["awsCredentialsFile"] or \ + "none" in settings_dct["awsCredentialsFile"]: settings_dct["awsCredentialsFile"] = None - if "None" in settings_dct["anatomical_scan"] or \ + if not settings_dct["anatomical_scan"]: + settings_dct["anatomical_scan"] = None + elif "None" in settings_dct["anatomical_scan"] or \ "none" in settings_dct["anatomical_scan"]: settings_dct["anatomical_scan"] = None diff --git a/CPAC/utils/datasource.py b/CPAC/utils/datasource.py index d79301d365..d632cf1088 100644 --- a/CPAC/utils/datasource.py +++ b/CPAC/utils/datasource.py @@ -39,7 +39,6 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): inputnode.iterables = [('scan', scan_names)] for scan in scan_names: - scan_resources = rest_dict[scan] # have this here for now because of the big change in the data @@ -49,7 +48,7 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): except AttributeError: err = "\n[!] The data configuration file you provided is " \ "missing a level under the 'func:' key. CPAC versions " \ - "1.0.4 and later use data configurations with an " \ + "1.2 and later use data configurations with an " \ "additional level of nesting.\n\nExample\nfunc:\n " \ "rest01:\n scan: /path/to/rest01_func.nii.gz\n" \ " scan parameters: /path/to/scan_params.json\n\n" \ From 4bdd95bf9db98072a77dfa5c1efcb2f1f4974902 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Mon, 9 Apr 2018 18:28:07 -0400 Subject: [PATCH 37/75] Updates to paired two-group difference preset. --- CPAC/utils/create_group_analysis_files.py | 132 ++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_group_analysis_files.py index b39a832963..94d638fb22 100644 --- a/CPAC/utils/create_group_analysis_files.py +++ b/CPAC/utils/create_group_analysis_files.py @@ -432,6 +432,123 @@ def preset_unpaired_two_group(group_list, pheno_df, groups, pheno_sub_label, return design_df, contrasts_df, group_config +def preset_paired_two_group(group_list, conditions, condition_type="session", + output_dir=None, + model_name="two_sample_unpaired_T-test"): + """Set up the design matrix and contrasts matrix for running an paired + two-group difference (two-sample paired T-test).""" + + # TODO: NEXT!!!!!!!!!!!!!!!!! + # if the conditions are delineated by sessions, then the matrix will + # have to have both copies of the sub_ses_id in the design matrix + # conversely, if they are delineated by scans, then we have to have a + # design matrix with only one copy of the subs, then let the internal + # (infernal?) machinery do the doubling + + import os + + if not output_dir: + output_dir = os.getcwd() + + # TODO: handle conditions (sessons? scans?) + # TODO: make sure the 1, -1 vector doesn't get clobbered by Patsy + ''' + we need, to give in a list of sub_ses. and a list of the two sessions. + and this needs to spit out a design df that has the sub_ses doubled + in appropriate order, with the 1 and -1. and the other columns (avoid + letting the cpac thing process it if you can avoid it). + + but what if it's the scans instead? now it gets ugly. + here's a list of just sub_ses (with one ses). and a list of the two + scans, right? now what? + you have to send it in and let cpac handle it, unfortunately. + TODO: would it be worth it to quickly enable a custom pheno + input? I don't know if cpac is going to do the subs + columns properly, especially into the custom contrasts.. + ''' + + if len(conditions) != 2: + # TODO: msg + raise Exception + + design_df = create_design_matrix_df(group_list) + + if condition_type == "session": + # make the "condition" EV (the 1's and -1's delineating the two + # conditions) + condition_ev = [] + for sub_ses_id in design_df["participant_id"]: + if sub_ses_id.split("_")[-1] == conditions[0]: + condition_ev.append(1) + elif sub_ses_id.split("_")[-1] == conditions[1]: + condition_ev.append(-1) + + # let's check to make sure it came out right + # first half + for val in condition_ev[0:(len(condition_ev)/2)-1]: + if val != 1: + # TODO: msg + raise Exception + # second half + for val in condition_ev[(len(condition_ev)/2):]: + if val != -1: + # TODO: msg + raise Exception + + design_df["condition"] = condition_ev + + # start the contrasts + contrast_one = {"contrasts": "{0} - {1}".format(groups[0], groups[1])} + contrast_two = {"contrasts": "{0} - {1}".format(groups[1], groups[0])} + + for col in design_df.columns: + if col not in id_cols: + if col == groups[0]: + contrast_one.update({col: 1}) + contrast_two.update({col: -1}) + elif col == groups[1]: + contrast_one.update({col: -1}) + contrast_two.update({col: 1}) + else: + contrast_one.update({col: 0}) + contrast_two.update({col: 0}) + + contrasts = [contrast_one, contrast_two] + + contrasts_df = create_contrasts_template_df(design_df, conditions) + + elif condition_type == "scan": + + else: + # TODO: msg + raise Exception + + # create design and contrasts matrix file paths + design_mat_path = os.path.join(output_dir, model_name, + "design_matrix_{0}.csv".format(model_name)) + + contrasts_mat_path = os.path.join(output_dir, model_name, + "contrasts_matrix_{0}.csv" + "".format(model_name)) + + # start group config yaml dictionary + design_formula = "{0} + {1}".format(groups[0], groups[1]) + + group_config = {"pheno_file": design_mat_path, + "ev_selections": {"demean": [], + "categorical": groups}, + "design_formula": design_formula, + "group_sep": "Off", + "grouping_var": None, + "sessions_list": [], + "series_list": [], + "custom_contrasts": contrasts_mat_path, + "model_name": model_name, + "output_dir": os.path.join(output_dir, model_name)} + + return design_df, contrasts_df, group_config + + def run(group_list_text_file, derivative_list, z_thresh, p_thresh, preset=None, pheno_file=None, pheno_sub_label=None, output_dir=None, model_name=None, covariate=None): @@ -544,6 +661,21 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, group_config.update(group_config_update) + elif preset == "paired_two": + # run a two-sample paired T-test + + # we need it as repeated measures- either session or scan + # and the list of subs. that's it. + + design_df, contrasts_df, group_config_update = \ + preset_paired_two_group(group_list, + conditions=covariate, + output_dir=output_dir, + model_name=model_name) + + pass + + else: # TODO: not a real preset! raise Exception("not one of the valid presets") From 1d8dce55d2b1d8578e27eebbeab0cc99c7b1bcf8 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Tue, 10 Apr 2018 18:51:50 -0400 Subject: [PATCH 38/75] wrote changes to output directory --- CPAC/pipeline/cpac_pipeline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 514d4b9b2d..66b24b0ad4 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -5646,9 +5646,9 @@ def is_number(s): ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) # Write QC outputs to log directory if 'qc' in key.lower(): - ds.inputs.base_directory = c.logDirectory - else: ds.inputs.base_directory = c.outputDirectory + else: + ds.inputs.base_directory = c.logDirectory # For each pipeline ID, generate the QC pages # for pip_id in pip_ids: # Define pipeline-level logging for QC @@ -5850,7 +5850,7 @@ def is_number(s): create_log_node(None, None, count, scan).run() for pip_id in pip_ids: try: - pipeline_base = os.path.join(c.logDirectory, 'pipeline_%s' % pip_id) + pipeline_base = os.path.join(c.outputDirectory, 'pipeline_%s' % pip_id) qc_output_folder = os.path.join(pipeline_base, subject_id, 'qc_files_here') generateQCPages(qc_output_folder,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) #create_all_qc.run(pipeline_base) From a126e1fe5a6a7062296089028ca40ead68559a0c Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Tue, 10 Apr 2018 18:52:30 -0400 Subject: [PATCH 39/75] code cleanup --- CPAC/qc/utils.py | 51 +++++++++++------------------------------------- 1 file changed, 11 insertions(+), 40 deletions(-) diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index e68096d7bb..7d2ccf2d43 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -373,6 +373,7 @@ def grp_pngs_by_id(pngs_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_ return dict(dict_a), dict(dict_s), dict(dict_hist), dict(dict_plot), list(all_ids) + def add_head(f_html_, f_html_0, f_html_1): """ @@ -414,11 +415,12 @@ def add_head(f_html_, f_html_0, f_html_1): print >>f_html_0, "" print >>f_html_0, "" print >>f_html_0, "" - print >>f_html_0, "" + print >>f_html_0, "" print >>f_html_0, "
" print >>f_html_0, "
" print >>f_html_0, "

" - print >>f_html_0, "\"Logo\"/"%(p.resource_filename('CPAC',"GUI/resources/html/_static/cpac_logo.jpg")) + print >>f_html_0, "

" + print >>f_html_0, "\"Logo\"/"%(p.resource_filename('CPAC',"GUI/resources/html/_static/cmi_logo.jpg")) print >>f_html_0, "

" print >>f_html_0, "

Table Of Contents

" print >>f_html_0, "
    " @@ -455,28 +457,7 @@ def add_tail(f_html_, f_html_0, f_html_1): None """ -# print >>f_html_0, "
" -# print >>f_html_0, "
" -# print >>f_html_0, "
" -# print >>f_html_0, "
\ -#
\ -# Recommendation for QC
\ -# 1
\ -# 2
\ -# 3
\ -# 4
\ -# 5
\ -#
Notes\ -#
\ -#
\ -#
\ -# \ -#
\ -#
\ -#
\ -#
" + print >>f_html_0, "" print >>f_html_0, "
" print >>f_html_0, "
" @@ -520,8 +501,6 @@ def feed_line_nav(id_, None """ - - ### add general user readable link names for QC navigation bar image_readable = image_name if image_name == 'skullstrip_vis': image_readable = 'Visual Result of Skull Strip' @@ -570,12 +549,10 @@ def feed_line_nav(id_, if image_name == 'falff_smooth': image_readable = 'fractional Amplitude of Low-Frequency Fluctuation' if image_name == 'falff_smooth_hist': - image_readable = 'Histogram of fractional Amplitude of Low-Frequency Fluctuation' + image_readable = 'Histogram of fractional Amplitude of Low-Frequency Fluctuation' print >>f_html_0, "
  • %s
  • " % (f_html_1.name, \ - anchor, image_readable) ### - - + anchor, image_readable) def feed_line_body(image_name, anchor, image, f_html_1): """ @@ -603,8 +580,6 @@ def feed_line_body(image_name, anchor, image, f_html_1): None """ - - ### add general user readable link names for QC body image_readable = image_name if image_name == 'skullstrip_vis': image_readable = 'Visual Result of Skull Strip' @@ -653,14 +628,14 @@ def feed_line_body(image_name, anchor, image, f_html_1): if image_name == 'falff_smooth': image_readable = 'fractional Amplitude of Low-Frequency Fluctuation' if image_name == 'falff_smooth_hist': - image_readable = 'Histogram of fractional Amplitude of Low-Frequency Fluctuation' + image_readable = 'Histogram of fractional Amplitude of Low-Frequency Fluctuation' print >>f_html_1, "

    %s TOP

    " %(anchor, image_readable) ### ###data_uri = open(image, 'rb').read().encode('base64').replace('\n', '') ###img_tag = '
    '.format(data_uri) - + img_tag = "
    %s" %(image, image_readable) ### print >>f_html_1, img_tag @@ -882,8 +857,6 @@ def feed_lines_html(id_, if idx == 0: - - # add general user readable link names for QC navigation bar if image_name_a_nav == 'skullstrip_vis': image_readable = 'Visual Result of Skull Strip' if image_name_a_nav == 'csf_gm_wm': @@ -929,11 +902,9 @@ def feed_lines_html(id_, if image_name_a_nav == 'falff_smooth': image_readable = 'fractional Amplitude of Low-Frequency Fluctuation' if image_name_a_nav == 'falff_smooth_hist': - image_readable = 'Histogram of fractional Amplitude of Low-Frequency Fluctuation' - + image_readable = 'Histogram of fractional Amplitude of Low-Frequency Fluctuation' feed_line_nav(id_, \ image_name_a_nav, \ - #image_readable, \ id_a, \ f_html_0, \ f_html_1) @@ -1121,7 +1092,7 @@ def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist #actually make the html page for the file_ make_page(os.path.join(qc_path, file_), qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) - + def generateQCPages(qc_path,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): From 00b381392729d6f4a2a9b84cb553c62a5685d194 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Wed, 11 Apr 2018 14:57:49 -0400 Subject: [PATCH 40/75] Improved data config builder message. --- CPAC/utils/build_data_config.py | 14 ++++++++++---- CPAC/utils/datasource.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index 140f20b213..ba7d9fc109 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -1234,10 +1234,16 @@ def get_nonBIDS_data(anat_template, func_template, file_list=None, "that were missed. Here are a few examples:\n".format(err) for anat in possible_anats[0:5]: err = "{0}{1}\n".format(err, anat) - err = "{0}\nIf you are using the 'anatomical_scan' option in " \ - "the data settings, check the setting to make sure " \ - "you are properly selecting which anatomical scan to " \ - "use for your analysis.\n\n".format(err) + err = "{0}\nCPAC only needs one anatomical scan defined for " \ + "each participant-session. If there are multiple " \ + "anatomical scans per participant-session, you can use " \ + "the 'Which Anatomical Scan?' (anatomical_scan) " \ + "parameter to choose which anatomical to " \ + "select.\n".format(err) + err = "{0}\nIf you are already using the 'anatomical_scan' " \ + "option in the data settings, check the setting to make " \ + "sure you are properly selecting which anatomical scan " \ + "to use for your analysis.\n\n".format(err) raise Exception(err) # now gather the functionals diff --git a/CPAC/utils/datasource.py b/CPAC/utils/datasource.py index d632cf1088..edb78bda59 100644 --- a/CPAC/utils/datasource.py +++ b/CPAC/utils/datasource.py @@ -85,7 +85,7 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): s3_scan_params.inputs.file_path = \ scan_resources["scan_parameters"] - s3_scan_params.inputs.dl_dir = dl_dir + #s3_scan_params.inputs.dl_dir = dl_dir wf.connect(inputnode, 'creds_path', s3_scan_params, 'creds_path') From 7ecc32473e9d2dc8f3a069c68a7f2f59ba711414 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Thu, 12 Apr 2018 18:37:08 -0400 Subject: [PATCH 41/75] Some fixes to the paired two-group difference preset, and the original model builder pipeline. --- CPAC/GUI/interface/utils/constants.py | 4 +- .../GUI/interface/utils/modelconfig_window.py | 13 +- CPAC/pipeline/cpac_ga_model_generator.py | 26 ++- CPAC/pipeline/cpac_group_runner.py | 70 +++++-- CPAC/utils/create_group_analysis_files.py | 197 ++++++++++++------ scripts/cpac_cli.py | 11 +- 6 files changed, 228 insertions(+), 93 deletions(-) diff --git a/CPAC/GUI/interface/utils/constants.py b/CPAC/GUI/interface/utils/constants.py index 4ae1cfa415..a5259c0405 100644 --- a/CPAC/GUI/interface/utils/constants.py +++ b/CPAC/GUI/interface/utils/constants.py @@ -31,9 +31,9 @@ def enum(**enums): 'ANTS & FSL': 11, '3dAutoMask & BET': 12, 'ALFF':'alff_to_standard_zstd', - 'ALFF (smoothed)':'alff_to_standard_smooth_zstd', + 'ALFF (smoothed)':'alff_to_standard_zstd_smooth', 'f/ALFF':'falff_to_standard_zstd', - 'f/ALFF (smoothed)':'falff_to_standard_smooth_zstd', + 'f/ALFF (smoothed)':'falff_to_standard_zstd_smooth', 'ReHo':'reho_to_standard_zstd', 'ReHo (smoothed)':'reho_to_standard_smooth_zstd', 'ROI Average SCA':'sca_roi_files_to_standard_fisher_zstd', diff --git a/CPAC/GUI/interface/utils/modelconfig_window.py b/CPAC/GUI/interface/utils/modelconfig_window.py index e80ac3f345..6e02edd4f6 100644 --- a/CPAC/GUI/interface/utils/modelconfig_window.py +++ b/CPAC/GUI/interface/utils/modelconfig_window.py @@ -304,7 +304,7 @@ def __init__(self, parent, gpa_settings=None): new_grouping_var += "{0},".format(cov) new_grouping_var = new_grouping_var.rstrip(",") - ctrl.set_value(new_grouping_var) + ctrl.set_value(new_grouping_var) if ("list" in name) and (name != "participant_list"): @@ -956,9 +956,9 @@ def testFile(filepath, paramName): raise Exception(err) # enforce the sub ID label to "Participant" - pheno_df.rename(columns={self.gpa_settings["participant_id_label"]:"Participant"}, \ + pheno_df.rename(columns={self.gpa_settings["participant_id_label"]:"participant_id"}, \ inplace=True) - pheno_df["Participant"] = pheno_df["Participant"].astype(str) + pheno_df["participant_id"] = pheno_df["participant_id"].astype(str) # let's create dummy columns for MeanFD, Measure_Mean, and # Custom_ROI_Mask (if included in the Design Matrix Formula) just so we @@ -995,8 +995,11 @@ def testFile(filepath, paramName): if len(list(self.gpa_settings["sessions_list"])) > 0: from CPAC.pipeline.cpac_group_runner import pheno_sessions_to_repeated_measures pheno_df = pheno_sessions_to_repeated_measures(pheno_df, list(self.gpa_settings["sessions_list"])) - self.gpa_settings["ev_selections"]["categorical"].append("Session") - formula = formula + " + Session" + if "Session" in pheno_df.columns: + # if the model builder is automatically creating the Session + # and participant columns + self.gpa_settings["ev_selections"]["categorical"].append("Session") + formula = formula + " + Session" repeated_sessions = True if len(list(self.gpa_settings["series_list"])) > 0: diff --git a/CPAC/pipeline/cpac_ga_model_generator.py b/CPAC/pipeline/cpac_ga_model_generator.py index 04bb76a8da..fe43446287 100755 --- a/CPAC/pipeline/cpac_ga_model_generator.py +++ b/CPAC/pipeline/cpac_ga_model_generator.py @@ -391,7 +391,8 @@ def split_groups(pheno_df, group_ev, ev_list, cat_list): def patsify_design_formula(formula, categorical_list, encoding="Treatment"): - + print formula + print categorical_list closer = ")" if encoding == "Treatment": closer = ")" @@ -720,19 +721,24 @@ def prep_group_analysis_workflow(model_df, pipeline_config_path, model_name, # prep design for repeated measures, if applicable if len(group_config_obj.sessions_list) > 0: - design_formula = design_formula + " + Session" - if "Session" not in cat_list: - cat_list.append("Session") + if "Session" in model_df.columns: + # if these columns were added by the model builder automatically + design_formula = design_formula + " + Session" + if "Session" not in cat_list: + cat_list.append("Session") if len(group_config_obj.series_list) > 0: design_formula = design_formula + " + Series" if "Series" not in cat_list: cat_list.append("Series") - for col in list(model_df.columns): - # should only grab the repeated measures-designed participant_{ID} - # columns, not the "participant_id" column! - if "participant_" in col and "_id" not in col: - design_formula = design_formula + " + %s" % col - cat_list.append(col) + + if "Session" in model_df.columns: + # if these columns were added by the model builder automatically + for col in list(model_df.columns): + # should only grab the repeated measures-designed participant_{ID} + # columns, not the "participant_id" column! + if "participant_" in col and "_id" not in col: + design_formula = design_formula + " + %s" % col + cat_list.append(col) # parse out the EVs in the design formula at this point in time # this is essentially a list of the EVs that are to be included diff --git a/CPAC/pipeline/cpac_group_runner.py b/CPAC/pipeline/cpac_group_runner.py index 2cdc8eb648..a71b1954c1 100644 --- a/CPAC/pipeline/cpac_group_runner.py +++ b/CPAC/pipeline/cpac_group_runner.py @@ -360,11 +360,46 @@ def gather_outputs(pipeline_folder, resource_list, inclusion_list, \ def pheno_sessions_to_repeated_measures(pheno_df, sessions_list): - - # take in the selected sessions, and match them to the participant - # unique IDs appropriately - - import pandas as pd + """Take in the selected session names, and match them to the unique + participant-session IDs appropriately for an FSL FEAT repeated measures + analysis. + + More info: + https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FEAT/ + UserGuide#Paired_Two-Group_Difference_.28Two-Sample_Paired_T-Test.29 + + Sample input: + pheno_df + sub01_ses01 + sub01_ses02 + sub02_ses01 + sub02_ses02 + sessions_list + [ses01, ses02] + + Expected output: + pheno_df Session participant_sub01 participant_sub02 + sub01_ses01 ses01 1 0 + sub02_ses01 ses01 0 1 + sub01_ses02 ses02 1 0 + sub02_ses02 ses02 0 1 + """ + + # first, check to see if this design matrix setup has already been done + # in the pheno CSV file + num_partic_cols = 0 + for col_names in pheno_df.columns: + if "participant" in col_names: + num_partic_cols += 1 + if num_partic_cols > 1 and "session" in pheno_df.columns: + for part_ses_id in pheno_df["participant_id"]: + if "participant_{0}".format(part_ses_id.split("_")[0]) in pheno_df.columns: + continue + break + else: + # if it's already set up properly, then just send the pheno_df + # back and bypass all the machinery below + return pheno_df # there are no new rows, since the phenotype file will have all of the # subject_site_session combo unique IDs on each row!!! @@ -374,7 +409,6 @@ def pheno_sessions_to_repeated_measures(pheno_df, sessions_list): # participant IDs new columns participant_id_cols = {} i = 0 - for participant_unique_id in list(pheno_df["participant_id"]): part_col = [0] * len(pheno_df["participant_id"]) for session in sessions_list: @@ -392,7 +426,7 @@ def pheno_sessions_to_repeated_measures(pheno_df, sessions_list): else: participant_id_cols[header_title][i] = 1 i += 1 - + pheno_df["Session"] = sessions_col pheno_df["participant"] = part_ids_col @@ -495,9 +529,9 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # TODO # make this a global list somewhere derivatives = ['alff_to_standard_zstd', - 'alff_to_standard_smooth_zstd', + 'alff_to_standard_zstd_smooth', 'falff_to_standard_zstd', - 'falff_to_standard_smooth_zstd', + 'falff_to_standard_zstd_smooth', 'reho_to_standard_zstd', 'reho_to_standard_smooth_zstd', 'sca_roi_files_to_standard_fisher_zstd', @@ -668,14 +702,14 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): if repeated_measures == True: - if repeated_sessions == True: + if repeated_sessions: new_pheno_df = pheno_sessions_to_repeated_measures( \ new_pheno_df, group_model.sessions_list) # create new rows for all of the series, if applicable # ex. if 10 subjects and two sessions, 10 rows -> 20 rows - if repeated_series == True: + if repeated_series: new_pheno_df = pheno_series_to_repeated_measures( \ new_pheno_df, group_model.series_list, @@ -714,7 +748,7 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): new_pheno_df = pd.merge(new_pheno_df, output_df, how="inner", on=join_columns) - if repeated_sessions == True: + if repeated_sessions: # this can be removed/modified once sessions are no # longer integrated in the full unique participant IDs new_pheno_df, dropped_parts = \ @@ -750,10 +784,14 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # this can be removed/modified once sessions are no # longer integrated in the full unique participant IDs - newer_pheno_df, dropped_parts = \ - balance_repeated_measures(newer_pheno_df, - group_model.sessions_list, - None) + if "Session" in newer_pheno_df.columns: + # TODO: re-visit why there is a "participant_ID" + # TODO: column? will this still work without + # TODO: presets? + newer_pheno_df, dropped_parts = \ + balance_repeated_measures(newer_pheno_df, + group_model.sessions_list, + None) # unique_resource = # (output_measure_type, preprocessing strategy) diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_group_analysis_files.py index 94d638fb22..b80190c114 100644 --- a/CPAC/utils/create_group_analysis_files.py +++ b/CPAC/utils/create_group_analysis_files.py @@ -45,6 +45,10 @@ def write_group_list_text_file(group_list, out_file=None): for part_id in group_list: f.write("{0}\n".format(part_id)) + if os.path.exists(out_file): + print "Group-level analysis participant list written:" \ + "\n{0}\n".format(out_file) + return out_file @@ -63,6 +67,9 @@ def write_dataframe_to_csv(matrix_df, out_file=None): matrix_df.to_csv(out_file, index=False) + if os.path.exists(out_file): + print "CSV file written:\n{0}\n".format(out_file) + def write_config_dct_to_yaml(config_dct, out_file=None): """Write out a configuration dictionary into a YAML file.""" @@ -98,6 +105,10 @@ def write_config_dct_to_yaml(config_dct, out_file=None): val = config_dct[key] f.write("{0}: {1}\n\n".format(key, val)) + if os.path.exists(out_file): + print "Group-level analysis configuration YAML file written:\n" \ + "{0}\n".format(out_file) + def create_design_matrix_df(group_list, pheno_df=None, ev_selections=None, pheno_sub_label=None, @@ -132,8 +143,11 @@ def create_design_matrix_df(group_list, pheno_df=None, map_df = map_df.rename( columns={0: 'participant', 1: 'session', 2: 'participant_id'}) - # sort by sub_id and then ses_id - map_df = map_df.sort_values(by=['participant_id']) + # sort by ses_id, then sub_id + # need everything grouped by session first, in case of the paired + # analyses where the first condition is all on top and the second is + # all on the bottom + map_df = map_df.sort_values(by=['session', 'participant']) # drop unique_id column (does it ever need to really be included?) # was just keeping it in up until here for mental book-keeping if anything @@ -473,56 +487,109 @@ def preset_paired_two_group(group_list, conditions, condition_type="session", design_df = create_design_matrix_df(group_list) + # make the "condition" EV (the 1's and -1's delineating the two + # conditions, with the "conditions" being the two sessions or two scans) + condition_ev = [] + if condition_type == "session": - # make the "condition" EV (the 1's and -1's delineating the two - # conditions) - condition_ev = [] + # note: the participant_id column in design_df should be in order, so + # the condition_ev should come out in order: + # 1,1,1,1,-1,-1,-1,-1 (this is checked further down) for sub_ses_id in design_df["participant_id"]: if sub_ses_id.split("_")[-1] == conditions[0]: condition_ev.append(1) elif sub_ses_id.split("_")[-1] == conditions[1]: condition_ev.append(-1) - # let's check to make sure it came out right - # first half - for val in condition_ev[0:(len(condition_ev)/2)-1]: - if val != 1: - # TODO: msg - raise Exception - # second half - for val in condition_ev[(len(condition_ev)/2):]: - if val != -1: - # TODO: msg - raise Exception - - design_df["condition"] = condition_ev - - # start the contrasts - contrast_one = {"contrasts": "{0} - {1}".format(groups[0], groups[1])} - contrast_two = {"contrasts": "{0} - {1}".format(groups[1], groups[0])} + group_config = {"sessions_list": conditions, "series_list": []} - for col in design_df.columns: - if col not in id_cols: - if col == groups[0]: - contrast_one.update({col: 1}) - contrast_two.update({col: -1}) - elif col == groups[1]: - contrast_one.update({col: -1}) - contrast_two.update({col: 1}) - else: - contrast_one.update({col: 0}) - contrast_two.update({col: 0}) + elif condition_type == "scan": + # TODO: re-visit later, when session/scan difference in how to run + # TODO: group-level analysis repeated measures is streamlined and + # TODO: simplified + # the information needed in this part is not encoded in the group + # sublist! user inputs the two scan names, and we have a list of + # sub_ses (which needs to be doubled), with each scan paired to each + # half of this list (will need to ensure these scans exist for each + # selected derivative in the output directory later on) - contrasts = [contrast_one, contrast_two] + for sub_ses_id in design_df["participant_id"]: + condition_ev.append(1) + for sub_ses_id in design_df["participant_id"]: + condition_ev.append(-1) - contrasts_df = create_contrasts_template_df(design_df, conditions) + # NOTE: there is only one iteration of the sub_ses list in + # design_df["participant_id"] at this point! so use append to + # double that column: + design_df = design_df.append(design_df) - elif condition_type == "scan": + group_config = {"sessions_list": [], "series_list": conditions} else: # TODO: msg raise Exception + # let's check to make sure it came out right + # first half + for val in condition_ev[0:(len(condition_ev) / 2) - 1]: + if val != 1: + # TODO: msg + raise Exception + # second half + for val in condition_ev[(len(condition_ev) / 2):]: + if val != -1: + # TODO: msg + raise Exception + + design_df[condition_type] = condition_ev + + # initalize the contrast dct's + contrast_one = {} + contrast_two = {} + + design_formula = "{0}".format(condition_type) + + # create the participant identity columns + for sub_ses_id in design_df["participant_id"]: + new_part_col = [] + sub_id = sub_ses_id.split("_")[0] + new_part_label = "participant_{0}".format(sub_id) + for moving_sub_ses_id in design_df["participant_id"]: + moving_sub_id = moving_sub_ses_id.split("_")[0] + if moving_sub_id == sub_id: + new_part_col.append(1) + else: + new_part_col.append(0) + design_df[new_part_label] = new_part_col + contrast_one.update({new_part_label: 0}) + contrast_two.update({new_part_label: 0}) + if new_part_label not in design_formula: + design_formula = "{0} + {1}".format(design_formula, + new_part_label) + + # finish the contrasts + # should be something like + # ses,sub,sub,sub, etc. + # ses-1 - ses-2: 1, 0, 0, 0, 0... + # ses-2 - ses-1: -1, 0, 0, 0, etc. + contrast_one.update({ + "contrasts": "{0}-{1} - {2}-{3}".format(condition_type, + conditions[0], + condition_type, + conditions[1])}) + contrast_two.update({ + "contrasts": "{0}-{1} - {2}-{3}".format(condition_type, + conditions[1], + condition_type, + conditions[0])}) + + contrast_one.update({condition_type: 1}) + contrast_two.update({condition_type: -1}) + + contrasts = [contrast_one, contrast_two] + + contrasts_df = create_contrasts_template_df(design_df, contrasts) + # create design and contrasts matrix file paths design_mat_path = os.path.join(output_dir, model_name, "design_matrix_{0}.csv".format(model_name)) @@ -532,26 +599,22 @@ def preset_paired_two_group(group_list, conditions, condition_type="session", "".format(model_name)) # start group config yaml dictionary - design_formula = "{0} + {1}".format(groups[0], groups[1]) - - group_config = {"pheno_file": design_mat_path, - "ev_selections": {"demean": [], - "categorical": groups}, - "design_formula": design_formula, - "group_sep": "Off", - "grouping_var": None, - "sessions_list": [], - "series_list": [], - "custom_contrasts": contrasts_mat_path, - "model_name": model_name, - "output_dir": os.path.join(output_dir, model_name)} + group_config.update({"pheno_file": design_mat_path, + "ev_selections": {"demean": [], + "categorical": []}, + "design_formula": design_formula, + "group_sep": "Off", + "grouping_var": None, + "custom_contrasts": contrasts_mat_path, + "model_name": model_name, + "output_dir": os.path.join(output_dir, model_name)}) return design_df, contrasts_df, group_config def run(group_list_text_file, derivative_list, z_thresh, p_thresh, preset=None, pheno_file=None, pheno_sub_label=None, output_dir=None, - model_name=None, covariate=None): + model_name=None, covariate=None, run=False): # TODO: set this up to run regular group analysis with no changes to its # TODO: original flow- use the generated pheno as the pheno, use the @@ -582,11 +645,9 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, # write out a group analysis sublist text file so that it can be # linked in the group analysis config yaml - out_list = os.path.join(output_dir, model_name, - "gpa_participant_list_" - "{0}.txt".format(model_name)) - group_list_text_file = write_group_list_text_file(group_list, - out_list) + group_list_text_file = os.path.join(output_dir, model_name, + "gpa_participant_list_" + "{0}.txt".format(model_name)) else: group_list = read_group_list_text_file(group_list_text_file) @@ -665,7 +726,19 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, # run a two-sample paired T-test # we need it as repeated measures- either session or scan - # and the list of subs. that's it. + # and the list of subs + # also: the two session or scan names (in a list together), and + # whether they are sessions or scans + + if not covariate: + # TODO: message + raise Exception("the two conditions were not provided") + + # we're assuming covariate (which in this case, is the two sessions, + # or two scans) will be coming in as a string of either one covariate + # name, or a string with two covariates separated by a comma + # either way, it needs to be in list form in this case, not string + covariate = covariate.split(",") design_df, contrasts_df, group_config_update = \ preset_paired_two_group(group_list, @@ -673,13 +746,16 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, output_dir=output_dir, model_name=model_name) - pass - + group_config.update(group_config_update) else: # TODO: not a real preset! raise Exception("not one of the valid presets") + # write participant list text file + group_list_text_file = write_group_list_text_file(design_df["participant_id"], + group_list_text_file) + # write design matrix CSV write_dataframe_to_csv(design_df, group_config["pheno_file"]) @@ -690,3 +766,8 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, out_config = os.path.join(output_dir, model_name, "gpa_fsl_config_{0}.yml".format(model_name)) write_config_dct_to_yaml(group_config, out_config) + + if run: + # TODO: we need to separate the individual-level pipeline config from + # TODO: the group-level one, it's too restrictive + pass diff --git a/scripts/cpac_cli.py b/scripts/cpac_cli.py index a70732c895..b7a70ee637 100644 --- a/scripts/cpac_cli.py +++ b/scripts/cpac_cli.py @@ -55,13 +55,20 @@ def main(): if not args.output_dir: output_dir = os.getcwd() else: - output_dir = args.output_dir + output_dir = os.path.abspath(args.output_dir) + + cpac_outputs = os.path.abspath(args.cpac_outputs) if not args.include: - include = [x for x in os.listdir(args.cpac_outputs) if os.path.isdir(x)] + include = [x for x in os.listdir(cpac_outputs) if + os.path.isdir(os.path.join(cpac_outputs, x))] else: include = args.include + if not include: + # TODO: msg + raise Exception("no sub_ses IDs found!") + derivatives_list = args.derivatives.split(" ") create_group_analysis_files.run(include, derivatives_list, args.z_thresh, From dd6d35174cb82fef0519df1e17245366e8ba11ab Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 13 Apr 2018 14:00:55 -0400 Subject: [PATCH 42/75] Opening up option to select scan condition type for two-sample paired t-test preset. --- CPAC/utils/create_group_analysis_files.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_group_analysis_files.py index b80190c114..1946c6a0f1 100644 --- a/CPAC/utils/create_group_analysis_files.py +++ b/CPAC/utils/create_group_analysis_files.py @@ -614,7 +614,7 @@ def preset_paired_two_group(group_list, conditions, condition_type="session", def run(group_list_text_file, derivative_list, z_thresh, p_thresh, preset=None, pheno_file=None, pheno_sub_label=None, output_dir=None, - model_name=None, covariate=None, run=False): + model_name=None, covariate=None, condition_type=None, run=False): # TODO: set this up to run regular group analysis with no changes to its # TODO: original flow- use the generated pheno as the pheno, use the @@ -734,6 +734,11 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, # TODO: message raise Exception("the two conditions were not provided") + if not condition_type: + # TODO: message + raise Exception("you didn't specify whether the two groups are " + "sessions or series/scans") + # we're assuming covariate (which in this case, is the two sessions, # or two scans) will be coming in as a string of either one covariate # name, or a string with two covariates separated by a comma @@ -743,6 +748,7 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, design_df, contrasts_df, group_config_update = \ preset_paired_two_group(group_list, conditions=covariate, + condition_type=condition_type, output_dir=output_dir, model_name=model_name) From 0315bd9e7a9f94f48ed1de401bae259f96c537fc Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 13 Apr 2018 16:00:30 -0400 Subject: [PATCH 43/75] Fixes and reconstruction of func_datasource after some testing. Added docstrings also. --- CPAC/pipeline/cpac_pipeline.py | 2 + CPAC/utils/datasource.py | 288 +++++++++++++++++++-------------- 2 files changed, 165 insertions(+), 125 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index f815845042..caf45750e7 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -1097,6 +1097,8 @@ def getNodeList(strategy): funcFlow.inputs.inputnode.subject = subject_id funcFlow.inputs.inputnode.creds_path = input_creds_path funcFlow.inputs.inputnode.dl_dir = c.workingDirectory + funcFlow.get_node('inputnode').iterables = \ + ("scan", func_paths_dict.keys()) except Exception as xxx: logger.info("Error create_func_datasource failed. " "(%s:%d)" % dbg_file_lineno()) diff --git a/CPAC/utils/datasource.py b/CPAC/utils/datasource.py index edb78bda59..de4fb2b580 100644 --- a/CPAC/utils/datasource.py +++ b/CPAC/utils/datasource.py @@ -2,9 +2,25 @@ import nipype.interfaces.utility as util -def get_rest(scan, rest_dict): - # return the time-series NIFTI file of the chosen series/scan - return rest_dict[scan]["scan"] +def get_rest(scan, rest_dict, resource="scan"): + """Return the file path of the chosen resource stored in the functional + file dictionary, if it exists. + + scan: the scan/series name or label + rest_dict: the dictionary read in from the data configuration YAML file + (sublist) nested under 'func:' + resource: the dictionary key + scan - the functional timeseries + scan_parameters - path to the scan parameters JSON file, or + a dictionary containing scan parameters + information (to be phased out in the + future) + """ + try: + file_path = rest_dict[scan][resource] + except KeyError: + file_path = None + return file_path def extract_scan_params_dct(scan_params_dct): @@ -16,8 +32,46 @@ def get_map(map, map_dct): return map_dct[map] +def check_func_scan(func_scan_dct, scan): + """Run some checks on the functional timeseries-related files for a given + series/scan name or label.""" + + scan_resources = func_scan_dct[scan] + + try: + keys = scan_resources.keys() + except AttributeError: + err = "\n[!] The data configuration file you provided is " \ + "missing a level under the 'func:' key. CPAC versions " \ + "1.2 and later use data configurations with an " \ + "additional level of nesting.\n\nExample\nfunc:\n " \ + "rest01:\n scan: /path/to/rest01_func.nii.gz\n" \ + " scan parameters: /path/to/scan_params.json\n\n" \ + "See the User Guide for more information.\n\n" + raise Exception(err) + + # actual 4D time series file + if "scan" not in scan_resources.keys(): + err = "\n\n[!] The {0} scan is missing its actual time-series " \ + "scan file, which should be a filepath labeled with the " \ + "'scan' key.\n\n".format(scan) + raise Exception(err) + + # Nipype restriction (may have changed) + if '.' in scan or '+' in scan or '*' in scan: + raise Exception('\n\n[!] Scan names cannot contain any special ' + 'characters (., +, *, etc.). Please update this ' + 'and try again.\n\nScan: {0}' + '\n\n'.format(scan)) + + def create_func_datasource(rest_dict, wf_name='func_datasource'): + """Return the functional timeseries-related file paths for each + series/scan, from the dictionary of functional files described in the data + configuration (sublist) YAML file. + Scan input (from inputnode) is an iterable. + """ import nipype.pipeline.engine as pe import nipype.interfaces.utility as util @@ -35,130 +89,30 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): 'magnitude']), name='outputspec') - scan_names = rest_dict.keys() - inputnode.iterables = [('scan', scan_names)] - - for scan in scan_names: - scan_resources = rest_dict[scan] - - # have this here for now because of the big change in the data - # configuration format - try: - keys = scan_resources.keys() - except AttributeError: - err = "\n[!] The data configuration file you provided is " \ - "missing a level under the 'func:' key. CPAC versions " \ - "1.2 and later use data configurations with an " \ - "additional level of nesting.\n\nExample\nfunc:\n " \ - "rest01:\n scan: /path/to/rest01_func.nii.gz\n" \ - " scan parameters: /path/to/scan_params.json\n\n" \ - "See the User Guide for more information.\n\n" - raise Exception(err) - - # actual 4D time series file - if "scan" not in scan_resources.keys(): - err = "\n\n[!] The {0} scan is missing its actual time-series " \ - "scan file, which should be a filepath labeled with the " \ - "'scan' key.\n\n".format(scan) - raise Exception(err) - - # Nipype restriction (may have changed) - if '.' in scan or '+' in scan or '*' in scan: - raise Exception('\n\n[!] Scan names cannot contain any special ' - 'characters (., +, *, etc.). Please update this ' - 'and try again.\n\nScan: {0}' - '\n\n'.format(scan)) - - # scan parameters CSV - if "scan_parameters" in scan_resources.keys(): - if isinstance(scan_resources["scan_parameters"], str): - if "s3://" in scan_resources["scan_parameters"]: - # if the scan parameters file is on AWS S3, download it - s3_scan_params = \ - pe.Node(util.Function(input_names=['file_path', - 'creds_path', - 'dl_dir', - 'img_type'], - output_names=['local_path'], - function=check_for_s3), - name='s3_scan_params') - - s3_scan_params.inputs.file_path = \ - scan_resources["scan_parameters"] - #s3_scan_params.inputs.dl_dir = dl_dir - - wf.connect(inputnode, 'creds_path', - s3_scan_params, 'creds_path') - wf.connect(s3_scan_params, 'local_path', - outputnode, 'scan_params') - wf.connect(inputnode, 'dl_dir', s3_scan_params, 'dl_dir') - - elif isinstance(scan_resources["scan_parameters"], dict): - get_scan_params_dct = \ - pe.Node(util.Function(input_names=['scan_params_dct'], - output_names=['scan_params_dct'], - function=extract_scan_params_dct), - name='get_scan_params_dct') - get_scan_params_dct.inputs.scan_params_dct = \ - scan_resources["scan_parameters"] - wf.connect(get_scan_params_dct, 'scan_params_dct', - outputnode, 'scan_params') - - # field map files (if applicable) - if "fmap_phase" in scan_resources.keys(): - - fmap_phase = scan_resources["fmap_phase"] - - if "fmap_mag" not in scan_resources.keys(): - err = "\n\n[!] The field map phase difference file has " \ - "been listed for scan {0}, but there is no field " \ - "map magnitude file.\n\nPhase difference file " \ - "listed: {1}\n\n".format(scan, fmap_phase) - raise Exception(err) - - s3_fmap_phase = pe.Node(util.Function(input_names=['file_path', - 'creds_path', - 'dl_dir', - 'img_type'], - output_names=['local_path'], - function=check_for_s3), - name='s3_fmap_phase') - s3_fmap_phase.inputs.file_path = fmap_phase - s3_fmap_phase.inputs.img_type = "other" - wf.connect(inputnode, 'creds_path', s3_fmap_phase, 'creds_path') - wf.connect(inputnode, 'dl_dir', s3_fmap_phase, 'dl_dir') - wf.connect(s3_fmap_phase, 'local_path', outputnode, 'phase_diff') - - if "fmap_mag" in scan_resources.keys(): - - fmap_mag = scan_resources["fmap_mag"] - - if "fmap_phase" not in scan_resources.keys(): - err = "\n\n[!] The field map magnitude file has been" \ - "listed for scan {0}, but there is no field map phase" \ - "difference file.\n\nPhase magnitude file " \ - "listed: {1}\n\n".format(scan, fmap_mag) - raise Exception(err) - - s3_fmap_mag = pe.Node(util.Function(input_names=['file_path', - 'creds_path', - 'dl_dir', - 'img_type'], - output_names=['local_path'], - function=check_for_s3), - name='s3_fmap_mag') - s3_fmap_mag.inputs.file_path = fmap_mag - s3_fmap_mag.inputs.img_type = "other" - wf.connect(inputnode, 'creds_path', s3_fmap_mag, 'creds_path') - wf.connect(inputnode, 'dl_dir', s3_fmap_mag, 'dl_dir') - wf.connect(s3_fmap_mag, 'local_path', outputnode, 'magnitude') - - selectrest = pe.Node(util.Function(input_names=['scan', 'rest_dict'], - output_names=['rest'], + # have this here for now because of the big change in the data + # configuration format + check_scan = pe.Node(util.Function(input_names=['func_scan_dct', + 'scan'], + output_names=[], + function=check_func_scan), + name='check_func_scan') + + check_scan.inputs.func_scan_dct = rest_dict + wf.connect(inputnode, 'scan', check_scan, 'scan') + + # get the functional scan itself + selectrest = pe.Node(util.Function(input_names=['scan', + 'rest_dict', + 'resource'], + output_names=['file_path'], function=get_rest), name='selectrest') selectrest.inputs.rest_dict = rest_dict + selectrest.inputs.resource = "scan" + wf.connect(inputnode, 'scan', selectrest, 'scan') + # check to see if it's on an Amazon AWS S3 bucket, and download it, if it + # is - otherwise, just return the local file path check_s3_node = pe.Node(util.Function(input_names=['file_path', 'creds_path', 'dl_dir', @@ -167,16 +121,88 @@ def create_func_datasource(rest_dict, wf_name='func_datasource'): function=check_for_s3), name='check_for_s3') - wf.connect(selectrest, 'rest', check_s3_node, 'file_path') + wf.connect(selectrest, 'file_path', check_s3_node, 'file_path') wf.connect(inputnode, 'creds_path', check_s3_node, 'creds_path') wf.connect(inputnode, 'dl_dir', check_s3_node, 'dl_dir') check_s3_node.inputs.img_type = 'func' - wf.connect(inputnode, 'scan', selectrest, 'scan') wf.connect(inputnode, 'subject', outputnode, 'subject') wf.connect(check_s3_node, 'local_path', outputnode, 'rest') wf.connect(inputnode, 'scan', outputnode, 'scan') + # scan parameters CSV + select_scan_params = pe.Node(util.Function(input_names=['scan', + 'rest_dict', + 'resource'], + output_names=['file_path'], + function=get_rest), + name='select_scan_params') + select_scan_params.inputs.rest_dict = rest_dict + select_scan_params.inputs.resource = "scan_parameters" + wf.connect(inputnode, 'scan', select_scan_params, 'scan') + + # if the scan parameters file is on AWS S3, download it + s3_scan_params = pe.Node(util.Function(input_names=['file_path', + 'creds_path', + 'dl_dir', + 'img_type'], + output_names=['local_path'], + function=check_for_s3), + name='s3_scan_params') + + wf.connect(select_scan_params, 'file_path', s3_scan_params, 'file_path') + wf.connect(inputnode, 'creds_path', s3_scan_params, 'creds_path') + wf.connect(inputnode, 'dl_dir', s3_scan_params, 'dl_dir') + wf.connect(s3_scan_params, 'local_path', outputnode, 'scan_params') + + # field map phase file, for field map distortion correction + select_fmap_phase = pe.Node(util.Function(input_names=['scan', + 'rest_dict', + 'resource'], + output_names=['file_path'], + function=get_rest), + name='select_fmap_phase') + select_fmap_phase.inputs.rest_dict = rest_dict + select_fmap_phase.inputs.resource = "fmap_phase" + wf.connect(inputnode, 'scan', select_fmap_phase, 'scan') + + s3_fmap_phase = pe.Node(util.Function(input_names=['file_path', + 'creds_path', + 'dl_dir', + 'img_type'], + output_names=['local_path'], + function=check_for_s3), + name='s3_fmap_phase') + s3_fmap_phase.inputs.img_type = "other" + wf.connect(select_fmap_phase, 'file_path', s3_fmap_phase, 'file_path') + wf.connect(inputnode, 'creds_path', s3_fmap_phase, 'creds_path') + wf.connect(inputnode, 'dl_dir', s3_fmap_phase, 'dl_dir') + wf.connect(s3_fmap_phase, 'local_path', outputnode, 'phase_diff') + + # field map magnitude file, for field map distortion correction + select_fmap_mag = pe.Node(util.Function(input_names=['scan', + 'rest_dict', + 'resource'], + output_names=['file_path'], + function=get_rest), + name='select_fmap_mag') + select_fmap_mag.inputs.rest_dict = rest_dict + select_fmap_mag.inputs.resource = "fmap_mag" + wf.connect(inputnode, 'scan', select_fmap_mag, 'scan') + + s3_fmap_mag = pe.Node(util.Function(input_names=['file_path', + 'creds_path', + 'dl_dir', + 'img_type'], + output_names=['local_path'], + function=check_for_s3), + name='s3_fmap_mag') + s3_fmap_mag.inputs.img_type = "other" + wf.connect(select_fmap_mag, 'file_path', s3_fmap_mag, 'file_path') + wf.connect(inputnode, 'creds_path', s3_fmap_mag, 'creds_path') + wf.connect(inputnode, 'dl_dir', s3_fmap_mag, 'dl_dir') + wf.connect(s3_fmap_mag, 'local_path', outputnode, 'magnitude') + return wf @@ -195,6 +221,18 @@ def check_for_s3(file_path, creds_path, dl_dir=None, img_type='anat'): if dl_dir is None: dl_dir = os.getcwd() + if file_path is None: + # in case it's something like scan parameters or field map files, but + # we don't have any + local_path = file_path + return local_path + + # TODO: remove this once scan parameter input as dictionary is phased out + if isinstance(file_path, dict): + # if this is a dictionary, just skip altogether + local_path = file_path + return local_path + # Explicitly lower-case the "s3" if file_path.lower().startswith(s3_str): file_path_sp = file_path.split('/') From 3f73fd5c41a4bffe327833ec1245cc6154f2612f Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 13 Apr 2018 19:53:16 -0400 Subject: [PATCH 44/75] Made some fixes for the two-sample paired group FSL preset when the condition type is for scans. --- CPAC/pipeline/cpac_group_runner.py | 96 ++++++++++++++++++----- CPAC/utils/create_fsl_model.py | 43 ++++------ CPAC/utils/create_group_analysis_files.py | 14 +++- scripts/cpac_cli.py | 5 +- 4 files changed, 106 insertions(+), 52 deletions(-) diff --git a/CPAC/pipeline/cpac_group_runner.py b/CPAC/pipeline/cpac_group_runner.py index a71b1954c1..58bc9a390f 100644 --- a/CPAC/pipeline/cpac_group_runner.py +++ b/CPAC/pipeline/cpac_group_runner.py @@ -437,8 +437,8 @@ def pheno_sessions_to_repeated_measures(pheno_df, sessions_list): return pheno_df -def pheno_series_to_repeated_measures(pheno_df, series_list, \ - repeated_sessions=False): +def pheno_series_to_repeated_measures(pheno_df, series_list, + repeated_sessions=False): # take in the selected series/scans, and create all of the permutations # of unique participant IDs (participant_site_session) and series/scans @@ -448,7 +448,23 @@ def pheno_series_to_repeated_measures(pheno_df, series_list, \ # enter the regular one import pandas as pd - + + # first, check to see if this design matrix setup has already been done + # in the pheno CSV file + num_partic_cols = 0 + for col_names in pheno_df.columns: + if "participant" in col_names: + num_partic_cols += 1 + if num_partic_cols > 1 and "scan" in pheno_df.columns: + for part_ses_id in pheno_df["participant_id"]: + if "participant_{0}".format(part_ses_id.split("_")[0]) in pheno_df.columns: + continue + break + else: + # if it's already set up properly, then just send the pheno_df + # back and bypass all the machinery below + return pheno_df + new_rows = [] for series in series_list: sub_pheno_df = pheno_df.copy() @@ -456,8 +472,7 @@ def pheno_series_to_repeated_measures(pheno_df, series_list, \ new_rows.append(sub_pheno_df) pheno_df = pd.concat(new_rows) - if repeated_sessions == False: - + if not repeated_sessions: # participant IDs new columns participant_id_cols = {} i = 0 @@ -533,14 +548,14 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): 'falff_to_standard_zstd', 'falff_to_standard_zstd_smooth', 'reho_to_standard_zstd', - 'reho_to_standard_smooth_zstd', + 'reho_to_standard_zstd_smooth', 'sca_roi_files_to_standard_fisher_zstd', - 'sca_roi_files_to_standard_smooth_fisher_zstd', + 'sca_roi_files_to_standard_fisher_zstd_smooth', 'sca_tempreg_maps_zstat_files', 'sca_tempreg_maps_zstat_files_smooth', 'vmhc_fisher_zstd_zstat_map', 'centrality_outputs_zstd', - 'centrality_outputs_smoothed_zstd', + 'centrality_outputs_zstd_smooth', 'dr_tempreg_maps_zstat_files_to_standard', 'dr_tempreg_maps_zstat_files_to_standard_smooth'] @@ -700,20 +715,23 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): if repeated_sessions or repeated_series: repeated_measures = True - if repeated_measures == True: - + if repeated_measures: if repeated_sessions: - new_pheno_df = pheno_sessions_to_repeated_measures( \ - new_pheno_df, - group_model.sessions_list) + # IF USING FSL PRESETS: new_pheno_df will get passed + # through unchanged + new_pheno_df = \ + pheno_sessions_to_repeated_measures(new_pheno_df, + group_model.sessions_list) # create new rows for all of the series, if applicable # ex. if 10 subjects and two sessions, 10 rows -> 20 rows if repeated_series: - new_pheno_df = pheno_series_to_repeated_measures( \ - new_pheno_df, - group_model.series_list, - repeated_sessions) + # IF USING FSL PRESETS: new_pheno_df will get passed + # through unchanged + new_pheno_df = \ + pheno_series_to_repeated_measures(new_pheno_df, + group_model.series_list, + repeated_sessions) # drop the pheno rows - if there are participants missing in # the output files (ex. if ReHo did not complete for 2 of the @@ -732,10 +750,46 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): join_columns = ["participant_id"] - # if Series is one of the categorically-encoded covariates, - # make sure we only are including the series the user has - # selected to include in the repeated measures analysis - if "Series" in new_pheno_df: + if "scan" in new_pheno_df: + # TODO: maybe come up with something more unique than + # TODO: "session" or "scan" for covariate names to signal + # TODO: when presets are being used? + # if we're using one of the FSL presets! + # IMPT: we need to match the rows with the actual scans + + # ALSO IMPT: we're going to rely on the series_list from + # the group model config to match, so always + # make sure the order remains the same + # example: the 1,1,1,-1,-1,-1 condition vector + # in the preset should be the first + # scan in the list for 1,1,1 and the + # second for -1,-1,-1 + scan_label_col = [] + for val in new_pheno_df["scan"]: + if len(group_model.series_list) == 2: + if val == 1: + scan_label_col.append( + group_model.series_list[0]) + elif val == -1: + scan_label_col.append( + group_model.series_list[1]) + new_pheno_df["Series"] = scan_label_col + + # now make sure the 1,1,1,-1,-1,-1,...etc. matches + # properly with the actual scans by merging + join_columns.append("Series") + new_pheno_df = pd.merge(new_pheno_df, output_df, + how="inner", on=join_columns) + run_label = "repeated_measures_multiple_series" + + analysis_dict[(model_name, group_config_file, resource_id, strat_info, run_label)] = \ + new_pheno_df + + elif "Series" in new_pheno_df: + # if Series is one of the categorically-encoded covariates + # make sure we only are including the series the user has + # selected to include in the repeated measures analysis + # check in case the pheno has series IDs that doesn't # exist in the output directory, first new_pheno_df = \ diff --git a/CPAC/utils/create_fsl_model.py b/CPAC/utils/create_fsl_model.py index 3941b10f81..d91f119290 100644 --- a/CPAC/utils/create_fsl_model.py +++ b/CPAC/utils/create_fsl_model.py @@ -1111,14 +1111,13 @@ def create_con_file(con_dict, col_names, file_name, current_output, out_dir): f.write("\n") -def create_fts_file(ftest_list, con_dict, model_name, current_output, \ - out_dir): +def create_fts_file(ftest_list, con_dict, model_name, current_output, + out_dir): import os import numpy as np try: - print "\nFound f-tests in your model, writing f-tests file " \ "(.fts)..\n" @@ -1146,7 +1145,6 @@ def create_fts_file(ftest_list, con_dict, model_name, current_output, \ fts_n = np.array(ftst) - # print labels for the columns - mainly for double-checking your # model col_string = '\n' @@ -1171,8 +1169,8 @@ def create_fts_file(ftest_list, con_dict, model_name, current_output, \ raise Exception(errmsg) -def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ - column_names, coding_scheme, group_sep): +def create_con_ftst_file(con_file, model_name, current_output, output_dir, + column_names, coding_scheme, group_sep): """ Create the contrasts and fts file @@ -1187,8 +1185,13 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ evs = evs.rstrip('\r\n').split(',') count_ftests = 0 + # TODO: this needs to be re-visited, but I think this was originally added + # TODO: to counteract the fact that if someone was making a custom + # TODO: contrasts matrix CSV, they wouldn't know that the design matrix + # TODO: would have the Intercept added to it? but what if it wasn't? + # TODO: comment out for now... but test # remove "Contrasts" label and replace it with "Intercept" - evs[0] = "Intercept" + #evs[0] = "Intercept" fTest = False @@ -1199,17 +1202,13 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ if count_ftests > 0: fTest = True - try: - data = np.genfromtxt(con_file, names=True, delimiter=',', dtype=None) except: - print "Error: Could not successfully read in contrast file: ",con_file raise Exception - lst = data.tolist() ftst = [] @@ -1225,7 +1224,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ # with the zeroes being the vector of contrasts for that contrast for tp in lst: - contrast_names.append(tp[0]) # create a list of integers that is the vector for the contrast @@ -1235,18 +1233,18 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ fts_vector = list(tp)[(length-count_ftests):length] fts_columns.append(fts_vector) + # TODO: see note about Intercept above # add Intercept column - if group_sep == False: - if coding_scheme == "Treatment": - con_vector.insert(0, 0) - elif coding_scheme == "Sum": - con_vector.insert(0, 1) + # if not group_sep: + # if coding_scheme == "Treatment": + # con_vector.insert(0, 0) + # elif coding_scheme == "Sum": + # con_vector.insert(0, 1) contrasts.append(con_vector) # contrast_names = list of the names of the contrasts (not regressors) # contrasts = list of lists with the contrast vectors - num_EVs_in_con_file = len(contrasts[0]) contrasts = np.array(contrasts, dtype=np.float16) @@ -1255,7 +1253,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ # if there are f-tests, create the array for them if fTest: - if len(contrast_names) < 2: errmsg = "\n\n[!] CPAC says: Not enough contrasts for running " \ "f-tests.\nTip: Do you have only one contrast in your " \ @@ -1267,7 +1264,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ fts_n = fts_columns.T if len(column_names) != (num_EVs_in_con_file): - err_string = "\n\n[!] CPAC says: The number of EVs in your model " \ "design matrix (found in the %s.mat file) does not " \ "match the number of EVs (columns) in your custom " \ @@ -1283,9 +1279,7 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ raise Exception(err_string) for design_mat_col, con_csv_col in zip(column_names, evs[1:]): - if con_csv_col not in design_mat_col: - errmsg = "\n\n[!] CPAC says: The names of the EVs in your " \ "custom contrasts .csv file do not match the names or " \ "order of the EVs in the design matrix. Please make " \ @@ -1298,7 +1292,6 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ out_dir = os.path.join(output_dir, model_name + '.con') with open(out_dir,"wt") as f: - idx = 1 pp_str = '/PPheights' re_str = '/RequiredEffect' @@ -1320,19 +1313,15 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, \ col_string = col_string + ev + '\t' print >>f, col_string, '\n' - print >>f, '/Matrix' - np.savetxt(f, contrasts, fmt='%1.5e', delimiter='\t') if fTest: - print "\nFound f-tests in your model, writing f-tests file (.fts)..\n" ftest_out_dir = os.path.join(output_dir, model_name + '.fts') with open(ftest_out_dir,"wt") as f: - print >>f, '/NumWaves\t', (contrasts.shape)[0] print >>f, '/NumContrasts\t', count_ftests diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_group_analysis_files.py index 1946c6a0f1..c861264337 100644 --- a/CPAC/utils/create_group_analysis_files.py +++ b/CPAC/utils/create_group_analysis_files.py @@ -32,6 +32,14 @@ def write_group_list_text_file(group_list, out_file=None): import os + # prevent duplicates - depending on how the design matrix is set up, we + # might have multiples of the sub_ses_ID's, like if we're doing repeated + # measures with series/scans + new_group_list = [] + for sub_ses_id in group_list: + if sub_ses_id not in new_group_list: + new_group_list.append(sub_ses_id) + if not out_file: out_file = os.path.join(os.getcwd(), "group_analysis_participant_" "list.txt") @@ -42,7 +50,7 @@ def write_group_list_text_file(group_list, out_file=None): os.makedirs(dir_path) with open(out_file, "wt") as f: - for part_id in group_list: + for part_id in new_group_list: f.write("{0}\n".format(part_id)) if os.path.exists(out_file): @@ -759,8 +767,8 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, raise Exception("not one of the valid presets") # write participant list text file - group_list_text_file = write_group_list_text_file(design_df["participant_id"], - group_list_text_file) + write_group_list_text_file(design_df["participant_id"], + group_list_text_file) # write design matrix CSV write_dataframe_to_csv(design_df, group_config["pheno_file"]) diff --git a/scripts/cpac_cli.py b/scripts/cpac_cli.py index b7a70ee637..b8cd204be9 100644 --- a/scripts/cpac_cli.py +++ b/scripts/cpac_cli.py @@ -49,6 +49,9 @@ def main(): parser.add_argument("--covariate", type=str, default=None, help="the additional covariate for single-group " "average") + parser.add_argument("--condition_type", type=str, default=None, + help="if running two-sample T-tests, if the two " + "groups are 'session' or 'scan'") args = parser.parse_args() @@ -75,7 +78,7 @@ def main(): args.p_thresh, args.analysis_preset, args.pheno_file, args.pheno_sub_label, output_dir, args.model_name, - args.covariate) + args.covariate, args.condition_type) if __name__ == "__main__": From 59c9cf95932beb36497aa789b5e5e943b692cab5 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Mon, 16 Apr 2018 01:11:45 -0400 Subject: [PATCH 45/75] Fixed a .con file header labeling error. --- CPAC/pipeline/cpac_ga_model_generator.py | 5 ++--- CPAC/utils/create_flame_model_files.py | 16 ++++------------ CPAC/utils/create_fsl_model.py | 9 ++++++--- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/CPAC/pipeline/cpac_ga_model_generator.py b/CPAC/pipeline/cpac_ga_model_generator.py index fe43446287..c91ccf23c9 100755 --- a/CPAC/pipeline/cpac_ga_model_generator.py +++ b/CPAC/pipeline/cpac_ga_model_generator.py @@ -304,7 +304,7 @@ def calculate_custom_roi_mean_in_df(model_df, roi_mask): def parse_out_covariates(design_formula): - patsy_ops = ["~","+","-","*","/",":","**",")","("] + patsy_ops = ["~", "+", "-", "*", "/", ":", "**", ")", "("] for op in patsy_ops: if op in design_formula: @@ -391,8 +391,7 @@ def split_groups(pheno_df, group_ev, ev_list, cat_list): def patsify_design_formula(formula, categorical_list, encoding="Treatment"): - print formula - print categorical_list + closer = ")" if encoding == "Treatment": closer = ")" diff --git a/CPAC/utils/create_flame_model_files.py b/CPAC/utils/create_flame_model_files.py index a3ba290131..133401ee99 100644 --- a/CPAC/utils/create_flame_model_files.py +++ b/CPAC/utils/create_flame_model_files.py @@ -7,7 +7,6 @@ def create_dummy_string(length): return ppstring - def write_mat_file(design_matrix, output_dir, model_name, \ depatsified_EV_names, current_output=None): @@ -23,7 +22,6 @@ def write_mat_file(design_matrix, output_dir, model_name, \ else: dimx, dimy = design_matrix.shape - ppstring = '/PPheights' for i in range(0, dimy): @@ -32,14 +30,12 @@ def write_mat_file(design_matrix, output_dir, model_name, \ ppstring += '\n' - filename = model_name + ".mat" out_file = os.path.join(output_dir, filename) if not os.path.exists(output_dir): os.makedirs(output_dir) - with open(out_file, 'wt') as f: print >>f, '/NumWaves\t%d' %dimy @@ -61,7 +57,6 @@ def write_mat_file(design_matrix, output_dir, model_name, \ return out_file - def create_grp_file(design_matrix, grp_file_vector, output_dir, model_name): import os @@ -90,7 +85,6 @@ def create_grp_file(design_matrix, grp_file_vector, output_dir, model_name): return out_file - def create_con_file(con_vecs, con_names, col_names, model_name, current_output, out_dir): @@ -131,7 +125,6 @@ def create_con_file(con_vecs, con_names, col_names, model_name, return out_file - def create_fts_file(ftest_list, con_names, model_name, current_output, out_dir): @@ -169,7 +162,6 @@ def create_fts_file(ftest_list, con_names, model_name, fts_n = np.array(ftst) - # print labels for the columns - mainly for double-checking your # model col_string = '\n' @@ -182,7 +174,6 @@ def create_fts_file(ftest_list, con_names, model_name, for i in range(fts_n.shape[0]): print >>f, ' '.join(fts_n[i].astype('str')) - except Exception as e: filepath = os.path.join(out_dir, "model_files", current_output, \ @@ -220,7 +211,7 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, raise Exception # remove "Contrasts" label and replace it with "Intercept" - evs[0] = "Intercept" + #evs[0] = "Intercept" # Count the number of f tests defined count_ftests = len([ev for ev in evs if "f_test" in ev ]) @@ -266,7 +257,7 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, fts_columns.append(fts_vector) # add Intercept column - if group_sep == False: + if not group_sep: if False: # The following insertion gives an error further down the # line, because this suggests that there will be an intercept @@ -358,7 +349,8 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, # print labels for the columns - mainly for double-checking your model col_string = '\n' for ev in evs: - col_string = col_string + ev + '\t' + if "contrast" not in ev and "Contrast" not in ev: + col_string = col_string + ev + '\t' print >>f, col_string, '\n' print >>f, '/Matrix' diff --git a/CPAC/utils/create_fsl_model.py b/CPAC/utils/create_fsl_model.py index d91f119290..f700577848 100644 --- a/CPAC/utils/create_fsl_model.py +++ b/CPAC/utils/create_fsl_model.py @@ -1080,6 +1080,9 @@ def create_con_file(con_dict, col_names, file_name, current_output, out_dir): import os + print "col names: " + print col_names + with open(os.path.join(out_dir, file_name) + ".con",'w+') as f: # write header @@ -1171,7 +1174,6 @@ def create_fts_file(ftest_list, con_dict, model_name, current_output, def create_con_ftst_file(con_file, model_name, current_output, output_dir, column_names, coding_scheme, group_sep): - """ Create the contrasts and fts file """ @@ -1179,7 +1181,7 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, import os import numpy as np - with open(con_file,"r") as f: + with open(con_file, "r") as f: evs = f.readline() evs = evs.rstrip('\r\n').split(',') @@ -1194,7 +1196,8 @@ def create_con_ftst_file(con_file, model_name, current_output, output_dir, #evs[0] = "Intercept" fTest = False - + print "evs: " + print evs for ev in evs: if "f_test" in ev: count_ftests += 1 From a4f8b091ed1fa8ac49d04e5026b0cf81922b9a41 Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Tue, 17 Apr 2018 15:06:29 -0400 Subject: [PATCH 46/75] Ignore files generated by Cython --- .gitignore | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 66f1b707ff..baae6229c2 100755 --- a/.gitignore +++ b/.gitignore @@ -21,15 +21,19 @@ pip-log.txt .coverage .tox -#Translations +# Translations *.mo -#Mr Developer +# Mr Developer .mr.developer.cfg -#CPAC Specific +# CPAC Specific /doc/_build -#Eclipse project files +# Eclipse project files .project .pydevproject + +# Transpiling and compiling +*.c +*.so \ No newline at end of file From 1dc13ff84c9cbbd92ba3136271690c9328ede633 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Wed, 18 Apr 2018 12:41:07 -0400 Subject: [PATCH 47/75] Initialized group list to fix merge error --- CPAC/utils/build_data_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index 8d94057e3e..3ae9588c6a 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -1601,7 +1601,7 @@ def run(data_settings_yml): # put data_dct contents in an ordered list for the YAML dump data_list = [] - + group_list = [] included = {'site': [], 'sub': []} num_sess = num_scan = 0 From 0b6a0a4bdcc66bdb62b4e2bfd11b39c94823b63a Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Thu, 19 Apr 2018 12:52:31 -0400 Subject: [PATCH 48/75] Generate QA pages option for GUI added --- CPAC/GUI/interface/pages/settings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CPAC/GUI/interface/pages/settings.py b/CPAC/GUI/interface/pages/settings.py index 53ed1f2b44..c449077813 100644 --- a/CPAC/GUI/interface/pages/settings.py +++ b/CPAC/GUI/interface/pages/settings.py @@ -242,12 +242,12 @@ def __init__(self, parent, counter=0): "version of the output directory.", values=["On", "Off"]) - #self.page.add(label="Enable Quality Control Interface ", - # control=control.CHOICE_BOX, - # name='generateQualityControlImages', - # type=dtype.LSTR, - # comment="Generate quality control pages containing preprocessing and derivative outputs.", - # values=["On", "Off"]) + self.page.add(label="Enable Quality Control Interface ", + control=control.CHOICE_BOX, + name='generateQualityControlImages', + type=dtype.LSTR, + comment="Generate quality control pages containing preprocessing and derivative outputs.", + values=["On", "Off"]) self.page.add(label="Remove Working Directory ", control=control.CHOICE_BOX, From 36b2a30f94ec2d7fcd434c001ad2f2f52c7136c7 Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Thu, 19 Apr 2018 16:24:18 -0400 Subject: [PATCH 49/75] update require future lib --- CPAC/info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CPAC/info.py b/CPAC/info.py index a48c0b1ef9..849310404f 100644 --- a/CPAC/info.py +++ b/CPAC/info.py @@ -138,7 +138,7 @@ def get_cpac_gitversion(): "pyyaml (>=3.0)", "pygraphviz (>=1.3)", "nibabel (>=2.0.1)", "nipype (==0.13.1)", "patsy (>=0.3)", "psutil (>=2.1)", "boto3 (>=1.2)", - "future (==0.15.2)", "prov (>=1.4.0)", + "future (==0.16.0)", "prov (>=1.4.0)", "simplejson (>=3.8.0)", "cython (>=0.12.1)", "Jinja2 (>=2.6)", "pandas (>=0.15)", "INDI_Tools (>=0.0.6)", "memory_profiler (>=0.41)", @@ -146,7 +146,7 @@ def get_cpac_gitversion(): INSTALL_REQUIRES = ["matplotlib >=1.2", "lockfile >=0.9", "pyyaml >=3.0", "pygraphviz >=1.3", "nibabel >=2.0.1", "nipype ==0.13.1", "patsy >=0.3", "psutil >=2.1", - "boto3 >=1.2", "future ==0.15.2", "prov >=1.4.0", + "boto3 >=1.2", "future ==0.16.0", "prov >=1.4.0", "simplejson >=3.8.0", "cython >=0.12.1", "Jinja2 >=2.6", "pandas >=0.15", "INDI-Tools >=0.0.6", "memory_profiler >=0.41", "ipython >=5.1"] From 1a106216ff6cff2e5ea2038e6b18f5fbea09b1da Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Thu, 19 Apr 2018 16:29:37 -0400 Subject: [PATCH 50/75] Remove unused modules --- CPAC/utils/__init__.py | 1 - CPAC/utils/extract_data.py | 1 - 2 files changed, 2 deletions(-) diff --git a/CPAC/utils/__init__.py b/CPAC/utils/__init__.py index 461ff0bbda..22ed63beb0 100644 --- a/CPAC/utils/__init__.py +++ b/CPAC/utils/__init__.py @@ -3,7 +3,6 @@ import create_fsl_model import extract_parameters import build_data_config -import build_sublist from utils import * from .extract_data import run from .datasource import create_anat_datasource diff --git a/CPAC/utils/extract_data.py b/CPAC/utils/extract_data.py index 181830d7cf..f8065162e0 100644 --- a/CPAC/utils/extract_data.py +++ b/CPAC/utils/extract_data.py @@ -662,7 +662,6 @@ def run(data_config): "in CPAC configuration\n") s_param_map = None - CPAC.utils.build_sublist.build_sublist(data_config) generate_supplementary_files(c.outputSubjectListLocation, c.subjectListName) From b11117598d5cce1f63710f11ef699136f62b5ec6 Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Thu, 19 Apr 2018 17:10:37 -0400 Subject: [PATCH 51/75] fix package versions to met dependencies --- scripts/cpac_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cpac_install.sh b/scripts/cpac_install.sh index b5681f832e..becb4228ff 100755 --- a/scripts/cpac_install.sh +++ b/scripts/cpac_install.sh @@ -80,7 +80,7 @@ ubuntu1610_packages=("libmotif-dev" "xutils-dev" "libtool" "libx11-dev" "x11prot conda_packages=("pandas" "cython" "numpy==1.11" "scipy" "matplotlib" "networkx==1.11" "traits" "pyyaml" "jinja2==2.7.2" "nose" "ipython" "pip" "wxpython==3.0.0.0") -pip_packages=("future==0.15.2" "prov==1.5.0" "simplejson" "lockfile" "pygraphviz" "nibabel" "nipype==0.13.1" "patsy" "memory_profiler" "psutil" "configparser" "INDI-Tools" "fs==0.5.4" "boto3") +pip_packages=("future==0.16.0" "python-dateutil==2.2" "prov==1.5.0" "simplejson" "lockfile" "pygraphviz" "nibabel" "nipype==0.13.1" "patsy" "memory_profiler" "psutil" "configparser" "INDI-Tools" "fs==0.5.4" "boto3") ##### Helper functions for installing system dependencies. From 3f976a1339063ffb013908046126caab657b5b8a Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Thu, 19 Apr 2018 18:43:23 -0400 Subject: [PATCH 52/75] fix resource pool keys --- CPAC/pipeline/cpac_pipeline.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index b33adcb51a..a427250a23 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -4238,13 +4238,6 @@ def calc_avg(output_name, strat, num_strat, map_node=False): if 1 in c.generateQualityControlImages: - preproc, out_file = strat.get_node_from_resource_pool('preprocessed') - brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') - func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') - anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') - mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') - - #register color palettes register_pallete(os.path.realpath( os.path.join(CPAC.__path__[0], 'qc', 'red.py')), 'red') @@ -4258,13 +4251,17 @@ def calc_avg(output_name, strat, num_strat, map_node=False): os.path.join(CPAC.__path__[0], 'qc', 'cyan_to_yellow.py')), 'cyan_to_yellow') hist = pe.Node(util.Function(input_names=['measure_file','measure'],output_names = ['hist_path'],function = gen_histogram),name = 'histogram') - - for strat in strat_list: nodes = getNodeList(strat) + preproc, out_file = strat.get_node_from_resource_pool('functional_preprocessed') + brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') + func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') + anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') + mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') + #make SNR plot try: @@ -4439,7 +4436,7 @@ def calc_avg(output_name, strat, num_strat, map_node=False): # make QC montages for mni normalized anatomical image try: - mni_anat_underlay, out_file = strat.get_node_from_resource_pool('mni_normalized_anatomical') + mni_anat_underlay, out_file = strat.get_node_from_resource_pool('mean_functional_in_anat') montage_mni_anat = create_montage('montage_mni_anat_%d' % num_strat, 'red', 'mni_anat') From 74266eb83c1dc0ca7f2bbc09b18eb4af0bd0ed31 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Fri, 20 Apr 2018 12:27:44 -0400 Subject: [PATCH 53/75] Fixed merge errors for QC + nipype version error --- CPAC/pipeline/cpac_pipeline.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index a427250a23..9bff7c2b9f 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -4237,7 +4237,7 @@ def calc_avg(output_name, strat, num_strat, map_node=False): """"""""""""""""""""""""""""""""""""""""""""""""""" if 1 in c.generateQualityControlImages: - + #register color palettes register_pallete(os.path.realpath( os.path.join(CPAC.__path__[0], 'qc', 'red.py')), 'red') @@ -4255,17 +4255,14 @@ def calc_avg(output_name, strat, num_strat, map_node=False): for strat in strat_list: nodes = getNodeList(strat) - - preproc, out_file = strat.get_node_from_resource_pool('functional_preprocessed') - brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') - func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') - anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') - mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') - #make SNR plot try: - + preproc, out_file = strat.get_node_from_resource_pool('functional_preprocessed') + brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') + func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') + anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') + mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') hist_ = hist.clone('hist_snr_%d' % num_strat) hist_.inputs.measure = 'snr' @@ -5151,6 +5148,8 @@ def is_number(s): 'nipype repo at https:/github.com/fcp-indi/nipype.\n' \ 'Error: %s' % (os.path.dirname(nipype.__file__), exc) logger.error(err_msg) + if nipype.__version__ != '0.13.1': + print "This version of nipype may not be compatible with CPAC v1.2, please install version 0.13.1" # raise Exception(err_msg) # Actually run the pipeline now, for the current subject From 12d911a41be171802fb4c1e21f5b27a0acbca380 Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Fri, 20 Apr 2018 12:43:19 -0400 Subject: [PATCH 54/75] do not override custom pipeline name --- CPAC/pipeline/cpac_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CPAC/pipeline/cpac_runner.py b/CPAC/pipeline/cpac_runner.py index 4617bad871..5ddea6387a 100644 --- a/CPAC/pipeline/cpac_runner.py +++ b/CPAC/pipeline/cpac_runner.py @@ -411,7 +411,7 @@ def run(config_file, subject_list_file, p_name=None, plugin=None, validate(c) # Get the pipeline name - p_name = c.pipelineName + p_name = p_name or c.pipelineName # Load in subject list try: From fd2726b73fc289df3bb19b1d4acad9fe15c56c00 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Mon, 23 Apr 2018 18:39:37 -0400 Subject: [PATCH 55/75] Added checks needed for the tripled T-test preset. --- CPAC/pipeline/cpac_group_runner.py | 9 +- CPAC/utils/create_group_analysis_files.py | 300 ++++++++++++++++++++-- 2 files changed, 279 insertions(+), 30 deletions(-) diff --git a/CPAC/pipeline/cpac_group_runner.py b/CPAC/pipeline/cpac_group_runner.py index 58bc9a390f..0351a2f8f4 100644 --- a/CPAC/pipeline/cpac_group_runner.py +++ b/CPAC/pipeline/cpac_group_runner.py @@ -387,11 +387,12 @@ def pheno_sessions_to_repeated_measures(pheno_df, sessions_list): # first, check to see if this design matrix setup has already been done # in the pheno CSV file + # NOTE: this is mainly for PRESET GROUP ANALYSIS MODELS!!! num_partic_cols = 0 for col_names in pheno_df.columns: if "participant" in col_names: num_partic_cols += 1 - if num_partic_cols > 1 and "session" in pheno_df.columns: + if num_partic_cols > 1 and ("session" in pheno_df.columns or "session_column_one" in pheno_df.columns): for part_ses_id in pheno_df["participant_id"]: if "participant_{0}".format(part_ses_id.split("_")[0]) in pheno_df.columns: continue @@ -412,10 +413,9 @@ def pheno_sessions_to_repeated_measures(pheno_df, sessions_list): for participant_unique_id in list(pheno_df["participant_id"]): part_col = [0] * len(pheno_df["participant_id"]) for session in sessions_list: - if session in participant_unique_id: + if session in participant_unique_id.split("_")[1]: # generate/update sessions categorical column - part_id = participant_unique_id.replace(session, "") - part_id = part_id.replace("_","") + part_id = participant_unique_id.split("_")[0] part_ids_col.append(part_id) sessions_col.append(session) header_title = "participant_%s" % part_id @@ -838,6 +838,7 @@ def prep_analysis_df_dict(config_file, pipeline_output_folder): # this can be removed/modified once sessions are no # longer integrated in the full unique participant IDs + print newer_pheno_df if "Session" in newer_pheno_df.columns: # TODO: re-visit why there is a "participant_ID" # TODO: column? will this still work without diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_group_analysis_files.py index c861264337..5158266148 100644 --- a/CPAC/utils/create_group_analysis_files.py +++ b/CPAC/utils/create_group_analysis_files.py @@ -342,7 +342,27 @@ def preset_unpaired_two_group(group_list, pheno_df, groups, pheno_sub_label, output_dir=None, model_name="two_sample_unpaired_T-test"): """Set up the design matrix and contrasts matrix for running an unpaired - two-group difference (two-sample unpaired T-test).""" + two-group difference (two-sample unpaired T-test). + + group_list: a list of strings- sub_ses unique IDs + pheno_df: a Pandas DataFrame object of the phenotypic file CSV/matrix + groups: a list of either one or two strings- design matrix EV/covariate + labels to take from the phenotype DF and include in the model + pheno_sub_label: a string of the label name of the column in the phenotype + file that holds the participant/session ID for each row + output_dir: (optional) string of the output directory path + model_name: (optional) name/label of the model to run + + Sets up the model described here: + https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FEAT/UserGuide + #Unpaired_Two-Group_Difference_.28Two-Sample_Unpaired_T-Test.29 + + Only one "group" will be provided usually if the two groups in the + phenotypic information you wish to compare are encoded in one covariate + column, as categorical information. Thus, providing this one name will + pull it from the phenotype file, and this function will break it out into + two columns using dummy-coding. + """ import os @@ -458,37 +478,27 @@ def preset_paired_two_group(group_list, conditions, condition_type="session", output_dir=None, model_name="two_sample_unpaired_T-test"): """Set up the design matrix and contrasts matrix for running an paired - two-group difference (two-sample paired T-test).""" - - # TODO: NEXT!!!!!!!!!!!!!!!!! - # if the conditions are delineated by sessions, then the matrix will - # have to have both copies of the sub_ses_id in the design matrix - # conversely, if they are delineated by scans, then we have to have a - # design matrix with only one copy of the subs, then let the internal - # (infernal?) machinery do the doubling + two-group difference (two-sample paired T-test). + + group_list: a list of strings- sub_ses unique IDs + conditions: a two-item list of strings- session or series/scan names of + the two sessions or two scans (per participant) you wish to + compare + condition_type: a string, either "session" or "scan", depending on what + is in "conditions" + output_dir: (optional) string of the output directory path + model_name: (optional) name/label of the model to run + + Sets up the model described here: + https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FEAT/UserGuide + #Paired_Two-Group_Difference_.28Two-Sample_Paired_T-Test.29 + """ import os if not output_dir: output_dir = os.getcwd() - # TODO: handle conditions (sessons? scans?) - # TODO: make sure the 1, -1 vector doesn't get clobbered by Patsy - ''' - we need, to give in a list of sub_ses. and a list of the two sessions. - and this needs to spit out a design df that has the sub_ses doubled - in appropriate order, with the 1 and -1. and the other columns (avoid - letting the cpac thing process it if you can avoid it). - - but what if it's the scans instead? now it gets ugly. - here's a list of just sub_ses (with one ses). and a list of the two - scans, right? now what? - you have to send it in and let cpac handle it, unfortunately. - TODO: would it be worth it to quickly enable a custom pheno - input? I don't know if cpac is going to do the subs - columns properly, especially into the custom contrasts.. - ''' - if len(conditions) != 2: # TODO: msg raise Exception @@ -620,6 +630,211 @@ def preset_paired_two_group(group_list, conditions, condition_type="session", return design_df, contrasts_df, group_config +def preset_tripled_two_group(group_list, conditions, condition_type="session", + output_dir=None, + model_name="tripled_T-test"): + """Set up the design matrix and contrasts matrix for running a tripled + two-group difference ('tripled' T-test). + + group_list: a list of strings- sub_ses unique IDs + conditions: a three-item list of strings- session or series/scan names of + the three sessions or three scans (per participant) you wish + to compare + condition_type: a string, either "session" or "scan", depending on what + is in "conditions" + output_dir: (optional) string of the output directory path + model_name: (optional) name/label of the model to run + + Sets up the model described here: + https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FEAT/UserGuide + #Tripled_Two-Group_Difference_.28.22Tripled.22_T-Test.29 + """ + + import os + + if not output_dir: + output_dir = os.getcwd() + + if len(conditions) != 3: + # TODO: msg + raise Exception + + design_df = create_design_matrix_df(group_list) + + # make the "condition" EVs (the 1's, -1's, and 0's delineating the three + # conditions, with the "conditions" being the three sessions or three + # scans) + condition_ev_one = [] + condition_ev_two = [] + + if condition_type == "session": + # note: the participant_id column in design_df should be in order, so + # the condition_ev's should come out in order: + # 1,1,1,-1,-1,-1, 0, 0, 0 (this is checked further down) + # 1,1,1, 0, 0, 0,-1,-1,-1 + for sub_ses_id in design_df["participant_id"]: + if sub_ses_id.split("_")[-1] == conditions[0]: + condition_ev_one.append(1) + condition_ev_two.append(1) + elif sub_ses_id.split("_")[-1] == conditions[1]: + condition_ev_one.append(-1) + condition_ev_two.append(0) + elif sub_ses_id.split("_")[-1] == conditions[2]: + condition_ev_one.append(0) + condition_ev_two.append(-1) + + group_config = {"sessions_list": conditions, "series_list": []} + + elif condition_type == "scan": + # TODO: re-visit later, when session/scan difference in how to run + # TODO: group-level analysis repeated measures is streamlined and + # TODO: simplified + # the information needed in this part is not encoded in the group + # sublist! user inputs the two scan names, and we have a list of + # sub_ses (which needs to be doubled), with each scan paired to each + # half of this list (will need to ensure these scans exist for each + # selected derivative in the output directory later on) + + for sub_ses_id in design_df["participant_id"]: + condition_ev_one.append(1) + condition_ev_two.append(1) + for sub_ses_id in design_df["participant_id"]: + condition_ev_one.append(-1) + condition_ev_two.append(0) + for sub_ses_id in design_df["participant_id"]: + condition_ev_one.append(0) + condition_ev_two.append(-1) + + # NOTE: there is only one iteration of the sub_ses list in + # design_df["participant_id"] at this point! so use append + # (twice) triple that column: + design_df_double = design_df.append(design_df) + design_df = design_df_double.append(design_df) + + group_config = {"sessions_list": [], "series_list": conditions} + + else: + # TODO: msg + raise Exception + + # let's check to make sure it came out right + # first third + for val in condition_ev_one[0:(len(condition_ev_one) / 3) - 1]: + if val != 1: + # TODO: msg + raise Exception + # second third + for val in condition_ev_one[(len(condition_ev_one) / 3):(len(condition_ev_one)/3)*2]: + if val != -1: + # TODO: msg + raise Exception + # third... third + for val in condition_ev_one[((len(condition_ev_one)/3)*2 + 1):]: + if val != 0: + # TODO: msg + raise Exception + # first third + for val in condition_ev_two[0:(len(condition_ev_two) / 3) - 1]: + if val != 1: + # TODO: msg + raise Exception + # second third + for val in condition_ev_two[(len(condition_ev_two) / 3):(len(condition_ev_two)/3)*2]: + if val != 0: + # TODO: msg + raise Exception + # third... third + for val in condition_ev_two[((len(condition_ev_two)/3)*2 + 1):]: + if val != -1: + # TODO: msg + raise Exception + + # label the two covariate columns which encode the three conditions + column_one = "{0}_column_one".format(condition_type) + column_two = "{0}_column_two".format(condition_type) + + design_df[column_one] = condition_ev_one + design_df[column_two] = condition_ev_two + + # initalize the contrast dct's + contrast_one = {} + contrast_two = {} + contrast_three = {} + + design_formula = "{0} + {1}".format(column_one, column_two) + + # create the participant identity columns + for sub_ses_id in design_df["participant_id"]: + new_part_col = [] + sub_id = sub_ses_id.split("_")[0] + new_part_label = "participant_{0}".format(sub_id) + for moving_sub_ses_id in design_df["participant_id"]: + moving_sub_id = moving_sub_ses_id.split("_")[0] + if moving_sub_id == sub_id: + new_part_col.append(1) + else: + new_part_col.append(0) + design_df[new_part_label] = new_part_col + contrast_one.update({new_part_label: 0}) + contrast_two.update({new_part_label: 0}) + contrast_three.update({new_part_label: 0}) + if new_part_label not in design_formula: + design_formula = "{0} + {1}".format(design_formula, + new_part_label) + + # finish the contrasts + # should be something like + # ses,ses,sub,sub,sub, etc. + # ses-1 - ses-2: 2, 1, 0, 0, 0... + # ses-1 - ses-3: 1, 2, 0, 0, 0... + # ses-2 - ses-3: -1, 1, 0, 0, 0, etc. + contrast_one.update({ + "contrasts": "{0}-{1} - {2}-{3}".format(condition_type, + conditions[0], + condition_type, + conditions[1])}) + contrast_two.update({ + "contrasts": "{0}-{1} - {2}-{3}".format(condition_type, + conditions[0], + condition_type, + conditions[2])}) + + contrast_three.update({ + "contrasts": "{0}-{1} - {2}-{3}".format(condition_type, + conditions[1], + condition_type, + conditions[2])}) + + contrast_one.update({column_one: 2, column_two: 1}) + contrast_two.update({column_one: 1, column_two: 2}) + contrast_three.update({column_one: -1, column_two: 1}) + + contrasts = [contrast_one, contrast_two, contrast_three] + + contrasts_df = create_contrasts_template_df(design_df, contrasts) + + # create design and contrasts matrix file paths + design_mat_path = os.path.join(output_dir, model_name, + "design_matrix_{0}.csv".format(model_name)) + + contrasts_mat_path = os.path.join(output_dir, model_name, + "contrasts_matrix_{0}.csv" + "".format(model_name)) + + # start group config yaml dictionary + group_config.update({"pheno_file": design_mat_path, + "ev_selections": {"demean": [], + "categorical": []}, + "design_formula": design_formula, + "group_sep": "Off", + "grouping_var": None, + "custom_contrasts": contrasts_mat_path, + "model_name": model_name, + "output_dir": os.path.join(output_dir, model_name)}) + + return design_df, contrasts_df, group_config + + def run(group_list_text_file, derivative_list, z_thresh, p_thresh, preset=None, pheno_file=None, pheno_sub_label=None, output_dir=None, model_name=None, covariate=None, condition_type=None, run=False): @@ -762,6 +977,39 @@ def run(group_list_text_file, derivative_list, z_thresh, p_thresh, group_config.update(group_config_update) + elif preset == "tripled_two": + # run a "tripled" T-test + + # we need it as repeated measures- either session or scan + # and the list of subs + # also: the two session or scan names (in a list together), and + # whether they are sessions or scans + + if not covariate: + # TODO: message + raise Exception("the three conditions were not provided") + + if not condition_type: + # TODO: message + raise Exception("you didn't specify whether the three groups are " + "sessions or series/scans") + + # we're assuming covariate (which in this case, is the three sessions, + # or three scans) will be coming in as a string of either one + # covariate name, or a string with three covariates separated by a + # comma + # either way, it needs to be in list form in this case, not string + covariate = covariate.split(",") + + design_df, contrasts_df, group_config_update = \ + preset_tripled_two_group(group_list, + conditions=covariate, + condition_type=condition_type, + output_dir=output_dir, + model_name=model_name) + + group_config.update(group_config_update) + else: # TODO: not a real preset! raise Exception("not one of the valid presets") From 959ae10c3299ee22343d23aeb936e18e96970dde Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Tue, 24 Apr 2018 12:45:06 -0400 Subject: [PATCH 56/75] fixed QC merge errors Added an option before the QC html page generation to prevent errors popping up when QC is disabled. --- CPAC/pipeline/cpac_pipeline.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 9bff7c2b9f..6ad6ae95dc 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -5210,18 +5210,19 @@ def is_number(s): for count, scanID in enumerate(pip_ids): for scan in scan_ids: create_log_node(None, None, count, scan).run() - for pip_id in pip_ids: - try: - pipeline_base = os.path.join(c.outputDirectory, 'pipeline_%s' % pip_id) - qc_output_folder = os.path.join(pipeline_base, subject_id, 'qc_files_here') - generateQCPages(qc_output_folder,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) + if 1 in c.generateQualityControlImages: + for pip_id in pip_ids: + try: + pipeline_base = os.path.join(c.outputDirectory, 'pipeline_%s' % pip_id) + qc_output_folder = os.path.join(pipeline_base, subject_id, 'qc_files_here') + generateQCPages(qc_output_folder,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) #create_all_qc.run(pipeline_base) - except Exception as e: - print "Error: this function is not running" - print "" - print e - print type(e) - raise Exception + except Exception as e: + print "Error: The QC function page generation is not running" + print "" + print e + print type(e) + raise Exception From 4cff36c96d024773224223974e6178e4d1548f56 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Thu, 26 Apr 2018 13:52:37 -0400 Subject: [PATCH 57/75] renamed QC-node names to prevent montage errors --- CPAC/pipeline/cpac_pipeline.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 6ad6ae95dc..5de9ca21a3 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -4237,6 +4237,13 @@ def calc_avg(output_name, strat, num_strat, map_node=False): """"""""""""""""""""""""""""""""""""""""""""""""""" if 1 in c.generateQualityControlImages: + + preproc, out_file = strat.get_node_from_resource_pool('functional_preprocessed') + brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') + func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') + anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') + mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') + #register color palettes register_pallete(os.path.realpath( @@ -4258,11 +4265,11 @@ def calc_avg(output_name, strat, num_strat, map_node=False): #make SNR plot try: - preproc, out_file = strat.get_node_from_resource_pool('functional_preprocessed') - brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') - func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') - anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') - mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') + #preproc, out_file = strat.get_node_from_resource_pool('functional_preprocessed') + #brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') + #func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') + #anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') + #mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') hist_ = hist.clone('hist_snr_%d' % num_strat) hist_.inputs.measure = 'snr' @@ -4618,8 +4625,8 @@ def QA_montages(measure, idx): if 1 in c.runZScoring: if c.fwhm != None: - QA_montages('alff_to_standard_smooth_zstd', 11) - QA_montages('falff_to_standard_smooth_zstd', 12) + QA_montages('alff_to_standard_zstd_smooth', 11) + QA_montages('falff_to_standard_zstd_smooth', 12) else: QA_montages('alff_to_standard_zstd', 13) @@ -4636,7 +4643,7 @@ def QA_montages(measure, idx): if 1 in c.runZScoring: if c.fwhm != None: - QA_montages('reho_to_standard_smooth_fisher_zstd', 17) + QA_montages('reho_to_standard_zstd_fisher_smooth', 17) else: QA_montages('reho_to_standard_fisher_zstd', 18) @@ -4652,7 +4659,7 @@ def QA_montages(measure, idx): if 1 in c.runZScoring: if c.fwhm != None: - QA_montages('sca_roi_to_standard_smooth_fisher_zstd', 22) + QA_montages('sca_roi_to_standard_zstd_fisher_smooth', 22) else: QA_montages('sca_roi_to_standard_fisher_zstd', 21) @@ -4669,7 +4676,7 @@ def QA_montages(measure, idx): if 1 in c.runZScoring: if c.fwhm != None: - QA_montages('sca_seed_to_standard_smooth_fisher_zstd', 26) + QA_montages('sca_seed_to_standard_zstd_fisher_smooth', 26) else: QA_montages('sca_seed_to_standard_fisher_zstd', 25) From ae1d33c9b20c38cac7b64c7617ef9e6626479809 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Thu, 26 Apr 2018 14:06:22 -0400 Subject: [PATCH 58/75] fixed reho QA montage failure --- CPAC/pipeline/cpac_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 5de9ca21a3..3d2a188bd2 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -4643,7 +4643,7 @@ def QA_montages(measure, idx): if 1 in c.runZScoring: if c.fwhm != None: - QA_montages('reho_to_standard_zstd_fisher_smooth', 17) + QA_montages('reho_to_standard_zstd_smooth', 17) else: QA_montages('reho_to_standard_fisher_zstd', 18) From 6252b15688f802abc4858fd4d4372afbbf49db9c Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Thu, 26 Apr 2018 14:46:19 -0400 Subject: [PATCH 59/75] Maybe-anat_edge error --- CPAC/qc/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index 7d2ccf2d43..db114ac79c 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -1229,8 +1229,8 @@ def make_edge(file_): new_fname = os.path.join(os.getcwd(), os.path.basename(new_fname)) cmd = "3dedge3 -input %s -prefix %s" % (file_, new_fname) - print cmd - print commands.getoutput(cmd) + #print cmd + #print commands.getoutput(cmd) return new_fname From 615bf2837fa19b6404def1fd37fa65d3034bdd17 Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Fri, 27 Apr 2018 10:55:31 -0400 Subject: [PATCH 60/75] anat_edge error --- CPAC/pipeline/cpac_pipeline.py | 63 +++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 3d2a188bd2..7e8c4411ca 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -4419,16 +4419,22 @@ def calc_avg(output_name, strat, num_strat, map_node=False): skull, out_file_s = strat.get_node_from_resource_pool('anatomical_reorient') montage_skull = create_montage('montage_skull_%d' % num_strat,'red', 'skull_vis') ### + + + skull_edge = make_edge(wf_name= 'skull_edge_%d' % num_strat) + workflow.connect(skull, out_file_s, skull_edge, 'inputspec.file_') + workflow.connect(skull_edge, 'outputspec.new_fname', montage_skull, 'inputspec.overlay') + workflow.connect(anat_underlay, out_file,montage_skull,'inputspec.underlay') + strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, 'outputspec.axial_png'),'qc___skullstrip_vis_s': (montage_skull, 'outputspec.sagittal_png')}) + #skull_edge = #pe.Node(util.Function(input_names=['file_'],output_names=['new_fname#'],function=make_edge),name='skull_edge_%d' % num_strat) - skull_edge = pe.Node(util.Function(input_names=['file_'],output_names=['new_fname'],function=make_edge),name='skull_edge_%d' % num_strat) - - workflow.connect(skull, out_file_s,skull_edge, 'file_') + #workflow.connect(skull, out_file_s,skull_edge, 'file_') - workflow.connect(anat_underlay, out_file,montage_skull,'inputspec.underlay') + #workflow.connect(anat_underlay, out_file,montage_skull,'inputspec.underlay') - workflow.connect(skull_edge, 'new_fname',montage_skull,'inputspec.overlay') + # workflow.connect(skull_edge, 'new_fname',montage_skull,'inputspec.overlay') - strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, 'outputspec.axial_png'),'qc___skullstrip_vis_s': (montage_skull, 'outputspec.sagittal_png')}) + # strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, #'outputspec.axial_png'),'qc___skullstrip_vis_s': (montage_skull, #'outputspec.sagittal_png')}) if not 1 in qc_montage_id_a: qc_montage_id_a[1] = 'skullstrip_vis_a' @@ -4507,25 +4513,34 @@ def calc_avg(output_name, strat, num_strat, map_node=False): m_f_a, out_file_mfa = strat.get_node_from_resource_pool('mean_functional_in_anat') montage_anat = create_montage('montage_anat_%d' % num_strat, - 'red', 't1_edge_on_mean_func_in_t1') ### - - anat_edge = pe.Node(util.Function(input_names=['file_'], - output_names=['new_fname'], - function=make_edge), - name='anat_edge_%d' % num_strat) - - workflow.connect(anat, out_file, - anat_edge, 'file_') - - + 'red', 't1_edge_on_mean_func_in_t1')### + anat_edge = make_edge(wf_name= 'anat_edge_%d' % num_strat) + workflow.connect(anat, out_file, anat_edge, 'inputspec.file_' ) + workflow.connect(anat_edge,'outputspec.new_fname',montage_anat,'inputspec.overlay') workflow.connect(m_f_a, out_file_mfa, - montage_anat, 'inputspec.underlay') - - workflow.connect(anat_edge, 'new_fname', - montage_anat, 'inputspec.overlay') - - strat.update_resource_pool({'qc___mean_func_with_t1_edge_a': (montage_anat, 'outputspec.axial_png'), - 'qc___mean_func_with_t1_edge_s': (montage_anat, 'outputspec.sagittal_png')}) + montage_anat, 'inputspec.underlay') + #workflow.connect(anat_edge, 'new_fname', + # montage_anat, 'inputspec.overlay') + + strat.update_resource_pool({'qc___mean_func_with_t1_edge_a': (montage_anat, 'outputspec.axial_png'),'qc___mean_func_with_t1_edge_s': (montage_anat, 'outputspec.sagittal_png')}) + +#anat_edge = pe.Node(util.Function(input_names=['file_'], +# output_names=['new_fname'], +# function=make_edge), +# name='anat_edge_%d' % num_strat) + +# workflow.connect(anat, out_file, +# anat_edge, 'file_') + +# +# workflow.connect(m_f_a, out_file_mfa, +# montage_anat, 'inputspec.underlay') +# +# workflow.connect(anat_edge, 'new_fname', +# montage_anat, 'inputspec.overlay') +# +# strat.update_resource_pool({'qc___mean_func_with_t1_edge_a': (montage_anat, #'outputspec.axial_png'), +# 'qc___mean_func_with_t1_edge_s': (montage_anat, #'outputspec.sagittal_png')}) if not 4 in qc_montage_id_a: qc_montage_id_a[4] = 'mean_func_with_t1_edge_a' From 8aac93909904c73052efaea7d8c2738d9cc72a8a Mon Sep 17 00:00:00 2001 From: nrajamani3 Date: Fri, 27 Apr 2018 10:56:01 -0400 Subject: [PATCH 61/75] anat_edge error --- CPAC/qc/utils.py | 72 ++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index db114ac79c..7e727f303d 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -4,6 +4,10 @@ import pkg_resources as p matplotlib.use('Agg') import os +import nipype.pipeline.engine as pe +from nipype.interfaces import afni,fsl +import nipype.interfaces.utility as util + def append_to_files_in_dict_way(list_files, file_): @@ -1197,43 +1201,51 @@ def generateQCPages(qc_path,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_his # print f -def make_edge(file_): - +def make_edge(wf_name ='create_edge'): + """ - Make edge file from a scan image - - Parameters - ---------- - - file_ : string + Make edge file from a scan image + + Parameters + ---------- + + file_ : string path to the scan - - Returns - ------- - - new_fname : string + + Returns + ------- + + new_fname : string path to edge file - - """ - + + """ + import commands import os - - remainder, ext_ = os.path.splitext(file_) - - remainder, ext1_ = os.path.splitext(remainder) - - ext = ''.join([ext1_, ext_]) - - new_fname = ''.join([remainder, '_edge', ext]) - new_fname = os.path.join(os.getcwd(), os.path.basename(new_fname)) - - cmd = "3dedge3 -input %s -prefix %s" % (file_, new_fname) + + wf_name = pe.Workflow(name = wf_name) + + inputNode = pe.Node(util.IdentityInterface(fields=['file_']),name = 'inputspec') + outputNode = pe.Node(util.IdentityInterface(fields=['new_fname']),name = 'outputspec') + + prepare = pe.Node(interface=afni.Edge3(),name ='prepare') + wf_name.connect(inputNode,'file_',prepare,'in_file') + wf_name.connect(prepare,'out_file',outputNode,'new_fname') + + #remainder, ext_ = os.path.splitext(file_) + + #remainder, ext1_ = os.path.splitext(remainder) + + #ext = ''.join([ext1_, ext_]) + + # new_fname = ''.join([remainder, '_edge', ext]) + # new_fname = os.path.join(os.getcwd(), os.path.basename(new_fname)) + + #cmd = "3dedge3 -input %s -prefix %s" % (file_, new_fname) #print cmd #print commands.getoutput(cmd) - - return new_fname - + + return wf_name def gen_func_anat_xfm(func_, ref_, xfm_, interp_): From 1ef215d23c24e78578cf9c7475cf4fe258667fc7 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 27 Apr 2018 12:52:34 -0400 Subject: [PATCH 62/75] Completed some testing/modifications to the FLAME preset generator. --- CPAC/GUI/interface/utils/custom_control.py | 80 ++- .../utils/fsl_flame_presets_window.py | 670 ++++++++++++++++++ .../GUI/interface/utils/modelDesign_window.py | 1 - .../GUI/interface/utils/modelconfig_window.py | 9 +- CPAC/GUI/interface/windows/config_window.py | 4 +- ...is_files.py => create_fsl_flame_preset.py} | 0 6 files changed, 720 insertions(+), 44 deletions(-) create mode 100644 CPAC/GUI/interface/utils/fsl_flame_presets_window.py rename CPAC/utils/{create_group_analysis_files.py => create_fsl_flame_preset.py} (100%) diff --git a/CPAC/GUI/interface/utils/custom_control.py b/CPAC/GUI/interface/utils/custom_control.py index e53c2f11d1..fa20d88adc 100644 --- a/CPAC/GUI/interface/utils/custom_control.py +++ b/CPAC/GUI/interface/utils/custom_control.py @@ -2,7 +2,7 @@ import wx.combo import os from wx.lib.masked import NumCtrl -import modelconfig_window, modelDesign_window +import modelconfig_window, modelDesign_window, fsl_flame_presets_window import wx.lib.agw.balloontip as BT import pkg_resources as p @@ -125,7 +125,6 @@ def onButtonClick(self,event): self.Close() - class StringBoxFrame(wx.Frame): def __init__(self, parent, values, title, label): @@ -162,7 +161,6 @@ def onButtonClick(self,event): self.Close() - class TextBoxFrame(wx.Frame): def __init__(self, parent, values): @@ -219,7 +217,6 @@ def onButtonClick(self,event): self.Close() - class ResampleNumBoxFrame(wx.Frame): def __init__(self, parent, values): @@ -266,7 +263,6 @@ def onButtonClick(self,event): self.Close() - class FilepathBoxFrame(wx.Frame): def __init__(self, parent): @@ -317,36 +313,42 @@ def onButtonClick(self,event): else: parent.add_checkbox_grid_value(val) self.Close() - class ConfigFslFrame(wx.Frame): def __init__(self, parent, values): - wx.Frame.__init__(self, parent, \ - title="Specify FSL Model and Subject List", \ - size = (680,200)) - sizer = wx.BoxSizer(wx.VERTICAL) + wx.Frame.__init__(self, parent, + title="Specify FSL FLAME Model", + size = (680,200)) + sizer_vert = wx.BoxSizer(wx.VERTICAL) + sizer_horz = wx.BoxSizer(wx.HORIZONTAL) panel = wx.Panel(self) - - button1 = wx.Button(panel, -1, 'Create or Load FSL Model', \ - size= (210,50)) + + button0 = wx.Button(panel, -1, 'Choose FLAME Model Preset', + size=(210, 50)) + button0.Bind(wx.EVT_BUTTON, self.onPresetClick) + sizer_horz.Add(button0, 0, wx.ALIGN_CENTER | wx.TOP, border=15) + + button1 = wx.Button(panel, -1, 'FLAME Model Builder/Editor', + size= (210, 50)) button1.Bind(wx.EVT_BUTTON, self.onButtonClick) - sizer.Add(button1, 0, wx.ALIGN_CENTER|wx.TOP, border = 15) + sizer_horz.Add(button1, 1, wx.ALIGN_CENTER | wx.TOP, border=15) + + sizer_vert.Add(sizer_horz, 0, wx.ALIGN_CENTER, border=15) flexsizer = wx.FlexGridSizer(cols=2, hgap=5, vgap=10) - img = wx.Image(p.resource_filename('CPAC', \ - 'GUI/resources/images/help.png'),\ + img = wx.Image(p.resource_filename('CPAC', + 'GUI/resources/images/help.png'), wx.BITMAP_TYPE_ANY).ConvertToBitmap() - - label2 = wx.StaticText(panel, -1, label = 'FSL Model Config') - self.box2 = FSLModelSelectorCombo(panel, id = wx.ID_ANY, \ + label2 = wx.StaticText(panel, -1, label='FSL FLAME Model Config') + self.box2 = FSLModelSelectorCombo(panel, id = wx.ID_ANY, size = (500, -1)) hbox2 = wx.BoxSizer(wx.HORIZONTAL) help2 = wx.BitmapButton(panel, id=-1, bitmap=img, - pos=(10, 20), size = (img.GetWidth()+5, \ + pos=(10, 20), size = (img.GetWidth()+5, img.GetHeight()+5)) help2.Bind(wx.EVT_BUTTON, lambda event: \ self.OnShowDoc(event, 2)) @@ -366,16 +368,19 @@ def __init__(self, parent, values): hbox.Add(button3, 1, wx.EXPAND, border =5) hbox.Add(button2, 1, wx.EXPAND, border =5) - sizer.Add(flexsizer, 1, wx.EXPAND | wx.ALL, 10) - sizer.Add(hbox,0, wx.ALIGN_CENTER, 5) - panel.SetSizer(sizer) + sizer_vert.Add(flexsizer, 1, wx.EXPAND | wx.ALL, 10) + sizer_vert.Add(hbox,0, wx.ALIGN_CENTER, 5) + panel.SetSizer(sizer_vert) self.Show() def onCancel(self, event): self.Close() - - def onButtonClick(self,event): + + def onPresetClick(self, event): + fsl_flame_presets_window.FlamePresetsOne(self) + + def onButtonClick(self, event): modelconfig_window.ModelConfig(self) def onOK(self, event): @@ -385,28 +390,30 @@ def onOK(self, event): parent.listbox.Append(val) self.Close() else: - wx.MessageBox("Please provide the path to the fsl model " \ - "config file.") + wx.MessageBox("Please provide the path to the fsl model " + "config file.") def OnShowDoc(self, event, flag): if flag == 1: - wx.TipWindow(self, "Full path to a directory containing files " \ - "for a single FSL model. All models must " \ - "include .con, .mat, and .grp files. Models " \ - "in which an F-Test is specified must also " \ + wx.TipWindow(self, "Full path to a directory containing files " + "for a single FSL model. All models must " + "include .con, .mat, and .grp files. Models " + "in which an F-Test is specified must also " "include a .fts file.", 500) elif flag == 2: - wx.TipWindow(self, "Full path to a CPAC FSL model configuration "\ - "file to be used.\n\nFor more information, " \ + wx.TipWindow(self, "Full path to a CPAC FSL FLAME group analysis " + "model configuration YAML file. You can " + "either create one using one of the presets, " + "or by using the model builder to build one " + "from scratch.\n\nFor more information, " "please refer to the user guide.", 500) - class ContrastsFrame(wx.Frame): def __init__(self, parent, values, dmatrix_obj): - wx.Frame.__init__(self, parent, title="Add Contrast Description", \ + wx.Frame.__init__(self, parent, title="Add Contrast Description", size = (300,80)) self.dmatrix_obj = dmatrix_obj @@ -431,7 +438,6 @@ def __init__(self, parent, values, dmatrix_obj): panel.SetSizer(sizer) self.Show() - def onButtonClick(self, event): @@ -459,7 +465,7 @@ def onButtonClick(self, event): class f_test_frame(wx.Frame): def __init__(self, parent, values): - wx.Frame.__init__(self, parent, title="Select Contrasts for f-Test", \ + wx.Frame.__init__(self, parent, title="Select Contrasts for f-Test", size = (280,200)) sizer = wx.BoxSizer(wx.VERTICAL) diff --git a/CPAC/GUI/interface/utils/fsl_flame_presets_window.py b/CPAC/GUI/interface/utils/fsl_flame_presets_window.py new file mode 100644 index 0000000000..428de694e0 --- /dev/null +++ b/CPAC/GUI/interface/utils/fsl_flame_presets_window.py @@ -0,0 +1,670 @@ +import wx +import generic_class +from .constants import control, dtype, substitution_map +import os +import yaml + +from ....utils import create_fsl_flame_preset + +ID_RUN = 11 + + +class FlamePresetsOne(wx.Frame): + + # this creates the wx.Frame mentioned above in the class declaration + def __init__(self, parent, gpa_settings=None): + + wx.Frame.__init__( + self, parent=parent, title="CPAC - FSL FLAME Presets", + size=(900, 550)) + + self.parent = parent + + mainSizer = wx.BoxSizer(wx.VERTICAL) + vertSizer = wx.BoxSizer(wx.VERTICAL) + + self.panel = wx.Panel(self) + self.window = wx.ScrolledWindow(self.panel, size=(-1, 255)) + self.page = generic_class.GenericClass(self.window, + " FSL FLAME Group-Level " + "Analysis - Model Presets") + + if not gpa_settings: + # if this window is being opened for the first time + self.gpa_settings = {} + self.gpa_settings["flame_preset"] = "" + self.gpa_settings["participant_list"] = "" + self.gpa_settings['derivative_list'] = "" + self.gpa_settings['z_threshold'] = 2.3 + self.gpa_settings['p_threshold'] = 0.05 + self.gpa_settings["model_name"] = "" + self.gpa_settings["output_dir"] = "" + else: + # if we're coming back from the "next" window + self.gpa_settings = gpa_settings + + self.page.add(label="Choose Preset: ", + control=control.CHOICE_BOX, + name="flame_preset", + type=dtype.LSTR, + comment="", + values=["Single Group Average (One-Sample T-Test)", + "Single Group Average with Additional Covariate", + "Unpaired Two-Group Difference (Two-Sample Unpaired T-Test)", + "Paired Two-Group Difference (Two-Sample Paired T-Test)", + "Tripled Two-Group Difference ('Tripled' T-Test)"]) + + self.page.add(label="Participant List ", + control=control.COMBO_BOX, + name="participant_list", + type=dtype.STR, + comment="Full path to the group-level analysis " + "participant list text file. This should be a " + "file with each participant_session ID you " + "want included in the model, on each line.\n\n" + "A sample group analysis participant list file " + "is generated when you run the data config" + "uration builder for individual-level " + "analysis. This can be used as is, or " + "modified, or used as a template.", + values="") + + self.page.add(label="Select Derivatives ", + control=control.CHECKLIST_BOX, + name="derivative_list", + type=dtype.LSTR, + values=['ALFF', + 'ALFF (smoothed)', + 'f/ALFF', + 'f/ALFF (smoothed)', + 'ReHo', + 'ReHo (smoothed)', + 'ROI Average SCA', + 'ROI Average SCA (smoothed)', + 'Dual Regression', + 'Dual Regression (smoothed)', + 'Multiple Regression SCA', + 'Multiple Regression SCA (smoothed)', + 'Network Centrality', + 'Network Centrality (smoothed)', + 'VMHC'], + comment="Select which derivatives you would like to " + "include when running group analysis.\n\nWhen " + "including Dual Regression, make sure to " + "correct your P-value for the number of maps " + "you are comparing.\n\nWhen including " + "Multiple Regression SCA, you must have more " + "degrees of freedom (subjects) than there were " + "time series.", + size=(350,160)) + + self.page.add(label="Z threshold ", + control=control.FLOAT_CTRL, + name='z_threshold', + type=dtype.NUM, + comment="Only voxels with a Z-score higher than this " + "value will be considered significant.", + values=2.3) + + self.page.add(label="Cluster Significance Threshold (P-value) ", + control=control.FLOAT_CTRL, + name='p_threshold', + type=dtype.NUM, + comment="Significance threshold (P-value) to use when " + "doing cluster correction for multiple " + "comparisons.", + values=0.05) + + self.page.add(label="Model Name ", + control=control.TEXT_BOX, + name="model_name", + type=dtype.STR, + comment="Specify a name for the new model. Output and " + "working directories for group analysis, as " + "well as the FLAME model files (.mat, .con, " + ".grp, etc.) will be labeled with this name.", + values=self.gpa_settings['model_name'], + size=(200, -1)) + + self.page.add(label="Output Directory ", + control=control.DIR_COMBO_BOX, + name="output_dir", + type=dtype.STR, + comment="Full path to the directory where CPAC should " + "place the model files (.mat, .con, .grp) and " + "the outputs of group analysis.", + values=self.gpa_settings['output_dir']) + + if gpa_settings: + # manually re-set the preset, participant list, and derivatives + for ctrl in self.page.get_ctrl_list(): + + name = ctrl.get_name() + + if name == 'z_threshold': + ctrl.set_value(self.gpa_settings['z_threshold']) + + elif name == 'p_threshold': + ctrl.set_value(self.gpa_settings['p_threshold']) + + elif ("list" in name) and (name != "participant_list"): + + value = self.gpa_settings[name] + + if isinstance(value, str): + value = value.replace("[", "").replace("]", "") + if "\"" in value: + value = value.replace("\"", "") + if "'" in value: + value = value.replace("'", "") + values = value.split(",") + else: + # instead, is a list- most likely when clicking + # "Back" on the modelDesign_window + values = value + + new_derlist = [] + + for val in values: + new_derlist.append(val) + + ctrl.set_value(new_derlist) + + else: + ctrl.set_value(self.gpa_settings[name]) + + self.page.set_sizer() + mainSizer.Add(self.window, 1, wx.EXPAND) + + btnPanel = wx.Panel(self.panel, -1) + hbox = wx.BoxSizer(wx.HORIZONTAL) + + buffer = wx.StaticText(btnPanel, label="\t\t\t\t\t\t") + hbox.Add(buffer) + + cancel = wx.Button(btnPanel, wx.ID_CANCEL, "Cancel", (220, 10), + wx.DefaultSize, 0) + self.Bind(wx.EVT_BUTTON, self.cancel, id=wx.ID_CANCEL) + hbox.Add(cancel, 0, flag=wx.LEFT | wx.BOTTOM, border=5) + + next = wx.Button(btnPanel, 3, "OK", (200, -1), wx.DefaultSize, 0) + self.Bind(wx.EVT_BUTTON, self.load_next_stage, id=3) + hbox.Add(next, 0.6, flag=wx.LEFT | wx.BOTTOM, border=5) + + # reminder: functions bound to buttons require arguments + # (self, event) + btnPanel.SetSizer(hbox) + + mainSizer.Add( + btnPanel, 0.5, flag=wx.ALIGN_RIGHT | wx.RIGHT, border=20) + + self.panel.SetSizer(mainSizer) + + self.Show() + + def gather_form_data(self): + for ctrl in self.page.get_ctrl_list(): + name = ctrl.get_name() + + if ("list" in name) and (name != "participant_list"): + # for the options that are actual lists + # ex. derivative_list, sessions_list, etc. + self.gpa_settings[name] = [] + for option in list(ctrl.get_selection()): + self.gpa_settings[name].append(option) + else: + self.gpa_settings[name] = str(ctrl.get_selection()) + + def cancel(self, event): + self.Close() + + def display(self, win, msg): + wx.MessageBox(msg, "Error") + win.SetBackgroundColour("pink") + win.SetFocus() + win.Refresh() + raise ValueError + + ''' button: OK ''' + def load_next_stage(self, event): + + self.gather_form_data() + + if "Covariate" in self.gpa_settings["flame_preset"] or \ + "Unpaired" in self.gpa_settings["flame_preset"]: + # open the next window! + FlamePresetsTwoPheno(self.parent, self.gpa_settings) + elif "Two-Sample Paired" in self.gpa_settings["flame_preset"] or \ + "Tripled" in self.gpa_settings["flame_preset"]: + # open the next window! + FlamePresetsTwoConditions(self.parent, self.gpa_settings) + elif "One-Sample" in self.gpa_settings["flame_preset"]: + # no additional info is needed, and we can run the preset + # generation straight away + print "Generating FLAME model configuration...\n" + create_fsl_flame_preset.run(self.gpa_settings["participant_list"], + self.gpa_settings["derivative_list"], + self.gpa_settings["z_threshold"], + self.gpa_settings["p_threshold"], + "single_grp", + output_dir=self.gpa_settings["output_dir"], + model_name=self.gpa_settings["model_name"]) + + yaml_path = os.path.join(self.gpa_settings["output_dir"], + self.gpa_settings["model_name"], + "gpa_fsl_config_{0}.yml" + "".format( + self.gpa_settings["model_name"])) + self.Parent.box2.GetTextCtrl().SetValue(yaml_path) + self.Close() + + self.Close() + + +class FlamePresetsTwoPheno(wx.Frame): + def __init__(self, parent, gpa_settings): + + wx.Frame.__init__(self, parent=parent, + title="CPAC - FSL Flame Presets", + size=(700, 275)) + + self.parent = parent + self.gpa_settings = gpa_settings + + if "pheno_file" not in self.gpa_settings.keys(): + self.gpa_settings["pheno_file"] = "" + if "participant_id_label" not in self.gpa_settings.keys(): + self.gpa_settings["participant_id_label"] = "" + if "covariate" not in self.gpa_settings.keys(): + self.gpa_settings["covariate"] = "" + + mainSizer = wx.BoxSizer(wx.VERTICAL) + self.panel = wx.Panel(self) + self.window = wx.ScrolledWindow(self.panel) + + self.page = generic_class.GenericClass(self.window, + " FSL FLAME Group-Level " + "Analysis - Model Presets") + + self.page.add(label="Phenotype/EV File ", + control=control.COMBO_BOX, + name="pheno_file", + type=dtype.STR, + comment="Full path to a .csv file containing EV " + "information for each subject.\n\nTip: A " + "file in this format (containing a single " + "column listing all subjects run through " + "CPAC) was generated along with the main " + "CPAC subject list (see phenotypic_template_" + "X.csv).", + values=self.gpa_settings['pheno_file']) + + self.page.add(label="Participant Column Name ", + control=control.TEXT_BOX, + name="participant_id_label", + type=dtype.STR, + comment="Name of the participants column in your EV " + "file.", + values=self.gpa_settings['participant_id_label'], + style=wx.EXPAND | wx.ALL, + size=(160, -1)) + + if "Covariate" in self.gpa_settings["flame_preset"]: + self.page.add(label="Phenotype covariate to include: ", + control=control.TEXT_BOX, + name='covariate', + type=dtype.STR, + values="", + comment="For the additional covariate for the " + "single group average, enter the column " + "name of the covariate in the phenotype " + "file provided.", + size=(160, -1)) + elif "Unpaired" in self.gpa_settings["flame_preset"]: + self.page.add(label="Two groups from pheno to compare: ", + control=control.TEXT_BOX, + name='covariate', + type=dtype.STR, + values="", + comment="Enter the two column names of the group " + "variables in the phenotype provided, " + "separated by a comma. If the two groups " + "are encoded in a single column, enter the " + "name of that one column.", + size=(320, -1)) + + self.page.set_sizer() + + mainSizer.Add(self.window, 1, wx.EXPAND) + + btnPanel = wx.Panel(self.panel, -1) + hbox = wx.BoxSizer(wx.HORIZONTAL) + + buffer = wx.StaticText(btnPanel, label="\t\t\t\t\t\t") + hbox.Add(buffer) + + cancel = wx.Button(btnPanel, wx.ID_CANCEL, "Cancel", (220, 10), + wx.DefaultSize, 0) + self.Bind(wx.EVT_BUTTON, self.cancel, id=wx.ID_CANCEL) + hbox.Add(cancel, 0, flag=wx.LEFT | wx.BOTTOM, border=5) + + cancel = wx.Button(btnPanel, wx.ID_CANCEL, "< Back", (220, 10), + wx.DefaultSize, 0) + self.Bind(wx.EVT_BUTTON, self.go_back, id=wx.ID_CANCEL) + hbox.Add(cancel, 0, flag=wx.LEFT | wx.BOTTOM, border=5) + + next = wx.Button(btnPanel, 3, "Generate Model", (200, -1), + wx.DefaultSize, 0) + self.Bind(wx.EVT_BUTTON, self.click_OK, id=3) + hbox.Add(next, 0.6, flag=wx.LEFT | wx.BOTTOM, border=5) + + # reminder: functions bound to buttons require arguments + # (self, event) + btnPanel.SetSizer(hbox) + + mainSizer.Add( + btnPanel, 0.5, flag=wx.ALIGN_RIGHT | wx.RIGHT, border=20) + + self.panel.SetSizer(mainSizer) + + # TODO + # text blurb depending on the specific preset + + self.Show() + + def gather_form_data(self): + for ctrl in self.page.get_ctrl_list(): + name = ctrl.get_name() + self.gpa_settings[name] = str(ctrl.get_selection()) + if "covariate" in name: + # control for any spaces in the string if there are two + # covariate/column names listed, i.e. "male, female" + self.gpa_settings["covariate"] = \ + self.gpa_settings["covariate"].replace(", ", ",") + + def cancel(self, event): + self.Close() + + def go_back(self, event): + self.gather_form_data() + FlamePresetsOne(self.parent, self.gpa_settings) + self.Close() + + def testFile(self, filepath, paramName): + try: + fileTest = open(filepath) + fileTest.close() + except: + errDlgFileTest = wx.MessageDialog( + self, 'Error reading file - either it does not exist ' \ + 'or you do not have read access. \n\n' \ + 'Parameter: %s' % paramName, + 'File Access Error', + wx.OK | wx.ICON_ERROR) + errDlgFileTest.ShowModal() + errDlgFileTest.Destroy() + raise Exception + + def test_pheno_contents(self): + with open(os.path.abspath(self.gpa_settings['pheno_file']), "rU") as phenoFile: + phenoHeaderString = phenoFile.readline().rstrip('\r\n') + self.phenoHeaderItems = phenoHeaderString.split(',') + + if self.gpa_settings['participant_id_label'] in self.phenoHeaderItems: + self.phenoHeaderItems.remove(self.gpa_settings['participant_id_label']) + else: + errSubID = wx.MessageDialog( + self, 'Please enter the name of the participant ID column' + ' as it is labeled in the phenotype file.', + 'Blank/Incorrect Participant Header Input', + wx.OK | wx.ICON_ERROR) + errSubID.ShowModal() + errSubID.Destroy() + raise Exception + + for col_name in self.gpa_settings["covariate"].split(","): + if col_name not in self.phenoHeaderItems and \ + col_name.replace(" ", "") not in self.phenoHeaderItems: + err = wx.MessageDialog(self, "The covariate name entered " + "does not exist in the pheno" + "type file provided.\n\nName: " + "{0}".format(col_name), + 'Incorrect Covariate Input', + wx.OK | wx.ICON_ERROR) + err.ShowModal() + err.Destroy() + raise Exception + + def substitute_derivative_names(self): + # change the human-friendly strings of the derivative names to the + # CPAC output directory derivative names using constants.py + new_deriv_list = [] + for deriv_string in self.gpa_settings["derivative_list"]: + new_deriv_list.append(substitution_map.get(deriv_string)) + self.gpa_settings["derivative_list"] = new_deriv_list + + def click_OK(self, event): + + # gather data + self.gather_form_data() + self.substitute_derivative_names() + + # check pheno file + self.testFile(self.gpa_settings['pheno_file'], 'Phenotype/EV File') + self.test_pheno_contents() + + # which preset? + if "Covariate" in self.gpa_settings["flame_preset"]: + preset = "single_grp_cov" + elif "Unpaired" in self.gpa_settings["flame_preset"]: + preset = "unpaired_two" + + # generate the preset files + print "Generating FLAME model configuration...\n" + create_fsl_flame_preset.run(self.gpa_settings["participant_list"], + self.gpa_settings["derivative_list"], + self.gpa_settings["z_threshold"], + self.gpa_settings["p_threshold"], + preset, + self.gpa_settings["pheno_file"], + self.gpa_settings["participant_id_label"], + output_dir=self.gpa_settings[ + "output_dir"], + model_name=self.gpa_settings[ + "model_name"], + covariate=self.gpa_settings["covariate"]) + + yaml_path = os.path.join(self.gpa_settings["output_dir"], + self.gpa_settings["model_name"], + "gpa_fsl_config_{0}.yml" + "".format(self.gpa_settings["model_name"])) + self.Parent.box2.GetTextCtrl().SetValue(yaml_path) + self.Close() + + +class FlamePresetsTwoConditions(wx.Frame): + def __init__(self, parent, gpa_settings): + + wx.Frame.__init__( + self, parent=parent, title="CPAC - FSL Flame Presets", + size=(550, 325)) + + self.parent = parent + self.gpa_settings = gpa_settings + + if "condition_type" not in self.gpa_settings.keys(): + self.gpa_settings["condition_type"] = "Sessions" + if "conditions_list" not in self.gpa_settings.keys(): + self.gpa_settings["conditions_list"] = [] + + if "Two-Sample Paired" in self.gpa_settings["flame_preset"]: + num_condition = "two" + elif "Tripled" in self.gpa_settings["flame_preset"]: + num_condition = "three" + + mainSizer = wx.BoxSizer(wx.VERTICAL) + self.panel = wx.Panel(self) + self.window = wx.ScrolledWindow(self.panel) + + self.page = generic_class.GenericClass(self.window, + " Group-Level Analysis - FSL " + "FLAME Presets") + + # TODO + # text blurb depending on the specific preset + + self.page.add(label="Conditions: Sessions or Series/Scans? ", + control=control.CHOICE_BOX, + name="condition_type", + type=dtype.LSTR, + comment="Choose whether the {0} paired conditions you " + "wish to compare using the {1} are Sessions or " + "Series/Scans.\n\nFor example, if each " + "participant has {0} conditions each, are they " + "separated by session, or by multiple " + "functional series or scans within each a " + "single session each?".format(num_condition, + self.gpa_settings["flame_preset"]), + values=["Sessions", "Series/Scans"]) + + self.page.add(label='Session or Series/Scan IDs: ', + control=control.LISTBOX_COMBO, + name='conditions_list', + type=dtype.LSTR, + values=self.gpa_settings['conditions_list'], + comment="Enter the {0} session or series/scan ID names " + "you wish to compare.\n\nIf they are sessions, " + "they can be found under 'unique_id' in the " + "individual-level data configuration YAML file " + "for the data in question, or in the " + "'participant_session' IDs that you would see " + "in the individual-level analysis output dir" + "ectory (ex. '3005_1' would be participant " + "3005, session 1).\n\nIf they are series/" + "scans, they will be the functional scan " + "names, which can be found in the data config" + "uration YAML file nested under " + "'func'.".format(num_condition), + size=(200, 100), + combo_type=7) + + self.page.set_sizer() + + mainSizer.Add(self.window, 1, wx.EXPAND) + + btnPanel = wx.Panel(self.panel, -1) + hbox = wx.BoxSizer(wx.HORIZONTAL) + + buffer = wx.StaticText(btnPanel, label="\t\t\t\t\t\t") + hbox.Add(buffer) + + cancel = wx.Button(btnPanel, wx.ID_CANCEL, "Cancel", (220, 10), + wx.DefaultSize, 0) + self.Bind(wx.EVT_BUTTON, self.cancel, id=wx.ID_CANCEL) + hbox.Add(cancel, 0, flag=wx.LEFT | wx.BOTTOM, border=5) + + cancel = wx.Button(btnPanel, wx.ID_CANCEL, "< Back", (220, 10), + wx.DefaultSize, 0) + self.Bind(wx.EVT_BUTTON, self.go_back, id=wx.ID_CANCEL) + hbox.Add(cancel, 0, flag=wx.LEFT | wx.BOTTOM, border=5) + + next = wx.Button(btnPanel, 3, "Generate Model", (200, -1), + wx.DefaultSize, 0) + self.Bind(wx.EVT_BUTTON, self.click_OK, id=3) + hbox.Add(next, 0.6, flag=wx.LEFT | wx.BOTTOM, border=5) + + # reminder: functions bound to buttons require arguments + # (self, event) + btnPanel.SetSizer(hbox) + + mainSizer.Add( + btnPanel, 0.5, flag=wx.ALIGN_RIGHT | wx.RIGHT, border=20) + + self.panel.SetSizer(mainSizer) + + # TODO + # text blurb depending on the specific preset + + for ctrl in self.page.get_ctrl_list(): + + name = ctrl.get_name() + value = self.gpa_settings[name] + + if "conditions_list" in name: + if isinstance(value, str): + value = value.replace("[", "").replace("]", "") + if "\"" in value: + value = value.replace("\"", "") + if "'" in value: + value = value.replace("'", "") + values = value.split(",") + else: + # instead, is a list- most likely when clicking + # "Back" on the modelDesign_window + values = value + + new_derlist = [] + + for val in values: + new_derlist.append(val) + + ctrl.set_value(new_derlist) + elif "condition_type" in name: + ctrl.set_value(value) + + self.Show() + + def gather_form_data(self): + for ctrl in self.page.get_ctrl_list(): + name = ctrl.get_name() + self.gpa_settings[name] = ctrl.get_selection() + + def cancel(self, event): + self.Close() + + def go_back(self, event): + self.gather_form_data() + FlamePresetsOne(self.parent, self.gpa_settings) + self.Close() + + def substitute_derivative_names(self): + # change the human-friendly strings of the derivative names to the + # CPAC output directory derivative names using constants.py + new_deriv_list = [] + for deriv_string in self.gpa_settings["derivative_list"]: + new_deriv_list.append(substitution_map.get(deriv_string)) + self.gpa_settings["derivative_list"] = new_deriv_list + + def click_OK(self): + # gather data + self.gather_form_data() + self.substitute_derivative_names() + + # which preset? + if "Two-Sample Paired" in self.gpa_settings["flame_preset"]: + preset = "paired_two" + elif "Tripled" in self.gpa_settings["flame_preset"]: + preset = "tripled_two" + + # generate the preset files + print "Generating FLAME model configuration...\n" + create_fsl_flame_preset.run(self.gpa_settings["participant_list"], + self.gpa_settings["derivative_list"], + self.gpa_settings["z_threshold"], + self.gpa_settings["p_threshold"], + preset, + output_dir=self.gpa_settings[ + "output_dir"], + model_name=self.gpa_settings[ + "model_name"], + covariate=self.gpa_settings[ + "conditions_list"], + condition_type=self.gpa_settings[ + "condition_type"]) + + yaml_path = os.path.join(self.gpa_settings["output_dir"], + self.gpa_settings["model_name"], + "gpa_fsl_config_{0}.yml" + "".format(self.gpa_settings["model_name"])) + self.Parent.box2.GetTextCtrl().SetValue(yaml_path) + self.Close() diff --git a/CPAC/GUI/interface/utils/modelDesign_window.py b/CPAC/GUI/interface/utils/modelDesign_window.py index aa0167e707..ec76bbc599 100644 --- a/CPAC/GUI/interface/utils/modelDesign_window.py +++ b/CPAC/GUI/interface/utils/modelDesign_window.py @@ -28,7 +28,6 @@ def check_contrast_equation(frame, dmatrix_obj, contrast_equation): return 0 - class ModelDesign(wx.Frame): def __init__(self, parent, gpa_settings, dmatrix_obj, varlist): diff --git a/CPAC/GUI/interface/utils/modelconfig_window.py b/CPAC/GUI/interface/utils/modelconfig_window.py index 6e02edd4f6..67760d2e0e 100644 --- a/CPAC/GUI/interface/utils/modelconfig_window.py +++ b/CPAC/GUI/interface/utils/modelconfig_window.py @@ -17,7 +17,7 @@ def __init__(self, parent, gpa_settings=None): wx.Frame.__init__( self, parent=parent, title="CPAC - Create New FSL Model", size=(900, 650)) - if gpa_settings == None: + if not gpa_settings: self.gpa_settings = {} self.gpa_settings['participant_list'] = '' self.gpa_settings['pheno_file'] = '' @@ -47,7 +47,8 @@ def __init__(self, parent, gpa_settings=None): self.window = wx.ScrolledWindow(self.panel, size=(-1,300)) self.page = generic_class.GenericClass(self.window, - " FSL Model Setup") + " FSL FLAME Group-Level " + "Analysis - Model Builder") self.page.add(label="Participant List ", control=control.COMBO_BOX, @@ -393,8 +394,8 @@ def load(self, event): self.gpa_settings['pheno_file'])) raise Exception(err) - # update the 'Model Setup' box and populate it with the EVs and - # their associated checkboxes for categorical and demean + # update the 'Model Setup' box and populate it with the EVs + # and their associated checkboxes for categorical and demean for ctrl in self.page.get_ctrl_list(): if ctrl.get_name() == 'model_setup': diff --git a/CPAC/GUI/interface/windows/config_window.py b/CPAC/GUI/interface/windows/config_window.py index 24292ab209..5f8cd5d7d8 100644 --- a/CPAC/GUI/interface/windows/config_window.py +++ b/CPAC/GUI/interface/windows/config_window.py @@ -157,8 +157,8 @@ def __init__(self, parent): self.AddPage(page39, "After Warping", wx.ID_ANY) self.AddSubPage(page40, "After Warping Options", wx.ID_ANY) - self.AddPage(page45, "Group Analysis", wx.ID_ANY) - self.AddSubPage(page46, "Group Analysis Settings", wx.ID_ANY) + self.AddPage(page45, "Group Analysis Settings", wx.ID_ANY) + self.AddSubPage(page46, "FSL FEAT Group Analysis", wx.ID_ANY) self.Bind(wx.EVT_TREEBOOK_PAGE_CHANGED, self.OnPageChanged) self.Bind(wx.EVT_TREEBOOK_PAGE_CHANGING, self.OnPageChanging) diff --git a/CPAC/utils/create_group_analysis_files.py b/CPAC/utils/create_fsl_flame_preset.py similarity index 100% rename from CPAC/utils/create_group_analysis_files.py rename to CPAC/utils/create_fsl_flame_preset.py From ca96c039f6e10aa24694ebda12f2e38662ca1cfe Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Fri, 27 Apr 2018 14:11:47 -0400 Subject: [PATCH 63/75] use log_nodes_cb from nipype==0.14.0 --- CPAC/GUI/interface/windows/main_window.py | 4 +--- CPAC/pipeline/cpac_pipeline.py | 22 ++++++++----------- .../resting_state_centrality_test.py | 5 ++++- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/CPAC/GUI/interface/windows/main_window.py b/CPAC/GUI/interface/windows/main_window.py index 953574aca2..957e422c8b 100644 --- a/CPAC/GUI/interface/windows/main_window.py +++ b/CPAC/GUI/interface/windows/main_window.py @@ -258,11 +258,9 @@ def runAnalysis1(self, pipeline, sublist, p): print "Error importing CPAC" print e - from nipype.pipeline.plugins.callback_log import log_nodes_cb c = Configuration(yaml.load(open(os.path.realpath(pipeline), 'r'))) plugin_args = {'n_procs': c.maxCoresPerParticipant, - 'memory_gb': c.maximumMemoryPerParticipant, - 'callback_log': log_nodes_cb} + 'memory_gb': c.maximumMemoryPerParticipant} # TODO: make this work if self.pids: diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 7e8c4411ca..59b94266b5 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -16,6 +16,7 @@ import pkg_resources as p # Nipype packages +import nipype import nipype.pipeline.engine as pe import nipype.interfaces.fsl as fsl import nipype.interfaces.io as nio @@ -5159,20 +5160,15 @@ def is_number(s): cb_logger.addHandler(handler) # Add status callback function that writes in callback log - try: - from nipype.pipeline.plugins.callback_log import log_nodes_cb - plugin_args['status_callback'] = log_nodes_cb - except ImportError as exc: - import nipype - err_msg = 'Version of nipype found in %s does not contain the ' \ - 'MultiProc plugin. Please check installation is the ' \ - 'most up-to-date or download and install the FCP-INDI ' \ - 'nipype repo at https:/github.com/fcp-indi/nipype.\n' \ - 'Error: %s' % (os.path.dirname(nipype.__file__), exc) + if nipype.__version__ not in ('0.13.1', '0.14.0'): + err_msg = "This version of nipype may not be compatible with CPAC v%s, please install version 0.14.0\n" \ + % (CPAC.__version__) logger.error(err_msg) - if nipype.__version__ != '0.13.1': - print "This version of nipype may not be compatible with CPAC v1.2, please install version 0.13.1" - # raise Exception(err_msg) + else: + if nipype.__version__ == '0.13.1': + plugin_args['status_callback'] = nipype.pipeline.plugins.callback_log.log_nodes_cb + else: + plugin_args['status_callback'] = nipype.utils.profiler.log_nodes_cb # Actually run the pipeline now, for the current subject workflow.run(plugin=plugin, plugin_args=plugin_args) diff --git a/test/reg/network_centrality/resting_state_centrality_test.py b/test/reg/network_centrality/resting_state_centrality_test.py index 7662646787..70494fc712 100644 --- a/test/reg/network_centrality/resting_state_centrality_test.py +++ b/test/reg/network_centrality/resting_state_centrality_test.py @@ -239,7 +239,10 @@ def _run_wf_and_map_outputs(self, method, thresh_option, thresh): # Import packages import os - from nipype.pipeline.plugins.callback_log import log_nodes_cb + try: + from nipype.pipeline.plugins.callback_log import log_nodes_cb + except: + from nipype.utils.profiler impor log_nodes_cb # Init workflow centrality_wf = self._init_centrality_wf(method, thresh_option, thresh) From 52cb45b9b76da93babb93d7e28cda2e87f4a1245 Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Fri, 27 Apr 2018 16:42:50 -0400 Subject: [PATCH 64/75] fix participants.tsv path finding, remove creds_path from output when credential is null, deal with undefined tags --- CPAC/utils/build_data_config.py | 39 ++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/CPAC/utils/build_data_config.py b/CPAC/utils/build_data_config.py index 3ae9588c6a..652cba9049 100644 --- a/CPAC/utils/build_data_config.py +++ b/CPAC/utils/build_data_config.py @@ -654,7 +654,9 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, ses = True # check if there is a participants.tsv file - part_tsv = glob.glob(part_tsv_glob) + part_tsv_finds = glob.glob(part_tsv_glob) + if part_tsv_finds: + part_tsv = part_tsv_finds[0] for glob_str in site_json_globs: site_jsons = site_jsons + glob.glob(glob_str) @@ -670,15 +672,11 @@ def get_BIDS_data_dct(bids_base_dir, file_list=None, anat_scan=None, # this would contain site information if the dataset is multi-site import csv - if file_list: + if part_tsv.startswith("s3://"): print "\n\nFound a participants.tsv file in your BIDS data " \ "set on the S3 bucket. Downloading..\n" part_tsv = download_single_s3_path(part_tsv, config_dir, aws_creds_path, overwrite=True) - else: - # then participants.tsv is local, and we need to pull it out of - # the glob return list - part_tsv = part_tsv[0] print "Checking participants.tsv file for site information:" \ "\n{0}".format(part_tsv) @@ -1168,8 +1166,10 @@ def update_data_dct(file_path, file_template, data_dct=None, data_type="anat", temp_sub_dct = {'subject_id': sub_id, 'unique_id': ses_id, 'site': site_id, - 'anat': file_path, - 'creds_path': str(aws_creds_path)} + 'anat': file_path } + + if aws_creds_path: + temp_sub_dct.update({ 'creds_path': str(aws_creds_path) }) if site_id not in data_dct.keys(): data_dct[site_id] = {} @@ -1487,34 +1487,36 @@ def run(data_settings_yml): with open(data_settings_yml, "r") as f: settings_dct = yaml.load(f) - if not settings_dct["awsCredentialsFile"]: + if "awsCredentialsFile" not in settings_dct or \ + not settings_dct["awsCredentialsFile"]: settings_dct["awsCredentialsFile"] = None elif "None" in settings_dct["awsCredentialsFile"] or \ "none" in settings_dct["awsCredentialsFile"]: settings_dct["awsCredentialsFile"] = None - if not settings_dct["anatomical_scan"]: + if "anatomical_scan" not in settings_dct or \ + not settings_dct["anatomical_scan"]: settings_dct["anatomical_scan"] = None elif "None" in settings_dct["anatomical_scan"] or \ "none" in settings_dct["anatomical_scan"]: settings_dct["anatomical_scan"] = None # inclusion lists - incl_dct = format_incl_excl_dct(settings_dct['siteList'], 'sites') - incl_dct.update(format_incl_excl_dct(settings_dct['subjectList'], + incl_dct = format_incl_excl_dct(settings_dct.get('siteList', None), 'sites') + incl_dct.update(format_incl_excl_dct(settings_dct.get('subjectList', None), 'participants')) - incl_dct.update(format_incl_excl_dct(settings_dct['sessionList'], + incl_dct.update(format_incl_excl_dct(settings_dct.get('sessionList', None), 'sessions')) - incl_dct.update(format_incl_excl_dct(settings_dct['scanList'], 'scans')) + incl_dct.update(format_incl_excl_dct(settings_dct.get('scanList', None), 'scans')) # exclusion lists - excl_dct = format_incl_excl_dct(settings_dct['exclusionSiteList'], + excl_dct = format_incl_excl_dct(settings_dct.get('exclusionSiteList', None), 'sites') - excl_dct.update(format_incl_excl_dct(settings_dct['exclusionSubjectList'], + excl_dct.update(format_incl_excl_dct(settings_dct.get('exclusionSubjectList', None), 'participants')) - excl_dct.update(format_incl_excl_dct(settings_dct['exclusionSessionList'], + excl_dct.update(format_incl_excl_dct(settings_dct.get('exclusionSessionList', None), 'sessions')) - excl_dct.update(format_incl_excl_dct(settings_dct['exclusionScanList'], + excl_dct.update(format_incl_excl_dct(settings_dct.get('exclusionScanList', None), 'scans')) if 'BIDS' in settings_dct['dataFormat'] or \ @@ -1602,6 +1604,7 @@ def run(data_settings_yml): # put data_dct contents in an ordered list for the YAML dump data_list = [] group_list = [] + included = {'site': [], 'sub': []} num_sess = num_scan = 0 From b9fc251bdabf31907a5ae3b0c630542baa9a8448 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Sat, 28 Apr 2018 16:55:23 -0400 Subject: [PATCH 65/75] Fixed some of the QC pages connections for the FD plot, and moved AFNI 3dedge3 to subprocess (for now) because the interface was acting strangely. Also fixed a datasink base directory switch. --- CPAC/pipeline/cpac_pipeline.py | 217 +++++++++++++++++---------------- CPAC/pipeline/cpac_runner.py | 3 - CPAC/qc/qc.py | 31 ++--- CPAC/qc/utils.py | 114 ++++++++++------- 4 files changed, 188 insertions(+), 177 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 59b94266b5..4622c51acc 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -4244,9 +4244,8 @@ def calc_avg(output_name, strat, num_strat, map_node=False): func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') - - - #register color palettes + + # register color palettes register_pallete(os.path.realpath( os.path.join(CPAC.__path__[0], 'qc', 'red.py')), 'red') register_pallete(os.path.realpath( @@ -4263,8 +4262,8 @@ def calc_avg(output_name, strat, num_strat, map_node=False): for strat in strat_list: nodes = getNodeList(strat) - #make SNR plot - + + # make SNR plot try: #preproc, out_file = strat.get_node_from_resource_pool('functional_preprocessed') #brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') @@ -4301,8 +4300,7 @@ def calc_avg(output_name, strat, num_strat, map_node=False): snr_val = pe.Node(util.Function(input_names=['measure_file'], output_names=['snr_storefl'], function=cal_snr_val),name='snr_val%d' % num_strat) - - + std_dev_anat.inputs.interp_ = 'trilinear' @@ -4334,16 +4332,14 @@ def calc_avg(output_name, strat, num_strat, map_node=False): drop_percent, 'measure_file') workflow.connect(snr, 'new_fname', - snr_val, 'measure_file') ### - - + snr_val, 'measure_file') + workflow.connect(drop_percent, 'modified_measure_file', - montage_snr, 'inputspec.overlay') + montage_snr, 'inputspec.overlay') workflow.connect(anat_ref, ref_file, - montage_snr, 'inputspec.underlay') - - + montage_snr, 'inputspec.underlay') + strat.update_resource_pool({'qc___snr_a': (montage_snr, 'outputspec.axial_png'), 'qc___snr_s': (montage_snr, 'outputspec.sagittal_png'), 'qc___snr_hist': (hist_, 'hist_path'), @@ -4352,13 +4348,12 @@ def calc_avg(output_name, strat, num_strat, map_node=False): qc_montage_id_a[3] = 'snr_a' qc_montage_id_s[3] = 'snr_s' qc_hist_id[3] = 'snr_hist' - - + except: logStandardError('QC', 'unable to get resources for SNR plot', '0051') raise - #make motion parameters plot - + + # make motion parameters plot try: mov_param, out_file = strat.get_node_from_resource_pool('movement_parameters') @@ -4376,12 +4371,11 @@ def calc_avg(output_name, strat, num_strat, map_node=False): if not 7 in qc_plot_id: qc_plot_id[7] = 'movement_rot_plot' - except: logStandardError('QC', 'unable to get resources for Motion Parameters plot', '0052') raise -# make FD plot and volumes removed + # make FD plot and volumes removed if 'gen_motion_stats' in nodes: if 1 in c.runNuisance: @@ -4390,56 +4384,66 @@ def calc_avg(output_name, strat, num_strat, map_node=False): fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_power') else: fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_jenkinson') - if ("De-Spiking" in c.runMotionSpike): - excluded, out_file_ex = strat.get_node_from_resource_pool('despiking_frames_excluded') - elif ("Scrubbing" in c.runMotionSpike): - excluded, out_file_ex = strat.get_node_from_resource_pool('scrubbing_frames_excluded') - - - fd_plot = pe.Node(util.Function(input_names=['arr', - 'ex_vol', - 'measure'], - output_names=['hist_path'], - function=gen_plot_png), - name='fd_plot_%d' % num_strat) - fd_plot.inputs.measure = 'FD' - workflow.connect(fd, out_file,fd_plot, 'arr') - workflow.connect(excluded, out_file_ex,fd_plot, 'ex_vol') - strat.update_resource_pool({'qc___fd_plot': (fd_plot, 'hist_path')}) - if not 8 in qc_plot_id: - qc_plot_id[8] = 'fd_plot' - - + + fd_plot = \ + pe.Node(util.Function(input_names=['arr', + 'measure', + 'ex_vol'], + output_names=['hist_path'], + function=gen_plot_png), + name='fd_plot_%d' % num_strat) + fd_plot.inputs.measure = 'FD' + + workflow.connect(fd, out_file,fd_plot, 'arr') + + if "De-Spiking" in c.runMotionSpike: + excluded, out_file_ex = strat.get_node_from_resource_pool('despiking_frames_excluded') + workflow.connect(excluded, out_file_ex, + fd_plot, 'ex_vol') + elif "Scrubbing" in c.runMotionSpike: + excluded, out_file_ex = strat.get_node_from_resource_pool('scrubbing_frames_excluded') + workflow.connect(excluded, out_file_ex, + fd_plot, 'ex_vol') + + strat.update_resource_pool({'qc___fd_plot': (fd_plot, 'hist_path')}) + + if not 8 in qc_plot_id: + qc_plot_id[8] = 'fd_plot' + except: logStandardError('QC', 'unable to get resources for FD plot', '0053') raise # make QC montages for Skull Stripping Visualization try: - anat_underlay, out_file = strat.get_node_from_resource_pool('anatomical_brain') - skull, out_file_s = strat.get_node_from_resource_pool('anatomical_reorient') - - montage_skull = create_montage('montage_skull_%d' % num_strat,'red', 'skull_vis') ### - - - skull_edge = make_edge(wf_name= 'skull_edge_%d' % num_strat) - workflow.connect(skull, out_file_s, skull_edge, 'inputspec.file_') - workflow.connect(skull_edge, 'outputspec.new_fname', montage_skull, 'inputspec.overlay') - workflow.connect(anat_underlay, out_file,montage_skull,'inputspec.underlay') - strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, 'outputspec.axial_png'),'qc___skullstrip_vis_s': (montage_skull, 'outputspec.sagittal_png')}) - #skull_edge = #pe.Node(util.Function(input_names=['file_'],output_names=['new_fname#'],function=make_edge),name='skull_edge_%d' % num_strat) - - #workflow.connect(skull, out_file_s,skull_edge, 'file_') - - #workflow.connect(anat_underlay, out_file,montage_skull,'inputspec.underlay') - - # workflow.connect(skull_edge, 'new_fname',montage_skull,'inputspec.overlay') - - # strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, #'outputspec.axial_png'),'qc___skullstrip_vis_s': (montage_skull, #'outputspec.sagittal_png')}) - - if not 1 in qc_montage_id_a: - qc_montage_id_a[1] = 'skullstrip_vis_a' - qc_montage_id_s[1] = 'skullstrip_vis_s' + anat_underlay, out_file = strat.get_node_from_resource_pool('anatomical_brain') + skull, out_file_s = strat.get_node_from_resource_pool('anatomical_reorient') + + montage_skull = create_montage('montage_skull_%d' % num_strat, + 'red', 'skull_vis') + skull_edge = make_edge(wf_name='skull_edge_%d' % num_strat) + + workflow.connect(skull, out_file_s, + skull_edge, 'inputspec.file_') + workflow.connect(skull_edge, 'outputspec.new_fname', + montage_skull, 'inputspec.overlay') + workflow.connect(anat_underlay, out_file, + montage_skull, 'inputspec.underlay') + + strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, 'outputspec.axial_png'),'qc___skullstrip_vis_s': (montage_skull, 'outputspec.sagittal_png')}) + #skull_edge = #pe.Node(util.Function(input_names=['file_'],output_names=['new_fname#'],function=make_edge),name='skull_edge_%d' % num_strat) + + #workflow.connect(skull, out_file_s,skull_edge, 'file_') + + #workflow.connect(anat_underlay, out_file,montage_skull,'inputspec.underlay') + + # workflow.connect(skull_edge, 'new_fname',montage_skull,'inputspec.overlay') + + # strat.update_resource_pool({'qc___skullstrip_vis_a': (montage_skull, #'outputspec.axial_png'),'qc___skullstrip_vis_s': (montage_skull, #'outputspec.sagittal_png')}) + + if not 1 in qc_montage_id_a: + qc_montage_id_a[1] = 'skullstrip_vis_a' + qc_montage_id_s[1] = 'skullstrip_vis_s' except: logStandardError('QC', 'Cannot generate QC montages for Skull Stripping: Resources Not Found', '0054') @@ -4450,7 +4454,7 @@ def calc_avg(output_name, strat, num_strat, map_node=False): mni_anat_underlay, out_file = strat.get_node_from_resource_pool('mean_functional_in_anat') montage_mni_anat = create_montage('montage_mni_anat_%d' % num_strat, - 'red', 'mni_anat') + 'red', 'mni_anat') workflow.connect(mni_anat_underlay, out_file, montage_mni_anat, 'inputspec.underlay') @@ -4468,10 +4472,7 @@ def calc_avg(output_name, strat, num_strat, map_node=False): logStandardError('QC', 'Cannot generate QC montages for MNI normalized anatomical: Resources Not Found', '0054') raise - - # make QC montages for CSF WM GM - if 'seg_preproc' in nodes: try: @@ -4506,67 +4507,69 @@ def calc_avg(output_name, strat, num_strat, map_node=False): logStandardError('QC', 'Cannot generate QC montages for WM GM CSF masks: Resources Not Found', '0055') raise - # make QC montage for Mean Functional in T1 with T1 edge - try: anat, out_file = strat.get_node_from_resource_pool('anatomical_brain') m_f_a, out_file_mfa = strat.get_node_from_resource_pool('mean_functional_in_anat') montage_anat = create_montage('montage_anat_%d' % num_strat, - 'red', 't1_edge_on_mean_func_in_t1')### - anat_edge = make_edge(wf_name= 'anat_edge_%d' % num_strat) - workflow.connect(anat, out_file, anat_edge, 'inputspec.file_' ) - workflow.connect(anat_edge,'outputspec.new_fname',montage_anat,'inputspec.overlay') + 'red', 't1_edge_on_mean_func_in_t1') + + anat_edge = make_edge(wf_name='anat_edge_%d' % num_strat) + + workflow.connect(anat, out_file, anat_edge, 'inputspec.file_') + + workflow.connect(anat_edge, 'outputspec.new_fname', + montage_anat, 'inputspec.overlay') + workflow.connect(m_f_a, out_file_mfa, - montage_anat, 'inputspec.underlay') - #workflow.connect(anat_edge, 'new_fname', - # montage_anat, 'inputspec.overlay') + montage_anat, 'inputspec.underlay') + + # workflow.connect(anat_edge, 'new_fname', montage_anat, + # 'inputspec.overlay') strat.update_resource_pool({'qc___mean_func_with_t1_edge_a': (montage_anat, 'outputspec.axial_png'),'qc___mean_func_with_t1_edge_s': (montage_anat, 'outputspec.sagittal_png')}) -#anat_edge = pe.Node(util.Function(input_names=['file_'], -# output_names=['new_fname'], -# function=make_edge), -# name='anat_edge_%d' % num_strat) - -# workflow.connect(anat, out_file, -# anat_edge, 'file_') - -# -# workflow.connect(m_f_a, out_file_mfa, -# montage_anat, 'inputspec.underlay') -# -# workflow.connect(anat_edge, 'new_fname', -# montage_anat, 'inputspec.overlay') -# -# strat.update_resource_pool({'qc___mean_func_with_t1_edge_a': (montage_anat, #'outputspec.axial_png'), -# 'qc___mean_func_with_t1_edge_s': (montage_anat, #'outputspec.sagittal_png')}) + #anat_edge = pe.Node(util.Function(input_names=['file_'], + # output_names=['new_fname'], + # function=make_edge), + # name='anat_edge_%d' % num_strat) + + # workflow.connect(anat, out_file, + # anat_edge, 'file_') + + # + # workflow.connect(m_f_a, out_file_mfa, + # montage_anat, 'inputspec.underlay') + # + # workflow.connect(anat_edge, 'new_fname', + # montage_anat, 'inputspec.overlay') + # + # strat.update_resource_pool({'qc___mean_func_with_t1_edge_a': (montage_anat, #'outputspec.axial_png'), + # 'qc___mean_func_with_t1_edge_s': (montage_anat, #'outputspec.sagittal_png')}) if not 4 in qc_montage_id_a: qc_montage_id_a[4] = 'mean_func_with_t1_edge_a' qc_montage_id_s[4] = 'mean_func_with_t1_edge_s' - except: logStandardError('QC', 'Cannot generate QC montages for Mean Functional in T1 with T1 edge: Resources Not Found', '0056') raise # make QC montage for Mean Functional in MNI with MNI edge - try: m_f_i, out_file = strat.get_node_from_resource_pool('mean_functional_to_standard') montage_mfi = create_montage('montage_mfi_%d' % num_strat, 'red', 'MNI_edge_on_mean_func_mni') ### -# MNI_edge = pe.Node(util.Function(input_names=['file_'], -# output_names=['new_fname'], -# function=make_edge), -# name='MNI_edge_%d' % num_strat) -# #MNI_edge.inputs.file_ = c.template_brain_only_for_func -# workflow.connect(MNI_edge, 'new_fname', -# montage_mfi, 'inputspec.overlay') + # MNI_edge = pe.Node(util.Function(input_names=['file_'], + # output_names=['new_fname'], + # function=make_edge), + # name='MNI_edge_%d' % num_strat) + # #MNI_edge.inputs.file_ = c.template_brain_only_for_func + # workflow.connect(MNI_edge, 'new_fname', + # montage_mfi, 'inputspec.overlay') workflow.connect(m_f_i, out_file, montage_mfi, 'inputspec.underlay') @@ -5029,11 +5032,12 @@ def is_number(s): continue ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) + # Write QC outputs to log directory if 'qc' in key.lower(): - ds.inputs.base_directory = c.outputDirectory - else: ds.inputs.base_directory = c.logDirectory + else: + ds.inputs.base_directory = c.outputDirectory # For each pipeline ID, generate the QC pages # for pip_id in pip_ids: # Define pipeline-level logging for QC @@ -5055,9 +5059,6 @@ def is_number(s): node, out_file = rp[key] workflow.connect(node, out_file, ds, key) - #logger.info('node, out_file, key: ' - # '%s, %s, %s' % (node, out_file, key)) - link_node = pe.Node(interface=util.Function( input_names=['in_file', 'strategies', 'subject_id', 'pipeline_id', 'helper', diff --git a/CPAC/pipeline/cpac_runner.py b/CPAC/pipeline/cpac_runner.py index 5ddea6387a..d837f9a42c 100644 --- a/CPAC/pipeline/cpac_runner.py +++ b/CPAC/pipeline/cpac_runner.py @@ -97,10 +97,7 @@ def make_entries(paths, path_iterables): sub_entries.append(path_iterables[indx] + '_' + value) indx += 1 - ### remove single quote in the paths sub_entries = map(lambda x: x.replace("'", ""), sub_entries) - print "sub entries: " - print sub_entries entries.append(sub_entries) diff --git a/CPAC/qc/qc.py b/CPAC/qc/qc.py index bcd606fdd3..bc03506847 100644 --- a/CPAC/qc/qc.py +++ b/CPAC/qc/qc.py @@ -23,43 +23,35 @@ def create_montage(wf_name, cbar_name, png_name): 'resampled_overlay']), name='outputspec') - resample_u = pe.Node(util.Function(input_names=['file_'], output_names=['new_fname'], function=resample_1mm), - name='resample_u') + name='resample_u') + wf.connect(inputNode, 'underlay', resample_u, 'file_') resample_o = resample_u.clone('resample_o') - - + wf.connect(inputNode, 'overlay', resample_o, 'file_') montage_a = pe.Node(util.Function(input_names=['overlay', - 'underlay', - 'png_name', - 'cbar_name'], + 'underlay', + 'png_name', + 'cbar_name'], output_names=['png_name'], function=montage_axial), - name='montage_a') + name='montage_a') montage_a.inputs.cbar_name = cbar_name montage_a.inputs.png_name = png_name + '_a.png' - montage_s = pe.Node(util.Function(input_names=['overlay', 'underlay', 'png_name', 'cbar_name'], output_names=['png_name'], function=montage_sagittal), - name='montage_s') + name='montage_s') montage_s.inputs.cbar_name = cbar_name montage_s.inputs.png_name = png_name + '_s.png' - wf.connect(inputNode, 'underlay', - resample_u, 'file_') - - wf.connect(inputNode, 'overlay', - resample_o, 'file_') - wf.connect(resample_u, 'new_fname', montage_a, 'underlay') @@ -84,9 +76,9 @@ def create_montage(wf_name, cbar_name, png_name): wf.connect(montage_s, 'png_name', outputNode, 'sagittal_png') - return wf + def create_montage_gm_wm_csf(wf_name, png_name): wf = pe.Workflow(name=wf_name) @@ -105,7 +97,6 @@ def create_montage_gm_wm_csf(wf_name, png_name): 'resampled_overlay_gm']), name='outputspec') - resample_u = pe.Node(util.Function(input_names=['file_'], output_names=['new_fname'], function=resample_1mm), @@ -115,8 +106,6 @@ def create_montage_gm_wm_csf(wf_name, png_name): resample_o_wm = resample_u.clone('resample_o_wm') resample_o_gm = resample_u.clone('resample_o_gm') - - montage_a = pe.Node(util.Function(input_names=['overlay_csf', 'overlay_wm', 'overlay_gm', @@ -127,7 +116,6 @@ def create_montage_gm_wm_csf(wf_name, png_name): name='montage_a') montage_a.inputs.png_name = png_name + '_a.png' - montage_s = pe.Node(util.Function(input_names=['overlay_csf', 'overlay_wm', 'overlay_gm', @@ -192,5 +180,4 @@ def create_montage_gm_wm_csf(wf_name, png_name): wf.connect(montage_s, 'png_name', outputNode, 'sagittal_png') - return wf diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index 7e727f303d..1742be231c 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -5,7 +5,7 @@ matplotlib.use('Agg') import os import nipype.pipeline.engine as pe -from nipype.interfaces import afni,fsl +from nipype.interfaces import afni import nipype.interfaces.utility as util @@ -1201,7 +1201,27 @@ def generateQCPages(qc_path,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_his # print f -def make_edge(wf_name ='create_edge'): +def afni_edge(in_file): + """Run AFNI 3dedge3 on the input file - temporary function until the + interface issue in Nipype is sorted out.""" + + out_file = os.path.join(os.getcwd(), + "{0}_edge.nii.gz".format(os.path.basename(in_file))) + + cmd_string = ["3dedge3", "-input", in_file, "-prefix", out_file] + + try: + retcode = subprocess.check_output(cmd_string) + except Exception as e: + err = "\n\n[!] Something went wrong with AFNI 3dedge3 while " \ + "creating the an overlay for the QA pages.\n\nError details: " \ + "{0}\n\n".format(e) + raise Exception(err) + + return out_file + + +def make_edge(wf_name='create_edge'): """ Make edge file from a scan image @@ -1220,33 +1240,39 @@ def make_edge(wf_name ='create_edge'): """ - import commands - import os - - wf_name = pe.Workflow(name = wf_name) + wf_name = pe.Workflow(name=wf_name) - inputNode = pe.Node(util.IdentityInterface(fields=['file_']),name = 'inputspec') - outputNode = pe.Node(util.IdentityInterface(fields=['new_fname']),name = 'outputspec') - - prepare = pe.Node(interface=afni.Edge3(),name ='prepare') - wf_name.connect(inputNode,'file_',prepare,'in_file') - wf_name.connect(prepare,'out_file',outputNode,'new_fname') - - #remainder, ext_ = os.path.splitext(file_) - - #remainder, ext1_ = os.path.splitext(remainder) - - #ext = ''.join([ext1_, ext_]) - - # new_fname = ''.join([remainder, '_edge', ext]) - # new_fname = os.path.join(os.getcwd(), os.path.basename(new_fname)) - - #cmd = "3dedge3 -input %s -prefix %s" % (file_, new_fname) - #print cmd - #print commands.getoutput(cmd) + inputNode = pe.Node(util.IdentityInterface(fields=['file_']), + name='inputspec') + outputNode = pe.Node(util.IdentityInterface(fields=['new_fname']), + name='outputspec') + + ''' + try: + prepare = pe.Node(interface=afni.Edge3(), name='afni_3dedge3') + prepare.inputs.outputtype = "NIFTI_GZ" + prepare.inputs.out_file = "{0}_edge.nii.gz".format(wf_name) + prepare.inputs.terminal_output = "file" + except AttributeError: + raise Exception("\n\n[!] Could not find the 'Edge3' AFNI interface " + "in your Nipype install - double-check that you are " + "using the correct version.\n\n") + ''' + + run_afni_edge_imports = ["import os", "import subprocess"] + + run_afni_edge = pe.Node(util.Function(input_names=['in_file'], + output_names=['out_file'], + function=afni_edge, + imports=run_afni_edge_imports), + name='afni_3dedge3') + + wf_name.connect(inputNode, 'file_', run_afni_edge, 'in_file') + wf_name.connect(run_afni_edge, 'out_file', outputNode, 'new_fname') return wf_name + def gen_func_anat_xfm(func_, ref_, xfm_, interp_): """ @@ -1279,7 +1305,8 @@ def gen_func_anat_xfm(func_, ref_, xfm_, interp_): new_fname = os.path.join(os.getcwd(), 'std_dev_anat.nii.gz') - cmd = 'applywarp --ref=%s --in=%s --out=%s --premat=%s --interp=%s' % (ref_, func_, new_fname, xfm_, interp_) + cmd = 'applywarp --ref=%s --in=%s --out=%s --premat=%s ' \ + '--interp=%s' % (ref_, func_, new_fname, xfm_, interp_) print cmd print commands.getoutput(cmd) @@ -1435,7 +1462,7 @@ def drange(min_, max_): return range_ -def gen_plot_png(arr, ex_vol, measure): +def gen_plot_png(arr, measure, ex_vol=None): """ Generate Motion FD Plot. Shows which volumes were dropped. @@ -1446,12 +1473,11 @@ def gen_plot_png(arr, ex_vol, measure): arr : list Frame wise Displacements - ex_vol : list - Volumes excluded - measure : string Label of the Measure + ex_vol : list + Volumes excluded Returns ------- @@ -1460,38 +1486,40 @@ def gen_plot_png(arr, ex_vol, measure): path to the generated plot png """ - import matplotlib - import commands -# matplotlib.use('Agg') from matplotlib import pyplot - matplotlib.rcParams.update({'font.size': 8}) import matplotlib.cm as cm - import numpy as np import os + matplotlib.rcParams.update({'font.size': 8}) arr = np.loadtxt(arr) - try: - ex_vol = np.genfromtxt(ex_vol, delimiter=',', dtype=int) - ex_vol = ex_vol[ex_vol > 0] - except: + + if ex_vol: + try: + ex_vol = np.genfromtxt(ex_vol, delimiter=',', dtype=int) + ex_vol = ex_vol[ex_vol > 0] + except: + ex_vol = [] + else: ex_vol = [] + arr = arr[1:] del_el = [x for x in ex_vol if x < len(arr)] ex_vol = np.array(del_el) - fig = pyplot.figure(figsize=(10, 6)) pyplot.plot([i for i in xrange(len(arr))], arr, '-') - fig.suptitle('%s plot with Mean %s = %0.4f' % (measure, measure, arr.mean())) + fig.suptitle('%s plot with Mean %s = %0.4f' % (measure, measure, + arr.mean())) if measure == 'FD' and len(ex_vol) > 0: pyplot.scatter(ex_vol, arr[ex_vol], c="red", zorder=2) for x in ex_vol: - pyplot.annotate('( %d , %0.3f)' % (x, arr[x]), xy=(x, arr[x]), arrowprops=dict(facecolor='black', shrink=0.0)) + pyplot.annotate('( %d , %0.3f)' % (x, arr[x]), xy=(x, arr[x]), + arrowprops=dict(facecolor='black', shrink=0.0)) pyplot.xlabel('Volumes') pyplot.ylabel('%s' % measure) @@ -2084,8 +2112,6 @@ def make_montage_axial(overlay, underlay, png_name, cbar_name): return png_name - - def montage_sagittal(overlay, underlay, png_name, cbar_name): """ From 02494c38b9a6e45878dbdff5adbc3707dd04a43e Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Mon, 30 Apr 2018 12:01:19 -0400 Subject: [PATCH 66/75] fix package imports, allow both nipype versions on setup --- CPAC/info.py | 4 ++-- CPAC/pipeline/cpac_pipeline.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CPAC/info.py b/CPAC/info.py index 849310404f..9df1b56e40 100644 --- a/CPAC/info.py +++ b/CPAC/info.py @@ -136,7 +136,7 @@ def get_cpac_gitversion(): VERSION = __version__ REQUIRES = ["matplotlib (>=1.2)", "lockfile (>=0.9)", "pyyaml (>=3.0)", "pygraphviz (>=1.3)", - "nibabel (>=2.0.1)", "nipype (==0.13.1)", + "nibabel (>=2.0.1)", "nipype (>=0.13.1,<=0.14.0)", "patsy (>=0.3)", "psutil (>=2.1)", "boto3 (>=1.2)", "future (==0.16.0)", "prov (>=1.4.0)", "simplejson (>=3.8.0)", "cython (>=0.12.1)", @@ -145,7 +145,7 @@ def get_cpac_gitversion(): "ipython (>=5.1)"] INSTALL_REQUIRES = ["matplotlib >=1.2", "lockfile >=0.9", "pyyaml >=3.0", "pygraphviz >=1.3", "nibabel >=2.0.1", - "nipype ==0.13.1", "patsy >=0.3", "psutil >=2.1", + "nipype >=0.13.1,<=0.14.0", "patsy >=0.3", "psutil >=2.1", "boto3 >=1.2", "future ==0.16.0", "prov >=1.4.0", "simplejson >=3.8.0", "cython >=0.12.1", "Jinja2 >=2.6", "pandas >=0.15", "INDI-Tools >=0.0.6", diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 4622c51acc..8e7af0d0e9 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -5167,9 +5167,11 @@ def is_number(s): logger.error(err_msg) else: if nipype.__version__ == '0.13.1': - plugin_args['status_callback'] = nipype.pipeline.plugins.callback_log.log_nodes_cb + from nipype.pipeline.plugins.callback_log import log_nodes_cb + plugin_args['status_callback'] = log_nodes_cb else: - plugin_args['status_callback'] = nipype.utils.profiler.log_nodes_cb + from nipype.utils.profiler import log_nodes_cb + plugin_args['status_callback'] = log_nodes_cb # Actually run the pipeline now, for the current subject workflow.run(plugin=plugin, plugin_args=plugin_args) From 9d3299780850827c206da12533d2d733d7a7dcd6 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Mon, 30 Apr 2018 12:15:53 -0400 Subject: [PATCH 67/75] Minor mods. --- CPAC/pipeline/cpac_pipeline.py | 6 +++--- CPAC/sca/sca.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 4622c51acc..559ddee145 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -5032,7 +5032,7 @@ def is_number(s): continue ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) - + # Write QC outputs to log directory if 'qc' in key.lower(): ds.inputs.base_directory = c.logDirectory @@ -5162,7 +5162,8 @@ def is_number(s): # Add status callback function that writes in callback log if nipype.__version__ not in ('0.13.1', '0.14.0'): - err_msg = "This version of nipype may not be compatible with CPAC v%s, please install version 0.14.0\n" \ + err_msg = "This version of nipype may not be compatible with " \ + "CPAC v%s, please install version 0.14.0\n" \ % (CPAC.__version__) logger.error(err_msg) else: @@ -5174,7 +5175,6 @@ def is_number(s): # Actually run the pipeline now, for the current subject workflow.run(plugin=plugin, plugin_args=plugin_args) - # Dump subject info pickle file to subject log dir subject_info['status'] = 'Completed' subject_info_pickle = open( diff --git a/CPAC/sca/sca.py b/CPAC/sca/sca.py index 82b558e65d..7116813734 100644 --- a/CPAC/sca/sca.py +++ b/CPAC/sca/sca.py @@ -106,7 +106,7 @@ def create_sca(name_sca='sca'): ]), name='outputspec') - # # 2. Compute voxel-wise correlation with Seed Timeseries + # 2. Compute voxel-wise correlation with Seed Timeseries corr = pe.Node(interface=preprocess.TCorr1D(), name='3dTCorr1D') corr.inputs.pearson = True @@ -139,8 +139,7 @@ def create_sca(name_sca='sca'): get_roi_num_list.inputs.prefix = "sca" rename_rois = pe.MapNode(interface=util.Rename(), name='output_rois', - iterfield=['in_file','format_string']) - + iterfield=['in_file', 'format_string']) rename_rois.inputs.keep_ext = True sca.connect(corr, 'out_file', concat, 'in_files') From 63d147183278f9083068fbefc8a593f615a4d34e Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Mon, 30 Apr 2018 12:35:34 -0400 Subject: [PATCH 68/75] FD plot import fix, before/after smoothing switch. --- CPAC/GUI/interface/pages/smoothing.py | 10 +- CPAC/GUI/resources/config_parameters.txt | 3 + CPAC/pipeline/cpac_pipeline.py | 170 +++++++++++++++-------- CPAC/qc/utils.py | 3 +- CPAC/utils/utils.py | 7 +- 5 files changed, 134 insertions(+), 59 deletions(-) diff --git a/CPAC/GUI/interface/pages/smoothing.py b/CPAC/GUI/interface/pages/smoothing.py index 2f52788a71..ecef5babe4 100644 --- a/CPAC/GUI/interface/pages/smoothing.py +++ b/CPAC/GUI/interface/pages/smoothing.py @@ -54,7 +54,15 @@ def __init__(self, parent, counter = 0): "individual-level analysis pipeline, such " "that all derivatives are output both " "smoothed and unsmoothed.") - + + self.page.add(label="Smooth Before/After z-Scoring ", + control=control.CHOICE_BOX, + name='smoothing_order', + type=dtype.LSTR, + comment="Choose whether to smooth outputs before or " + "after z-scoring.", + values=["Before", "After"]) + self.page.add(label="z-score Standardize Derivatives ", control=control.CHOICE_BOX, name='runZScoring', diff --git a/CPAC/GUI/resources/config_parameters.txt b/CPAC/GUI/resources/config_parameters.txt index 93338abde2..bb54c94f9d 100644 --- a/CPAC/GUI/resources/config_parameters.txt +++ b/CPAC/GUI/resources/config_parameters.txt @@ -17,6 +17,7 @@ s3Encryption,S3 Encryption,Output Settings write_func_outputs,Write Extra Functional Outputs,Output Settings write_debugging_outputs,Write Debugging Outputs,Output Settings runSymbolicLinks,Create Symbolic Links,Output Settings +generateQualityControlImages,Enable Quality Control Interface,Output Settings removeWorkingDir,Remove Working Directory,Output Settings run_logging,Run Logging,Output Settings reGenerateOutputs,Regenerate Outputs,Output Settings @@ -98,6 +99,8 @@ lfcdWeightOptions,Local Functional Connectivity Density Weight Options,Network C lfcdCorrelationThresholdOption,Local Functional Connectivity Density Threshold Type,Network Centrality Options lfcdCorrelationThreshold,Local Functional Connectivity Density Threshold Value,Network Centrality Options memoryAllocatedForDegreeCentrality,Maximum RAM Use (GB),Network Centrality Options +run_smoothing,Run Smoothing,After Warping Options +smoothing_order,Smoothing Before/After z-Scoring,After Warping Options fwhm,Kernel FWHM (in mm),After Warping Options runZScoring,Z-score Standardize Derivatives,After Warping Options numGPAModelsAtOnce,Number of Models to Run Simultaneously,Group Analysis Settings diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index b55e30d752..3d1acd8a95 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -4163,61 +4163,121 @@ def calc_avg(output_name, strat, num_strat, map_node=False): strat = output_to_standard(key, strat, num_strat, c, map_node=True) - if 1 in c.runZScoring: - rp = strat.get_resource_pool() - for key in sorted(rp.keys()): - # connect nodes for z-score standardization - if "sca_roi_files_to_standard" in key: - # correlation files need the r-to-z - strat = fisher_z_score_standardize(key, - "roi_timeseries_for_SCA", - strat, num_strat, - map_node=True) - elif "centrality" in key: - # specific mask - strat = z_score_standardize(key, - c.templateSpecificationFile, - strat, num_strat, - map_node=True) - elif key in outputs_template_raw: - # raw score, in template space - strat = z_score_standardize(key, - "functional_brain_mask_to_standard", - strat, num_strat) - elif key in outputs_template_raw_mult: - # same as above but multiple files so mapnode required - strat = z_score_standardize(key, - "functional_brain_mask_to_standard", - strat, num_strat, - map_node=True) - - if 1 in c.run_smoothing: - rp = strat.get_resource_pool() - for key in sorted(rp.keys()): - # connect nodes for smoothing - if "centrality" in key: - # centrality needs its own mask - strat = output_smooth(key, - c.templateSpecificationFile, - strat, num_strat, map_node=True) - elif key in outputs_native_nonsmooth: - # native space - strat = output_smooth(key, "functional_brain_mask", - strat, num_strat) - elif key in outputs_native_nonsmooth_mult: - # native space with multiple files (map nodes) - strat = output_smooth(key, "functional_brain_mask", - strat, num_strat, map_node=True) - elif key in outputs_template_nonsmooth: - # template space - strat = output_smooth(key, - "functional_brain_mask_to_standard", - strat, num_strat) - elif key in outputs_template_nonsmooth_mult: - # template space with multiple files (map nodes) - strat = output_smooth(key, - "functional_brain_mask_to_standard", - strat, num_strat, map_node=True) + if "Before" in c.smoothing_order: + # run smoothing before Z-scoring + if 1 in c.run_smoothing: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + # connect nodes for smoothing + if "centrality" in key: + # centrality needs its own mask + strat = output_smooth(key, + c.templateSpecificationFile, + strat, num_strat, map_node=True) + elif key in outputs_native_nonsmooth: + # native space + strat = output_smooth(key, "functional_brain_mask", + strat, num_strat) + elif key in outputs_native_nonsmooth_mult: + # native space with multiple files (map nodes) + strat = output_smooth(key, "functional_brain_mask", + strat, num_strat, map_node=True) + elif key in outputs_template_nonsmooth: + # template space + strat = output_smooth(key, + "functional_brain_mask_to_standard", + strat, num_strat) + elif key in outputs_template_nonsmooth_mult: + # template space with multiple files (map nodes) + strat = output_smooth(key, + "functional_brain_mask_to_standard", + strat, num_strat, map_node=True) + + if 1 in c.runZScoring: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + # connect nodes for z-score standardization + if "sca_roi_files_to_standard" in key: + # correlation files need the r-to-z + strat = fisher_z_score_standardize(key, + "roi_timeseries_for_SCA", + strat, num_strat, + map_node=True) + elif "centrality" in key: + # specific mask + strat = z_score_standardize(key, + c.templateSpecificationFile, + strat, num_strat, + map_node=True) + elif key in outputs_template_raw: + # raw score, in template space + strat = z_score_standardize(key, + "functional_brain_mask_to_standard", + strat, num_strat) + elif key in outputs_template_raw_mult: + # same as above but multiple files so mapnode required + strat = z_score_standardize(key, + "functional_brain_mask_to_standard", + strat, num_strat, + map_node=True) + + elif "After" in c.smoothing_order: + # run smoothing after Z-scoring + if 1 in c.runZScoring: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + # connect nodes for z-score standardization + if "sca_roi_files_to_standard" in key: + # correlation files need the r-to-z + strat = fisher_z_score_standardize(key, + "roi_timeseries_for_SCA", + strat, num_strat, + map_node=True) + elif "centrality" in key: + # specific mask + strat = z_score_standardize(key, + c.templateSpecificationFile, + strat, num_strat, + map_node=True) + elif key in outputs_template_raw: + # raw score, in template space + strat = z_score_standardize(key, + "functional_brain_mask_to_standard", + strat, num_strat) + elif key in outputs_template_raw_mult: + # same as above but multiple files so mapnode required + strat = z_score_standardize(key, + "functional_brain_mask_to_standard", + strat, num_strat, + map_node=True) + + if 1 in c.run_smoothing: + rp = strat.get_resource_pool() + for key in sorted(rp.keys()): + # connect nodes for smoothing + if "centrality" in key: + # centrality needs its own mask + strat = output_smooth(key, + c.templateSpecificationFile, + strat, num_strat, map_node=True) + elif key in outputs_native_nonsmooth: + # native space + strat = output_smooth(key, "functional_brain_mask", + strat, num_strat) + elif key in outputs_native_nonsmooth_mult: + # native space with multiple files (map nodes) + strat = output_smooth(key, "functional_brain_mask", + strat, num_strat, map_node=True) + elif key in outputs_template_nonsmooth: + # template space + strat = output_smooth(key, + "functional_brain_mask_to_standard", + strat, num_strat) + elif key in outputs_template_nonsmooth_mult: + # template space with multiple files (map nodes) + strat = output_smooth(key, + "functional_brain_mask_to_standard", + strat, num_strat, map_node=True) rp = strat.get_resource_pool() for key in sorted(rp.keys()): diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index 1742be231c..55b090e769 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -1486,10 +1486,11 @@ def gen_plot_png(arr, measure, ex_vol=None): path to the generated plot png """ + import os + import numpy as np import matplotlib from matplotlib import pyplot import matplotlib.cm as cm - import os matplotlib.rcParams.update({'font.size': 8}) diff --git a/CPAC/utils/utils.py b/CPAC/utils/utils.py index d501e0eeb2..ecdc368604 100644 --- a/CPAC/utils/utils.py +++ b/CPAC/utils/utils.py @@ -39,8 +39,6 @@ 'motion_correct': 'func', 'motion_correct_smooth': 'func', 'motion_correct_to_standard': 'func', - 'motion_correct_to_standard_other_resolutions': 'func', - 'motion_correct_to_standard_other_resolutions_smooth': 'func', 'motion_correct_to_standard_smooth': 'func', 'mean_functional_in_anat': 'func', 'coordinate_transformation': 'func', @@ -96,6 +94,8 @@ 'falff_to_standard_smooth': 'alff', 'alff_to_standard_zstd': 'alff', 'falff_to_standard_zstd': 'alff', + 'alff_to_standard_smooth_zstd': 'alff', + 'falff_to_standard_smooth_zstd': 'alff', 'alff_to_standard_zstd_smooth': 'alff', 'falff_to_standard_zstd_smooth': 'alff', 'reho': 'reho', @@ -103,6 +103,7 @@ 'reho_to_standard': 'reho', 'reho_to_standard_smooth': 'reho', 'reho_to_standard_zstd': 'reho', + 'reho_to_standard_smooth_zstd': 'reho', 'reho_to_standard_zstd_smooth': 'reho', 'voxel_timeseries': 'timeseries', 'roi_timeseries': 'timeseries', @@ -113,6 +114,7 @@ 'sca_roi_files_to_standard': 'sca_roi', 'sca_roi_files_to_standard_smooth': 'sca_roi', 'sca_roi_files_to_standard_fisher_zstd': 'sca_roi', + 'sca_roi_files_to_standard_smooth_fisher_zstd': 'sca_roi', 'sca_roi_files_to_standard_fisher_zstd_smooth': 'sca_roi', 'bbregister_registration': 'surface_registration', 'left_hemisphere_surface': 'surface_registration', @@ -121,6 +123,7 @@ 'centrality_outputs': 'centrality', 'centrality_outputs_smooth': 'centrality', 'centrality_outputs_zstd': 'centrality', + 'centrality_outputs_smooth_zstd': 'centrality', 'centrality_outputs_zstd_smooth': 'centrality', 'centrality_graphs': 'centrality', 'seg_probability_maps': 'anat', From f8d3bde7c2096a327e96ddf55c2626ba473d8cea Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Mon, 30 Apr 2018 22:41:29 -0400 Subject: [PATCH 69/75] Got the QC pages function nodes working with Nipype 0.13.1. --- CPAC/pipeline/cpac_pipeline.py | 170 ++++++++++++++----------- CPAC/qc/qc.py | 54 ++++++-- CPAC/qc/utils.py | 222 ++++++--------------------------- 3 files changed, 180 insertions(+), 266 deletions(-) diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 3d1acd8a95..1e2c179e7b 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -4325,74 +4325,83 @@ def calc_avg(output_name, strat, num_strat, map_node=False): # make SNR plot try: - #preproc, out_file = strat.get_node_from_resource_pool('functional_preprocessed') - #brain_mask, mask_file = strat.get_node_from_resource_pool('functional_brain_mask') - #func_to_anat_xfm, xfm_file = strat.get_node_from_resource_pool('functional_to_anat_linear_xfm') - #anat_ref, ref_file = strat.get_node_from_resource_pool('anatomical_brain') - #mfa, mfa_file = strat.get_node_from_resource_pool('mean_functional_in_anat') - hist_ = hist.clone('hist_snr_%d' % num_strat) - hist_.inputs.measure = 'snr' - - drop_percent = pe.Node(util.Function(input_names=['measure_file', - 'percent_'], - output_names=['modified_measure_file'], - function=drop_percent_), - name='dp_snr_%d' % num_strat) - drop_percent.inputs.percent_ = 99 - - montage_snr = create_montage('montage_snr_%d' % num_strat, - 'red_to_blue', 'snr') - - std_dev = pe.Node(util.Function(input_names=['mask_', 'func_'], + std_dev_imports = ['import os', 'import subprocess'] + std_dev = pe.Node(util.Function(input_names=['mask_', + 'func_'], output_names=['new_fname'], - function=gen_std_dev), + function=gen_std_dev, + imports=std_dev_imports), name='std_dev_%d' % num_strat) - + + workflow.connect(preproc, out_file, + std_dev, 'func_') + + workflow.connect(brain_mask, mask_file, + std_dev, 'mask_') + + std_dev_anat_imports = ['import os', + 'import subprocess'] std_dev_anat = pe.Node(util.Function(input_names=['func_', 'ref_', 'xfm_', 'interp_'], - output_names=['new_fname'], - function=gen_func_anat_xfm), - name='std_dev_anat_%d' % num_strat) - - snr = pe.Node(util.Function(input_names=['std_dev', 'mean_func_anat'],output_names=['new_fname'],function=gen_snr),name='snr_%d' % num_strat) - snr_val = pe.Node(util.Function(input_names=['measure_file'], - output_names=['snr_storefl'], - function=cal_snr_val),name='snr_val%d' % num_strat) + output_names=['new_fname'], + function=gen_func_anat_xfm, + imports=std_dev_anat_imports), + name='std_dev_anat_%d' % num_strat) - std_dev_anat.inputs.interp_ = 'trilinear' - - - workflow.connect(preproc, out_file, - std_dev, 'func_') - - workflow.connect(brain_mask, mask_file, - std_dev, 'mask_') - workflow.connect(std_dev, 'new_fname', std_dev_anat, 'func_') - + workflow.connect(func_to_anat_xfm, xfm_file, std_dev_anat, 'xfm_') - + workflow.connect(anat_ref, ref_file, std_dev_anat, 'ref_') - - workflow.connect(std_dev_anat, 'new_fname', - snr,'std_dev') - - workflow.connect(mfa, mfa_file, - snr, 'mean_func_anat') - + + snr_imports = ['import os', 'import subprocess'] + snr = pe.Node(util.Function(input_names=['std_dev', + 'mean_func_anat'], + output_names=['new_fname'], + function=gen_snr, + imports=snr_imports), + name='snr_%d' % num_strat) + + workflow.connect(std_dev_anat, 'new_fname', snr, 'std_dev') + workflow.connect(mfa, mfa_file, snr, 'mean_func_anat') + + snr_val_imports = ['import os', + 'import nibabel as nb', + 'import numpy.ma as ma'] + snr_val = pe.Node(util.Function(input_names=['measure_file'], + output_names=['snr_storefl'], + function=cal_snr_val, + imports=snr_val_imports), + name='snr_val%d' % num_strat) + std_dev_anat.inputs.interp_ = 'trilinear' + workflow.connect(snr, 'new_fname', - hist_, 'measure_file') - + snr_val, 'measure_file') + + hist_ = hist.clone('hist_snr_%d' % num_strat) + hist_.inputs.measure = 'snr' + workflow.connect(snr, 'new_fname', - drop_percent, 'measure_file') - + hist_, 'measure_file') + + drop_percent = pe.Node( + util.Function(input_names=['measure_file', + 'percent_'], + output_names=['modified_measure_file'], + function=drop_percent_), + name='dp_snr_%d' % num_strat) + drop_percent.inputs.percent_ = 99 + workflow.connect(snr, 'new_fname', - snr_val, 'measure_file') + drop_percent, 'measure_file') + + montage_snr = create_montage('montage_snr_%d' % num_strat, + 'red_to_blue', 'snr') workflow.connect(drop_percent, 'modified_measure_file', montage_snr, 'inputspec.overlay') @@ -4415,14 +4424,20 @@ def calc_avg(output_name, strat, num_strat, map_node=False): # make motion parameters plot try: - mov_param, out_file = strat.get_node_from_resource_pool('movement_parameters') + + mov_plot_imports = ['import os', 'import math', + 'import numpy as np', + 'from matplotlib import pyplot as plt'] mov_plot = pe.Node(util.Function(input_names=['motion_parameters'], output_names=['translation_plot', 'rotation_plot'], - function=gen_motion_plt), - name='motion_plt_%d' % num_strat) + function=gen_motion_plt, + imports=mov_plot_imports), + name='motion_plt_%d' % num_strat) + workflow.connect(mov_param, out_file, mov_plot, 'motion_parameters') + strat.update_resource_pool({'qc___movement_trans_plot': (mov_plot, 'translation_plot'),'qc___movement_rot_plot': (mov_plot, 'rotation_plot')}) if not 6 in qc_plot_id: @@ -4438,19 +4453,23 @@ def calc_avg(output_name, strat, num_strat, map_node=False): # make FD plot and volumes removed if 'gen_motion_stats' in nodes: if 1 in c.runNuisance: - try: if c.fdCalc == 'Power': fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_power') else: fd, out_file = strat.get_node_from_resource_pool('frame_wise_displacement_jenkinson') + fd_plot_imports = ['import os', 'import numpy as np', + 'import matplotlib', + 'from matplotlib import pyplot'] + fd_plot = \ pe.Node(util.Function(input_names=['arr', 'measure', 'ex_vol'], output_names=['hist_path'], - function=gen_plot_png), + function=gen_plot_png, + imports=fd_plot_imports), name='fd_plot_%d' % num_strat) fd_plot.inputs.measure = 'FD' @@ -4479,12 +4498,14 @@ def calc_avg(output_name, strat, num_strat, map_node=False): anat_underlay, out_file = strat.get_node_from_resource_pool('anatomical_brain') skull, out_file_s = strat.get_node_from_resource_pool('anatomical_reorient') - montage_skull = create_montage('montage_skull_%d' % num_strat, - 'red', 'skull_vis') skull_edge = make_edge(wf_name='skull_edge_%d' % num_strat) workflow.connect(skull, out_file_s, skull_edge, 'inputspec.file_') + + montage_skull = create_montage('montage_skull_%d' % num_strat, + 'red', 'skull_vis') + workflow.connect(skull_edge, 'outputspec.new_fname', montage_skull, 'inputspec.overlay') workflow.connect(anat_underlay, out_file, @@ -4572,13 +4593,13 @@ def calc_avg(output_name, strat, num_strat, map_node=False): anat, out_file = strat.get_node_from_resource_pool('anatomical_brain') m_f_a, out_file_mfa = strat.get_node_from_resource_pool('mean_functional_in_anat') - montage_anat = create_montage('montage_anat_%d' % num_strat, - 'red', 't1_edge_on_mean_func_in_t1') - anat_edge = make_edge(wf_name='anat_edge_%d' % num_strat) workflow.connect(anat, out_file, anat_edge, 'inputspec.file_') + montage_anat = create_montage('montage_anat_%d' % num_strat, + 'red', 't1_edge_on_mean_func_in_t1') + workflow.connect(anat_edge, 'outputspec.new_fname', montage_anat, 'inputspec.overlay') @@ -4651,29 +4672,29 @@ def calc_avg(output_name, strat, num_strat, map_node=False): # QA pages function def QA_montages(measure, idx): - try: - - histogram = hist.clone('hist_%s_%d' % (measure, num_strat)) - histogram.inputs.measure = measure + overlay, out_file = strat.get_node_from_resource_pool(measure) drop_percent = pe.MapNode(util.Function(input_names=['measure_file', - 'percent_'], - output_names=['modified_measure_file'], function=drop_percent_), - name='dp_%s_%d' % (measure, num_strat), iterfield=['measure_file']) + 'percent_'], + output_names=['modified_measure_file'], + function=drop_percent_), + name='dp_%s_%d' % (measure, num_strat), + iterfield=['measure_file']) drop_percent.inputs.percent_ = 99.999 - overlay, out_file = strat.get_node_from_resource_pool(measure) + workflow.connect(overlay, out_file, + drop_percent, 'measure_file') montage = create_montage('montage_%s_%d' % (measure, num_strat),'cyan_to_yellow', measure) montage.inputs.inputspec.underlay = c.template_brain_only_for_func - workflow.connect(overlay, out_file, - drop_percent, 'measure_file') - workflow.connect(drop_percent, 'modified_measure_file', montage, 'inputspec.overlay') + histogram = hist.clone('hist_%s_%d' % (measure, num_strat)) + histogram.inputs.measure = measure + workflow.connect(overlay, out_file, histogram, 'measure_file') @@ -4687,7 +4708,8 @@ def QA_montages(measure, idx): qc_hist_id[idx] = '%s_hist' % measure except Exception as e: - print "[!] Creation of QA montages for %s has failed.\n" % measure + print "[!] Connection of QA montages workflow for %s " \ + "has failed.\n" % measure print "Error: %s" % e pass diff --git a/CPAC/qc/qc.py b/CPAC/qc/qc.py index bc03506847..e8de62257a 100644 --- a/CPAC/qc/qc.py +++ b/CPAC/qc/qc.py @@ -21,33 +21,44 @@ def create_montage(wf_name, cbar_name, png_name): 'sagittal_png', 'resampled_underlay', 'resampled_overlay']), - name='outputspec') + name='outputspec') + + resample_u_imports = ['from CPAC.qc.utils import make_resample_1mm'] resample_u = pe.Node(util.Function(input_names=['file_'], output_names=['new_fname'], - function=resample_1mm), + function=resample_1mm, + imports=resample_u_imports), name='resample_u') wf.connect(inputNode, 'underlay', resample_u, 'file_') resample_o = resample_u.clone('resample_o') wf.connect(inputNode, 'overlay', resample_o, 'file_') + montage_a_imports = ['import os', + 'from CPAC.qc.utils import make_montage_axial'] + montage_a = pe.Node(util.Function(input_names=['overlay', 'underlay', 'png_name', 'cbar_name'], output_names=['png_name'], - function=montage_axial), + function=montage_axial, + imports=montage_a_imports), name='montage_a') montage_a.inputs.cbar_name = cbar_name montage_a.inputs.png_name = png_name + '_a.png' + montage_s_imports = ['import os', + 'from CPAC.qc.utils import make_montage_sagittal'] + montage_s = pe.Node(util.Function(input_names=['overlay', 'underlay', 'png_name', 'cbar_name'], output_names=['png_name'], - function=montage_sagittal), + function=montage_sagittal, + imports=montage_s_imports), name='montage_s') montage_s.inputs.cbar_name = cbar_name montage_s.inputs.png_name = png_name + '_s.png' @@ -97,33 +108,56 @@ def create_montage_gm_wm_csf(wf_name, png_name): 'resampled_overlay_gm']), name='outputspec') + resample_u_imports = ['from CPAC.qc.utils import make_resample_1mm'] + resample_u = pe.Node(util.Function(input_names=['file_'], output_names=['new_fname'], - function=resample_1mm), - name='resample_u') + function=resample_1mm, + imports=resample_u_imports), + name='resample_u') resample_o_csf = resample_u.clone('resample_o_csf') resample_o_wm = resample_u.clone('resample_o_wm') resample_o_gm = resample_u.clone('resample_o_gm') + montage_a_imports = ['import os', + 'from CPAC.qc.utils import determine_start_and_end, get_spacing', + 'import numpy as np', + 'from mpl_toolkits.axes_grid1 import ImageGrid as ImageGrid1', + 'from mpl_toolkits.axes_grid import ImageGrid as ImageGrid', + 'import matplotlib.pyplot as plt', + 'import nibabel as nb', + 'import matplotlib.cm as cm'] + montage_a = pe.Node(util.Function(input_names=['overlay_csf', 'overlay_wm', 'overlay_gm', 'underlay', 'png_name'], output_names=['png_name'], - function=montage_gm_wm_csf_axial), - name='montage_a') + function=montage_gm_wm_csf_axial, + imports=montage_a_imports), + name='montage_a') montage_a.inputs.png_name = png_name + '_a.png' + montage_s_imports = ['import os', + 'from CPAC.qc.utils import determine_start_and_end, get_spacing', + 'import numpy as np', + 'from mpl_toolkits.axes_grid1 import ImageGrid as ImageGrid1', + 'from mpl_toolkits.axes_grid import ImageGrid as ImageGrid', + 'import matplotlib.pyplot as plt', + 'import matplotlib.cm as cm', + 'import nibabel as nb'] + montage_s = pe.Node(util.Function(input_names=['overlay_csf', 'overlay_wm', 'overlay_gm', 'underlay', 'png_name'], output_names=['png_name'], - function=montage_gm_wm_csf_sagittal), - name='montage_s') + function=montage_gm_wm_csf_sagittal, + imports=montage_s_imports), + name='montage_s') montage_s.inputs.png_name = png_name + '_s.png' wf.connect(inputNode, 'underlay', diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index 55b090e769..a6353181a2 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -75,7 +75,6 @@ def append_to_files_in_dict_way(list_files, file_): f_2.close - def first_pass_organizing_files(qc_path): @@ -146,18 +145,13 @@ def first_pass_organizing_files(qc_path): flag_ = 0 for key_ in strat_dict.keys(): - - print str_, ' ~~ ', key_ if str_ in key_: append_to_files_in_dict_way(strat_dict[key_], file_) flag_ = 1 if flag_ == 1: - os.system('rm -f %s' % file_) - else: - strat_dict[str_] = [file_] @@ -206,12 +200,10 @@ def second_pass_organizing_files(qc_path): str_ = str_.replace('____', '_') str_ = str_.replace('___', '_') str_ = str_.replace('__', '_') - print '~~>', str_ fwhm_val_ = '' - #organize all derivatives excluding alff falff + # organize all derivatives excluding alff falff if '_bandpass_freqs_' in str_: - if not str_ in strat_dict: strat_dict[str_] = [file_] else: @@ -219,10 +211,8 @@ def second_pass_organizing_files(qc_path): raise print strat_dict - #organize alff falff + # organize alff falff elif ('_hp_' in str_) and ('_lp_' in str_): - - key_ = '' key_1 = '' hp_lp_ = '' @@ -236,23 +226,16 @@ def second_pass_organizing_files(qc_path): key_, hp_lp_ = str_.split('_hp_') hp_lp_ = '_hp_' + hp_lp_ - flag_ = 0 for key in strat_dict.keys(): - - print key, '~~', key_, '~~', key_1 if (key_ in key) and (key_1 in key): append_to_files_in_dict_way(strat_dict[key], file_) str_ = strat_dict[key][0].replace('.txt', '') new_fname = str_ + hp_lp_ + '.txt' - - print '~~>', new_fname os.system('mv %s %s' %(strat_dict[key][0], new_fname)) del strat_dict[key] flag_ = 1 - - if flag_ == 1: os.system('rm -f %s' % file_) @@ -377,7 +360,6 @@ def grp_pngs_by_id(pngs_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_ return dict(dict_a), dict(dict_s), dict(dict_hist), dict(dict_plot), list(all_ids) - def add_head(f_html_, f_html_0, f_html_1): """ @@ -558,6 +540,7 @@ def feed_line_nav(id_, print >>f_html_0, "
  • %s
  • " % (f_html_1.name, \ anchor, image_readable) + def feed_line_body(image_name, anchor, image, f_html_1): """ Write to html file that has to contain images @@ -858,7 +841,6 @@ def feed_lines_html(id_, id_a = '_'.join([id_a, str(idx), 'a']) id_s = '_'.join([id_s, str(idx), 's']) id_h = '_'.join([id_h, str(idx), 'h' ]) - if idx == 0: if image_name_a_nav == 'skullstrip_vis': @@ -998,8 +980,6 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): from CPAC.qc.utils import grp_pngs_by_id, add_head, add_tail, \ feed_lines_html - - f_ = open(file_, 'r') pngs_ = [line.rstrip('\r\n') for line in f_.readlines()] f_.close() @@ -1016,7 +996,6 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): f_html_0 = open(html_f_name_0, 'wb') f_html_1 = open(html_f_name_1, 'wb') - dict_a, dict_s, dict_hist, dict_plot, all_ids = grp_pngs_by_id(pngs_, qc_montage_id_a, \ qc_montage_id_s, qc_plot_id, qc_hist_id) @@ -1025,7 +1004,6 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): add_head(f_html_, f_html_0, f_html_1) - for id_ in sorted(all_ids): feed_lines_html(id_, dict_a, @@ -1039,7 +1017,6 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): f_html_0, f_html_1) - add_tail(f_html_, f_html_0, f_html_1) f_html_.close() @@ -1096,8 +1073,6 @@ def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist #actually make the html page for the file_ make_page(os.path.join(qc_path, file_), qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) - - def generateQCPages(qc_path,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): @@ -1300,22 +1275,18 @@ def gen_func_anat_xfm(func_, ref_, xfm_, interp_): path to the transformed scan """ - import os - import commands - new_fname = os.path.join(os.getcwd(), 'std_dev_anat.nii.gz') - cmd = 'applywarp --ref=%s --in=%s --out=%s --premat=%s ' \ - '--interp=%s' % (ref_, func_, new_fname, xfm_, interp_) - print cmd + cmd = ['applywarp', '--ref={0}'.format(ref_), '--in={0}'.format(func_), + '--out={0}'.format(new_fname), '--premat={0}'.format(xfm_), + '--interp={0}'.format(interp_)] - print commands.getoutput(cmd) + retcode = subprocess.check_output(cmd) return new_fname def gen_snr(std_dev, mean_func_anat): - """ Generate SNR file @@ -1335,20 +1306,18 @@ def gen_snr(std_dev, mean_func_anat): new_fname : string path to the snr file """ - import os - import commands new_fname = os.path.join(os.getcwd(), 'snr.nii.gz') - cmd = '3dcalc -a %s -b %s -expr \"b/a\" -prefix %s' % (std_dev, mean_func_anat, new_fname) + cmd = ['3dcalc', '-a', '{0}'.format(std_dev), '-b', + '{0}'.format(mean_func_anat), '-expr', '"b/a"', '-prefix', + '{0}'.format(new_fname)] - print cmd - print commands.getoutput(cmd) + retcode = subprocess.check_output(cmd) return new_fname -### def cal_snr_val(measure_file): """ @@ -1371,10 +1340,6 @@ def cal_snr_val(measure_file): """ - import numpy.ma as ma - import nibabel as nb - import os - data = nb.load(measure_file).get_data() data_flat = data.flatten() data_no0 = data_flat[data_flat > 0] @@ -1382,19 +1347,12 @@ def cal_snr_val(measure_file): avg_snr_file = os.path.join(os.getcwd(), 'average_snr_file.txt') f = open(avg_snr_file, 'w') - f.write(str(snr_val) + '\n') - - #f.write(str(measure_file) + '\n') - #f.write(str(avg_snr_file) + '\n') - - f.close() + with open(avg_snr_file, 'wt') as f: + f.write(str(snr_val) + '\n') return avg_snr_file - - - def gen_std_dev(mask_, func_): """ @@ -1416,15 +1374,12 @@ def gen_std_dev(mask_, func_): path to standard deviation file """ - import os - import commands - new_fname = os.path.join(os.getcwd(), 'std_dev.nii.gz') + cmd = ["3dTstat", "-stdev", "-mask", "{0}".format(mask_), "-prefix", + "{0}".format(new_fname), "{0}".format(func_)] - cmd = "3dTstat -stdev -mask %s -prefix %s %s" % (mask_, new_fname, func_) - - print commands.getoutput(cmd) + retcode = subprocess.check_output(cmd) return new_fname @@ -1486,12 +1441,6 @@ def gen_plot_png(arr, measure, ex_vol=None): path to the generated plot png """ - import os - import numpy as np - import matplotlib - from matplotlib import pyplot - import matplotlib.cm as cm - matplotlib.rcParams.update({'font.size': 8}) arr = np.loadtxt(arr) @@ -1554,15 +1503,6 @@ def gen_motion_plt(motion_parameters): """ - import matplotlib - import commands -# matplotlib.use('Agg') - import matplotlib.cm as cm - import numpy as np - from matplotlib import pyplot as plt - import math - import os - png_name1 = 'motion_trans_plot.png' png_name2 = 'motion_rot_plot.png' data = np.loadtxt(motion_parameters) @@ -1588,9 +1528,7 @@ def gen_motion_plt(motion_parameters): plt.close() for i in range(3, 6): - for j in range(len(data_t[i])): - data_t[i][j] = math.degrees(data_t[i][j]) plt.gca().set_color_cycle(['red', 'green', 'blue']) @@ -1601,7 +1539,6 @@ def gen_motion_plt(motion_parameters): plt.ylabel('Rotation (degrees)') plt.xlabel('Volume') - plt.savefig(os.path.join(os.getcwd(), png_name2)) plt.close() @@ -1841,11 +1778,8 @@ def get_spacing(across, down, dimension): """ -# across = 6 -# down = 3 space = 10 - prod = (across*down*space) if prod > dimension: @@ -1855,7 +1789,6 @@ def get_spacing(across, down, dimension): while(across*down*space) < dimension: space += 1 - return space @@ -1901,7 +1834,6 @@ def determine_start_and_end(data, direction, percent): start = None end = None - if 'axial' in direction: while(zz2 > 0): @@ -1945,7 +1877,6 @@ def determine_start_and_end(data, direction, percent): return start, end - def montage_axial(overlay, underlay, png_name, cbar_name): """ @@ -1973,8 +1904,7 @@ def montage_axial(overlay, underlay, png_name, cbar_name): png_name : Path to generated PNG """ - import os - from CPAC.qc.utils import make_montage_axial + pngs = None if isinstance(overlay, list): pngs = [] @@ -1991,8 +1921,6 @@ def montage_axial(overlay, underlay, png_name, cbar_name): return png_name - - def make_montage_axial(overlay, underlay, png_name, cbar_name): """ @@ -2060,30 +1988,22 @@ def make_montage_axial(overlay, underlay, png_name, cbar_name): zz = z1 for i in range(6*3): - if zz >= z2: break - im = grid[i].imshow(np.rot90(Y[:, :, zz]), cmap=cm.Greys_r) zz += spacing x, y, z = X.shape X[X == 0.0] = np.nan max_ = np.nanmax(np.abs(X.flatten())) - print '~~', max_ - zz = z1 im = None - print '~~~', z1, ' ', z2 for i in range(6*3): - - if zz >= z2: break if cbar_name is 'red_to_blue': - - im = grid[i].imshow(np.rot90(X[:, :, zz]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) ### + im = grid[i].imshow(np.rot90(X[:, :, zz]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) elif cbar_name is 'green': im = grid[i].imshow(np.rot90(X[:, :, zz]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) else: @@ -2098,11 +2018,9 @@ def make_montage_axial(overlay, underlay, png_name, cbar_name): if 'snr' in png_name: cbar.ax.set_yticks(drange(0, max_)) - elif ('reho' in png_name) or ('vmhc' in png_name) or ('sca_' in png_name) or ('alff' in png_name) or ('centrality' in png_name) or ('temporal_regression_sca' in png_name) or ('temporal_dual_regression' in png_name): + elif ('reho' in png_name) or ('vmhc' in png_name) or ('sca_' in png_name) or ('alff' in png_name) or ('centrality' in png_name) or ('temporal_regression_sca' in png_name) or ('temporal_dual_regression' in png_name): cbar.ax.set_yticks(drange(-max_, max_)) - -# plt.show() plt.axis("off") png_name = os.path.join(os.getcwd(), png_name) plt.savefig(png_name, dpi=200, bbox_inches='tight') @@ -2114,10 +2032,9 @@ def make_montage_axial(overlay, underlay, png_name, cbar_name): def montage_sagittal(overlay, underlay, png_name, cbar_name): - """ Draws Montage using overlay on Anatomical brain in Sagittal Direction - calls make_montage_sagittal + calls make_montage_sagittal Parameters ---------- @@ -2140,18 +2057,15 @@ def montage_sagittal(overlay, underlay, png_name, cbar_name): png_name : Path to generated PNG """ - import os - from CPAC.qc.utils import make_montage_sagittal + pngs = None + if isinstance(overlay, list): pngs = [] - for ov in overlay: fname = os.path.basename(os.path.splitext(os.path.splitext(ov)[0])[0]) pngs.append(make_montage_sagittal(ov, underlay, fname + '_' + png_name, cbar_name)) - else: - pngs = make_montage_sagittal(overlay, underlay, png_name, cbar_name) png_name = pngs @@ -2159,7 +2073,6 @@ def montage_sagittal(overlay, underlay, png_name, cbar_name): def make_montage_sagittal(overlay, underlay, png_name, cbar_name): - """ Draws Montage using overlay on Anatomical brain in Sagittal Direction @@ -2186,19 +2099,15 @@ def make_montage_sagittal(overlay, underlay, png_name, cbar_name): """ from CPAC.qc.utils import determine_start_and_end, get_spacing import matplotlib - import commands -# matplotlib.use('Agg') import os import numpy as np matplotlib.rcParams.update({'font.size': 5}) - ### try: from mpl_toolkits.axes_grid1 import ImageGrid except: from mpl_toolkits.axes_grid import ImageGrid import matplotlib.cm as cm import matplotlib.pyplot as plt - import matplotlib.colors as col import nibabel as nb Y = nb.load(underlay).get_data() @@ -2206,8 +2115,7 @@ def make_montage_sagittal(overlay, underlay, png_name, cbar_name): X = X.astype(np.float32) Y = Y.astype(np.float32) - - if 'skull_vis' in png_name: + if 'skull_vis' in png_name: X[X < 20.0] = 0.0 if 'skull_vis' in png_name or 't1_edge_on_mean_func_in_t1' in png_name or 'MNI_edge_on_mean_func_mni' in png_name: max_ = np.nanmax(np.abs(X.flatten())) @@ -2227,10 +2135,8 @@ def make_montage_sagittal(overlay, underlay, png_name, cbar_name): xx = x1 for i in range(6*3): - if xx >= x2: break - im = grid[i].imshow(np.rot90(Y[xx, :, :]), cmap=cm.Greys_r) grid[i].get_xaxis().set_visible(False) grid[i].get_yaxis().set_visible(False) @@ -2239,17 +2145,13 @@ def make_montage_sagittal(overlay, underlay, png_name, cbar_name): x, y, z = X.shape X[X == 0.0] = np.nan max_ = np.nanmax(np.abs(X.flatten())) - print '~~', max_ xx = x1 for i in range(6*3): - - if xx >= x2: break im = None if cbar_name is 'red_to_blue': - - im = grid[i].imshow(np.rot90(X[xx, :, :]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) ### + im = grid[i].imshow(np.rot90(X[xx, :, :]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) elif cbar_name is 'green': im = grid[i].imshow(np.rot90(X[xx, :, :]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) else: @@ -2260,11 +2162,9 @@ def make_montage_sagittal(overlay, underlay, png_name, cbar_name): if 'snr' in png_name: cbar.ax.set_yticks(drange(0, max_)) - elif ('reho' in png_name) or ('vmhc' in png_name) or ('sca_' in png_name) or ('alff' in png_name) or ('centrality' in png_name) or ('temporal_regression_sca' in png_name) or ('temporal_dual_regression' in png_name): + elif ('reho' in png_name) or ('vmhc' in png_name) or ('sca_' in png_name) or ('alff' in png_name) or ('centrality' in png_name) or ('temporal_regression_sca' in png_name) or ('temporal_dual_regression' in png_name): cbar.ax.set_yticks(drange(-max_, max_)) - -# plt.show() plt.axis("off") png_name = os.path.join(os.getcwd(), png_name) plt.savefig(png_name, dpi=200, bbox_inches='tight') @@ -2304,22 +2204,6 @@ def montage_gm_wm_csf_axial(overlay_csf, overlay_wm, overlay_gm, underlay, png_n """ - import os - import matplotlib - import commands -# matplotlib.use('Agg') - from CPAC.qc.utils import determine_start_and_end, get_spacing - import numpy as np - ### - try: - from mpl_toolkits.axes_grid1 import ImageGrid - except: - from mpl_toolkits.axes_grid import ImageGrid - import matplotlib.pyplot as plt - import matplotlib.colors as col - import nibabel as nb - import matplotlib.cm as cm - Y = nb.load(underlay).get_data() z1, z2 = determine_start_and_end(Y, 'axial', 0.0001) spacing = get_spacing(6, 3, z2 - z1) @@ -2340,14 +2224,18 @@ def montage_gm_wm_csf_axial(overlay_csf, overlay_wm, overlay_gm, underlay, png_n x, y, z = Y.shape fig = plt.figure(1) max_ = np.max(np.abs(Y)) - grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, aspect=True, cbar_mode="None", direction="row") + + try: + grid = ImageGrid1(fig, 111, nrows_ncols=(3, 6), share_all=True, + aspect=True, cbar_mode="None", direction="row") + except: + grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, + aspect=True, cbar_mode="None", direction="row") zz = z1 for i in range(6*3): - if zz >= z2: break - im = grid[i].imshow(np.rot90(Y[:, :, zz]), cmap=cm.Greys_r) zz += spacing @@ -2355,17 +2243,12 @@ def montage_gm_wm_csf_axial(overlay_csf, overlay_wm, overlay_gm, underlay, png_n X_csf[X_csf == 0.0] = np.nan X_wm[X_wm == 0.0] = np.nan X_gm[X_gm == 0.0] = np.nan - print '~~', max_ - zz = z1 im = None for i in range(6*3): - - if zz >= z2: break - im = grid[i].imshow(np.rot90(X_csf[:, :, zz]), cmap=cm.get_cmap('green'), alpha=0.82, vmin=0, vmax=max_csf) ### im = grid[i].imshow(np.rot90(X_wm[:, :, zz]), cmap=cm.get_cmap('blue'), alpha=0.82, vmin=0, vmax=max_wm) im = grid[i].imshow(np.rot90(X_gm[:, :, zz]), cmap=cm.get_cmap('red'), alpha=0.82, vmin=0, vmax=max_gm) @@ -2376,7 +2259,6 @@ def montage_gm_wm_csf_axial(overlay_csf, overlay_wm, overlay_gm, underlay, png_n cbar = grid.cbar_axes[0].colorbar(im) -# plt.show() plt.axis("off") png_name = os.path.join(os.getcwd(), png_name) plt.savefig(png_name, dpi=200, bbox_inches='tight') @@ -2415,22 +2297,6 @@ def montage_gm_wm_csf_sagittal(overlay_csf, overlay_wm, overlay_gm, underlay, pn """ - import os - import matplotlib - import commands -# matplotlib.use('Agg') - from CPAC.qc.utils import determine_start_and_end, get_spacing - import numpy as np - ### - try: - from mpl_toolkits.axes_grid1 import ImageGrid - except: - from mpl_toolkits.axes_grid import ImageGrid - import matplotlib.pyplot as plt - import matplotlib.colors as col - import matplotlib.cm as cm - import nibabel as nb - Y = nb.load(underlay).get_data() x1, x2 = determine_start_and_end(Y, 'sagittal', 0.0001) spacing = get_spacing(6, 3, x2 - x1) @@ -2451,14 +2317,18 @@ def montage_gm_wm_csf_sagittal(overlay_csf, overlay_wm, overlay_gm, underlay, pn x, y, z = Y.shape fig = plt.figure(1) max_ = np.max(np.abs(Y)) - grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, aspect=True, cbar_mode="None", direction="row") + + try: + grid = ImageGrid1(fig, 111, nrows_ncols=(3, 6), share_all=True, + aspect=True, cbar_mode="None", direction="row") + except: + grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, + aspect=True, cbar_mode="None", direction="row") zz = x1 for i in range(6*3): - if zz >= x2: break - im = grid[i].imshow(np.rot90(Y[zz, :, :]), cmap=cm.Greys_r) zz += spacing @@ -2466,14 +2336,10 @@ def montage_gm_wm_csf_sagittal(overlay_csf, overlay_wm, overlay_gm, underlay, pn X_csf[X_csf == 0.0] = np.nan X_wm[X_wm == 0.0] = np.nan X_gm[X_gm == 0.0] = np.nan - print '~~', max_ - zz = x1 im = None for i in range(6*3): - - if zz >= x2: break @@ -2487,7 +2353,6 @@ def montage_gm_wm_csf_sagittal(overlay_csf, overlay_wm, overlay_gm, underlay, pn cbar = grid.cbar_axes[0].colorbar(im) -# plt.show() plt.axis("off") png_name = os.path.join(os.getcwd(), png_name) plt.savefig(png_name, dpi=200, bbox_inches='tight') @@ -2518,9 +2383,6 @@ def register_pallete(file_, cbar_name): """ - import matplotlib - import commands -# matplotlib.use('Agg') import matplotlib.colors as col import matplotlib.cm as cm f = open(file_, 'r') @@ -2556,16 +2418,12 @@ def resample_1mm(file_): """ - from CPAC.qc.utils import make_resample_1mm - new_fname = None + if isinstance(file_, list): new_fname = [] - for f in file_: - new_fname.append(make_resample_1mm(f)) - else: new_fname = make_resample_1mm(file_) From c16caaeb291e2edc6f97f9bbf87fa4d141cab090 Mon Sep 17 00:00:00 2001 From: anibalsolon Date: Tue, 1 May 2018 16:43:00 -0400 Subject: [PATCH 70/75] fix 3dcalc command --- CPAC/qc/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index a6353181a2..9d5957972c 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -1310,7 +1310,7 @@ def gen_snr(std_dev, mean_func_anat): new_fname = os.path.join(os.getcwd(), 'snr.nii.gz') cmd = ['3dcalc', '-a', '{0}'.format(std_dev), '-b', - '{0}'.format(mean_func_anat), '-expr', '"b/a"', '-prefix', + '{0}'.format(mean_func_anat), '-expr', 'b/a', '-prefix', '{0}'.format(new_fname)] retcode = subprocess.check_output(cmd) From 66a204fedcd15ff221c6733d9b204ec48fac4ccc Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Tue, 1 May 2018 18:37:22 -0400 Subject: [PATCH 71/75] Minor change. --- CPAC/qc/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index a6353181a2..6e1dd6c854 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -2268,7 +2268,6 @@ def montage_gm_wm_csf_axial(overlay_csf, overlay_wm, overlay_gm, underlay, png_n def montage_gm_wm_csf_sagittal(overlay_csf, overlay_wm, overlay_gm, underlay, png_name): - """ Draws Montage using GM WM and CSF overlays on Anatomical brain in Sagittal Direction From 01f2418e0079e0247926695944a8aff0154ff9ed Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Thu, 10 May 2018 23:40:43 -0400 Subject: [PATCH 72/75] Improved 3dedge3 error message and cleaned some code. --- CPAC/GUI/interface/pages/functional_tab.py | 2 +- .../GUI/interface/utils/modelconfig_window.py | 3 +- CPAC/pipeline/cpac_pipeline.py | 95 +++++-------------- CPAC/qc/utils.py | 20 ++-- 4 files changed, 32 insertions(+), 88 deletions(-) diff --git a/CPAC/GUI/interface/pages/functional_tab.py b/CPAC/GUI/interface/pages/functional_tab.py index cf631097be..e64c0b1c52 100644 --- a/CPAC/GUI/interface/pages/functional_tab.py +++ b/CPAC/GUI/interface/pages/functional_tab.py @@ -46,7 +46,7 @@ def __init__(self, parent, counter =0): comment="Interpolate voxel time courses so they are " "sampled at the same time points.", values=["On", "Off", "On/Off"], - wkf_switch = True) + wkf_switch=True) self.page.add(label="TR (in seconds) ", control=control.TEXT_BOX, diff --git a/CPAC/GUI/interface/utils/modelconfig_window.py b/CPAC/GUI/interface/utils/modelconfig_window.py index 67760d2e0e..1bb5f926c8 100644 --- a/CPAC/GUI/interface/utils/modelconfig_window.py +++ b/CPAC/GUI/interface/utils/modelconfig_window.py @@ -468,7 +468,8 @@ def read_phenotypic(self, pheno_file, ev_selections): # Read in the phenotypic CSV file into a dictionary named pheno_dict # while preserving the header fields as they correspond to the data - p_reader = csv.DictReader(open(os.path.abspath(ph), 'rU'), skipinitialspace=True) + p_reader = csv.DictReader(open(os.path.abspath(ph), 'rU'), + skipinitialspace=True) # dictionary to store the data in a format Patsy can use # i.e. a dictionary where each header is a key, and the value is a diff --git a/CPAC/pipeline/cpac_pipeline.py b/CPAC/pipeline/cpac_pipeline.py index 1e2c179e7b..826de6020e 100644 --- a/CPAC/pipeline/cpac_pipeline.py +++ b/CPAC/pipeline/cpac_pipeline.py @@ -60,8 +60,6 @@ get_cent_zscore from CPAC.utils.datasource import * from CPAC.utils import Configuration, create_all_qc - -# TODO - QA pages - re-introduce these from CPAC.qc.qc import create_montage, create_montage_gm_wm_csf from CPAC.qc.utils import register_pallete, make_edge, drop_percent_, \ gen_histogram, gen_plot_png, gen_motion_plt, \ @@ -1333,17 +1331,17 @@ def getNodeList(strategy): epi_distcorr.get_node('dwell_asym_ratio_input').iterables = ('dwell_asym_ratio',c.fmap_distcorr_dwell_asym_ratio) try: - node,out_file = strat.get_leaf_properties() - workflow.connect(node,out_file,epi_distcorr,'inputspec.func_file') + node, out_file = strat.get_leaf_properties() + workflow.connect(node, out_file, epi_distcorr,'inputspec.func_file') - node,out_file = strat.get_node_from_resource_pool('anatomical_reorient') - workflow.connect(node,out_file,epi_distcorr,'inputspec.anat_file') + node, out_file = strat.get_node_from_resource_pool('anatomical_reorient') + workflow.connect(node, out_file, epi_distcorr,'inputspec.anat_file') node, out_file = strat.get_node_from_resource_pool('fmap_phase_diff') workflow.connect(node, out_file, epi_distcorr, 'inputspec.fmap_pha') - node,out_file = strat.get_node_from_resource_pool('fmap_magnitude') - workflow.connect(node,out_file,epi_distcorr, 'inputspec.fmap_mag') + node, out_file = strat.get_node_from_resource_pool('fmap_magnitude') + workflow.connect(node, out_file, epi_distcorr, 'inputspec.fmap_mag') except: logConnectionError('EPI_DistCorr Workflow', num_strat,strat.get_resource_pool(), '0004') @@ -4317,7 +4315,11 @@ def calc_avg(output_name, strat, num_strat, map_node=False): register_pallete(os.path.realpath( os.path.join(CPAC.__path__[0], 'qc', 'cyan_to_yellow.py')), 'cyan_to_yellow') - hist = pe.Node(util.Function(input_names=['measure_file','measure'],output_names = ['hist_path'],function = gen_histogram),name = 'histogram') + hist = pe.Node(util.Function(input_names=['measure_file', + 'measure'], + output_names=['hist_path'], + function=gen_histogram), + name='histogram') for strat in strat_list: @@ -4473,7 +4475,7 @@ def calc_avg(output_name, strat, num_strat, map_node=False): name='fd_plot_%d' % num_strat) fd_plot.inputs.measure = 'FD' - workflow.connect(fd, out_file,fd_plot, 'arr') + workflow.connect(fd, out_file, fd_plot, 'arr') if "De-Spiking" in c.runMotionSpike: excluded, out_file_ex = strat.get_node_from_resource_pool('despiking_frames_excluded') @@ -5245,7 +5247,7 @@ def is_number(s): # Add status callback function that writes in callback log if nipype.__version__ not in ('0.13.1', '0.14.0'): err_msg = "This version of nipype may not be compatible with " \ - "CPAC v%s, please install version 0.14.0\n" \ + "CPAC v%s, please install version 0.13.1\n" \ % (CPAC.__version__) logger.error(err_msg) else: @@ -5266,78 +5268,25 @@ def is_number(s): pickle.dump(subject_info, subject_info_pickle) subject_info_pickle.close() - ''' - # Actually run the pipeline now - try: - - workflow.run(plugin='MultiProc', plugin_args={'n_procs': c.numCoresPerSubject}) - - except: - - crashString = "\n\n" + "ERROR: CPAC run stopped prematurely with an error - see above.\n" + ("pipeline configuration- %s \n" % c.pipelineName) + \ - ("subject workflow- %s \n\n" % wfname) + ("Elapsed run time before crash (minutes): %s \n\n" % ((time.time() - pipeline_start_time)/60)) + \ - ("Timing information saved in %s/cpac_timing_%s_%s.txt \n" % (c.outputDirectory, c.pipelineName, pipeline_starttime_string)) + \ - ("System time of start: %s \n" % pipeline_start_datetime) + ("System time of crash: %s" % strftime("%Y-%m-%d %H:%M:%S")) + "\n\n" - - logger.info(crashString) - - print >>timing, "ERROR: CPAC run stopped prematurely with an error." - print >>timing, "Pipeline configuration: %s" % c.pipelineName - print >>timing, "Subject workflow: %s" % wfname - print >>timing, "\n" + "Elapsed run time before crash (minutes): ", ((time.time() - pipeline_start_time)/60) - print >>timing, "System time of crash: ", strftime("%Y-%m-%d %H:%M:%S") - print >>timing, "\n\n" - - timing.close() - - raise Exception - ''' - - ''' - try: - - workflow.run(plugin='MultiProc', plugin_args={'n_procs': c.numCoresPerSubject}) - - except Exception as e: - - print "Error: CPAC Pipeline has failed." - print "" - print e - print type(e) - ###raise Exception - ''' - - # subject_dir = os.path.join(c.outputDirectory, 'pipeline_' + pipeline_id, subject_id) - # create_output_mean_csv(subject_dir) - for count, scanID in enumerate(pip_ids): for scan in scan_ids: create_log_node(None, None, count, scan).run() + if 1 in c.generateQualityControlImages: for pip_id in pip_ids: try: - pipeline_base = os.path.join(c.outputDirectory, 'pipeline_%s' % pip_id) - qc_output_folder = os.path.join(pipeline_base, subject_id, 'qc_files_here') - generateQCPages(qc_output_folder,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) - #create_all_qc.run(pipeline_base) + pipeline_base = os.path.join(c.outputDirectory, + 'pipeline_%s' % pip_id) + qc_output_folder = os.path.join(pipeline_base, subject_id, + 'qc_files_here') + generateQCPages(qc_output_folder, qc_montage_id_a, + qc_montage_id_s, qc_plot_id, qc_hist_id) except Exception as e: - print "Error: The QC function page generation is not running" - print "" + print "Error: The QC function page generation is not " \ + "running\n\n" print e print type(e) raise Exception - - - - # Generate the QC pages -- this function isn't even running, because there is noparameter for qc_montage_id_a/qc_montage_id_s/qc_plot_id,qc_hist_id - #two methods can be done here: - #i) group all the qc_montage_ids in the resource pool or - #add a loop for all the files in the qc output folder, generate the html pages using the same functions, but with different parameters - # generateQCPages(qc_output_folder, qc_montage_id_a, - # qc_montage_id_s, qc_plot_id, qc_hist_id) - # Automatically generate QC index page - #create_all_qc.run(pipeline_out_base) - # pipeline timing code starts here diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index e4b81e1463..1acb1b1062 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -978,7 +978,7 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): """ import os from CPAC.qc.utils import grp_pngs_by_id, add_head, add_tail, \ - feed_lines_html + feed_lines_html f_ = open(file_, 'r') pngs_ = [line.rstrip('\r\n') for line in f_.readlines()] @@ -999,9 +999,6 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): dict_a, dict_s, dict_hist, dict_plot, all_ids = grp_pngs_by_id(pngs_, qc_montage_id_a, \ qc_montage_id_s, qc_plot_id, qc_hist_id) - #for k, v in dict_plot.items(): - # print '_a~~~> ', k, v - add_head(f_html_, f_html_0, f_html_1) for id_ in sorted(all_ids): @@ -1025,8 +1022,6 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): - - """ Calls make page @@ -1061,20 +1056,18 @@ def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist """ import os from CPAC.qc.utils import make_page - - qc_files = os.listdir(qc_path) for file_ in qc_files: - if not (file_.endswith('.txt')): continue - #actually make the html page for the file_ - make_page(os.path.join(qc_path, file_), qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) + make_page(os.path.join(qc_path, file_), qc_montage_id_a, + qc_montage_id_s, qc_plot_id, qc_hist_id) -def generateQCPages(qc_path,qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): +def generateQCPages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, + qc_hist_id): """ parafile = open('QC_input_para.txt', 'w') @@ -1190,7 +1183,8 @@ def afni_edge(in_file): except Exception as e: err = "\n\n[!] Something went wrong with AFNI 3dedge3 while " \ "creating the an overlay for the QA pages.\n\nError details: " \ - "{0}\n\n".format(e) + "{0}\n\nAttempted command: {1}" \ + "\n\n".format(e, " ".join(cmd_string)) raise Exception(err) return out_file From 5384f203572a38688b10f777380d09e0a5b1ee49 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Fri, 11 May 2018 18:22:44 -0400 Subject: [PATCH 73/75] Some fixes to QC pages, the outputs CSV, and updated the logo. --- CPAC/GUI/resources/html/_static/cpac_logo.jpg | Bin 15280 -> 150168 bytes .../resources/html/_static/cpac_old_logo.jpg | Bin 0 -> 15280 bytes CPAC/GUI/resources/images/cpac_logo.jpg | Bin 15280 -> 150168 bytes CPAC/GUI/resources/images/cpac_new_logo.png | Bin 22945 -> 20156 bytes CPAC/__init__.py | 23 +-- CPAC/info.py | 5 +- CPAC/pipeline/cpac_pipeline.py | 102 +++--------- CPAC/qc/utils.py | 150 ++++-------------- CPAC/resources/cpac_outputs.csv | 2 +- scripts/cpac_rerun_after_preproc.py | 75 +++++++++ 10 files changed, 144 insertions(+), 213 deletions(-) create mode 100644 CPAC/GUI/resources/html/_static/cpac_old_logo.jpg mode change 100755 => 100644 CPAC/GUI/resources/images/cpac_logo.jpg mode change 100755 => 100644 CPAC/GUI/resources/images/cpac_new_logo.png create mode 100644 scripts/cpac_rerun_after_preproc.py diff --git a/CPAC/GUI/resources/html/_static/cpac_logo.jpg b/CPAC/GUI/resources/html/_static/cpac_logo.jpg index b190448562cb39f8019c022875e5e9f46492d566..fccb0571087a7b8dd51d03ff61d23f9205ce71af 100644 GIT binary patch literal 150168 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¬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¬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 Qvi?*CK@HAZBIL~f0N?AHs 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 a;Qs-y&Ihar3KA6n0000>f_html_, "" print >>f_html_, "" print >>f_html_, "QC" @@ -406,7 +395,7 @@ def add_head(f_html_, f_html_0, f_html_1): print >>f_html_0, "
    " print >>f_html_0, "

    " print >>f_html_0, "

    " - print >>f_html_0, "\"Logo\"/"%(p.resource_filename('CPAC',"GUI/resources/html/_static/cmi_logo.jpg")) + print >>f_html_0, "\"Logo\"/"%(p.resource_filename('CPAC', "GUI/resources/html/_static/cmi_logo.jpg")) print >>f_html_0, "

    " print >>f_html_0, "

    Table Of Contents

    " print >>f_html_0, "
      " @@ -419,8 +408,6 @@ def add_head(f_html_, f_html_0, f_html_1): def add_tail(f_html_, f_html_0, f_html_1): - - """ Write HTML Tail Tags to various html files @@ -617,13 +604,9 @@ def feed_line_body(image_name, anchor, image, f_html_1): if image_name == 'falff_smooth_hist': image_readable = 'Histogram of fractional Amplitude of Low-Frequency Fluctuation' + print >>f_html_1, "

      %s TOP

      " %(anchor, image_readable) - print >>f_html_1, "

      %s TOP

      " %(anchor, image_readable) ### - - ###data_uri = open(image, 'rb').read().encode('base64').replace('\n', '') - ###img_tag = '
      '.format(data_uri) - - img_tag = "
      %s" %(image, image_readable) ### + img_tag = "
      %s" %(image, image_readable) print >>f_html_1, img_tag @@ -693,9 +676,7 @@ def get_map_and_measure(png_a): measure_name = None map_name = None if '_fwhm_' in png_a: - measure_name = os.path.basename(os.path.dirname(os.path.dirname(png_a))) - else: measure_name = os.path.basename(os.path.dirname((png_a))) @@ -705,22 +686,17 @@ def get_map_and_measure(png_a): map_name = 'seed' if 'sca_roi' in png_a: - map_name = get_map_id(str_, 'ROI_number_') if 'temporal_regression_sca' in png_a: - map_name = get_map_id(str_, 'roi_') if 'temporal_dual_regression' in png_a: - map_name = get_map_id(str_, 'map_z_') if 'centrality' in png_a: - map_name = get_map_id(str_, 'centrality_') - return map_name, measure_name @@ -791,7 +767,6 @@ def feed_lines_html(id_, from CPAC.qc.utils import feed_line_body from CPAC.qc.utils import get_map_and_measure - #print 'id_ :', id_ if id_ in dict_a: dict_a[id_] = sorted(dict_a[id_]) @@ -817,7 +792,6 @@ def feed_lines_html(id_, if idxs > 1: map_name, measure_name =get_map_and_measure(png_a) - id_a = str(id_) id_s = str(id_) + '_s' id_h = str(id_) + '_' + str(id_) @@ -895,7 +869,6 @@ def feed_lines_html(id_, f_html_0, \ f_html_1) - feed_line_body( image_name_a, \ id_a, \ @@ -908,7 +881,6 @@ def feed_lines_html(id_, png_s, \ f_html_1) - if id_ in dict_hist.keys(): if idx == 0: @@ -980,17 +952,21 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): from CPAC.qc.utils import grp_pngs_by_id, add_head, add_tail, \ feed_lines_html - f_ = open(file_, 'r') - pngs_ = [line.rstrip('\r\n') for line in f_.readlines()] - f_.close() + with open(file_, 'r') as f: + pngs_ = [line.rstrip('\r\n') for line in f.readlines()] html_f_name = file_.replace('.txt', '') - - html_f_name = html_f_name.replace("'", "") ###fixed in front, don't have to now + html_f_name = html_f_name.replace("'", "") - html_f_name_0 = html_f_name + '_0.html' - html_f_name_1 = html_f_name + '_1.html' - html_f_name = html_f_name + '.html' + html_f_name_0 = html_f_name + '_navbar.html' + html_f_name_1 = html_f_name + '_page.html' + + # TODO: this is a temporary patch until the completed QC interface is + # TODO: implemented + # pop the combined (navbar + content) page back into the output directory + # and give it a more obvious name + html_f_name = "{0}.html".format(html_f_name.replace("qc_scan", "QC-interface_scan")) + html_f_name = html_f_name.replace("/qc_files_here", "") f_html_ = open(html_f_name, 'wb') f_html_0 = open(html_f_name_0, 'wb') @@ -1068,29 +1044,6 @@ def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist def generateQCPages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): - - """ - parafile = open('QC_input_para.txt', 'w') - parafile.write(qc_path) - parafile.write('qc_montage_id_a: ') - for key, value in qc_montage_id_a.iteritems() - parafile.write(key) - parafile.write(value) - parafile.write('qc_montage_id_s: ') - for key, value in qc_montage_id_s.iteritems() - parafile.write(key) - parafile.write(value) - parafile.write('qc_plot_id: ') - for key, value in qc_plot_id.iteritems() - parafile.write(key) - parafile.write(value) - parafile.write('qc_hist_id: ') - for key, value in qc_hist_id.iteritems() - parafile.write(key) - parafile.write(value) - parafile.close() - """ - """ Calls make_qc_page and organizes qc path files @@ -1123,58 +1076,31 @@ def generateQCPages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, None """ + import os - from CPAC.qc.utils import first_pass_organizing_files, second_pass_organizing_files + from CPAC.qc.utils import first_pass_organizing_files, \ + second_pass_organizing_files from CPAC.qc.utils import make_qc_pages - #os.system('rm -rf %s/*.html') - - #according to preprocessing strategy combines the files + # according to preprocessing strategy combines the files first_pass_organizing_files(qc_path) - #according to bandpass and hp_lp and smoothing iterables combines the files + + # according to bandpass and hp_lp and smoothing iterables combines the + # files second_pass_organizing_files(qc_path) - make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id) - - #generate pages from qc files -# text_file_list = os.listdir(qc_path) -# print text_file_list -# image_path_list = [] -# for text_file in text_file_list: -# if text_file[0] != '.': -# f = open(os.path.join(qc_path, text_file),'r') -# eof = 0 -# while (not eof): -# temp_line = f.readline() -# if temp_line == '': -# eof=1 #This is to check if reading the file is complete -# else: -# if temp_line[-2:] == "\n": -# temp_line = temp_line[:-1] -# image_path_list.append(temp_line) -# f.close() -# f = open(os.path.join(qc_path, "QC_index.html"),'w') -# header = """ -# QA Images -#

      """ -# footer = """

      -# """ - -# f.write(header) -# for image in image_path_list: -# image_html = "" -# f.write(image_html) - -# f.write(footer) -# f.close() -# print f + + make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, + qc_hist_id) def afni_edge(in_file): """Run AFNI 3dedge3 on the input file - temporary function until the interface issue in Nipype is sorted out.""" + in_file = os.path.abspath(in_file) + out_file = os.path.join(os.getcwd(), - "{0}_edge.nii.gz".format(os.path.basename(in_file))) + "{0}".format(os.path.basename(in_file).replace(".nii", "_edge.nii"))) cmd_string = ["3dedge3", "-input", in_file, "-prefix", out_file] @@ -1216,18 +1142,6 @@ def make_edge(wf_name='create_edge'): outputNode = pe.Node(util.IdentityInterface(fields=['new_fname']), name='outputspec') - ''' - try: - prepare = pe.Node(interface=afni.Edge3(), name='afni_3dedge3') - prepare.inputs.outputtype = "NIFTI_GZ" - prepare.inputs.out_file = "{0}_edge.nii.gz".format(wf_name) - prepare.inputs.terminal_output = "file" - except AttributeError: - raise Exception("\n\n[!] Could not find the 'Edge3' AFNI interface " - "in your Nipype install - double-check that you are " - "using the correct version.\n\n") - ''' - run_afni_edge_imports = ["import os", "import subprocess"] run_afni_edge = pe.Node(util.Function(input_names=['in_file'], diff --git a/CPAC/resources/cpac_outputs.csv b/CPAC/resources/cpac_outputs.csv index 0eb1c07e8c..a9bcb7b6d4 100644 --- a/CPAC/resources/cpac_outputs.csv +++ b/CPAC/resources/cpac_outputs.csv @@ -1 +1 @@ -Resource,Space,Values,Multiple outputs,Functional timeseries,Derivative,Warp to template,Calculate z-scores,Calculate averages,Optional outputs: Extra functionals,Optional outputs: Native space,Optional outputs: Raw scores,Optional outputs: Non-smoothed,Optional outputs: Debugging outputs,Override optional alff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, alff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, alff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, alff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, alff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, alff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, anatomical_brain,anatomical,raw,,,,,,,,,,,, anatomical_csf_mask,anatomical,binary,,,,,,,,,,,, anatomical_gm_mask,anatomical,binary,,,,,,,,,,,, anatomical_reorient,anatomical,raw,,,,,,,,,,,, anatomical_to_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_to_standard,template,raw,,,,,,,,,,,, anatomical_to_symmetric_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_wm_mask,anatomical,binary,,,,,,,,,,,, ants_affine_xfm,,transform,,,,,,,,,,,, ants_initial_xfm,,transform,,,,,,,,,,,, ants_rigid_xfm,,transform,,,,,,,,,,,, ants_symmetric_affine_xfm,,transform,,,,,,,,,,,, ants_symmetric_initial_xfm,,transform,,,,,,,,,,,, ants_symmetric_rigid_xfm,,transform,,,,,,,,,,,, centrality_outputs,template,raw,yes,,yes,,yes,,,,yes,yes,, centrality_outputs_smooth,template,raw,yes,,yes,,,,,,yes,,, centrality_outputs_zstd,template,z-score,yes,,yes,,,,,,,yes,, centrality_outputs_zstd_smooth,template,z-score,yes,,yes,,,,,,,,, coordinate_transformation,,,,,,,,,,,,,yes, despiked_fieldmap,,,,,,,,,,,,,yes, dr_tempreg_maps_files,functional,GLM betas,yes,,yes,yes,,yes,,yes,,yes,, dr_tempreg_maps_files_smooth,functional,GLM betas,yes,,yes,,,yes,,yes,,,, dr_tempreg_maps_files_to_standard,template,GLM betas,yes,,yes,,,yes,,,,yes,, dr_tempreg_maps_files_to_standard_smooth,template,GLM betas,yes,,yes,,,yes,,,,,, dr_tempreg_maps_zstat_files,functional,z-stat,yes,,yes,yes,,,,yes,,yes,yes, dr_tempreg_maps_zstat_files_smooth,functional,z-stat,yes,,yes,,,,,yes,,,yes, dr_tempreg_maps_zstat_files_to_standard,template,z-stat,yes,,yes,,,,,,,yes,yes, dr_tempreg_maps_zstat_files_to_standard_smooth,template,z-stat,yes,,yes,,,,,,,,yes, falff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, falff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, falff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, falff_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, falff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, falff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, fmap_magnitude,,,,,,,,,,,,,yes, fmap_phase_diff,,,,,,,,,,,,,yes, frame_wise_displacement_jenkinson,,,,,,,,,,,,,, frame_wise_displacement_power,,,,,,,,,,,,,, functional_brain_mask,functional,binary,,,,,,,,,,,, functional_brain_mask_to_standard,template,binary,,,,,,,,,,,, functional_freq_filtered,functional,raw,,yes,,,,,,,,,, functional_nuisance_regressors,,,,,,,,,,,,,, functional_nuisance_residuals,functional,,,yes,,,,,yes,,,,yes, functional_preprocessed,functional,raw,,yes,,,,,yes,,,,, functional_preprocessed_mask,functional,binary,,,,,,,yes,,,,, functional_to_anat_linear_xfm,,transform,,,,,,,,,,,, functional_to_standard,template,raw,,yes,,,,,,,,,, max_displacement,,,,,,,,,,,,,yes, mean_functional,functional,raw,,,,,,,yes,,,,, mean_functional_in_anat,anatomical,raw,,,,,,,yes,,,,, mean_functional_to_standard,template,raw,,,,,,,,,,,, mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, motion_correct,functional,raw,,yes,,yes,,,,,,yes,, motion_correct_smooth,functional,raw,,yes,,,,,yes,,,,, motion_correct_to_standard,template,raw,,yes,,,,,yes,,,yes,, motion_correct_to_standard_smooth,template,raw,,yes,,,,,yes,,,,, motion_params,,,,,,,,,,,,,, movement_parameters,,,,,,,,,,,,,yes, output_means,,,,,,,,,,,,,, power_params,,,,,,,,,,,,,yes, raw_functional,functional,raw,,yes,,,,,yes,,,,, reho,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, reho_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, reho_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, reho_to_standard_smooth,template,raw,,,yes,,,yes,,,yes,,, reho_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, reho_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, roi_timeseries,,,,,,,,,,,,,, roi_timeseries_for_SCA,,,,,,,,,,,,,, roi_timeseries_for_SCA_multreg,,,,,,,,,,,,,, sca_roi_files,functional,Pearson's r,yes,,yes,yes,,yes,,yes,yes,yes,,yes sca_roi_files_smooth,functional,Pearson's r,yes,,yes,,,yes,,yes,yes,,,yes sca_roi_files_to_standard,template,Pearson's r,yes,,yes,,yes,yes,,,yes,yes,,yes sca_roi_files_to_standard_smooth,template,Pearson's r,yes,,yes,,,yes,,,yes,,,yes sca_roi_files_to_standard_fisher_zstd,template,r-to-z,yes,,yes,,,,,,,yes,,yes sca_roi_files_to_standard_fisher_zstd_smooth,template,r-to-z,yes,,yes,,,,,,,,,yes sca_tempreg_maps_files,template,GLM betas,yes,,yes,,,yes,,,,yes,,yes sca_tempreg_maps_files_smooth,template,GLM betas,yes,,yes,,,yes,,,,,,yes sca_tempreg_maps_zstat_files,template,z-stat,yes,,yes,,,,,,,yes,,yes sca_tempreg_maps_zstat_files_smooth,template,z-stat,yes,,yes,,,,,,,,,yes seg_mixeltype,,,,,,,,,,,,,yes, seg_partial_volume_files,anatomical,,,,,,,,,,,,yes, seg_partial_volume_map,anatomical,,,,,,,,,,,,yes, seg_probability_maps,anatomical,,,,,,,,,,,,yes, slice_time_corrected,functional,raw,,yes,,,,,yes,,,,, spatial_map_timeseries,,,,,,,,,,,,,, spatial_map_timeseries_for_DR,,,,,,,,,,,,,, symmetric_anatomical_to_standard,template,raw,,,,,,,,,,,, symmetric_mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, vmhc_fisher_zstd,template,r-to-z,,,yes,,,,,,,,yes, vmhc_fisher_zstd_zstat_map,template,z-stat,,,yes,,,,,,,,, vmhc_raw_score,template,Pearson's r,,,yes,,yes,,,,,,yes, \ No newline at end of file +Resource,Space,Values,Multiple outputs,Functional timeseries,Derivative,Warp to template,Calculate z-scores,Calculate averages,Optional outputs: Extra functionals,Optional outputs: Native space,Optional outputs: Raw scores,Optional outputs: Non-smoothed,Optional outputs: Debugging outputs,Override optional alff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, alff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, alff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, alff_to_standard_smooth,template,raw,,,yes,,yes,yes,,,yes,,, alff_to_standard_smooth_zstd,template,z-score,,,yes,,,,,,,,, alff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, alff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, anatomical_brain,anatomical,raw,,,,,,,,,,,, anatomical_csf_mask,anatomical,binary,,,,,,,,,,,, anatomical_gm_mask,anatomical,binary,,,,,,,,,,,, anatomical_reorient,anatomical,raw,,,,,,,,,,,, anatomical_to_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_to_standard,template,raw,,,,,,,,,,,, anatomical_to_symmetric_mni_nonlinear_xfm,,transform,,,,,,,,,,,, anatomical_wm_mask,anatomical,binary,,,,,,,,,,,, ants_affine_xfm,,transform,,,,,,,,,,,, ants_initial_xfm,,transform,,,,,,,,,,,, ants_rigid_xfm,,transform,,,,,,,,,,,, ants_symmetric_affine_xfm,,transform,,,,,,,,,,,, ants_symmetric_initial_xfm,,transform,,,,,,,,,,,, ants_symmetric_rigid_xfm,,transform,,,,,,,,,,,, centrality_outputs,template,raw,yes,,yes,,yes,,,,yes,yes,, centrality_outputs_smooth,template,raw,yes,,yes,,yes,,,,yes,,, centrality_outputs_smooth_zstd,template,z-score,yes,,yes,,,,,,,,, centrality_outputs_zstd,template,z-score,yes,,yes,,,,,,,yes,, centrality_outputs_zstd_smooth,template,z-score,yes,,yes,,,,,,,,, coordinate_transformation,,,,,,,,,,,,,yes, despiked_fieldmap,,,,,,,,,,,,,yes, dr_tempreg_maps_files,functional,GLM betas,yes,,yes,yes,,yes,,yes,,yes,, dr_tempreg_maps_files_smooth,functional,GLM betas,yes,,yes,,,yes,,yes,,,, dr_tempreg_maps_files_to_standard,template,GLM betas,yes,,yes,,,yes,,,,yes,, dr_tempreg_maps_files_to_standard_smooth,template,GLM betas,yes,,yes,,,yes,,,,,, dr_tempreg_maps_zstat_files,functional,z-stat,yes,,yes,yes,,,,yes,,yes,yes, dr_tempreg_maps_zstat_files_smooth,functional,z-stat,yes,,yes,,,,,yes,,,yes, dr_tempreg_maps_zstat_files_to_standard,template,z-stat,yes,,yes,,,,,,,yes,yes, dr_tempreg_maps_zstat_files_to_standard_smooth,template,z-stat,yes,,yes,,,,,,,,yes, falff,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, falff_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, falff_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, falff_to_standard_smooth,template,raw,,,yes,,yes,yes,,,yes,,, falff_to_standard_smooth_zstd,template,z-score,,,yes,,,,,,,,, falff_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, falff_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, fmap_magnitude,,,,,,,,,,,,,yes, fmap_phase_diff,,,,,,,,,,,,,yes, frame_wise_displacement_jenkinson,,,,,,,,,,,,,, frame_wise_displacement_power,,,,,,,,,,,,,, functional_brain_mask,functional,binary,,,,,,,,,,,, functional_brain_mask_to_standard,template,binary,,,,,,,,,,,, functional_freq_filtered,functional,raw,,yes,,,,,,,,,, functional_nuisance_regressors,,,,,,,,,,,,,, functional_nuisance_residuals,functional,,,yes,,,,,yes,,,,yes, functional_preprocessed,functional,raw,,yes,,,,,yes,,,,, functional_preprocessed_mask,functional,binary,,,,,,,yes,,,,, functional_to_anat_linear_xfm,,transform,,,,,,,,,,,, functional_to_standard,template,raw,,yes,,,,,,,,,, max_displacement,,,,,,,,,,,,,yes, mean_functional,functional,raw,,,,,,,yes,,,,, mean_functional_in_anat,anatomical,raw,,,,,,,yes,,,,, mean_functional_to_standard,template,raw,,,,,,,,,,,, mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, motion_correct,functional,raw,,yes,,yes,,,,,,yes,, motion_correct_smooth,functional,raw,,yes,,,,,yes,,,,, motion_correct_to_standard,template,raw,,yes,,,,,yes,,,yes,, motion_correct_to_standard_smooth,template,raw,,yes,,,,,yes,,,,, motion_params,,,,,,,,,,,,,, movement_parameters,,,,,,,,,,,,,yes, output_means,,,,,,,,,,,,,, power_params,,,,,,,,,,,,,yes, raw_functional,functional,raw,,yes,,,,,yes,,,,, reho,functional,raw,,,yes,yes,,yes,,yes,yes,yes,, reho_smooth,functional,raw,,,yes,,,yes,,yes,yes,,, reho_to_standard,template,raw,,,yes,,yes,yes,,,yes,yes,, reho_to_standard_smooth,template,raw,,,yes,,yes,yes,,,yes,,, reho_to_standard_smooth_zstd,template,z-score,,,yes,,,,,,,,, reho_to_standard_zstd,template,z-score,,,yes,,,,,,,yes,, reho_to_standard_zstd_smooth,template,z-score,,,yes,,,,,,,,, roi_timeseries,,,,,,,,,,,,,, roi_timeseries_for_SCA,,,,,,,,,,,,,, roi_timeseries_for_SCA_multreg,,,,,,,,,,,,,, sca_roi_files,functional,Pearson's r,yes,,yes,yes,,yes,,yes,yes,yes,,yes sca_roi_files_smooth,functional,Pearson's r,yes,,yes,,,yes,,yes,yes,,,yes sca_roi_files_to_standard,template,Pearson's r,yes,,yes,,yes,yes,,,yes,yes,,yes sca_roi_files_to_standard_smooth,template,Pearson's r,yes,,yes,,yes,yes,,,yes,,,yes sca_roi_files_to_standard_smooth_fisher_zstd,template,r-to-z,yes,,yes,,,,,,,,,yes sca_roi_files_to_standard_fisher_zstd,template,r-to-z,yes,,yes,,,,,,,yes,,yes sca_roi_files_to_standard_fisher_zstd_smooth,template,r-to-z,yes,,yes,,,,,,,,,yes sca_tempreg_maps_files,template,GLM betas,yes,,yes,,,yes,,,,yes,,yes sca_tempreg_maps_files_smooth,template,GLM betas,yes,,yes,,,yes,,,,,,yes sca_tempreg_maps_zstat_files,template,z-stat,yes,,yes,,,,,,,yes,,yes sca_tempreg_maps_zstat_files_smooth,template,z-stat,yes,,yes,,,,,,,,,yes seg_mixeltype,,,,,,,,,,,,,yes, seg_partial_volume_files,anatomical,,,,,,,,,,,,yes, seg_partial_volume_map,anatomical,,,,,,,,,,,,yes, seg_probability_maps,anatomical,,,,,,,,,,,,yes, slice_time_corrected,functional,raw,,yes,,,,,yes,,,,, spatial_map_timeseries,,,,,,,,,,,,,, spatial_map_timeseries_for_DR,,,,,,,,,,,,,, symmetric_anatomical_to_standard,template,raw,,,,,,,,,,,, symmetric_mni_to_anatomical_nonlinear_xfm,,transform,,,,,,,,,,,, vmhc_fisher_zstd,template,r-to-z,,,yes,,,,,,,,yes, vmhc_fisher_zstd_zstat_map,template,z-stat,,,yes,,,,,,,,, vmhc_raw_score,template,Pearson's r,,,yes,,yes,,,,,,yes, \ No newline at end of file diff --git a/scripts/cpac_rerun_after_preproc.py b/scripts/cpac_rerun_after_preproc.py new file mode 100644 index 0000000000..ebca7f81b2 --- /dev/null +++ b/scripts/cpac_rerun_after_preproc.py @@ -0,0 +1,75 @@ +#!/usr/bin/python + +working_dir_keep = ['_scan_', + 'anat_gather_', + 'anat_mni_ants_register_', + 'anat_symmetric_mni_ants_register_', + 'anat_mni_fnirt_register_', + 'anat_preproc_', + 'anat_symmetric_mni_fnirt_register_', + 'd3.js', + 'edit_func_', + 'func_gather_', + 'func_preproc_automask_', + 'func_to_anat_bbreg_', + 'func_to_anat_FLIRT_', + 'graph.dot', + 'graph.dot.png', + 'graph.json', + 'graph1.json', + 'graph_detailed.dot', + 'graph_detailed.dot.png', + 'index.html', + 'log_anat_mni_fnirt_register_', + 'log_anat_preproc_', + 'log_anat_symmetric_mni_fnirt_register_', + 'log_fristons_parameter_model_', + 'log_func_preproc_automask_', + 'log_gen_motion_stats_', + 'log_motion_correct_to_standard_smooth_', + 'log_seg_preproc_', + 'seg_preproc_'] + + +def main(): + """Clear a CPAC working directory of all node folders related to pipeline + steps which occur after pre-processing. + + This script allows easy clearing of the working directory in order to + re-run a pipeline without re-running registration and segmentation, which + are time-consuming. + """ + + import os + import shutil + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("working_directory", type=str, + help="file path to the working directory of the CPAC " + "run") + + args = parser.parse_args() + + work_dir = os.path.abspath(args.working_directory) + + for sub_id in os.listdir(work_dir): + for dirname in os.listdir(os.path.join(work_dir, sub_id)): + keep = False + for substring in working_dir_keep: + if substring in dirname: + keep = True + + if not keep: + try: + shutil.rmtree(os.path.join(work_dir, dirname)) + print ('Clearing {0} from the working ' + 'directory..'.format(dirname)) + except OSError: + pass + + print('Finished resetting working directory') + + +if __name__ == "__main__": + main() From b1ecbee02601e596f761c5baddfa3b6d8479704e Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Wed, 16 May 2018 12:20:35 -0400 Subject: [PATCH 74/75] Some QC interface fixes, and spruced it up a little. --- .../html/_sources/_static/cpac_logo.jpg | Bin 15280 -> 150168 bytes CPAC/pipeline/cpac_pipeline.py | 229 +++---- CPAC/qc/qc.py | 121 +--- CPAC/qc/utils.py | 581 ++++++++---------- CPAC/utils/utils.py | 28 +- 5 files changed, 437 insertions(+), 522 deletions(-) diff --git a/CPAC/GUI/resources/html/_sources/_static/cpac_logo.jpg b/CPAC/GUI/resources/html/_sources/_static/cpac_logo.jpg index b190448562cb39f8019c022875e5e9f46492d566..fccb0571087a7b8dd51d03ff61d23f9205ce71af 100644 GIT binary patch literal 150168 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¬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 Qvi?*CK@HAZBIL~f0anat transform workflow.connect(func_to_anat_xfm, xfm_file, std_dev_anat, 'xfm_') + # anatomical brain + workflow.connect(anat_ref, ref_file, std_dev_anat, 'ref_') - workflow.connect(anat_ref, ref_file, - std_dev_anat, 'ref_') - + # calculate SNR of functional in anatomical space snr_imports = ['import os', 'import subprocess'] snr = pe.Node(util.Function(input_names=['std_dev', 'mean_func_anat'], @@ -4369,9 +4372,12 @@ def calc_avg(output_name, strat, num_strat, map_node=False): imports=snr_imports), name='snr_%d' % num_strat) + # std dev of functional, in anatomical space workflow.connect(std_dev_anat, 'new_fname', snr, 'std_dev') + # mean functional, in anatomical space workflow.connect(mfa, mfa_file, snr, 'mean_func_anat') + # calculate the average SNR (one value) written to a text file snr_val_imports = ['import os', 'import nibabel as nb', 'import numpy.ma as ma'] @@ -4382,15 +4388,15 @@ def calc_avg(output_name, strat, num_strat, map_node=False): name='snr_val%d' % num_strat) std_dev_anat.inputs.interp_ = 'trilinear' - workflow.connect(snr, 'new_fname', - snr_val, 'measure_file') + # snr map into node + workflow.connect(snr, 'new_fname', snr_val, 'measure_file') + # histogram of the SNR hist_ = hist.clone('hist_snr_%d' % num_strat) hist_.inputs.measure = 'snr' + workflow.connect(snr, 'new_fname', hist_, 'measure_file') - workflow.connect(snr, 'new_fname', - hist_, 'measure_file') - + # zero out outlier voxels in measure files for the QC pages drop_percent = pe.Node( util.Function(input_names=['measure_file', 'percent_'], @@ -4398,16 +4404,16 @@ def calc_avg(output_name, strat, num_strat, map_node=False): function=drop_percent_), name='dp_snr_%d' % num_strat) drop_percent.inputs.percent_ = 99 - workflow.connect(snr, 'new_fname', drop_percent, 'measure_file') + # create the SNR montage montage_snr = create_montage('montage_snr_%d' % num_strat, 'red_to_blue', 'snr') - + # modified SNR measure file workflow.connect(drop_percent, 'modified_measure_file', montage_snr, 'inputspec.overlay') - + # anatomical brain for the underlay of the SNR image workflow.connect(anat_ref, ref_file, montage_snr, 'inputspec.underlay') @@ -4442,11 +4448,11 @@ def calc_avg(output_name, strat, num_strat, map_node=False): strat.update_resource_pool({'qc___movement_trans_plot': (mov_plot, 'translation_plot'),'qc___movement_rot_plot': (mov_plot, 'rotation_plot')}) - if not 6 in qc_plot_id: - qc_plot_id[6] = 'movement_trans_plot' - if not 7 in qc_plot_id: - qc_plot_id[7] = 'movement_rot_plot' + qc_plot_id[7] = 'movement_trans_plot' + + if not 8 in qc_plot_id: + qc_plot_id[8] = 'movement_rot_plot' except: logStandardError('QC', 'unable to get resources for Motion Parameters plot', '0052') @@ -4488,8 +4494,8 @@ def calc_avg(output_name, strat, num_strat, map_node=False): strat.update_resource_pool({'qc___fd_plot': (fd_plot, 'hist_path')}) - if not 8 in qc_plot_id: - qc_plot_id[8] = 'fd_plot' + if not 9 in qc_plot_id: + qc_plot_id[9] = 'fd_plot' except: logStandardError('QC', 'unable to get resources for FD plot', '0053') @@ -4539,8 +4545,8 @@ def calc_avg(output_name, strat, num_strat, map_node=False): 'qc___mni_normalized_anatomical_s': (montage_mni_anat, 'outputspec.sagittal_png')}) if not 6 in qc_montage_id_a: - qc_montage_id_a[6] = 'mni_normalized_anatomical_a' - qc_montage_id_s[6] = 'mni_normalized_anatomical_s' + qc_montage_id_a[6] = 'mni_normalized_anatomical_a' + qc_montage_id_s[6] = 'mni_normalized_anatomical_s' except: logStandardError('QC', 'Cannot generate QC montages for MNI normalized anatomical: Resources Not Found', '0054') @@ -4637,8 +4643,11 @@ def calc_avg(output_name, strat, num_strat, map_node=False): # QA pages function def QA_montages(measure, idx): try: + # get whatever "measure" is from resource pool overlay, out_file = strat.get_node_from_resource_pool(measure) + # zero out outlier voxels in measure files for the QC + # pages drop_percent = pe.MapNode(util.Function(input_names=['measure_file', 'percent_'], output_names=['modified_measure_file'], @@ -4650,12 +4659,17 @@ def QA_montages(measure, idx): workflow.connect(overlay, out_file, drop_percent, 'measure_file') - montage = create_montage('montage_%s_%d' % (measure, num_strat),'cyan_to_yellow', measure) + # connect the montage generation sub-workflow for each + # measure + montage = create_montage(wf_name='montage_%s_%d' % (measure, num_strat), + cbar_name='cyan_to_yellow', + png_name=measure) montage.inputs.inputspec.underlay = c.template_brain_only_for_func workflow.connect(drop_percent, 'modified_measure_file', montage, 'inputspec.overlay') + # node for the histogram for the measure histogram = hist.clone('hist_%s_%d' % (measure, num_strat)) histogram.inputs.measure = measure @@ -4666,6 +4680,8 @@ def QA_montages(measure, idx): 'qc___%s_s' % measure: (montage, 'outputspec.sagittal_png'), 'qc___%s_hist' % measure: (histogram, 'hist_path')}) + # update the dictionaries of the images keyed with numbers + # in order as they'll appear on the QC pages if not idx in qc_montage_id_a: qc_montage_id_a[idx] = '%s_a' % measure qc_montage_id_s[idx] = '%s_s' % measure @@ -4680,115 +4696,114 @@ def QA_montages(measure, idx): # ALFF and f/ALFF QA montages if 1 in c.runALFF: if 1 in c.runRegisterFuncToMNI: - QA_montages('alff_to_standard', 7) - QA_montages('falff_to_standard', 8) - if c.fwhm != None: - QA_montages('alff_to_standard_smooth', 9) - QA_montages('falff_to_standard_smooth', 10) + if 0 in c.runZScoring: + if 0 in c.run_smoothing: + QA_montages('alff_to_standard', 10) + QA_montages('falff_to_standard', 11) + if 1 in c.run_smoothing: + QA_montages('alff_to_standard_smooth', 12) + QA_montages('falff_to_standard_smooth', 13) if 1 in c.runZScoring: - if c.fwhm != None: + if 0 in c.run_smoothing: + QA_montages('alff_to_standard_zstd', 14) + QA_montages('falff_to_standard_zstd', 15) + if 1 in c.run_smoothing: if "Before" in c.smoothing_order: - QA_montages('alff_to_standard_smooth_zstd', 11) - QA_montages('falff_to_standard_smooth_zstd', 12) + QA_montages('alff_to_standard_smooth_zstd', 16) + QA_montages('falff_to_standard_smooth_zstd', 17) if "After" in c.smoothing_order: - QA_montages('alff_to_standard_zstd_smooth', 11) - QA_montages('falff_to_standard_zstd_smooth', 12) - - else: - QA_montages('alff_to_standard_zstd', 13) - QA_montages('falff_to_standard_zstd', 14) + QA_montages('alff_to_standard_zstd_smooth', 18) + QA_montages('falff_to_standard_zstd_smooth', 19) # ReHo QA montages if 1 in c.runReHo: if 1 in c.runRegisterFuncToMNI: - QA_montages('reho_to_standard', 15) - if c.fwhm != None: - QA_montages('reho_to_standard_smooth', 16) - + if 0 in c.runZScoring: + if 0 in c.run_smoothing: + QA_montages('reho_to_standard', 20) + if 1 in c.run_smoothing: + QA_montages('reho_to_standard_smooth', 21) if 1 in c.runZScoring: - if c.fwhm != None: + if 0 in c.run_smoothing: + QA_montages('reho_to_standard_fisher_zstd', 22) + if 1 in c.run_smoothing: if "Before" in c.smoothing_order: - QA_montages('reho_to_standard_smooth_zstd', 17) + QA_montages('reho_to_standard_smooth_zstd', 23) if "After" in c.smoothing_order: - QA_montages('reho_to_standard_zstd_smooth', 17) - else: - QA_montages('reho_to_standard_fisher_zstd', 18) + QA_montages('reho_to_standard_zstd_smooth', 24) # SCA ROI QA montages - if (1 in c.runSCA) and (1 in c.runROITimeseries): + if 1 in c.runSCA: if 1 in c.runRegisterFuncToMNI: - QA_montages('sca_roi_to_standard', 19) + QA_montages('sca_roi_files_to_standard', 25) if c.fwhm != None: - QA_montages('sca_roi_to_standard_smooth', 20) + QA_montages('sca_roi_files_to_standard_smooth', 26) if 1 in c.runZScoring: if c.fwhm != None: if "Before" in c.smoothing_order: - QA_montages('sca_roi_to_standard_smooth_fisher_zstd', 22) + QA_montages('sca_roi_files_to_standard_smooth_fisher_zstd', 27) if "After" in c.smoothing_order: - QA_montages('sca_roi_to_standard_fisher_zstd_smooth', 22) + QA_montages('sca_roi_files_to_standard_fisher_zstd_smooth', 28) else: - QA_montages('sca_roi_to_standard_fisher_zstd', 21) - - # SCA Seed QA montages - if (1 in c.runSCA) and ("Voxel" in ts_analysis_dict.keys()): #(1 in c.runVoxelTimeseries): - - if 1 in c.runRegisterFuncToMNI: - QA_montages('sca_seed_to_standard', 23) - - if c.fwhm != None: - QA_montages('sca_seed_to_standard_smooth', 24) - - if 1 in c.runZScoring: - if c.fwhm != None: - QA_montages('sca_seed_to_standard_zstd_fisher_smooth', 26) - else: - QA_montages('sca_seed_to_standard_fisher_zstd', 25) + QA_montages('sca_roi_files_to_standard_fisher_zstd', 29) # SCA Multiple Regression - if "MultReg" in sca_analysis_dict.keys(): #(1 in c.runMultRegSCA) and (1 in c.runROITimeseries): - + if "MultReg" in sca_analysis_dict.keys(): if 1 in c.runRegisterFuncToMNI: - QA_montages('sca_tempreg_maps_files', 27) - QA_montages('sca_tempreg_maps_zstat_files', 28) + QA_montages('sca_tempreg_maps_files', 30) + QA_montages('sca_tempreg_maps_zstat_files', 31) if c.fwhm != None: - QA_montages('sca_tempreg_maps_files_smooth', 29) - QA_montages('sca_tempreg_maps_zstat_files_smooth', 30) + QA_montages('sca_tempreg_maps_files_smooth', 32) + QA_montages('sca_tempreg_maps_zstat_files_smooth', 33) # Dual Regression QA montages - if ("DualReg" in sca_analysis_dict.keys()) and ("SpatialReg" in ts_analysis_dict.keys()): - - QA_montages('dr_tempreg_maps_files', 31) - QA_montages('dr_tempreg_maps_zstat_files', 32) + if "DualReg" in sca_analysis_dict.keys(): + if 0 in c.run_smoothing: + QA_montages('dr_tempreg_maps_files', 34) + if 1 in c.write_debugging_outputs: + QA_montages('dr_tempreg_maps_zstat_files', 35) if 1 in c.runRegisterFuncToMNI: - QA_montages('dr_tempreg_maps_files_to_standard', 33) - QA_montages('dr_tempreg_maps_zstat_files_to_standard', 34) - - if c.fwhm != None: - QA_montages('dr_tempreg_maps_files_to_standard_smooth', 35) - QA_montages('dr_tempreg_maps_zstat_files_to_standard_smooth', 36) + if 0 in c.run_smoothing: + QA_montages('dr_tempreg_maps_files_to_standard', 36) + if 1 in c.write_debugging_outputs: + QA_montages('dr_tempreg_maps_zstat_files_to_standard', 37) + if 1 in c.run_smoothing: + QA_montages('dr_tempreg_maps_files_to_standard_smooth', 38) + if 1 in c.write_debugging_outputs: + QA_montages('dr_tempreg_maps_zstat_files_to_standard_smooth', 39) # VMHC QA montages if 1 in c.runVMHC: - - QA_montages('vmhc_raw_score', 37) - QA_montages('vmhc_fisher_zstd', 38) - QA_montages('vmhc_fisher_zstd_zstat_map', 39) - + if 1 in c.write_debugging_outputs: + QA_montages('vmhc_raw_score', 40) + QA_montages('vmhc_fisher_zstd', 41) + QA_montages('vmhc_fisher_zstd_zstat_map', 42) + + # TODO: got it, the .png name before the png actually gets created + # TODO: is taken from the centrality RP key name which has zero info + # TODO: on the ROI/mask that was used. # Network Centrality QA montages if 1 in c.runNetworkCentrality: + if 0 in c.run_smoothing: + if 0 in c.runZScoring: + QA_montages('centrality_outputs', 43) + if 1 in c.runZScoring: + QA_montages('centrality_outputs_zstd', 44) - QA_montages('centrality_outputs', 40) - QA_montages('centrality_outputs_zstd', 41) - - if c.fwhm != None: - QA_montages('centrality_outputs_smoothed', 42) - QA_montages('centrality_outputs_smoothed_zstd', 43) + if 1 in c.run_smoothing: + if 0 in c.runZScoring: + QA_montages('centrality_outputs_smooth', 45) + if 1 in c.runZScoring: + if "Before" in c.smoothing_order: + QA_montages('centrality_outputs_smooth_zstd', 46) + if "After" in c.smoothing_order: + QA_montages('centrality_outputs_zstd_smooth', 47) num_strat += 1 @@ -5080,11 +5095,6 @@ def is_number(s): continue ds = pe.Node(nio.DataSink(), name='sinker_%d' % sink_idx) - - # Write QC outputs to log directory - #if 'qc' in key.lower(): - # ds.inputs.base_directory = c.logDirectory - #else: ds.inputs.base_directory = c.outputDirectory ds.inputs.creds_path = creds_path @@ -5160,7 +5170,6 @@ def is_number(s): '-') strat_no = 0 - subject_info['resource_pool'] = [] for strat in strat_list: diff --git a/CPAC/qc/qc.py b/CPAC/qc/qc.py index e8de62257a..8f1d2f0814 100644 --- a/CPAC/qc/qc.py +++ b/CPAC/qc/qc.py @@ -1,11 +1,4 @@ -import os -import sys -import commands -import nipype.pipeline.engine as pe -import nipype.interfaces.fsl as fsl -import nipype.interfaces.io as nio -import nipype.interfaces.utility as util -from CPAC.qc.qc import * + from CPAC.qc.utils import * @@ -23,8 +16,8 @@ def create_montage(wf_name, cbar_name, png_name): 'resampled_overlay']), name='outputspec') + # node for resampling images to 1mm for QC pages resample_u_imports = ['from CPAC.qc.utils import make_resample_1mm'] - resample_u = pe.Node(util.Function(input_names=['file_'], output_names=['new_fname'], function=resample_1mm, @@ -32,12 +25,13 @@ def create_montage(wf_name, cbar_name, png_name): name='resample_u') wf.connect(inputNode, 'underlay', resample_u, 'file_') + # same for overlays (resampling to 1mm) resample_o = resample_u.clone('resample_o') wf.connect(inputNode, 'overlay', resample_o, 'file_') + # node for axial montages montage_a_imports = ['import os', 'from CPAC.qc.utils import make_montage_axial'] - montage_a = pe.Node(util.Function(input_names=['overlay', 'underlay', 'png_name', @@ -49,9 +43,12 @@ def create_montage(wf_name, cbar_name, png_name): montage_a.inputs.cbar_name = cbar_name montage_a.inputs.png_name = png_name + '_a.png' + wf.connect(resample_u, 'new_fname', montage_a, 'underlay') + wf.connect(resample_o, 'new_fname', montage_a, 'overlay') + + # node for sagittal montages montage_s_imports = ['import os', 'from CPAC.qc.utils import make_montage_sagittal'] - montage_s = pe.Node(util.Function(input_names=['overlay', 'underlay', 'png_name', @@ -63,29 +60,13 @@ def create_montage(wf_name, cbar_name, png_name): montage_s.inputs.cbar_name = cbar_name montage_s.inputs.png_name = png_name + '_s.png' - wf.connect(resample_u, 'new_fname', - montage_a, 'underlay') - - wf.connect(resample_o, 'new_fname', - montage_a, 'overlay') - - wf.connect(resample_u, 'new_fname', - montage_s, 'underlay') - - wf.connect(resample_o, 'new_fname', - montage_s, 'overlay') - - wf.connect(resample_u, 'new_fname', - outputNode, 'resampled_underlay') + wf.connect(resample_u, 'new_fname', montage_s, 'underlay') + wf.connect(resample_o, 'new_fname', montage_s, 'overlay') - wf.connect(resample_o, 'new_fname', - outputNode, 'resampled_overlay') - - wf.connect(montage_a, 'png_name', - outputNode, 'axial_png') - - wf.connect(montage_s, 'png_name', - outputNode, 'sagittal_png') + wf.connect(resample_u, 'new_fname', outputNode, 'resampled_underlay') + wf.connect(resample_o, 'new_fname', outputNode, 'resampled_overlay') + wf.connect(montage_a, 'png_name', outputNode, 'axial_png') + wf.connect(montage_s, 'png_name', outputNode, 'sagittal_png') return wf @@ -109,7 +90,6 @@ def create_montage_gm_wm_csf(wf_name, png_name): name='outputspec') resample_u_imports = ['from CPAC.qc.utils import make_resample_1mm'] - resample_u = pe.Node(util.Function(input_names=['file_'], output_names=['new_fname'], function=resample_1mm, @@ -120,6 +100,11 @@ def create_montage_gm_wm_csf(wf_name, png_name): resample_o_wm = resample_u.clone('resample_o_wm') resample_o_gm = resample_u.clone('resample_o_gm') + wf.connect(inputNode, 'underlay', resample_u, 'file_') + wf.connect(inputNode, 'overlay_csf', resample_o_csf, 'file_') + wf.connect(inputNode, 'overlay_gm', resample_o_gm, 'file_') + wf.connect(inputNode, 'overlay_wm', resample_o_wm, 'file_') + montage_a_imports = ['import os', 'from CPAC.qc.utils import determine_start_and_end, get_spacing', 'import numpy as np', @@ -128,7 +113,6 @@ def create_montage_gm_wm_csf(wf_name, png_name): 'import matplotlib.pyplot as plt', 'import nibabel as nb', 'import matplotlib.cm as cm'] - montage_a = pe.Node(util.Function(input_names=['overlay_csf', 'overlay_wm', 'overlay_gm', @@ -140,6 +124,11 @@ def create_montage_gm_wm_csf(wf_name, png_name): name='montage_a') montage_a.inputs.png_name = png_name + '_a.png' + wf.connect(resample_u, 'new_fname', montage_a, 'underlay') + wf.connect(resample_o_csf, 'new_fname', montage_a, 'overlay_csf') + wf.connect(resample_o_gm, 'new_fname', montage_a, 'overlay_gm') + wf.connect(resample_o_wm, 'new_fname', montage_a, 'overlay_wm') + montage_s_imports = ['import os', 'from CPAC.qc.utils import determine_start_and_end, get_spacing', 'import numpy as np', @@ -148,7 +137,6 @@ def create_montage_gm_wm_csf(wf_name, png_name): 'import matplotlib.pyplot as plt', 'import matplotlib.cm as cm', 'import nibabel as nb'] - montage_s = pe.Node(util.Function(input_names=['overlay_csf', 'overlay_wm', 'overlay_gm', @@ -160,58 +148,17 @@ def create_montage_gm_wm_csf(wf_name, png_name): name='montage_s') montage_s.inputs.png_name = png_name + '_s.png' - wf.connect(inputNode, 'underlay', - resample_u, 'file_') - - wf.connect(inputNode, 'overlay_csf', - resample_o_csf, 'file_') - - wf.connect(inputNode, 'overlay_gm', - resample_o_gm, 'file_') - - wf.connect(inputNode, 'overlay_wm', - resample_o_wm, 'file_') - - wf.connect(resample_u, 'new_fname', - montage_a, 'underlay') + wf.connect(resample_u, 'new_fname', montage_s, 'underlay') + wf.connect(resample_o_csf, 'new_fname', montage_s, 'overlay_csf') + wf.connect(resample_o_gm, 'new_fname', montage_s, 'overlay_gm') + wf.connect(resample_o_wm, 'new_fname', montage_s, 'overlay_wm') + wf.connect(resample_u, 'new_fname', outputNode, 'resampled_underlay') wf.connect(resample_o_csf, 'new_fname', - montage_a, 'overlay_csf') - - wf.connect(resample_o_gm, 'new_fname', - montage_a, 'overlay_gm') - - wf.connect(resample_o_wm, 'new_fname', - montage_a, 'overlay_wm') - - wf.connect(resample_u, 'new_fname', - montage_s, 'underlay') - - wf.connect(resample_o_csf, 'new_fname', - montage_s, 'overlay_csf') - - wf.connect(resample_o_gm, 'new_fname', - montage_s, 'overlay_gm') - - wf.connect(resample_o_wm, 'new_fname', - montage_s, 'overlay_wm') - - wf.connect(resample_u, 'new_fname', - outputNode, 'resampled_underlay') - - wf.connect(resample_o_csf, 'new_fname', - outputNode, 'resampled_overlay_csf') - - wf.connect(resample_o_wm, 'new_fname', - outputNode, 'resampled_overlay_wm') - - wf.connect(resample_o_gm, 'new_fname', - outputNode, 'resampled_overlay_gm') - - wf.connect(montage_a, 'png_name', - outputNode, 'axial_png') - - wf.connect(montage_s, 'png_name', - outputNode, 'sagittal_png') + outputNode, 'resampled_overlay_csf') + wf.connect(resample_o_wm, 'new_fname', outputNode, 'resampled_overlay_wm') + wf.connect(resample_o_gm, 'new_fname', outputNode, 'resampled_overlay_gm') + wf.connect(montage_a, 'png_name', outputNode, 'axial_png') + wf.connect(montage_s, 'png_name', outputNode, 'sagittal_png') return wf diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index ea2f6f7fbf..5adcf7a193 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -10,25 +10,19 @@ def append_to_files_in_dict_way(list_files, file_): - - """ - Combine files so at each resource in file appears exactly once + """Combine files so at each resource in file appears exactly once. Parameters ---------- - list_files : list - file_ : string Returns ------- - None Notes ----- - Writes contents of file_ into list_files, ensuring list_files finally has each resource appearing exactly once @@ -37,30 +31,24 @@ def append_to_files_in_dict_way(list_files, file_): f_1 = open(file_, 'r') lines = f_1.readlines() - lines = [line.rstrip('\r\n') for line in lines] - one_dict = {} for line in lines: - if not line in one_dict: one_dict[line] = 1 - - f_1.close() for f_ in list_files: - two_dict = {} f_2 = open(f_, 'r') lines = f_2.readlines() f_2.close() f_2 = open(f_, 'w') lines = [line.rstrip('\r\n') for line in lines] - for line in lines: + for line in lines: if not line in one_dict: two_dict[line] = 1 @@ -69,30 +57,25 @@ def append_to_files_in_dict_way(list_files, file_): two_dict[key] = 1 for key in two_dict: - print >> f_2, key f_2.close def first_pass_organizing_files(qc_path): - """ - First Pass at organizing qc txt files + """First Pass at organizing qc txt files. Parameters ---------- - qc_path : string existing path of qc_files_here directory Returns ------- - None Notes ----- - Combines files with same strategy. First pass combines file names, where one file name is substring of the other. @@ -108,7 +91,6 @@ def first_pass_organizing_files(qc_path): strat_dict = {} for file_ in sorted(qc_files, reverse=True): - if not ('.txt' in file_): continue @@ -122,14 +104,14 @@ def first_pass_organizing_files(qc_path): str_ = str_.replace('___', '_') str_ = str_.replace('__', '_') - if '_hp_' in str_ and '_fwhm_' in str_ and not ('_bandpass_freqs_' in str_): - + if '_hp_' in str_ and '_fwhm_' in str_ and \ + not ('_bandpass_freqs_' in str_): str_, fwhm_val = str_.split('_fwhm_') fwhm_val = '_fwhm_' + fwhm_val str_, hp_lp_ = str_.split('_hp_') - hp_lp_ = '_hp_' + hp_lp_ + hp_lp_ = '_hp_' + hp_lp_ str_ = str_ + fwhm_val + hp_lp_ @@ -149,23 +131,19 @@ def first_pass_organizing_files(qc_path): def second_pass_organizing_files(qc_path): - """ - Second Pass at organizing qc txt files + """Second Pass at organizing qc txt files. Parameters ---------- - qc_path : string existing path of qc_files_here directory Returns ------- - None Notes ----- - Combines files with same strategy. combines files for derivative falff , alff with others @@ -198,10 +176,10 @@ def second_pass_organizing_files(qc_path): if not str_ in strat_dict: strat_dict[str_] = [file_] else: - print 'Error: duplicate keys for files in QC 2nd file_org pass: %s %s' % (strat_dict[str_], file_) + print 'Error: duplicate keys for files in QC 2nd file_org ' \ + 'pass: %s %s' % (strat_dict[str_], file_) raise - print strat_dict # organize alff falff elif ('_hp_' in str_) and ('_lp_' in str_): key_ = '' @@ -234,18 +212,16 @@ def second_pass_organizing_files(qc_path): if not str_ in strat_dict: strat_dict[str_] = [file_] else: - print 'Error: duplicate keys for files in QC 2nd file_org pass: %s %s' % (strat_dict[str_], file_) + print 'Error: duplicate keys for files in QC 2nd file_org ' \ + 'pass: %s %s' % (strat_dict[str_], file_) raise def organize(dict_, all_ids, png_, new_dict): - - """ - Organizes pngs according to their IDS in new_dict dictionary + """Organizes pngs according to their IDS in new_dict dictionary Parameters ---------- - dict_ : dictionary dict containing png id no and png type(montage/plot/hist) @@ -258,12 +234,11 @@ def organize(dict_, all_ids, png_, new_dict): new_dict : dictionary dictionary containg ids and png lists - Returns ------- - all_ids : list list of png id nos + """ for id_no, png_type in dict_.items(): @@ -279,17 +254,14 @@ def organize(dict_, all_ids, png_, new_dict): if not id_no in all_ids: all_ids.append(id_no) - return all_ids + return all_ids def grp_pngs_by_id(pngs_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): - - """ - Groups pngs by their ids + """Groups pngs by their ids. Parameters ---------- - pngs_ : list list of all pngs @@ -309,7 +281,6 @@ def grp_pngs_by_id(pngs_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_ dictionary of histogram pngs key : id no value is list of png types - Returns ------- dict_a : dictionary @@ -330,6 +301,7 @@ def grp_pngs_by_id(pngs_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_ all_ids : list list of png id nos + """ from CPAC.qc.utils import organize @@ -349,13 +321,11 @@ def grp_pngs_by_id(pngs_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_ return dict(dict_a), dict(dict_s), dict(dict_hist), dict(dict_plot), list(all_ids) -def add_head(f_html_, f_html_0, f_html_1): - """ - Write HTML Headers to various html files +def add_head(f_html_, f_html_0, f_html_1, name=None): + """Write HTML Headers to various html files. Parameters ---------- - f_html_ : string path to main html file @@ -365,55 +335,73 @@ def add_head(f_html_, f_html_0, f_html_1): f_html_1 : string path to html file contaning pngs and plots - Returns ------- - None """ print >>f_html_, "" print >>f_html_, "" - print >>f_html_, "QC" + print >>f_html_, "C-PAC QC" print >>f_html_, "" print >>f_html_, "" print >>f_html_, "" print >>f_html_, "" - print >>f_html_, " " %(f_html_0.name, f_html_1.name) + print >>f_html_, " " \ + "" %(f_html_0.name, f_html_1.name) print >>f_html_, "" print >>f_html_, "" print >>f_html_0, "" - print >>f_html_0, ""%(p.resource_filename('CPAC',"GUI/resources/html/_static/nature.css")) - print >>f_html_0, ""%(p.resource_filename('CPAC',"GUI/resources/html/_static/pygments.css")) + print >>f_html_0, ""%(p.resource_filename('CPAC',"GUI/resources/html/_static/nature.css")) + print >>f_html_0, ""%(p.resource_filename('CPAC',"GUI/resources/html/_static/pygments.css")) print >>f_html_0, "" print >>f_html_0, "" print >>f_html_0, "" - print >>f_html_0, "" + print >>f_html_0, "" print >>f_html_0, "
      " print >>f_html_0, "
      " - print >>f_html_0, "

      " + print >>f_html_0, "

      " print >>f_html_0, "

      " - print >>f_html_0, "\"Logo\"/"%(p.resource_filename('CPAC', "GUI/resources/html/_static/cmi_logo.jpg")) + print >>f_html_0, ""%(p.resource_filename('CPAC', "GUI/resources/html/_static/cpac_logo.jpg")) print >>f_html_0, "

      " - print >>f_html_0, "

      Table Of Contents

      " + print >>f_html_0, "

      Table Of Contents

      " print >>f_html_0, "
        " - print >>f_html_1, '' + print >>f_html_1, '' print >>f_html_1, "" print >>f_html_1, "" print >>f_html_1, "" print >>f_html_1, "" + if name: + print >>f_html_1, "

        C-PAC Visual Data Quality Control " \ + "Interface

        " + print >>f_html_1, "

        C-PAC Website: https://fcp-indi.github.io" \ + "

        " + print >>f_html_1, "C-PAC Support Forum: " \ + "https://groups.google.com/forum/#!forum/" \ + "cpax_forum" + print >>f_html_1, "

        Scan and strategy identifiers:" \ + "
        {0}".format(name) + print >>f_html_1, "


        " def add_tail(f_html_, f_html_0, f_html_1): - """ - Write HTML Tail Tags to various html files + """Write HTML Tail Tags to various html files. Parameters ---------- - f_html_ : string path to main html file @@ -426,7 +414,6 @@ def add_tail(f_html_, f_html_0, f_html_1): Returns ------- - None """ @@ -440,18 +427,11 @@ def add_tail(f_html_, f_html_0, f_html_1): print >>f_html_1, "" -def feed_line_nav(id_, - image_name, - anchor, - f_html_0, - f_html_1): - - """ - Write to navigation bar html file +def feed_line_nav(id_, image_name, anchor, f_html_0, f_html_1): + """Write to navigation bar html file. Parameters ---------- - id_ : string id of the image @@ -467,10 +447,8 @@ def feed_line_nav(id_, f_html_1 : string path to html file contaning pngs and plots - Returns ------- - None """ @@ -529,12 +507,10 @@ def feed_line_nav(id_, def feed_line_body(image_name, anchor, image, f_html_1): - """ - Write to html file that has to contain images + """Write to html file that has to contain images. Parameters ---------- - image_name : string name of image @@ -547,10 +523,8 @@ def feed_line_body(image_name, anchor, image, f_html_1): f_html_1 : string path to html file contaning pngs and plots - Returns ------- - None """ @@ -611,13 +585,10 @@ def feed_line_body(image_name, anchor, image, f_html_1): def get_map_id(str_, id_): - - """ - Returns the proper map name given identifier for it + """Returns the proper map name given identifier for it. Parameters ---------- - str_ : string string containing text for identifier @@ -626,42 +597,62 @@ def get_map_id(str_, id_): Returns ------- - map_id : string proper name for a map + """ + map_id = None - if 'centrality' in id_: - str_ = str_.split('_centrality_a.png')[0] - type_, str_ = str_.split(id_) + ''' + id_: centrality_ + str_: degree_centrality_binarize_99_1mm_centrality_outputs_a.png + str_ post-split: degree_centrality_binarize_99_1mm_centrality_outputs + 180515-20:46:14,382 workflow ERROR: + [!] Error: The QC interface page generator ran into a problem. + Details: too many values to unpack + ''' + + # so whatever goes into "type_" and then "map_id" becomes the "Map: " + # Mask: should be the ROI nifti, but right now it's the nuisance strat... + # Measure: should be eigenvector binarize etc., but it's just "centrality_outputs" + + if 'centrality' in id_ or 'lfcd' in id_: + # TODO: way too reliant on a very specific string format + # TODO: needs re-factoring + str_ = str_.split('_a.png')[0] + type_, str_ = str_.rsplit(id_, 1) + + if "_99_1mm_" in type_: + type_ = type_.replace("_99_1mm_", "") + map_id = type_ + + ''' str_ = str_.split('_')[0] - type_ = type_.replace('_', '') map_id = '_'.join([type_, id_, str_]) + ''' + return map_id else: str_ = str_.split(id_)[1] str_ = str_.split('_')[0] map_id = '_'.join([id_, str_]) + return map_id def get_map_and_measure(png_a): - - """ - Extract Map name and Measure name from png + """Extract Map name and Measure name from png. Parameters ---------- - png_a : string name of png Returns ------- - map_name : string proper name for map @@ -675,6 +666,7 @@ def get_map_and_measure(png_a): measure_name = None map_name = None + if '_fwhm_' in png_a: measure_name = os.path.basename(os.path.dirname(os.path.dirname(png_a))) else: @@ -682,17 +674,14 @@ def get_map_and_measure(png_a): str_ = os.path.basename(png_a) - if 'sca_seeds' in png_a: - map_name = 'seed' + if 'sca_tempreg' in png_a: + map_name = get_map_id(str_, 'maps_roi_') if 'sca_roi' in png_a: - map_name = get_map_id(str_, 'ROI_number_') - - if 'temporal_regression_sca' in png_a: - map_name = get_map_id(str_, 'roi_') + map_name = get_map_id(str_, 'ROI_') - if 'temporal_dual_regression' in png_a: - map_name = get_map_id(str_, 'map_z_') + if 'dr_tempreg' in png_a: + map_name = get_map_id(str_, 'temp_reg_map_') if 'centrality' in png_a: map_name = get_map_id(str_, 'centrality_') @@ -700,24 +689,13 @@ def get_map_and_measure(png_a): return map_name, measure_name -def feed_lines_html(id_, - dict_a, - dict_s, - dict_hist, - dict_plot, - qc_montage_id_a, - qc_montage_id_s, - qc_plot_id, - qc_hist_id, - f_html_0, - f_html_1): - - """ - Write HTML Tags to various html files and embeds images +def feed_lines_html(id_, dict_a, dict_s, dict_hist, dict_plot, + qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id, + f_html_0, f_html_1): + """Write HTML Tags to various html files and embeds images. Parameters ---------- - dict_a : dictionary dictionary of axial montages key : id no value is list of paths to axial montages @@ -756,10 +734,8 @@ def feed_lines_html(id_, f_html_1 : string path to html file contaning pngs and plots - Returns ------- - None """ @@ -773,12 +749,11 @@ def feed_lines_html(id_, dict_s[id_] = sorted(dict_s[id_]) if id_ in dict_hist: - dict_hist[id_] = sorted(dict_hist[id_]) idxs = len(dict_a[id_]) - for idx in range(0, idxs): + for idx in range(0, idxs): png_a = dict_a[id_][idx] png_s = dict_s[id_][idx] png_h = None @@ -790,7 +765,7 @@ def feed_lines_html(id_, map_name = None if idxs > 1: - map_name, measure_name =get_map_and_measure(png_a) + map_name, measure_name = get_map_and_measure(png_a) id_a = str(id_) id_s = str(id_) + '_s' @@ -803,9 +778,9 @@ def feed_lines_html(id_, if id_ in qc_hist_id: image_name_h_nav = qc_hist_id[id_] if map_name is not None: - image_name_a = 'Measure: ' + qc_montage_id_a[id_].replace('_a', '') + ' Mask: '+ measure_name + ' Map: ' + map_name + image_name_a = 'Measure: ' + qc_montage_id_a[id_].replace('_a', '') + ' Mask: ' + measure_name + ' Map: ' + map_name if id_ in qc_hist_id: - image_name_h = 'Measure: ' + qc_hist_id[id_] + ' Mask:'+ measure_name + ' Map: ' + map_name + image_name_h = 'Measure: ' + qc_hist_id[id_] + ' Mask:'+ measure_name + ' Map: ' + map_name else: image_name_a = qc_montage_id_a[id_].replace('_a', '') if id_ in qc_hist_id: @@ -863,65 +838,33 @@ def feed_lines_html(id_, image_readable = 'fractional Amplitude of Low-Frequency Fluctuation' if image_name_a_nav == 'falff_smooth_hist': image_readable = 'Histogram of fractional Amplitude of Low-Frequency Fluctuation' - feed_line_nav(id_, \ - image_name_a_nav, \ - id_a, \ - f_html_0, \ - f_html_1) - - feed_line_body( - image_name_a, \ - id_a, \ - png_a, \ - f_html_1) - - feed_line_body( - '', \ - id_s, \ - png_s, \ - f_html_1) + feed_line_nav(id_, image_name_a_nav, id_a, f_html_0, f_html_1) - if id_ in dict_hist.keys(): + feed_line_body(image_name_a, id_a, png_a, f_html_1) + feed_line_body('', id_s, png_s, f_html_1) + if id_ in dict_hist.keys(): if idx == 0: - feed_line_nav(id_, \ - image_name_h_nav, \ - id_h, \ - f_html_0, \ - f_html_1) - - feed_line_body( - image_name_h, \ - id_h, \ - png_h, \ - f_html_1) + feed_line_nav(id_, image_name_h_nav, id_h, f_html_0, + f_html_1) + + feed_line_body(image_name_h, id_h, png_h, f_html_1) if id_ in dict_plot: id_a = str(id_) image_name = qc_plot_id[id_] png_a = dict_plot[id_][0] - print 'png_: ', png_a - feed_line_nav(id_, \ - image_name, \ - id_a, \ - f_html_0, \ - f_html_1) - - feed_line_body( - image_name, \ - id_a, \ - png_a, \ - f_html_1) - + feed_line_nav(id_, image_name, id_a, f_html_0, f_html_1) + feed_line_body(image_name, id_a, png_a, f_html_1) -def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): - """ - Make Page +def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, + qc_hist_id): + """Convert a 'qc_files_here' text file in the CPAC output directory into + a QC HTML page. Parameters ---------- - file_ : string path to qc path file @@ -941,13 +884,12 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): dictionary of histogram pngs key : id no value is list of png types - Returns ------- - None """ + import os from CPAC.qc.utils import grp_pngs_by_id, add_head, add_tail, \ feed_lines_html @@ -965,30 +907,26 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): # TODO: implemented # pop the combined (navbar + content) page back into the output directory # and give it a more obvious name - html_f_name = "{0}.html".format(html_f_name.replace("qc_scan", "QC-interface_scan")) + html_f_name = "{0}.html".format(html_f_name.replace("qc_scan", + "QC-interface_scan")) html_f_name = html_f_name.replace("/qc_files_here", "") f_html_ = open(html_f_name, 'wb') f_html_0 = open(html_f_name_0, 'wb') f_html_1 = open(html_f_name_1, 'wb') - dict_a, dict_s, dict_hist, dict_plot, all_ids = grp_pngs_by_id(pngs_, qc_montage_id_a, \ - qc_montage_id_s, qc_plot_id, qc_hist_id) + dict_a, dict_s, dict_hist, dict_plot, all_ids = \ + grp_pngs_by_id(pngs_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, + qc_hist_id) + + qc_path_file_id = os.path.basename(html_f_name).replace(".html", "") - add_head(f_html_, f_html_0, f_html_1) + add_head(f_html_, f_html_0, f_html_1, qc_path_file_id) for id_ in sorted(all_ids): - feed_lines_html(id_, - dict_a, - dict_s, - dict_hist, - dict_plot, - qc_montage_id_a, - qc_montage_id_s, - qc_plot_id, - qc_hist_id, - f_html_0, - f_html_1) + feed_lines_html(id_, dict_a, dict_s, dict_hist, dict_plot, + qc_montage_id_a, qc_montage_id_s, qc_plot_id, + qc_hist_id, f_html_0, f_html_1) add_tail(f_html_, f_html_0, f_html_1) @@ -998,12 +936,11 @@ def make_page(file_, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): - """ - Calls make page + """Generates a QC HTML file for each text file in the 'qc_files_here' + folder in the CPAC output directory. Parameters ---------- - qc_path : string path to qc_files_here directory @@ -1026,7 +963,6 @@ def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist Returns ------- - None """ @@ -1044,12 +980,13 @@ def make_qc_pages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist def generateQCPages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, qc_hist_id): - """ - Calls make_qc_page and organizes qc path files + """Generates the QC HTML files populated with the QC images that were + created during the CPAC pipeline run. + + This function runs after the pipeline is over. Parameters ---------- - qc_path : string path to qc_files_here directory @@ -1069,15 +1006,12 @@ def generateQCPages(qc_path, qc_montage_id_a, qc_montage_id_s, qc_plot_id, dictionary of histogram pngs key : id no value is list of png types - Returns ------- - None """ - import os from CPAC.qc.utils import first_pass_organizing_files, \ second_pass_organizing_files from CPAC.qc.utils import make_qc_pages @@ -1117,9 +1051,7 @@ def afni_edge(in_file): def make_edge(wf_name='create_edge'): - - """ - Make edge file from a scan image + """Make edge file from a scan image Parameters ---------- @@ -1133,7 +1065,7 @@ def make_edge(wf_name='create_edge'): new_fname : string path to edge file - """ + """ wf_name = pe.Workflow(name=wf_name) @@ -1157,13 +1089,10 @@ def make_edge(wf_name='create_edge'): def gen_func_anat_xfm(func_, ref_, xfm_, interp_): - - """ - Transform functional file (std dev) into anatomical space + """Transform functional file (std dev) into anatomical space. Parameters ---------- - func_ : string functional scan @@ -1178,9 +1107,9 @@ def gen_func_anat_xfm(func_, ref_, xfm_, interp_): Returns ------- - new_fname : string path to the transformed scan + """ new_fname = os.path.join(os.getcwd(), 'std_dev_anat.nii.gz') @@ -1195,24 +1124,21 @@ def gen_func_anat_xfm(func_, ref_, xfm_, interp_): def gen_snr(std_dev, mean_func_anat): - """ - Generate SNR file + """Generate SNR file. Parameters ---------- - std_dev : string path to std dev file in anat space mean_func_anat : string path to mean functional scan in anatomical space - Returns ------- - new_fname : string path to the snr file + """ new_fname = os.path.join(os.getcwd(), 'snr.nii.gz') @@ -1227,23 +1153,16 @@ def gen_snr(std_dev, mean_func_anat): def cal_snr_val(measure_file): - - """ - Calculate average snr value for snr image. + """Calculate average snr value for snr image. Parameters ---------- - measure_file : string - path to input nifti file - Returns ------- - avg_snr_file : string - a text file store average snr value """ @@ -1262,13 +1181,10 @@ def cal_snr_val(measure_file): def gen_std_dev(mask_, func_): - - """ - Generate std dev file + """Generate std dev file. Parameters ---------- - mask_ : string path to whole brain mask file @@ -1277,9 +1193,9 @@ def gen_std_dev(mask_, func_): Returns ------- - new_fname : string path to standard deviation file + """ new_fname = os.path.join(os.getcwd(), 'std_dev.nii.gz') @@ -1293,23 +1209,18 @@ def gen_std_dev(mask_, func_): def drange(min_, max_): - - """ - Generate list of float values in a specified range. + """Generate list of float values in a specified range. Parameters ---------- - min_ : float Min value max_ : float Max value - Returns ------- - range_ : list list of float values in the min_ max_ range @@ -1326,13 +1237,10 @@ def drange(min_, max_): def gen_plot_png(arr, measure, ex_vol=None): - - """ - Generate Motion FD Plot. Shows which volumes were dropped. + """Generate Motion FD Plot. Shows which volumes were dropped. Parameters ---------- - arr : list Frame wise Displacements @@ -1344,7 +1252,6 @@ def gen_plot_png(arr, measure, ex_vol=None): Returns ------- - png_name : string path to the generated plot png """ @@ -1458,28 +1365,19 @@ def gen_motion_plt(motion_parameters): def gen_histogram(measure_file, measure): - - """ - Generates Histogram Image of intensities for a given input - nifti file. + """Generates Histogram Image of intensities for a given input nifti file. Parameters ---------- - measure_file : string - - path to input nifti file + path to input nifti file measure : string - Name of the measure label in the plot - Returns ------- - hist_path : string - Path to the generated histogram png """ @@ -1494,25 +1392,20 @@ def gen_histogram(measure_file, measure): measure = m_ if 'sca_roi' in measure.lower(): fname = os.path.basename(os.path.splitext(os.path.splitext(file_)[0])[0]) - fname = fname.split('ROI_number_')[1] - fname = 'SCA_ROI_Number_' + fname.split('_')[0] + fname = fname.split('ROI_')[1] + fname = 'sca_ROI_' + fname.split('_')[0] measure = fname - if 'temporal_regression_sca' in measure.lower(): - + if 'sca_tempreg' in measure.lower(): fname = os.path.basename(os.path.splitext(os.path.splitext(file_)[0])[0]) fname = fname.split('z_maps_roi_')[1] - fname = 'z_maps_roi_' + fname.split('_')[0] + fname = 'sca_mult_regression_maps_ROI_' + fname.split('_')[0] measure = fname - - if 'temporal_dual_regression' in measure.lower(): - + if 'dr_tempreg' in measure.lower(): fname = os.path.basename(os.path.splitext(os.path.splitext(file_)[0])[0]) - fname = fname.split('map_z_')[1] - fname = 'map_z_'+ fname.split('_')[0] + fname = fname.split('temp_reg_map_')[1] + fname = 'dual_regression_map_'+ fname.split('_')[0] measure = fname - if 'centrality' in measure.lower(): - fname = os.path.basename(os.path.splitext(os.path.splitext(file_)[0])[0]) type_, fname = fname.split('centrality_') fname = type_ + 'centrality_' + fname.split('_')[0] @@ -1553,11 +1446,7 @@ def make_histogram(measure_file, measure): """ - import matplotlib - import commands -# matplotlib.use('Agg') from matplotlib import pyplot - import matplotlib.cm as cm import numpy as np import nibabel as nb import os @@ -1593,10 +1482,9 @@ def make_histogram(measure_file, measure): def drop_percent_(measure_file, percent_): - """ - Zeros out voxels in measure filewhose intensity doesnt fall - in percent_ of voxel intensities + Zeros out voxels in measure files whose intensity doesnt fall in percent_ + of voxel intensities Parameters ---------- @@ -1611,9 +1499,7 @@ def drop_percent_(measure_file, percent_): ------- modified_measure_file : string - measure_file with 1 - percent_ voxels zeroed out - - + measure_file with 1 - percent_ voxels zeroed out """ import nibabel as nb @@ -1627,7 +1513,6 @@ def drop_percent_(measure_file, percent_): x, y, z = data.shape - max_val= float(commands.getoutput('fslstats %s -P %f' %(measure_file, percent_))) for i in range(x): @@ -1658,7 +1543,9 @@ def drop_percent_(measure_file, percent_): commands.getoutput("3dcalc -a %s -expr 'a' -prefix %s" % (saved_name, saved_name_correct_header)) - modified_measure_file = os.path.join(os.getcwd(), saved_name_correct_header) + modified_measure_file = os.path.join(os.getcwd(), + saved_name_correct_header) + return modified_measure_file @@ -1786,10 +1673,8 @@ def determine_start_and_end(data, direction, percent): def montage_axial(overlay, underlay, png_name, cbar_name): - - """ - Draws Montage using overlay on Anatomical brain in Axial Direction - calls make_montage_axial + """Draws Montage using overlay on Anatomical brain in Axial Direction, + calls make_montage_axial. Parameters ---------- @@ -1816,12 +1701,11 @@ def montage_axial(overlay, underlay, png_name, cbar_name): pngs = None if isinstance(overlay, list): pngs = [] - for ov in overlay: fname = os.path.basename(os.path.splitext(os.path.splitext(ov)[0])[0]) - pngs.append(make_montage_axial(ov, underlay, fname + '_' + png_name, cbar_name)) + pngs.append(make_montage_axial(ov, underlay, + fname + '_' + png_name, cbar_name)) else: - pngs = make_montage_axial(overlay, underlay, png_name, cbar_name) png_name = pngs @@ -1830,7 +1714,6 @@ def montage_axial(overlay, underlay, png_name, cbar_name): def make_montage_axial(overlay, underlay, png_name, cbar_name): - """ Draws Montage using overlay on Anatomical brain in Axial Direction @@ -1855,19 +1738,15 @@ def make_montage_axial(overlay, underlay, png_name, cbar_name): png_name : Path to generated PNG """ - import matplotlib - import commands -# matplotlib.use('Agg') import os + import matplotlib matplotlib.rcParams.update({'font.size': 5}) import matplotlib.cm as cm - ### try: from mpl_toolkits.axes_grid1 import ImageGrid except: from mpl_toolkits.axes_grid import ImageGrid import matplotlib.pyplot as plt - import matplotlib.colors as col import nibabel as nb import numpy as np from CPAC.qc.utils import determine_start_and_end, get_spacing @@ -1877,28 +1756,43 @@ def make_montage_axial(overlay, underlay, png_name, cbar_name): X = X.astype(np.float32) Y = Y.astype(np.float32) - if 'skull_vis' in png_name: + if 'skull_vis' in png_name: X[X < 20.0] = 0.0 - if 'skull_vis' in png_name or 't1_edge_on_mean_func_in_t1' in png_name or 'MNI_edge_on_mean_func_mni' in png_name: + if 'skull_vis' in png_name or \ + 't1_edge_on_mean_func_in_t1' in png_name or \ + 'MNI_edge_on_mean_func_mni' in png_name: max_ = np.nanmax(np.abs(X.flatten())) X[X != 0.0] = max_ - print '^^', np.unique(X) + z1, z2 = determine_start_and_end(Y, 'axial', 0.0001) spacing = get_spacing(6, 3, z2 - z1) x, y, z = Y.shape fig = plt.figure(1) max_ = np.max(np.abs(Y)) - if ('snr' in png_name) or ('reho' in png_name) or ('vmhc' in png_name) or ('sca_' in png_name) or ('alff' in png_name) or ('centrality' in png_name) or ('temporal_regression_sca' in png_name) or ('temporal_dual_regression' in png_name): - grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, aspect=True, cbar_mode="single", cbar_pad=0.2, direction="row") + if ('snr' in png_name) or ('reho' in png_name) or \ + ('vmhc' in png_name) or ('sca_' in png_name) or \ + ('alff' in png_name) or ('centrality' in png_name) or \ + ('dr_tempreg' in png_name): + grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, + aspect=True, cbar_mode="single", cbar_pad=0.2, + direction="row") else: - grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, aspect=True, direction="row") + grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, + aspect=True, direction="row") zz = z1 for i in range(6*3): if zz >= z2: break - im = grid[i].imshow(np.rot90(Y[:, :, zz]), cmap=cm.Greys_r) + try: + im = grid[i].imshow(np.rot90(Y[:, :, zz]), cmap=cm.Greys_r) + except IndexError as e: + # TODO: send this to the logger instead + print("\n[!] QC Interface: Had a problem with creating the " + "axial montage for {0}\n\nDetails:{1}" + "\n".format(png_name, e)) + pass zz += spacing x, y, z = X.shape @@ -1910,12 +1804,26 @@ def make_montage_axial(overlay, underlay, png_name, cbar_name): for i in range(6*3): if zz >= z2: break - if cbar_name is 'red_to_blue': - im = grid[i].imshow(np.rot90(X[:, :, zz]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) - elif cbar_name is 'green': - im = grid[i].imshow(np.rot90(X[:, :, zz]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) - else: - im = grid[i].imshow(np.rot90(X[:, :, zz]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=- max_, vmax=max_) + + try: + if cbar_name is 'red_to_blue': + im = grid[i].imshow(np.rot90(X[:, :, zz]), + cmap=cm.get_cmap(cbar_name), alpha=0.82, + vmin=0, vmax=max_) + elif cbar_name is 'green': + im = grid[i].imshow(np.rot90(X[:, :, zz]), + cmap=cm.get_cmap(cbar_name), alpha=0.82, + vmin=0, vmax=max_) + else: + im = grid[i].imshow(np.rot90(X[:, :, zz]), + cmap=cm.get_cmap(cbar_name), alpha=0.82, + vmin=- max_, vmax=max_) + except IndexError as e: + # TODO: send this to the logger instead + print("\n[!] QC Interface: Had a problem with creating the " + "axial montage for {0}\n\nDetails:{1}" + "\n".format(png_name, e)) + pass grid[i].axes.get_xaxis().set_visible(False) grid[i].axes.get_yaxis().set_visible(False) @@ -1926,7 +1834,9 @@ def make_montage_axial(overlay, underlay, png_name, cbar_name): if 'snr' in png_name: cbar.ax.set_yticks(drange(0, max_)) - elif ('reho' in png_name) or ('vmhc' in png_name) or ('sca_' in png_name) or ('alff' in png_name) or ('centrality' in png_name) or ('temporal_regression_sca' in png_name) or ('temporal_dual_regression' in png_name): + elif ('reho' in png_name) or ('vmhc' in png_name) or \ + ('sca_' in png_name) or ('alff' in png_name) or \ + ('centrality' in png_name) or ('dr_tempreg' in png_name): cbar.ax.set_yticks(drange(-max_, max_)) plt.axis("off") @@ -2025,10 +1935,11 @@ def make_montage_sagittal(overlay, underlay, png_name, cbar_name): if 'skull_vis' in png_name: X[X < 20.0] = 0.0 - if 'skull_vis' in png_name or 't1_edge_on_mean_func_in_t1' in png_name or 'MNI_edge_on_mean_func_mni' in png_name: + if 'skull_vis' in png_name or \ + 't1_edge_on_mean_func_in_t1' in png_name or \ + 'MNI_edge_on_mean_func_mni' in png_name: max_ = np.nanmax(np.abs(X.flatten())) X[X != 0.0] = max_ - print '^^', np.unique(X) x1, x2 = determine_start_and_end(Y, 'sagittal', 0.0001) spacing = get_spacing(6, 3, x2 - x1) @@ -2036,16 +1947,31 @@ def make_montage_sagittal(overlay, underlay, png_name, cbar_name): fig = plt.figure(1) max_ = np.max(np.abs(Y)) - if ('snr' in png_name) or ('reho' in png_name) or ('vmhc' in png_name) or ('sca_' in png_name) or ('alff' in png_name) or ('centrality' in png_name) or ('temporal_regression_sca' in png_name) or ('temporal_dual_regression' in png_name): - grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, aspect=True, cbar_mode="single", cbar_pad=0.5, direction="row") + if ('snr' in png_name) or ('reho' in png_name) or \ + ('vmhc' in png_name) or ('sca_' in png_name) or \ + ('alff' in png_name) or ('centrality' in png_name) or \ + ('dr_tempreg' in png_name): + grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, + aspect=True, cbar_mode="single", cbar_pad=0.5, + direction="row") else: - grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, aspect=True, cbar_mode="None", direction="row") + grid = ImageGrid(fig, 111, nrows_ncols=(3, 6), share_all=True, + aspect=True, cbar_mode="None", direction="row") xx = x1 for i in range(6*3): if xx >= x2: break - im = grid[i].imshow(np.rot90(Y[xx, :, :]), cmap=cm.Greys_r) + + try: + im = grid[i].imshow(np.rot90(Y[xx, :, :]), cmap=cm.Greys_r) + except IndexError as e: + # TODO: send this to the logger instead + print("\n[!] QC Interface: Had a problem with creating the " + "sagittal montage for {0}\n\nDetails:{1}" + "\n".format(png_name, e)) + pass + grid[i].get_xaxis().set_visible(False) grid[i].get_yaxis().set_visible(False) xx += spacing @@ -2058,19 +1984,36 @@ def make_montage_sagittal(overlay, underlay, png_name, cbar_name): if xx >= x2: break im = None - if cbar_name is 'red_to_blue': - im = grid[i].imshow(np.rot90(X[xx, :, :]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) - elif cbar_name is 'green': - im = grid[i].imshow(np.rot90(X[xx, :, :]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=0, vmax=max_) - else: - im = grid[i].imshow(np.rot90(X[xx, :, :]), cmap=cm.get_cmap(cbar_name), alpha=0.82, vmin=- max_, vmax=max_) + + try: + if cbar_name is 'red_to_blue': + im = grid[i].imshow(np.rot90(X[xx, :, :]), + cmap=cm.get_cmap(cbar_name), alpha=0.82, + vmin=0, vmax=max_) + elif cbar_name is 'green': + im = grid[i].imshow(np.rot90(X[xx, :, :]), + cmap=cm.get_cmap(cbar_name), alpha=0.82, + vmin=0, vmax=max_) + else: + im = grid[i].imshow(np.rot90(X[xx, :, :]), + cmap=cm.get_cmap(cbar_name), alpha=0.82, + vmin=- max_, vmax=max_) + except IndexError as e: + # TODO: send this to the logger instead + print("\n[!] QC Interface: Had a problem with creating the " + "sagittal montage for {0}\n\nDetails:{1}" + "\n".format(png_name, e)) + pass + xx += spacing + cbar = grid.cbar_axes[0].colorbar(im) if 'snr' in png_name: cbar.ax.set_yticks(drange(0, max_)) - - elif ('reho' in png_name) or ('vmhc' in png_name) or ('sca_' in png_name) or ('alff' in png_name) or ('centrality' in png_name) or ('temporal_regression_sca' in png_name) or ('temporal_dual_regression' in png_name): + elif ('reho' in png_name) or ('vmhc' in png_name) or \ + ('sca_' in png_name) or ('alff' in png_name) or \ + ('centrality' in png_name) or ('dr_tempreg' in png_name): cbar.ax.set_yticks(drange(-max_, max_)) plt.axis("off") @@ -2157,7 +2100,7 @@ def montage_gm_wm_csf_axial(overlay_csf, overlay_wm, overlay_gm, underlay, png_n for i in range(6*3): if zz >= z2: break - im = grid[i].imshow(np.rot90(X_csf[:, :, zz]), cmap=cm.get_cmap('green'), alpha=0.82, vmin=0, vmax=max_csf) ### + im = grid[i].imshow(np.rot90(X_csf[:, :, zz]), cmap=cm.get_cmap('green'), alpha=0.82, vmin=0, vmax=max_csf) im = grid[i].imshow(np.rot90(X_wm[:, :, zz]), cmap=cm.get_cmap('blue'), alpha=0.82, vmin=0, vmax=max_wm) im = grid[i].imshow(np.rot90(X_gm[:, :, zz]), cmap=cm.get_cmap('red'), alpha=0.82, vmin=0, vmax=max_gm) @@ -2250,9 +2193,15 @@ def montage_gm_wm_csf_sagittal(overlay_csf, overlay_wm, overlay_gm, underlay, pn if zz >= x2: break - im = grid[i].imshow(np.rot90(X_csf[zz, :, :]), cmap=cm.get_cmap('green'), alpha=0.82, vmin=0, vmax=max_csf) ### - im = grid[i].imshow(np.rot90(X_wm[zz, :, :]), cmap=cm.get_cmap('blue'), alpha=0.82, vmin=0, vmax=max_wm) - im = grid[i].imshow(np.rot90(X_gm[zz, :, :]), cmap=cm.get_cmap('red'), alpha=0.82, vmin=0, vmax=max_gm) + im = grid[i].imshow(np.rot90(X_csf[zz, :, :]), + cmap=cm.get_cmap('green'), alpha=0.82, vmin=0, + vmax=max_csf) + im = grid[i].imshow(np.rot90(X_wm[zz, :, :]), + cmap=cm.get_cmap('blue'), alpha=0.82, vmin=0, + vmax=max_wm) + im = grid[i].imshow(np.rot90(X_gm[zz, :, :]), + cmap=cm.get_cmap('red'), alpha=0.82, vmin=0, + vmax=max_gm) grid[i].axes.get_xaxis().set_visible(False) grid[i].axes.get_yaxis().set_visible(False) @@ -2366,8 +2315,8 @@ def make_resample_1mm(file_): new_fname = ''.join([remainder, '_1mm', ext]) new_fname = os.path.join(os.getcwd(), os.path.basename(new_fname)) - cmd = " 3dresample -dxyz 1.0 1.0 1.0 -prefix %s -inset %s " % (new_fname, file_) - print cmd + cmd = " 3dresample -dxyz 1.0 1.0 1.0 -prefix %s " \ + "-inset %s " % (new_fname, file_) commands.getoutput(cmd) return new_fname diff --git a/CPAC/utils/utils.py b/CPAC/utils/utils.py index ecdc368604..c91d95a66c 100644 --- a/CPAC/utils/utils.py +++ b/CPAC/utils/utils.py @@ -845,17 +845,17 @@ def create_paths_and_links(pipeline_id, relevant_strategies, path, subject_id, get_hplpfwhmseed_('/_sca_roi_', remainder_path)) - hp_str = '' + hp_str = None if '_hp_' in remainder_path: hp_str = get_hplpfwhmseed_('/_hp_', remainder_path) new_path = os.path.join(new_path, hp_str) - lp_str = '' + lp_str = None if '_lp_' in remainder_path: lp_str = get_hplpfwhmseed_('/_lp_', remainder_path) new_path = os.path.join(new_path, lp_str) - bp_freq = '' + bp_freq = None if '_bandpass_freqs_' in remainder_path: bp_freq = get_hplpfwhmseed_('/_bandpass_freqs_', remainder_path) new_path = os.path.join(new_path, bp_freq) @@ -882,7 +882,6 @@ def create_paths_and_links(pipeline_id, relevant_strategies, path, subject_id, # prepare paths and filenames for QC text files and output paths_file # text files that are written to the output directory - try: if wf == 'qc': # if the output file is QC related, send it over to @@ -911,11 +910,23 @@ def create_paths_and_links(pipeline_id, relevant_strategies, path, subject_id, # output paths_file text files descriptively if wf == 'qc': - f_n = os.path.join(new_f_path, 'qc_%s.txt') % ( - scan_info + '_' + strategy_identifier + '_' + bp_freq + '_' + hp_str + '_' + lp_str + '_' + fwhm_str) + qc_fn_string = "qc_{0}_{1}".format(scan_info, strategy_identifier) + if bp_freq: + qc_fn_string = "{0}_{1}".format(qc_fn_string, bp_freq) + if hp_str: + qc_fn_string = "{0}_{1}".format(qc_fn_string, hp_str) + if lp_str: + qc_fn_string = "{0}_{1}".format(qc_fn_string, lp_str) + f_n = os.path.join(new_f_path, '{0}.txt'.format(qc_fn_string)) else: - f_n = os.path.join(new_f_path, 'paths_file_%s.txt') % ( - scan_info + '_' + strategy_identifier + '_' + bp_freq + '_' + hp_str + '_' + lp_str + '_' + fwhm_str) + paths_file_fn_string = "paths_file_{0}_{1}".format(scan_info, strategy_identifier) + if bp_freq: + paths_file_fn_string = "{0}_{1}".format(paths_file_fn_string, bp_freq) + if hp_str: + paths_file_fn_string = "{0}_{1}".format(paths_file_fn_string, hp_str) + if lp_str: + paths_file_fn_string = "{0}_{1}".format(paths_file_fn_string, lp_str) + f_n = os.path.join(new_f_path, '{0}.txt'.format(paths_file_fn_string)) f = open(f_n, 'a') print >> f, path @@ -929,7 +940,6 @@ def create_paths_and_links(pipeline_id, relevant_strategies, path, subject_id, raise if create_sym_links is True: - # create the actual sym-links now # fname is the filename of the current individual level output From 14557b65725068af8f9a39b416e8b446ff63e513 Mon Sep 17 00:00:00 2001 From: sgiavasis Date: Thu, 17 May 2018 19:11:37 -0400 Subject: [PATCH 75/75] Small docs and file template updates. --- CPAC/GUI/interface/pages/settings.py | 19 ++++++++++--------- .../utils/fsl_flame_presets_window.py | 8 +++++++- CPAC/info.py | 4 ++-- CPAC/qc/utils.py | 5 +++-- .../configs/pipeline_config_template.yml | 10 +++++++--- README.md | 2 +- 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/CPAC/GUI/interface/pages/settings.py b/CPAC/GUI/interface/pages/settings.py index c449077813..4cfea5de9d 100644 --- a/CPAC/GUI/interface/pages/settings.py +++ b/CPAC/GUI/interface/pages/settings.py @@ -234,19 +234,12 @@ def __init__(self, parent, counter=0): "information is needed.", values=["Off", "On"]) - self.page.add(label="Create Symbolic Links ", - control=control.CHOICE_BOX, - name='runSymbolicLinks', - type=dtype.LSTR, - comment="Create a user-friendly, well organized " - "version of the output directory.", - values=["On", "Off"]) - self.page.add(label="Enable Quality Control Interface ", control=control.CHOICE_BOX, name='generateQualityControlImages', type=dtype.LSTR, - comment="Generate quality control pages containing preprocessing and derivative outputs.", + comment="Generate quality control pages containing " + "preprocessing and derivative outputs.", values=["On", "Off"]) self.page.add(label="Remove Working Directory ", @@ -277,6 +270,14 @@ def __init__(self, parent, counter=0): "symbolic links.\n\nRequires an intact " "Working Directory from a previous CPAC run.") + self.page.add(label="Create Symbolic Links ", + control=control.CHOICE_BOX, + name='runSymbolicLinks', + type=dtype.LSTR, + comment="Create a user-friendly, well organized " + "version of the output directory.", + values=["On", "Off"]) + self.page.set_sizer() parent.get_page_list().append(self) diff --git a/CPAC/GUI/interface/utils/fsl_flame_presets_window.py b/CPAC/GUI/interface/utils/fsl_flame_presets_window.py index 428de694e0..6b76c244af 100644 --- a/CPAC/GUI/interface/utils/fsl_flame_presets_window.py +++ b/CPAC/GUI/interface/utils/fsl_flame_presets_window.py @@ -47,7 +47,13 @@ def __init__(self, parent, gpa_settings=None): control=control.CHOICE_BOX, name="flame_preset", type=dtype.LSTR, - comment="", + comment="Select the type of preset you'd like to " + "generate. The preset generator will prompt " + "you for more information relevant to the " + "type of preset you selected on the next " + "window (except for the Single Group Average " + "(One-Sample T-Test), which needs no " + "additional information).", values=["Single Group Average (One-Sample T-Test)", "Single Group Average with Additional Covariate", "Unpaired Two-Group Difference (Two-Sample Unpaired T-Test)", diff --git a/CPAC/info.py b/CPAC/info.py index 34bd6145e7..e81012ad22 100644 --- a/CPAC/info.py +++ b/CPAC/info.py @@ -137,7 +137,7 @@ def get_cpac_gitversion(): VERSION = __version__ REQUIRES = ["matplotlib (>=1.2)", "lockfile (>=0.9)", "pyyaml (>=3.0)", "pygraphviz (>=1.3)", - "nibabel (>=2.0.1)", "nipype (>=0.13.1,<=0.14.0)", + "nibabel (>=2.0.1)", "nipype (==0.13.1)", "patsy (>=0.3)", "psutil (>=2.1)", "boto3 (>=1.2)", "future (==0.16.0)", "prov (>=1.4.0)", "simplejson (>=3.8.0)", "cython (>=0.12.1)", @@ -146,7 +146,7 @@ def get_cpac_gitversion(): "ipython (>=5.1)"] INSTALL_REQUIRES = ["matplotlib >=1.2", "lockfile >=0.9", "pyyaml >=3.0", "pygraphviz >=1.3", "nibabel >=2.0.1", - "nipype >=0.13.1,<=0.14.0", "patsy >=0.3", "psutil >=2.1", + "nipype ==0.13.1", "patsy >=0.3", "psutil >=2.1", "boto3 >=1.2", "future ==0.16.0", "prov >=1.4.0", "simplejson >=3.8.0", "cython >=0.12.1", "Jinja2 >=2.6", "pandas >=0.15", "INDI-Tools >=0.0.6", diff --git a/CPAC/qc/utils.py b/CPAC/qc/utils.py index 5adcf7a193..1a8b5b8108 100644 --- a/CPAC/qc/utils.py +++ b/CPAC/qc/utils.py @@ -502,8 +502,9 @@ def feed_line_nav(id_, image_name, anchor, f_html_0, f_html_1): if image_name == 'falff_smooth_hist': image_readable = 'Histogram of fractional Amplitude of Low-Frequency Fluctuation' - print >>f_html_0, "
      • %s
      • " % (f_html_1.name, \ - anchor, image_readable) + print >>f_html_0, "
      • %s
      • " % (f_html_1.name, + anchor, + image_readable) def feed_line_body(image_name, anchor, image, f_html_1): diff --git a/CPAC/resources/configs/pipeline_config_template.yml b/CPAC/resources/configs/pipeline_config_template.yml index 421a962188..94044b69d3 100644 --- a/CPAC/resources/configs/pipeline_config_template.yml +++ b/CPAC/resources/configs/pipeline_config_template.yml @@ -83,9 +83,8 @@ write_func_outputs: [0] write_debugging_outputs: [0] -# Create a user-friendly, well organized version of the output directory. -# We recommend all users enable this option. -runSymbolicLinks : [1] +# Generate quality control pages containing preprocessing and derivative outputs. +generateQualityControlImages: [1] # Deletes the contents of the Working Directory after running. @@ -102,6 +101,11 @@ run_logging : True reGenerateOutputs : False +# Create a user-friendly, well organized version of the output directory. +# We recommend all users enable this option. +runSymbolicLinks : [1] + + # The resolution to which anatomical images should be transformed during registration. # This is the resolution at which processed anatomical files will be output. resolution_for_anat : 2mm diff --git a/README.md b/README.md index b7368e5a3e..162b64fb1c 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ User documentation can be found here: http://fcp-indi.github.com/docs/user/index Developer documentation can be found here: http://fcp-indi.github.com/docs/developer/index.html -Documentation pertaining to this latest release can be found here: https://github.com/FCP-INDI/C-PAC/releases/tag/v1.0.3 +Documentation pertaining to this latest release can be found here: https://github.com/FCP-INDI/C-PAC/releases/tag/v1.1.0 Discussion Forum