From 3bb4ceaa98e82dea36c462c73bb5dddcd0945cb0 Mon Sep 17 00:00:00 2001 From: erikglee Date: Thu, 25 Jun 2020 17:19:22 -0500 Subject: [PATCH] updating imports --- build/lib/discovery_imaging_utils/__init__.py | 0 .../denoise_ts_dict.py | 888 ++++++++++++++++ .../dictionary_utils.py | 235 +++++ .../discovery_imaging_utils/func_denoising.py | 995 ++++++++++++++++++ .../discovery_imaging_utils/imaging_utils.py | 780 ++++++++++++++ .../imaging_visualizations.py | 305 ++++++ .../discovery_imaging_utils/nifti_utils.py | 94 ++ .../parc_ts_dictionary.py | 344 ++++++ .../triple_network_model.py | 172 +++ discovery_imaging_utils.egg-info/PKG-INFO | 24 + discovery_imaging_utils.egg-info/SOURCES.txt | 16 + .../dependency_links.txt | 1 + .../top_level.txt | 1 + ...overy_imaging_utils-0.1.0-py3-none-any.whl | Bin 0 -> 42265 bytes dist/discovery_imaging_utils-0.1.0.tar.gz | Bin 0 -> 37421 bytes ...overy_imaging_utils-0.1.1-py3-none-any.whl | Bin 0 -> 42264 bytes dist/discovery_imaging_utils-0.1.1.tar.gz | Bin 0 -> 37419 bytes 17 files changed, 3855 insertions(+) create mode 100644 build/lib/discovery_imaging_utils/__init__.py create mode 100644 build/lib/discovery_imaging_utils/denoise_ts_dict.py create mode 100644 build/lib/discovery_imaging_utils/dictionary_utils.py create mode 100644 build/lib/discovery_imaging_utils/func_denoising.py create mode 100644 build/lib/discovery_imaging_utils/imaging_utils.py create mode 100644 build/lib/discovery_imaging_utils/imaging_visualizations.py create mode 100644 build/lib/discovery_imaging_utils/nifti_utils.py create mode 100644 build/lib/discovery_imaging_utils/parc_ts_dictionary.py create mode 100644 build/lib/discovery_imaging_utils/triple_network_model.py create mode 100644 discovery_imaging_utils.egg-info/PKG-INFO create mode 100644 discovery_imaging_utils.egg-info/SOURCES.txt create mode 100644 discovery_imaging_utils.egg-info/dependency_links.txt create mode 100644 discovery_imaging_utils.egg-info/top_level.txt create mode 100644 dist/discovery_imaging_utils-0.1.0-py3-none-any.whl create mode 100644 dist/discovery_imaging_utils-0.1.0.tar.gz create mode 100644 dist/discovery_imaging_utils-0.1.1-py3-none-any.whl create mode 100644 dist/discovery_imaging_utils-0.1.1.tar.gz diff --git a/build/lib/discovery_imaging_utils/__init__.py b/build/lib/discovery_imaging_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/discovery_imaging_utils/denoise_ts_dict.py b/build/lib/discovery_imaging_utils/denoise_ts_dict.py new file mode 100644 index 0000000..5978f5e --- /dev/null +++ b/build/lib/discovery_imaging_utils/denoise_ts_dict.py @@ -0,0 +1,888 @@ +import os +import glob +import json +import _pickle as pickle +from discovery_imaging_utils import imaging_utils +import pandas as pd +import numpy as np + +import matplotlib.pyplot as plt +import scipy.interpolate as interp +from sklearn.decomposition import PCA + + + +def denoise(parc_dict, hpf_before_regression, scrub_criteria_dictionary, interpolation_method, noise_comps_dict, clean_comps_dict, high_pass, low_pass): + + """A function to denoise a dictionary with parcellated fMRI data. + + Function to denoise resting-state fMRI data. DOCUMENATION UNDERGOING TRANSITION... + + Using the tools in *parc_ts_dictionary*, a dictionary is generated containing all of the items that would be conventionally used in denoising the fMRI time signal. This dictionary can then be used to generated denoised data with the *denoise* function within *denoise_ts_dict*. The *denoise* function is seen as follows: + + .. code-block:: python + + denoise(parc_dict, + hpf_before_regression, + scrub_criteria_dictionary, + interpolation_method, + noise_comps_dict, + clean_comps_dict, + high_pass, + low_pass) + + Parameters + ---------- + + parc_dict : dict + This is a dictionary that has presumably been generated by parc_ts_dictionary. + + hpf_before_regression : bool or float + specifies whether or not the nuisance regressors and the time-signals of interest (i.e. the parcellated time-signals) are filtered before the nuisance regressors are regressed from the parcellated time-signals. If you do not want to do this, set to False. Otherwise set to the desired high-pass cutoff point. + + scrub_criteria_dictionary : dict or bool + This argument allows the user to define how scrubbing should be conducted. If you do not want to do scrubbing, set this argument to False. If you want to do scrubbing, there are a few different configuration options for the dictionary + + (1) {'std_dvars' : 1.2, 'framewise_displacement' : 0.5} + (2) {'Uniform' : [0.8, ['std_dvars', 'framewise_displacement']]} + + In the first example, any timepoints with std_dvars > 1.2, and framewise_displacement > 0.5 will be scrubbed. Any number of variables found under the confounds dictionary can be used to do scrubbing, with any cutoff point. In the second example, Uniform scrubbing is specified, with 0.8 meaning that the best 80% of timepoints should be kept. The sub-list with std_dvars and framewise_displacement says that std_dvars and framewise_displacement should be used to determine what the best volumes are. This means the two metrics will be demeaned and variance normalized and then added together, and the 80% of volumes with the lowest score on the combined metric will be kept. Any variables under the confounds dictionary (that are not groupings such as motion_regs_24) can be used to construct these dictionaries. + + + interpolation_method : str + Can choose between 'linear', 'cubic_spline', and 'spectral'. The spectral denoising takes the longest but is expected to perform the best (this is based off of the technique presented in Power's 2014 NeuroImage paper/Anish Mitra's work) + + noise_comps_dict : dict or bool + this dictionary configures what nuisance signals will be removed from the parcellated timeseries. Each element represents an entry to the confounds dictionary, where the key is the name of the confound (or group of confounds) to be regressed, and the entry is either False or an integer, which specifies whether the nuisance regressors should be reduced by PCA and if so how many principal components should be kept. Some examples are seen below: + .. code-block:: python + + #Include 24 motion parameters as regressors + denoise_dict = {'motion_regs_twentyfour' : False} + + #Include 24 motion parameters as regressors, reduced through PCA to 10 regressors + denoise_dict = {'motion_regs_twentyfour' : 10} + + #Include WM/CSF/GSR + motion parameters as regressors + denoise_dict = {'wmcsfgsr' : False, 'motion_regs_twentyfour' : False} + + #Include WM/CSF/GSR + ICA-AROMA Noise Timeseries as regressors + denoise_dict = {'wmcsfgsr' : False, 'aroma_noise_ics' : False} + + #Skip nuisance regression + denoise_dict = False + + * clean_comps_dict: The formatting of this dictionary is identical to the noise_comps_dict, but this dictionary is used for specifying components whose variance you do not want to be removed from the parcellated timeseries. During the denoising process a linear model will be fit to the parcellated time-series using both the signals specified by the noise_comps_dict and clean_comps_dict, but only the signal explained by the noise_comps_dict will be removed. + * high_pass: The cutoff frequency for the high-pass filter to be used in denoising. If you want to skip the high-pass filter, set to False. + * low_pass: The cutoff frequency for the low-pass filter to be used in denoising. If you want tot skip the low-pass filter, set to False. + + Running the function will output a dictionary containing the cleaned parcellated signal along with the settings used for denoising, other QC variables, and variables copied from the input dictionary. This includes: + + * cleaned_timeseries: The cleaned signal after denoising with shape . Any scrubbed timepoints, or timepoints removed at beginning of the scan will be NaN + * denoising_settings.json: The settings specified when using the *denoise* function + * dvars_pre_cleaning: DVARS calculated pre-cleaning on all input parcels (timepoints skipped at the beginning of the run + the next timepoint after the initial skipped timepoints will have DVARS set to -0.001) + * dvars_post_cleaning: DVARS calculated post-cleaning on all input parcels (scrubbed timepoints, timepoints at beginning of the run, and timepoints following scrubbed timepoints will have DVARS set to -0.001) + * dvars_stats.json: Different statistics about DVARS including (removed TPs not included in any stats): + - mean_dvars_pre_cleaning: temporal average dvars before cleaning + - mean_dvars_post_cleaning: temporal average dvars after cleaning + - dvars_remaining_ratio: mean_dvars_post_cleaning/mean_dvars_pre_cleaning + - max_dvars_pre_cleaning: highest dvars value before cleaning + - max_dvars_post_cleaning: highest dvars value after cleaning + * file_path_dictionary.json: copied from input, containing file paths involved in constructing the parcellated dictionary + * general_info.json: copied from input, containing relevant info such as the name of the subject/session, parcel labels, number of high motion and fd timepoints (calculated from fMRIPREP), etc. + * good_timepoint_inds: the indices for timepoints with defined signal (i.e. everything but the volumes dropped at the beginning of the scan and scrubbed timepoints) + * labels: another copy of the parcel label names + * mean_roi_signal_intensities.json: the mean signal intensities for raw fMRIPREP calculated csf, global_signal, and white_matter variables + * median_ts_intensities: The spatial mean of the temporal median of all voxels/vertices within each parcel (calculated on fMRIPREP output) + * num_good_timepoints: the total number of good timepoints left after scrubbing and removing initial volumes + * std_after_regression: The temporal standard deviation of each parcel's timesignal after nuisance regression (this is calcualated prior to the final filtering of the signal) + * std_before_regression: The temporal standard deviation of each parcel's timesignal prior to nuisance regression (if hpf_before_regression is used, this is calculated after that filtering step) + * std_regression_statistics + - mean_remaining_std_ratio: the average of std_before_regression/std_after_regression across all parcels + - least_remaining_std_ratio: the minimum of std_before_regression/std_after_regression across all parcels + + In totallity, processing follows the sequence below: + 1. Calculate DVARS on the input time-series. + 2. If hpf_before_regression is used, filter the parcellated time-series, and the signals specified by clean_comps_dict, and noise_comps_dict. + 3. Calculate the temporal standard deviation for each parcel (for std_before_regression) + 3. Fit the signals generated from clean_comps_dict and noise_comps_dict to the parcellated timeseries (using only defined, not scrubbed points) and remove the signal explained from the noise_comps_dict. + 4. Calculate the temporal standard deviation for each parcel (for std_after_regression) + 5. Interpolate over any scrubbed timepoints + 6. Apply either highpass, lowpass, or bandpass filter if specified + 7. Set all undefined timepoints to NaN + 8. Calculate DVARS on the output time-series + 9. Calculate remaining meta-data + + #Function inputs: + + #parc_dict = a parc_dict object generated from + #file "parc_ts_dictionary.py" which will contain both an + #uncleaned parcellated time series, and other nuisance variables + # etc. of interest + + #hpf_before_regression = the cutoff frequency for an optional high + #pass filter that can be applied to the nuisance regressors (noise/clean) and the + #uncleaned time signal before any regression or scrubbing occurs. Recommended + #value would be 0.01 or False (False for if you want to skip this step) + + #scrub_criteria_dictionary = a dictionary that describes how scrubbing should be + #implemented. Three main options are (1) instead of inputting a dictionary, setting this + #variable to False, which will skip scrubbing, (2) {'Uniform' : [AMOUNT_TO_KEEP, ['std_dvars', 'framewise_displacement']]}, + #which will automatically only keep the best timepoints (for if you want all subjects to be scrubbed an equivelant amount). + #This option will keep every timepoint if AMOUNT_TO_KEEP was 1, and no timepoints if it was 0. The list of confounds following + #AMOUNT_TO_KEEP must at least contain one metric (but can be as many as you want) from parc_object.confounds. If more than one + #metric is given, they will be z-transformed and their sum will be used to determine which timepoints should be + #kept, with larger values being interpreted as noiser (WHICH MEANS THIS OPTION SHOULD ONLY BE USED WITH METRICS WHERE + #ZERO OR NEGATIVE BASED VALUES ARE FINE AND LARGE POSITIVE VALUES ARE BAD) - this option could potentially produce + #slightly different numbers of timepoints accross subjects still if the bad timepoints overlap to varying degrees with + #the number of timepoints that are dropped at the beginning of the scan. (3) {'std_dvars' : 1.2, 'framewise_displacement' : 0.5} - + #similar to the "Uniform" option, the input metrics should be found in parc_object.confounds. Here only timepoints + #with values below all specified thresholds will be kept for further analyses + + + #interpolation_method: options are 'linear', 'cubic_spline' and 'spectral' (spectral uses more CPU time). + #While scrubbed values are not included to determine any of the weights in the denoising + #model, they will still be interpolated over and then "denoised" (have nuisance variance + #removed) so that we have values to put into the optional filter at the end of processing. + #The interpolated values only have any influence on the filtering proceedure, and will be again + #removed from the time signal after filtering and thus not included in the final output. Interpolation + #methods will do weird things if there aren't many timepoints after scrubbing. All interpolation + #schemes besides spectral are essentially wrappers over scipy's 1d interpolation methods. 'spectral' + #interpolation is implemented based on code from Anish Mitra/Jonathan Power + #as shown in Power's 2014 NeuroImage paper + + + #noise_comps_dict and clean_comps_dict both have the same syntax. The values + #specified by both of these matrices will be used (along with constant and linear trend) + #to construct the denoising regression model for the input timeseries, but only the + #noise explained by the noise_comps_dict will be removed from the input timeseries ( + #plus also the constant and linear trend). Unlike the scrub_criteria_dictionary, the + #data specifed here do not need to come from the confounds section of the parc_object, + #and because of this, if you want to include something found under parc_object.confounds, + #you will need to specify "confounds" in the name. An example of the dictionary can be seen below: + # + # clean_comps_dict = {'aroma_noise_ic_timeseries' : False} + # + # + # noise_comps_dict = {'aroma_noise_ic_timeseries' : 5, + # 'confounds.wmcsfgsr' : False + # 'confounds.motion_regs_twelve' : False + # } + # + # + #The dictionary key should specify an element to be included in the denoising process + #and the dictionary value should be False if you don't want to do a PCA reduction on + #the set of nuisance variables (this will be the case more often than not), alternatively + #if the key represents a grouping of confounds, then you can use the value to specify the + #number of principal components to kept from a reduction of the grouping. If hpf_before_regression + #is used, the filtering will happen after the PCA. + """ + + n_skip_vols = parc_dict['general_info.json']['n_skip_vols'] + TR = parc_dict['general_info.json']['TR'] + time_series = parc_dict['time_series'] + + initial_dvars = dvars(time_series, np.linspace(0,n_skip_vols - 1,n_skip_vols,dtype=int)) + + #Create an array with 1s for timepoints to use, and 0s for scrubbed timepointsx + good_timepoints = find_timepoints_to_scrub(parc_dict, scrub_criteria_dictionary) + + #Load the arrays with the data for both the clean and noise components to be used in regression + clean_comps_pre_filter = load_comps_dict(parc_dict, clean_comps_dict) + noise_comps_pre_filter = load_comps_dict(parc_dict, noise_comps_dict) + + #Apply an initial HPF to everything if necessary - this does not remove scrubbed timepoints, + #but does skips the first n_skip_vols (which will be set to 0 and not used in subsequent steps) + if hpf_before_regression != False: + + b, a = imaging_utils.construct_filter('highpass', [hpf_before_regression], TR, 6) + + #start with the clean comps matrix + if type(clean_comps_pre_filter) != type(False): + + clean_comps_post_filter = np.zeros(clean_comps_pre_filter.shape) + for clean_dim in range(clean_comps_pre_filter.shape[0]): + + clean_comps_post_filter[clean_dim, n_skip_vols:] = imaging_utils.apply_filter(b, a, clean_comps_pre_filter[clean_dim, n_skip_vols:]) + + #this option for both clean/noise indicates there is no input matrix to filter + else: + + clean_comps_post_filter = False + + #Move to the noise comps matrix + if type(noise_comps_pre_filter) != type(False): + + noise_comps_post_filter = np.zeros(noise_comps_pre_filter.shape) + for noise_dim in range(noise_comps_pre_filter.shape[0]): + + noise_comps_post_filter[noise_dim, n_skip_vols:] = imaging_utils.apply_filter(b, a, noise_comps_pre_filter[noise_dim, n_skip_vols:]) + + else: + + noise_comps_post_filter = False + + #then filter the original time signal + filtered_time_series = np.zeros(time_series.shape) + for original_ts_dim in range(time_series.shape[0]): + + filtered_time_series[original_ts_dim, n_skip_vols:] = imaging_utils.apply_filter(b, a, time_series[original_ts_dim, n_skip_vols:]) + + #If you don't want to apply the initial HPF, then + #just make a copy of the matrices of interest + else: + + clean_comps_post_filter = clean_comps_pre_filter + noise_comps_post_filter = noise_comps_pre_filter + filtered_time_series = time_series + + + + + #Now create the nuisance regression model. Only do this step if + #the noise_comps_post_filter isn't false. + good_timepoint_inds = np.where(good_timepoints == True)[0] + bad_timepoint_inds = np.where(good_timepoints == False)[0] + + if type(noise_comps_post_filter) == type(False): + + regressed_time_signal = filtered_time_series + original_std = None + + else: + + + #Calculate the standard deviation of the signal before nuisance regression + original_std = np.std(filtered_time_series[:,good_timepoint_inds], axis=1) + + #Weird thing where I need to swap dimensions here...(implemented correctly) + + #First add constant/linear trend to the denoising model + constant = np.ones((1,filtered_time_series.shape[1])) + linear_trend = np.linspace(0,filtered_time_series.shape[1],num=filtered_time_series.shape[1]) + linear_trend = np.reshape(linear_trend, (1,filtered_time_series.shape[1]))[0] + noise_comps_post_filter = np.vstack((constant, linear_trend, noise_comps_post_filter)) + + regressed_time_signal = np.zeros(filtered_time_series.shape).transpose() + filtered_time_series_T = filtered_time_series.transpose() + + #If there aren't any clean components, + #do a "hard" or "agressive" denosing + if type(clean_comps_post_filter) == type(False): + + noise_comps_post_filter_T_to_be_used = noise_comps_post_filter[:,good_timepoint_inds].transpose() + XT_X_Neg1_XT = imaging_utils.calculate_XT_X_Neg1_XT(noise_comps_post_filter_T_to_be_used) + for temp_time_signal_dim in range(filtered_time_series.shape[0]): + regressed_time_signal[good_timepoint_inds,temp_time_signal_dim] = imaging_utils.partial_clean_fast(filtered_time_series_T[good_timepoint_inds,temp_time_signal_dim], XT_X_Neg1_XT, noise_comps_post_filter_T_to_be_used) + + + + #If there are clean components, then + #do a "soft" denoising + else: + + full_matrix_to_be_used = np.vstack((noise_comps_post_filter, clean_comps_post_filter))[:,good_timepoint_inds].transpose() + noise_comps_post_filter_T_to_be_used = noise_comps_post_filter[:,good_timepoint_inds].transpose() + XT_X_Neg1_XT = imaging_utils.calculate_XT_X_Neg1_XT(full_matrix_to_be_used) + + for temp_time_signal_dim in range(filtered_time_series.shape[0]): + regressed_time_signal[good_timepoint_inds,temp_time_signal_dim] = imaging_utils.partial_clean_fast(filtered_time_series_T[good_timepoint_inds,temp_time_signal_dim], XT_X_Neg1_XT, noise_comps_post_filter_T_to_be_used) + + + #Put back into original dimensions + regressed_time_signal = regressed_time_signal.transpose() + + #Calculate the standard deviation of the signal after the nuisance regression + post_regression_std = np.std(regressed_time_signal[:,good_timepoint_inds], axis=1) + + + #Now apply interpolation + interpolated_time_signal = np.zeros(regressed_time_signal.shape) + + if interpolation_method == 'spectral': + + interpolated_time_signal = spectral_interpolation_fast(good_timepoints, regressed_time_signal, TR) + + else: + for dim in range(regressed_time_signal.shape[0]): + interpolated_time_signal[dim,:] = interpolate(good_timepoints, regressed_time_signal[dim,:], interpolation_method, TR) + + #Now if necessary, apply additional filterign: + if high_pass == False and low_pass == False: + + filtered_time_signal = interpolated_time_signal + + else: + + if high_pass != False and low_pass == False: + + b, a = imaging_utils.construct_filter('highpass', [high_pass], TR, 6) + + elif high_pass == False and low_pass != False: + + b, a = imaging_utils.construct_filter('lowpass', [low_pass], TR, 6) + + elif high_pass != False and low_pass != False: + + b, a = imaging_utils.construct_filter('bandpass', [high_pass, low_pass], TR, 6) + + filtered_time_signal = np.zeros(regressed_time_signal.shape) + for dim in range(regressed_time_signal.shape[0]): + + filtered_time_signal[dim,:] = imaging_utils.apply_filter(b,a,regressed_time_signal[dim,:]) + + final_dvars = dvars(filtered_time_signal, bad_timepoint_inds) + + #Now set all the undefined timepoints to Nan + cleaned_time_signal = filtered_time_signal + cleaned_time_signal[:,bad_timepoint_inds] = np.nan + + output_dict = {} + denoising_settings = {'hpf_before_regression' : hpf_before_regression, + 'scrub_criteria_dictionary' : scrub_criteria_dictionary, + 'interpolation_method' : interpolation_method, + 'noise_comps_dict' : noise_comps_dict, + 'clean_comps_dict' : clean_comps_dict, + 'high_pass' : high_pass, + 'low_pass' : low_pass} + + output_dict['denoising_settings.json'] = denoising_settings + output_dict['general_info.json'] = parc_dict['general_info.json'] + output_dict['cleaned_timeseries'] = cleaned_time_signal + output_dict['good_timepoint_inds'] = good_timepoint_inds + output_dict['num_good_timepoints'] = len(good_timepoint_inds) + output_dict['median_ts_intensities'] = parc_dict['median_ts_intensities'] + output_dict['labels'] = parc_dict['labels'] + output_dict['TR'] = TR + output_dict['file_path_dictionary.json'] = parc_dict['file_path_dictionary.json'] + + mean_roi_signal_intensities = {'global_signal' : np.nanmean(parc_dict['confounds']['global_signal']), + 'white_matter' : np.nanmean(parc_dict['confounds']['white_matter']), + 'csf' : np.nanmean(parc_dict['confounds']['csf'])} + + output_dict['mean_roi_signal_intensities.json'] = mean_roi_signal_intensities + + output_dict['dvars_pre_cleaning'] = initial_dvars + output_dict['dvars_post_cleaning'] = final_dvars + + dvars_stats = {} + dvars_stats['mean_dvars_pre_cleaning'] = np.mean(initial_dvars[(initial_dvars > 0)]) + dvars_stats['mean_dvars_post_cleaning'] = np.mean(final_dvars[(final_dvars > 0)]) + dvars_stats['max_dvars_pre_cleaning'] = np.max(initial_dvars) + dvars_stats['max_dvars_post_cleaning'] = np.max(final_dvars) + dvars_stats['dvars_remaining_ratio'] = np.mean(final_dvars[(final_dvars > 0)])/np.mean(initial_dvars[(initial_dvars > 0)]) + dvars_stats['def'] = 'DVARS calculated before any denoising steps (or filtering), and also after.\nBad timepoints not included in any stats.' + output_dict['dvars_stats.json'] = dvars_stats + + if type(original_std) != type(None): + + output_dict['std_before_regression'] = original_std + output_dict['std_after_regression'] = post_regression_std + + std_regression_statistics = {} + std_regression_statistics['mean_remaining_std_ratio'] = np.mean(post_regression_std/original_std) + std_regression_statistics['least_remaining_std_ratio'] = np.min(post_regression_std/original_std) + std_regression_statistics['mean_remaining_std_ratio_def'] = 'std(signal_before_regression)/std(signal_after_regression)) averaged across all regions' + std_regression_statistics['least_remaining_std_ratio_def'] = 'The most signal removed from any region (same as the mean stat only now with min)' + + output_dict['std_regression_statistics.json'] = std_regression_statistics + + + + return output_dict + + +def interpolate(timepoint_defined, signal, interp_type, TR): + """ + #defined_timepoints should be an array the length of the t with True at timepoints + #that are defined and False at timepoints that are not defined. signal should also + #be an array of length t. Timepoints at defined as False will be overwritten. This + #script supports extrapolation at beginning/end of the time signal. As a quality control + #for the spline interpolation, the most positive/negative values observed in the defined + #portion of the signal are set as bounds for the interpolated signal + + #interpolation types supported: + + #(1) linear - takes closest point before/after undefined timepoint and interpolates. + # in end cases, uses the two points before/after + #(2) cubic_spline - takes 5 closest time points before/after undefined timepoints + #and applies cubic spline to undefined points. Uses defined signal to determine maximum/minimum + #bounds for new interpolated points. + #(3) spectral based off of code from the 2014 Power + # paper + + """ + + timepoint_defined = np.array(timepoint_defined) + + true_inds = np.where(timepoint_defined == True)[0] + false_inds = np.where(timepoint_defined == False)[0] + + + signal_copy = np.array(signal) + + if interp_type == 'linear': + + #Still need to handle beginning/end cases + + for temp_timepoint in false_inds: + + + #past_timepoint = true_inds[np.sort(np.where(true_inds < temp_timepoint)[0])[-1]] + #future_timepoint = true_inds[np.sort(np.where(true_inds > temp_timepoint)[0])[0]] + + + #Be sure there is at least one future timepoint and one past timepoint. + #If there isn't, then grab either two past or two future timepoints and use those + #for interpolation. If there aren't even two total past + future timepoints, then + #just set the output to 0. Could also set the output to be unadjusted, but this + #is a way to make the issue more obvious. + temp_past_timepoint = np.sort(np.where(true_inds < temp_timepoint)[0]) + temp_future_timepoint = np.sort(np.where(true_inds > temp_timepoint)[0]) + + #If we don't have enough data to interpolate/extrapolate + if len(temp_past_timepoint) + len(temp_future_timepoint) < 2: + + signal_copy[temp_timepoint] = 0 + + #If we do have enough data to interpolate/extrapolate + else: + + if len(temp_past_timepoint) == 0: + past_timepoint = true_inds[temp_future_timepoint[1]] + else: + past_timepoint = true_inds[temp_past_timepoint[-1]] + + if len(temp_future_timepoint) == 0: + future_timepoint = true_inds[temp_past_timepoint[-2]] + else: + future_timepoint = true_inds[temp_future_timepoint[0]] + + #Find the appopriate past/future values + past_value = signal_copy[int(past_timepoint)] + future_value = signal_copy[int(future_timepoint)] + + #Use the interp1d function for interpolation + interp_object = interp.interp1d([past_timepoint, future_timepoint], [past_value, future_value], bounds_error=False, fill_value='extrapolate') + signal_copy[temp_timepoint] = interp_object(temp_timepoint).item(0) + + return signal_copy + + + #For cubic spline interpolation, instead of taking the past/future timepoint + #we will just take the closest 5 timepoints. If there aren't 5 timepoints, we will + #set the output to 0 + if interp_type == 'cubic_spline': + + sorted_good = np.sort(signal_copy[true_inds]) + min_bound = sorted_good[0] + max_bound = sorted_good[-1] + + #Continue if there are at least 5 good inds + true_inds_needed = 5 + if len(true_inds) >= true_inds_needed: + + for temp_timepoint in false_inds: + + closest_inds = true_inds[np.argsort(np.absolute(true_inds - temp_timepoint))] + closest_vals = signal_copy[closest_inds.astype(int)] + interp_object = interp.interp1d(closest_inds, closest_vals, kind = 'cubic', bounds_error=False, fill_value='extrapolate') + signal_copy[temp_timepoint.astype(int)] = interp_object(temp_timepoint).item(0) + + min_bound_exceded = np.where(signal_copy < min_bound)[0] + if len(min_bound_exceded) > 0: + + signal_copy[min_bound_exceded] = min_bound + + max_bound_exceded = np.where(signal_copy > max_bound)[0] + if len(max_bound_exceded) > 0: + + signal_copy[max_bound_exceded] = max_bound + + #If there aren't enough good timepoints, then set the bad timepoints = 0 + else: + + signal_copy[false_inds.astype(int)] = 0 + + + return signal_copy + + + if interp_type == 'spectral': + + signal_copy = spectral_interpolation(timepoint_defined, signal_copy, TR) + + return signal_copy + + +def load_comps_dict(parc_dict, comps_dict): + """ + #Internal function, which is given a "parc_dict", + #with different useful resting-state properties + #(made by module parc_ts_dictionary), and accesses + #different components specified by comp_dict, and + #outputs them as a 2d array. + + #All variables specified must be a key in the dictionary + #accessed by parc_dict['confounds'] + + #For pre-computed groupings of variables, this function + #supports PCA reduction of the variable grouping. + + #An example comps_dict is shown below: + # + # example_comps_dict = {'framewise_displacement' : False, + # 'twelve_motion_regs' : 3, + # 'aroma_noise_ics' : 3} + # + #This dictionary would form an output array <7,n_timepoints> including + #framewise displacement, 3 PCs from twelve motion regressors, and + #3 PCs from the aroma noise ICs. False specifies that no PC reduction + #should be done on the variable, and otherwise the value in the dictionary + #specifies the number of PCs to be reduced to. + # + #PCA is taken while ignoring the n_skip_vols + # + """ + + if comps_dict == False: + return False + comps_matrix = [] + + #Iterate through all key value pairs + for key, value in comps_dict.items(): + + #Load the current attribute of interest + temp_arr = parc_dict['confounds'][key] + + #If temp_arr is only 1d, at a second dimension for comparison + if len(temp_arr.shape) == 1: + + temp_arr = np.reshape(temp_arr, (temp_arr.shape[0],1)) + + #If necessary, use PCA on the temp_arr + if value != False: + + temp_arr = reduce_ics(temp_arr, value, parc_dict['general_info.json']['n_skip_vols']) + + #Either start a new array or stack to existing + if comps_matrix == []: + + comps_matrix = temp_arr + + else: + + comps_matrix = np.vstack((comps_matrix, temp_arr)) + + return comps_matrix + + + +def reduce_ics(input_matrix, num_dimensions, n_skip_vols): + """ + #Takes input_matrix . Returns + #the num_dimensions top PCs from the input_matrix which are derived excluding + #n_skip_vols, but zeros are padded to the beginning of the time series + #in place of the n_skip_vols. + """ + + + if input_matrix.shape[0] > input_matrix.shape[1]: + + raise NameError('Error: input_matrix should have longer dim1 than dim0') + + if input_matrix.shape[0] <= 1: + + raise NameError('Error: input matrix must have multiple matrices') + + input_matrix_transposed = input_matrix.transpose() + partial_input_matrix = input_matrix_transposed[n_skip_vols:,:] + + pca_temp = PCA(n_components=num_dimensions) + pca_temp.fit(partial_input_matrix) + transformed_pcs = pca_temp.transform(partial_input_matrix) + pca_time_signal = np.zeros((num_dimensions,input_matrix.shape[1])) + pca_time_signal[:,n_skip_vols:] = transformed_pcs.transpose()[0:num_dimensions,:] + + return pca_time_signal + +def demean_normalize(one_d_array): + """ + #Takes a 1d array and subtracts mean, and + #divides by standard deviation + """ + + temp_arr = one_d_array - np.nanmean(one_d_array) + + return temp_arr/np.nanstd(temp_arr) + + +def find_timepoints_to_scrub(parc_object, scrubbing_dictionary): + + """ + #This function is an internal function for the main denoising script. + #The purpose of this function is to return a array valued true for + #volumes to be included in subsequent analyses and a false for volumes + #that need to be scrubbed. + + #This script will also get rid of the n_skip_vols at the beginning of the + #scan. And these volumes don't get accounted for in Uniform. + + #If you don't want to scrub, just set scrubbing_dictionary equal to False, and + #this script will only get rid of the initial volumes + """ + + + if type(scrubbing_dictionary) == type(False): + + if scrubbing_dictionary == False: + + temp_val = parc_object['confounds']['framewise_displacement'] + good_arr = np.ones(temp_val.shape) + good_arr[0:parc_object['general_info.json']['n_skip_vols']] = 0 + return good_arr + + else: + + raise NameError ('Error, if scrubbing dictionary is a boolean it must be False') + + + if 'Uniform' in scrubbing_dictionary: + + amount_to_keep = scrubbing_dictionary.get('Uniform')[0] + evaluation_metrics = scrubbing_dictionary.get('Uniform')[1] + + + evaluation_array = [] + + for temp_metric in evaluation_metrics: + + if evaluation_array == []: + + evaluation_array = demean_normalize(parc_object['confounds'][temp_metric]) + + else: + + temp_val = np.absolute(demean_normalize(parc_object['confounds'][temp_metric])) + evaluation_array = np.add(evaluation_array, temp_val) + + num_timepoints_to_keep = int(evaluation_array.shape[0]*amount_to_keep) + sorted_inds = np.argsort(evaluation_array) + good_inds = sorted_inds[0:num_timepoints_to_keep] + good_arr = np.zeros(evaluation_array.shape) + good_arr[good_inds] = 1 + good_arr[0:parc_object['general_info.json']['n_skip_vols']] = 0 + + return good_arr + + + + #If neither of the first two options were used, we will assume + #they dictionary has appropriate key/value pairs describing scrubbing + #criteria + + temp_val = parc_object['confounds']['framewise_displacement'] + good_arr = np.ones(temp_val.shape) + good_arr[0:parc_object['general_info.json']['n_skip_vols']] = 0 + + #Iterate through all key/value pairs and set the good_arr + #value for indices which the nuisance threshold is exceeded + #equal to 0 + for temp_metric, temp_thresh in scrubbing_dictionary.items(): + + temp_values = parc_object['confounds'][temp_metric] + bad_inds = np.where(temp_values > temp_thresh)[0] + good_arr[bad_inds] = 0 + + return good_arr + + + +def spectral_interpolation(timepoint_defined, signal, TR): + + + + good_timepoint_inds = np.where(timepoint_defined == True)[0] + bad_timepoint_inds = np.where(timepoint_defined == False)[0] + num_timepoints = timepoint_defined.shape[0] + signal_copy = signal.copy() + + t = float(TR)*good_timepoint_inds + h = signal[good_timepoint_inds] + TH = np.linspace(0,(num_timepoints - 1)*TR,num=num_timepoints) + ofac = float(32) + hifac = float(1) + + N = h.shape[0] #Number of timepoints + T = np.max(t) - np.min(t) #Total observed timespan + + #Calculate sampling frequencies + f = np.linspace(1/(T*ofac), hifac*N/(2*T), num = int(((hifac*N/(2*T))/((1/(T*ofac))) + 1))) + + #angular frequencies and constant offsets + w = 2*np.pi*f + + + t1 = np.reshape(t,((1,t.shape[0]))) + w1 = np.reshape(w,((w.shape[0],1))) + + tan_a = np.sum(np.sin(np.matmul(w1,t1*2)), axis=1) + tan_b = np.sum(np.cos(np.matmul(w1,t1*2)), axis=1) + tau = np.divide(np.arctan2(tan_a,tan_b),2*w) + + #Calculate the spectral power sine and cosine terms + cterm = np.cos(np.matmul(w1,t1) - np.asarray([np.multiply(w,tau)]*t.shape[0]).transpose()) + sterm = np.sin(np.matmul(w1,t1) - np.asarray([np.multiply(w,tau)]*t.shape[0]).transpose()) + + D = np.reshape(h,(1,h.shape[0]) )#This already has the correct shape + + ##C_final = (sum(Cmult,2).^2)./sum(Cterm.^2,2) + #This calculation is done speerately for the numerator, denominator, and the division + Cmult = np.multiply(cterm, D) + numerator = np.sum(Cmult,axis=1) + + denominator = np.sum(np.power(cterm,2),axis=1) + c = np.divide(numerator, denominator) + + #Repeat the above for sine term + Smult = np.multiply(sterm,D) + numerator = np.sum(Smult, axis=1) + denominator = np.sum(np.power(sterm,2),axis=1) + s = np.divide(numerator,denominator) + + #The inverse function to re-construct the original time series + Time = TH + T_rep = np.asarray([Time]*w.shape[0]) + #already have w defined + prod = np.multiply(T_rep, w1) + sin_t = np.sin(prod) + cos_t = np.cos(prod) + sw_p = np.multiply(sin_t,np.reshape(s,(s.shape[0],1))) + cw_p = np.multiply(cos_t,np.reshape(c,(c.shape[0],1))) + S = np.sum(sw_p,axis=0) + C = np.sum(cw_p,axis=0) + H = C + S + + #Normalize the reconstructed spectrum, needed when ofac > 1 + Std_H = np.std(H) + Std_h = np.std(h) + norm_fac = np.divide(Std_H,Std_h) + H = np.divide(H,norm_fac) + + signal_copy[bad_timepoint_inds] = H[bad_timepoint_inds] + + return signal_copy + + +def spectral_interpolation_fast(timepoint_defined, signal, TR): + + + good_timepoint_inds = np.where(timepoint_defined == True)[0] + bad_timepoint_inds = np.where(timepoint_defined == False)[0] + num_timepoints = timepoint_defined.shape[0] + signal_copy = signal.copy() + + t = float(TR)*good_timepoint_inds + h = signal[:,good_timepoint_inds] + TH = np.linspace(0,(num_timepoints - 1)*TR,num=num_timepoints) + ofac = float(8) #Higher than this is slow without good quality improvements + hifac = float(1) + + N = timepoint_defined.shape[0] #Number of timepoints + T = np.max(t) - np.min(t) #Total observed timespan + + #Calculate sampling frequencies + f = np.linspace(1/(T*ofac), hifac*N/(2*T), num = int(((hifac*N/(2*T))/((1/(T*ofac))) + 1))) + + #angular frequencies and constant offsets + w = 2*np.pi*f + + t1 = np.reshape(t,((1,t.shape[0]))) + w1 = np.reshape(w,((w.shape[0],1))) + + tan_a = np.sum(np.sin(np.matmul(w1,t1*2)), axis=1) + tan_b = np.sum(np.cos(np.matmul(w1,t1*2)), axis=1) + tau = np.divide(np.arctan2(tan_a,tan_b),2*w) + + a1 = np.matmul(w1,t1) + b1 = np.asarray([np.multiply(w,tau)]*t.shape[0]).transpose() + cs_input = a1 - b1 + + #Calculate the spectral power sine and cosine terms + cterm = np.cos(cs_input) + sterm = np.sin(cs_input) + + cos_denominator = np.sum(np.power(cterm,2),axis=1) + sin_denominator = np.sum(np.power(sterm,2),axis=1) + + #The inverse function to re-construct the original time series pt. 1 + Time = TH + T_rep = np.asarray([Time]*w.shape[0]) + #already have w defined + prod = np.multiply(T_rep, w1) + sin_t = np.sin(prod) + cos_t = np.cos(prod) + + for i in range(h.shape[0]): + + ##C_final = (sum(Cmult,2).^2)./sum(Cterm.^2,2) + #This calculation is done speerately for the numerator, denominator, and the division + Cmult = np.multiply(cterm, h[i,:]) + numerator = np.sum(Cmult,axis=1) + + c = np.divide(numerator, cos_denominator) + + #Repeat the above for sine term + Smult = np.multiply(sterm,h[i,:]) + numerator = np.sum(Smult, axis=1) + s = np.divide(numerator,sin_denominator) + + #The inverse function to re-construct the original time series pt. 2 + sw_p = np.multiply(sin_t,np.reshape(s,(s.shape[0],1))) + cw_p = np.multiply(cos_t,np.reshape(c,(c.shape[0],1))) + + S = np.sum(sw_p,axis=0) + C = np.sum(cw_p,axis=0) + H = C + S + + #Normalize the reconstructed spectrum, needed when ofac > 1 + Std_H = np.std(H) + Std_h = np.std(h) + norm_fac = np.divide(Std_H,Std_h) + H = np.divide(H,norm_fac) + + signal_copy[i,bad_timepoint_inds] = H[bad_timepoint_inds] + + + return signal_copy + +def dvars(timeseries, bad_inds=None): + ''' Function to calculate DVARS based on definition + listed in Power's 2012 neuroimage paper. timeseries + should have shape and bad_inds + is an optional list of indices that have been scrubbed. + If bad_inds is included, then both the specified indices + plus the points prior to the bad inds have DVARS set to + -0.001. The output is an array with the same length as the + input timesignal and the first element will always be + -0.001. + ''' + + ts_deriv = np.zeros(timeseries.shape) + for i in range(1,timeseries.shape[1]): + + ts_deriv[:,i] = timeseries[:,i] - timeseries[:,i-1] + + ts_deriv_sqr = np.power(ts_deriv, 2) + ts_deriv_sqr_mean = np.mean(ts_deriv_sqr, axis=0) + dvars_out = np.power(ts_deriv_sqr_mean, 0.5) + + dvars_out[0] = -0.001 + + if type(bad_inds) != type(None): + + dvars_out[bad_inds] = -0.001 + bad_inds_deriv = bad_inds - 1 + bad_inds_deriv = bad_inds_deriv[(bad_inds_deriv >=0)] + dvars_out[bad_inds_deriv] = -0.001 + + return dvars_out diff --git a/build/lib/discovery_imaging_utils/dictionary_utils.py b/build/lib/discovery_imaging_utils/dictionary_utils.py new file mode 100644 index 0000000..c37be67 --- /dev/null +++ b/build/lib/discovery_imaging_utils/dictionary_utils.py @@ -0,0 +1,235 @@ +""" +Dictionary Utilities +-------------------- + +.. autofunction:: json_path_to_dict +.. autofunction:: save_dictionary +.. autofunction:: load_dictionary +.. autofunction:: flatten_dictionary + +""" + + + + + +import os +import glob +import json +import numpy as np + + + + +def json_path_to_dict(path_to_json_file): + """Function that takes an input path for json file and outputs its corresponding dict. + + Parameters + ---------- + path_to_json_file : str + a string containing the path to a json file + + Returns + ------- + dict + a dictionary containing the contents from the json pointed to by path_to_json_file + + """ + + with open(path_to_json_file,'r') as temp_file: + + json_contents = temp_file.read() + dict_object = json.loads(json_contents) + + return dict_object + + +def save_dictionary(dictionary, path_for_dictionary_dir, overwrite = False): + """Function that takes a dictionary and saves it as a directory structure. + + This function takes a dictionary and saves its heirarchical structure to + a directory. Any keys ending in .json (or .txt) file will be saved to + .json (or .txt) files. All other key/entries will be saved with numpy. + The results can be loaded again with the function load_dictionary. + + + Parameters + ---------- + dictionary : dict + the dictionary object that is to be saved + + path_for_dictionary_dir : str + the path to the directory that will be made to house the dictionary structure + + overwrite : bool + whether or not to overwrite directory at path_for_dictionary_dir if it already exists + + """ + + + if path_for_dictionary_dir[-5:] == '.json': + + with open(os.path.join(path_for_dictionary_dir), 'w') as temp_file: + json_dict = json.dumps(dictionary, indent = 4, sort_keys = True) + temp_file.write(json_dict) + + else: + + if os.path.exists(path_for_dictionary_dir) == True: + + if overwrite == False: + + raise NameError('Error: Dictionary Directory Already Exists at this path') + + else: + + os.makedirs(path_for_dictionary_dir) + + + + for temp_key in dictionary.keys(): + + + if type(dictionary[temp_key]) == dict: + + to_overwrite = overwrite + save_dictionary(dictionary[temp_key], os.path.join(path_for_dictionary_dir, temp_key), overwrite = to_overwrite) + + else: + + + if path_for_dictionary_dir[-3:] == 'txt': + + with open(os.path.join(path_for_dictionary_dir, temp_key), 'w') as temp_file: + temp_file.write(str(dictionary[temp_key])) + + else: + + np.save(os.path.join(path_for_dictionary_dir, temp_key), dictionary[temp_key]) + + + return + + + + + +def load_dictionary(dictionary_dir_path): + """Function that takes loads a formatted directory structure as a dictionary + + This function takes a path to a directory (that was probably generated by + save_dictionary) and recursively iterates through the contents to rebuild + the directory structure as a dictionary. THE ONLY supported file types that + can be present in the directory are *.npy, *.json, and *.txt files. + + + Parameters + ---------- + dictionary_dir_path : str + the path to the directory whose structure should be loaded as a dictionary + + Returns + ------- + dict + a dictionary constructed from the conentents found in dictionary_dir_path + + """ + + dictionary = {} + + os.chdir(dictionary_dir_path) + directory_contents = os.listdir() + directory_final_name = dictionary_dir_path.split('/')[-1] + for temp_file in directory_contents: + + if os.path.isdir(temp_file): + + dictionary[temp_file] = load_dictionary(os.path.join(dictionary_dir_path, temp_file)) + os.chdir(dictionary_dir_path) + + else: + + if temp_file[-5:] == '.json': + + with open(temp_file, 'r') as temp_json: + json_contents = temp_json.read() + dictionary[temp_file] = json.loads(json_contents) + + else: + + if dictionary_dir_path[-3:] == 'txt': + + with open(temp_file,'r') as temp_reading: + file_contents = temp_reading.read() + + try: + dictionary[temp_file.split('.')[0]] = float(file_contents) + except: + + if file_contents[0] == '[' and file_contents[-1] == ']': + split_contents = file_contents[1:-1].split(',') + dictionary[temp_file.split('.')[0]] = [] + for temp_item in split_contents: + split_limited_1 = temp_item.replace(' ','') + split_limited_2 = split_limited_1.replace("'","") + dictionary[temp_file.split('.')[0]].append(split_limited_2) + + else: + dictionary[temp_file.split('.')[0]] = file_contents + + else: + + dictionary[temp_file.split('.')[0]] = np.load(temp_file) + + + + return dictionary + + +def flatten_dictionary(dictionary): + + + + """Function that takes a heirarchical dictionary and flattens it to one level. + + The function takes an input dictionary containing multiple levels (i.e. dictionaries + within dictionaries), and restructures the contents as a single level dictionary with + underscores placed in between different levels of heirarchy. + + Parameters + ---------- + dictionary : dict + the dictionary to be flattened + + Returns + ------- + dict + the flattened dictionary + + """ + + + def inner_function(sub_dict, name_beginning): + + inner_dict = {} + + for temp_key in sub_dict.keys(): + + if name_beginning != '': + new_name_beginning = name_beginning + '_' + temp_key + else: + new_name_beginning = temp_key + + if type(sub_dict[temp_key]) == dict: + + new_dictionary = inner_function(sub_dict[temp_key], new_name_beginning) + for temp_inner_key in new_dictionary.keys(): + inner_dict[temp_inner_key] = new_dictionary[temp_inner_key] + + else: + inner_dict[new_name_beginning] = sub_dict[temp_key] + + return inner_dict + + flattened_dictionary = inner_function(dictionary, '') + return flattened_dictionary diff --git a/build/lib/discovery_imaging_utils/func_denoising.py b/build/lib/discovery_imaging_utils/func_denoising.py new file mode 100644 index 0000000..d562d8c --- /dev/null +++ b/build/lib/discovery_imaging_utils/func_denoising.py @@ -0,0 +1,995 @@ +import os +import glob +import json +import _pickle as pickle +from discovery_imaging_utils import imaging_utils +import pandas as pd +import numpy as np + +import matplotlib.pyplot as plt +import scipy.interpolate as interp +from sklearn.decomposition import PCA +import scipy.interpolate as interp + +def interpolate(timepoint_defined, signal, interp_type, TR): + #defined_timepoints should be an array the length of the t with True at timepoints + #that are defined and False at timepoints that are not defined. signal should also + #be an array of length t. Timepoints at defined as False will be overwritten. This + #script supports extrapolation at beginning/end of the time signal. As a quality control + #for the spline interpolation, the most positive/negative values observed in the defined + #portion of the signal are set as bounds for the interpolated signal + + #interpolation types supported: + + #(1) linear - takes closest point before/after undefined timepoint and interpolates. + # in end cases, uses the two points before/after + #(2) cubic_spline - takes 5 closest time points before/after undefined timepoints + #and applies cubic spline to undefined points. Uses defined signal to determine maximum/minimum + #bounds for new interpolated points. + #(3) spectral - yet to be implemented, will be based off of code from the 2014 Power + # paper + + timepoint_defined = np.array(timepoint_defined) + + true_inds = np.where(timepoint_defined == True)[0] + false_inds = np.where(timepoint_defined == False)[0] + + + signal_copy = np.array(signal) + + if interp_type == 'linear': + + #Still need to handle beginning/end cases + + for temp_timepoint in false_inds: + + + #past_timepoint = true_inds[np.sort(np.where(true_inds < temp_timepoint)[0])[-1]] + #future_timepoint = true_inds[np.sort(np.where(true_inds > temp_timepoint)[0])[0]] + + + #Be sure there is at least one future timepoint and one past timepoint. + #If there isn't, then grab either two past or two future timepoints and use those + #for interpolation. If there aren't even two total past + future timepoints, then + #just set the output to 0. Could also set the output to be unadjusted, but this + #is a way to make the issue more obvious. + temp_past_timepoint = np.sort(np.where(true_inds < temp_timepoint)[0]) + temp_future_timepoint = np.sort(np.where(true_inds > temp_timepoint)[0]) + + #If we don't have enough data to interpolate/extrapolate + if len(temp_past_timepoint) + len(temp_future_timepoint) < 2: + + signal_copy[temp_timepoint] = 0 + + #If we do have enough data to interpolate/extrapolate + else: + + if len(temp_past_timepoint) == 0: + past_timepoint = true_inds[temp_future_timepoint[1]] + else: + past_timepoint = true_inds[temp_past_timepoint[-1]] + + if len(temp_future_timepoint) == 0: + future_timepoint = true_inds[temp_past_timepoint[-2]] + else: + future_timepoint = true_inds[temp_future_timepoint[0]] + + #Find the appopriate past/future values + past_value = signal_copy[int(past_timepoint)] + future_value = signal_copy[int(future_timepoint)] + + #Use the interp1d function for interpolation + interp_object = interp.interp1d([past_timepoint, future_timepoint], [past_value, future_value], bounds_error=False, fill_value='extrapolate') + signal_copy[temp_timepoint] = interp_object(temp_timepoint).item(0) + + return signal_copy + + + #For cubic spline interpolation, instead of taking the past/future timepoint + #we will just take the closest 5 timepoints. If there aren't 5 timepoints, we will + #set the output to 0 + if interp_type == 'cubic_spline': + + sorted_good = np.sort(signal_copy[true_inds]) + min_bound = sorted_good[0] + max_bound = sorted_good[-1] + + #Continue if there are at least 5 good inds + true_inds_needed = 5 + if len(true_inds) >= true_inds_needed: + + for temp_timepoint in false_inds: + + closest_inds = true_inds[np.argsort(np.absolute(true_inds - temp_timepoint))] + closest_vals = signal_copy[closest_inds.astype(int)] + interp_object = interp.interp1d(closest_inds, closest_vals, kind = 'cubic', bounds_error=False, fill_value='extrapolate') + signal_copy[temp_timepoint.astype(int)] = interp_object(temp_timepoint).item(0) + + min_bound_exceded = np.where(signal_copy < min_bound)[0] + if len(min_bound_exceded) > 0: + + signal_copy[min_bound_exceded] = min_bound + + max_bound_exceded = np.where(signal_copy > max_bound)[0] + if len(max_bound_exceded) > 0: + + signal_copy[max_bound_exceded] = max_bound + + #If there aren't enough good timepoints, then set the bad timepoints = 0 + else: + + signal_copy[false_inds.astype(int)] = 0 + + + return signal_copy + + + if interp_type == 'spectral': + + signal_copy = spectral_interpolation(timepoint_defined, signal_copy, TR) + + return signal_copy + + + + +def load_comps_dict(parc_obj, comps_dict): + + #Internal function to load a specific dictionary + #file used in denoising. Supports multiple levels + #of properties such as 'confounds.framewise_displacement' + #and in cases where PCA reduction is used, does not include + #n_skip_vols in PCA reduction, but pads the beginning of the + #PCA reduction output with zeros to cover n_skip_vols. + # + # example_comps_dict = {'confounds.framewise_displacement' : False, + # 'confounds.twelve_motion_regs' : 3, + # 'aroma_noise_ics' : 3} + # + #This dictionary would form an output array <7,n_timepoints> including + #framewise displacement, 3 PCs from twelve motion regressors, and + #3 PCs from the aroma noise ICs + # + #In cases where dim0 > 3.5*dim1 for an extracted element, swaps element dimensions + + if comps_dict == False: + return False + comps_matrix = [] + + #Iterate through all key value pairs + for key, value in comps_dict.items(): + + #Load the current attribute of interest + #if key has '.' representing multiple levels, + #then recursively go through them to get the object + if len(key.split('.')) == 1: + + temp_arr = getattr(parc_obj, key) + + else: + + levels = key.split('.') + new_obj = getattr(parc_obj, levels[0]) + for temp_obj in levels[1:]: + new_obj = getattr(new_obj, temp_obj) + temp_arr = new_obj + + + #If temp_arr is only 1d, at a second dimension for comparison + if len(temp_arr.shape) == 1: + + temp_arr = np.reshape(temp_arr, (temp_arr.shape[0],1)) + + #Current fix to reshape the aroma noise ICs... should + #be addressing this at the parcel_timeseries object level though + if temp_arr.shape[0] > 3.5*temp_arr.shape[1]: + + temp_arr = np.transpose(temp_arr) + + #If necessary, use PCA on the temp_arr + if value != False: + + temp_arr = reduce_ics(temp_arr, value, parc_obj.n_skip_vols) + + #Either start a new array or stack to existing + if comps_matrix == []: + + comps_matrix = temp_arr + + else: + + comps_matrix = np.vstack((comps_matrix, temp_arr)) + + return comps_matrix + + +def reduce_ics(input_matrix, num_dimensions, n_skip_vols): + + #Takes input_matrix . Returns + #the num_dimensions top PCs from the input_matrix which are derived excluding + #n_skip_vols, but zeros are padded to the beginning of the time series + #in place of the n_skip_vols. + + + if input_matrix.shape[0] > input_matrix.shape[1]: + + raise NameError('Error: input_matrix should have longer dim1 than dim0') + + if input_matrix.shape[0] <= 1: + + raise NameError('Error: input matrix must have multiple matrices') + + input_matrix_transposed = input_matrix.transpose() + partial_input_matrix = input_matrix_transposed[n_skip_vols:,:] + + pca_temp = PCA() + pca_temp.fit(partial_input_matrix) + transformed_pcs = pca_temp.transform(partial_input_matrix) + pca_time_signal = np.zeros((num_dimensions,input_matrix.shape[1])) + pca_time_signal[:,n_skip_vols:] = transformed_pcs.transpose()[0:num_dimensions,:] + + #This section is from old iteration WITH ERROR!!! + #good_components_inds = np.linspace(0,num_dimensions - 1, num = num_dimensions).astype(int) + #pca_time_signal = np.zeros((num_dimensions, input_matrix.shape[1])) + #pca_time_signal[:,n_skip_vols:] = pca_temp.components_[good_components_inds,:] + + return pca_time_signal + + +def demean_normalize(one_d_array): + + #Takes a 1d array and subtracts mean, and + #divides by standard deviation + + temp_arr = one_d_array - np.nanmean(one_d_array) + + return temp_arr/np.nanstd(temp_arr) + +def find_timepoints_to_scrub(parc_object, scrubbing_dictionary): + + #This function is an internal function for the main denoising script. + #The purpose of this function is to return a array valued true for + #volumes to be included in subsequent analyses and a false for volumes + #that need to be scrubbed. + + #This script will also get rid of the n_skip_vols at the beginning of the + #scan. And these volumes don't get accounted for in Uniform. + + #If you don't want to scrub, just set scrubbing_dictionary equal to False, and + #this script will only get rid of the initial volumes + + + if type(scrubbing_dictionary) == type(False): + + if scrubbing_dictionary == False: + + temp_val = getattr(parc_object.confounds, 'framewise_displacement') + good_arr = np.ones(temp_val.shape) + good_arr[0:parc_object.n_skip_vols] = 0 + return good_arr + + else: + + raise NameError ('Error, if scrubbing dictionary is a boolean it must be False') + + + if 'Uniform' in scrubbing_dictionary: + + amount_to_keep = scrubbing_dictionary.get('Uniform')[0] + evaluation_metrics = scrubbing_dictionary.get('Uniform')[1] + + + evaluation_array = [] + + for temp_metric in evaluation_metrics: + + if evaluation_array == []: + + evaluation_array = demean_normalize(getattr(parc_object.confounds, temp_metric)) + + else: + + temp_val = np.absolute(demean_normalize(getattr(parc_object.confounds, temp_metric))) + evaluation_array = np.add(evaluation_array, temp_val) + + num_timepoints_to_keep = int(evaluation_array.shape[0]*amount_to_keep) + sorted_inds = np.argsort(evaluation_array) + good_inds = sorted_inds[0:num_timepoints_to_keep] + good_arr = np.zeros(evaluation_array.shape) + good_arr[good_inds] = 1 + good_arr[0:parc_object.n_skip_vols] = 0 + + return good_arr + + + + #If neither of the first two options were used, we will assume + #they dictionary has appropriate key/value pairs describing scrubbing + #criteria + + temp_val = getattr(parc_object.confounds, 'framewise_displacement') + good_arr = np.ones(temp_val.shape) + good_arr[0:parc_object.n_skip_vols] = 0 + + #Iterate through all key/value pairs and set the good_arr + #value for indices which the nuisance threshold is exceeded + #equal to 0 + for temp_key, temp_thresh in scrubbing_dictionary.items(): + + temp_values = getattr(parc_object.confounds, temp_key) + bad_inds = np.where(temp_values > temp_thresh)[0] + good_arr[bad_inds] = 0 + + return good_arr + + +def spectral_interpolation(timepoint_defined, signal, TR): + + + + good_timepoint_inds = np.where(timepoint_defined == True)[0] + bad_timepoint_inds = np.where(timepoint_defined == False)[0] + num_timepoints = timepoint_defined.shape[0] + signal_copy = signal.copy() + + t = float(TR)*good_timepoint_inds + h = signal[good_timepoint_inds] + TH = np.linspace(0,(num_timepoints - 1)*TR,num=num_timepoints) + ofac = float(32) + hifac = float(1) + + N = h.shape[0] #Number of timepoints + T = np.max(t) - np.min(t) #Total observed timespan + + #Calculate sampling frequencies + f = np.linspace(1/(T*ofac), hifac*N/(2*T), num = int(((hifac*N/(2*T))/((1/(T*ofac))) + 1))) + + #angular frequencies and constant offsets + w = 2*np.pi*f + + + t1 = np.reshape(t,((1,t.shape[0]))) + w1 = np.reshape(w,((w.shape[0],1))) + + tan_a = np.sum(np.sin(np.matmul(w1,t1*2)), axis=1) + tan_b = np.sum(np.cos(np.matmul(w1,t1*2)), axis=1) + tau = np.divide(np.arctan2(tan_a,tan_b),2*w) + + #Calculate the spectral power sine and cosine terms + cterm = np.cos(np.matmul(w1,t1) - np.asarray([np.multiply(w,tau)]*t.shape[0]).transpose()) + sterm = np.sin(np.matmul(w1,t1) - np.asarray([np.multiply(w,tau)]*t.shape[0]).transpose()) + + D = np.reshape(h,(1,h.shape[0]) )#This already has the correct shape + + ##C_final = (sum(Cmult,2).^2)./sum(Cterm.^2,2) + #This calculation is done speerately for the numerator, denominator, and the division + Cmult = np.multiply(cterm, D) + numerator = np.sum(Cmult,axis=1) + + denominator = np.sum(np.power(cterm,2),axis=1) + c = np.divide(numerator, denominator) + + #Repeat the above for sine term + Smult = np.multiply(sterm,D) + numerator = np.sum(Smult, axis=1) + denominator = np.sum(np.power(sterm,2),axis=1) + s = np.divide(numerator,denominator) + + #The inverse function to re-construct the original time series + Time = TH + T_rep = np.asarray([Time]*w.shape[0]) + #already have w defined + prod = np.multiply(T_rep, w1) + sin_t = np.sin(prod) + cos_t = np.cos(prod) + sw_p = np.multiply(sin_t,np.reshape(s,(s.shape[0],1))) + cw_p = np.multiply(cos_t,np.reshape(c,(c.shape[0],1))) + S = np.sum(sw_p,axis=0) + C = np.sum(cw_p,axis=0) + H = C + S + + #Normalize the reconstructed spectrum, needed when ofac > 1 + Std_H = np.std(H) + Std_h = np.std(h) + norm_fac = np.divide(Std_H,Std_h) + H = np.divide(H,norm_fac) + + signal_copy[bad_timepoint_inds] = H[bad_timepoint_inds] + + return signal_copy + + + +def flexible_denoise_parc(parc_obj, hpf_before_regression, scrub_criteria_dictionary, interpolation_method, noise_comps_dict, clean_comps_dict, high_pass, low_pass): + + + #Function inputs: + + #parc_object = a parcellated timeseries object generated from + #file "imaging_utility_classes.py" which will contain both an + #uncleaned parcellated time series, and other nuisance variables + # etc. of interest + + #hpf_before_regression = the cutoff frequency for an optional high + #pass filter that can be applied to the nuisance regressors (noise/clean) and the + #uncleaned time signal before any regression or scrubbing occurs. Recommended + #value would be 0.01 or False (False for if you want to skip this step) + + #scrub_criteria_dictionary = a dictionary that describes how scrubbing should be + #implemented. Three main options are (1) instead of inputting a dictionary, setting this + #variable to False, which will skip scrubbing, (2) {'Uniform' : [AMOUNT_TO_KEEP, ['std_dvars', 'framewise_displacement']]}, + #which will automatically only keep the best timepoints (for if you want all subjects to be scrubbed an equivelant amount). + #This option will keep every timepoint if AMOUNT_TO_KEEP was 1, and no timepoints if it was 0. The list of confounds following + #AMOUNT_TO_KEEP must at least contain one metric (but can be as many as you want) from parc_object.confounds. If more than one + #metric is given, they will be z-transformed and their sum will be used to determine which timepoints should be + #kept, with larger values being interpreted as noiser (WHICH MEANS THIS OPTION SHOULD ONLY BE USED WITH METRICS WHERE + #ZERO OR NEGATIVE BASED VALUES ARE FINE AND LARGE POSITIVE VALUES ARE BAD) - this option could potentially produce + #slightly different numbers of timepoints accross subjects still if the bad timepoints overlap to varying degrees with + #the number of timepoints that are dropped at the beginning of the scan. (3) {'std_dvars' : 1.2, 'framewise_displacement' : 0.5} - + #similar to the "Uniform" option, the input metrics should be found in parc_object.confounds. Here only timepoints + #with values below all specified thresholds will be kept for further analyses + + + #interpolation_method: options are 'linear', 'cubic_spline' and (IN FUTURE) 'spectral'. + #While scrubbed values are not included to determine any of the weights in the denoising + #model, they will still be interpolated over and then "denoised" (have nuisance variance + #removed) so that we have values to put into the optional filter at the end of processing. + #The interpolated values only have any influence on the filtering proceedure, and will be again + #removed from the time signal after filtering and thus not included in the final output. Interpolation + #methods will do weird things if there aren't many timepoints after scrubbing. All interpolation + #schemes besides spectral are essentially wrappers over scipy's 1d interpolation methods. 'spectral' + #interpolation is implemented based on code from Anish Mitra/Jonathan Power + #as shown in Power's 2014 NeuroImage paper + + + #noise_comps_dict and clean_comps_dict both have the same syntax. The values + #specified by both of these matrices will be used (along with constant and linear trend) + #to construct the denoising regression model for the input timeseries, but only the + #noise explained by the noise_comps_dict will be removed from the input timeseries ( + #plus also the constant and linear trend). Unlike the scrub_criteria_dictionary, the + #data specifed here do not need to come from the confounds section of the parc_object, + #and because of this, if you want to include something found under parc_object.confounds, + #you will need to specify "confounds" in the name. An example of the dictionary can be seen below: + # + # clean_comps_dict = {'aroma_clean_ics' : False} + # + # + # noise_comps_dict = {'aroma_noise_ics' : 5, + # 'confounds.wmcsfgsr' : False + # 'confounds.twelve_motion_regs' : False + # } + # + # + #The dictionary key should specify an element to be included in the denoising process + #and the dictionary value should be False if you don't want to do a PCA reduction on + #the set of nuisance variables (this will be the case more often than not), alternatively + #if the key represents a grouping of confounds, then you can use the value to specify the + #number of principal components to kept from a reduction of the grouping. If hpf_before_regression + #is used, the filtering will happen after the PCA. + # + # + # + + + #high_pass, low_pass: Filters to be applied as the last step in processing. + #set as False if you don't want to use them, otherwise set equal to the + #cutoff frequency + + # + #If any of the input parameters are set to True, they will be treated as if they were + #set to False, because True values wouldn't mean anything.... + # + # + # + ################################################################################################# + ################################################################################################# + ################################################################################################# + ################################################################################################# + ################################################################################################# + ################################################################################################# + + #Create an array with 1s for timepoints to use, and 0s for scrubbed timepointsx + good_timepoints = find_timepoints_to_scrub(parc_obj, scrub_criteria_dictionary) + + #Load the arrays with the data for both the clean and noise components to be used in regression + clean_comps_pre_filter = load_comps_dict(parc_obj, clean_comps_dict) + noise_comps_pre_filter = load_comps_dict(parc_obj, noise_comps_dict) + + #Apply an initial HPF to everything if necessary - this does not remove scrubbed timepoints, + #but does skips the first n_skip_vols (which will be set to 0 and not used in subsequent steps) + if hpf_before_regression != False: + + b, a = imaging_utils.construct_filter('highpass', [hpf_before_regression], parc_obj.TR, 6) + + #start with the clean comps matrix + if type(clean_comps_pre_filter) != type(False): + + clean_comps_post_filter = np.zeros(clean_comps_pre_filter.shape) + for clean_dim in range(clean_comps_pre_filter.shape[0]): + + clean_comps_post_filter[clean_dim, parc_obj.n_skip_vols:] = imaging_utils.apply_filter(b, a, clean_comps_pre_filter[clean_dim, parc_obj.n_skip_vols:]) + + #this option for both clean/noise indicates there is no input matrix to filter + else: + + clean_comps_post_filter = False + + #Move to the noise comps matrix + if type(noise_comps_pre_filter) != type(False): + + noise_comps_post_filter = np.zeros(noise_comps_pre_filter.shape) + for noise_dim in range(noise_comps_pre_filter.shape[0]): + + noise_comps_post_filter[noise_dim, parc_obj.n_skip_vols:] = imaging_utils.apply_filter(b, a, noise_comps_pre_filter[noise_dim, parc_obj.n_skip_vols:]) + + else: + + noise_comps_post_filter = False + + #then filter the original time signal + filtered_time_series = np.zeros(parc_obj.time_series.shape) + for original_ts_dim in range(parc_obj.time_series.shape[0]): + + filtered_time_series[original_ts_dim, parc_obj.n_skip_vols:] = imaging_utils.apply_filter(b, a, parc_obj.time_series[original_ts_dim, parc_obj.n_skip_vols:]) + + #If you don't want to apply the initial HPF, then + #just make a copy of the matrices of interest + else: + + clean_comps_post_filter = clean_comps_pre_filter + noise_comps_post_filter = noise_comps_pre_filter + filtered_time_series = parc_obj.time_series + + + + + #Now create the nuisance regression model. Only do this step if + #the noise_comps_post_filter isn't false. + good_timepoint_inds = np.where(good_timepoints == True)[0] + bad_timepoint_inds = np.where(good_timepoints == False)[0] + if type(noise_comps_post_filter) == type(False): + + regressed_time_signal = filtered_time_series + + else: + + + #Weird thing where I need to swap dimensions here...(implemented correctly) + + #First add constant/linear trend to the denoising model + constant = np.ones((1,filtered_time_series.shape[1])) + linear_trend = np.linspace(0,filtered_time_series.shape[1],num=filtered_time_series.shape[1]) + linear_trend = np.reshape(linear_trend, (1,filtered_time_series.shape[1]))[0] + noise_comps_post_filter = np.vstack((constant, linear_trend, noise_comps_post_filter)) + + regressed_time_signal = np.zeros(filtered_time_series.shape).transpose() + filtered_time_series_T = filtered_time_series.transpose() + + #If there aren't any clean components, + #do a "hard" or "agressive" denosing + if type(clean_comps_post_filter) == type(False): + + noise_comps_post_filter_T_to_be_used = noise_comps_post_filter[:,good_timepoint_inds].transpose() + XT_X_Neg1_XT = imaging_utils.calculate_XT_X_Neg1_XT(noise_comps_post_filter_T_to_be_used) + for temp_time_signal_dim in range(filtered_time_series.shape[0]): + regressed_time_signal[good_timepoint_inds,temp_time_signal_dim] = imaging_utils.partial_clean_fast(filtered_time_series_T[good_timepoint_inds,temp_time_signal_dim], XT_X_Neg1_XT, noise_comps_post_filter_T_to_be_used) + + + + #If there are clean components, then + #do a "soft" denoising + else: + + full_matrix_to_be_used = np.vstack((noise_comps_post_filter, clean_comps_post_filter))[:,good_timepoint_inds].transpose() + noise_comps_post_filter_T_to_be_used = noise_comps_post_filter[:,good_timepoint_inds].transpose() + XT_X_Neg1_XT = imaging_utils.calculate_XT_X_Neg1_XT(full_matrix_to_be_used) + + for temp_time_signal_dim in range(filtered_time_series.shape[0]): + regressed_time_signal[good_timepoint_inds,temp_time_signal_dim] = imaging_utils.partial_clean_fast(filtered_time_series_T[good_timepoint_inds,temp_time_signal_dim], XT_X_Neg1_XT, noise_comps_post_filter_T_to_be_used) + + + #Put back into original dimensions + regressed_time_signal = regressed_time_signal.transpose() + + + #Now apply interpolation + interpolated_time_signal = np.zeros(regressed_time_signal.shape) + + if interpolation_method == 'spectral': + + interpolated_time_signal = spectral_interpolation_fast(good_timepoints, regressed_time_signal, parc_obj.TR) + + else: + for dim in range(regressed_time_signal.shape[0]): + interpolated_time_signal[dim,:] = interpolate(good_timepoints, regressed_time_signal[dim,:], interpolation_method, parc_obj.TR) + + #Now if necessary, apply additional filterign: + if high_pass == False and low_pass == False: + + filtered_time_signal = interpolated_time_signal + + else: + + if high_pass != False and low_pass == False: + + b, a = imaging_utils.construct_filter('highpass', [high_pass], parc_obj.TR, 6) + + elif high_pass == False and low_pass != False: + + b, a = imaging_utils.construct_filter('lowpass', [low_pass], parc_obj.TR, 6) + + elif high_pass != False and low_pass != False: + + b, a = imaging_utils.construct_filter('bandpass', [high_pass, low_pass], parc_obj.TR, 6) + + filtered_time_signal = np.zeros(regressed_time_signal.shape) + for dim in range(regressed_time_signal.shape[0]): + + filtered_time_signal[dim,:] = imaging_utils.apply_filter(b,a,regressed_time_signal[dim,:]) + + + #Now set all the undefined timepoints to Nan + cleaned_time_signal = filtered_time_signal + cleaned_time_signal[:,bad_timepoint_inds] = np.nan + + return cleaned_time_signal, good_timepoint_inds + + + +def flexible_orth_denoise_parc(parc_obj, hpf_before_regression, scrub_criteria_dictionary, interpolation_method, noise_comps_dict, clean_comps_dict, high_pass, low_pass): + + #THIS FUNCTION IS THE SAME AS FLEXIBLE DENOISE PARC, + #EXCEPT FOR HERE, THE REGRESSORS IDENTIFIED BY CLEAN + #COMPS DICT ARE REGRESSED FROM THE REGRESSORS IDENTIFIED + #BY NOISE COMPS DICT PRIOR TO THE REGRESSORS FROM NOISE COMPS + #DICT BEING USED TO CLEAN THE TIMESERIES. THIS MEANS THE MODEL + #TO CLEAN THE TIMESERIES WILL ONLY CONTAIN THE ORTHOGONALIZED + #NUISANCE VARIABLES (filtering and other options will be applied + #as per usual) + + #Function inputs: + + #parc_object = a parcellated timeseries object generated from + #file "imaging_utility_classes.py" which will contain both an + #uncleaned parcellated time series, and other nuisance variables + # etc. of interest + + #hpf_before_regression = the cutoff frequency for an optional high + #pass filter that can be applied to the nuisance regressors (noise/clean) and the + #uncleaned time signal before any regression or scrubbing occurs. Recommended + #value would be 0.01 or False (False for if you want to skip this step) + + #scrub_criteria_dictionary = a dictionary that describes how scrubbing should be + #implemented. Three main options are (1) instead of inputting a dictionary, setting this + #variable to False, which will skip scrubbing, (2) {'Uniform' : [AMOUNT_TO_KEEP, ['std_dvars', 'framewise_displacement']]}, + #which will automatically only keep the best timepoints (for if you want all subjects to be scrubbed an equivelant amount). + #This option will keep every timepoint if AMOUNT_TO_KEEP was 1, and no timepoints if it was 0. The list of confounds following + #AMOUNT_TO_KEEP must at least contain one metric (but can be as many as you want) from parc_object.confounds. If more than one + #metric is given, they will be z-transformed and their sum will be used to determine which timepoints should be + #kept, with larger values being interpreted as noiser (WHICH MEANS THIS OPTION SHOULD ONLY BE USED WITH METRICS WHERE + #ZERO OR NEGATIVE BASED VALUES ARE FINE AND LARGE POSITIVE VALUES ARE BAD) - this option could potentially produce + #slightly different numbers of timepoints accross subjects still if the bad timepoints overlap to varying degrees with + #the number of timepoints that are dropped at the beginning of the scan. (3) {'std_dvars' : 1.2, 'framewise_displacement' : 0.5} - + #similar to the "Uniform" option, the input metrics should be found in parc_object.confounds. Here only timepoints + #with values below all specified thresholds will be kept for further analyses + + + #interpolation_method: options are 'linear', 'cubic_spline' and (IN FUTURE) 'spectral'. + #While scrubbed values are not included to determine any of the weights in the denoising + #model, they will still be interpolated over and then "denoised" (have nuisance variance + #removed) so that we have values to put into the optional filter at the end of processing. + #The interpolated values only have any influence on the filtering proceedure, and will be again + #removed from the time signal after filtering and thus not included in the final output. Interpolation + #methods will do weird things if there aren't many timepoints after scrubbing. All interpolation + #schemes besides spectral are essentially wrappers over scipy's 1d interpolation methods. 'spectral' + #interpolation is implemented based on code from Anish Mitra/Jonathan Power + #as shown in Power's 2014 NeuroImage paper + + + #noise_comps_dict and clean_comps_dict both have the same syntax. The values + #specified by both of these matrices will be used (along with constant and linear trend) + #to construct the denoising regression model for the input timeseries, but only the + #noise explained by the noise_comps_dict will be removed from the input timeseries ( + #plus also the constant and linear trend). Unlike the scrub_criteria_dictionary, the + #data specifed here do not need to come from the confounds section of the parc_object, + #and because of this, if you want to include something found under parc_object.confounds, + #you will need to specify "confounds" in the name. An example of the dictionary can be seen below: + # + # clean_comps_dict = {'aroma_clean_ics' : False} + # + # + # noise_comps_dict = {'aroma_noise_ics' : 5, + # 'confounds.wmcsfgsr' : False + # 'confounds.twelve_motion_regs' : False + # } + # + # + #The dictionary key should specify an element to be included in the denoising process + #and the dictionary value should be False if you don't want to do a PCA reduction on + #the set of nuisance variables (this will be the case more often than not), alternatively + #if the key represents a grouping of confounds, then you can use the value to specify the + #number of principal components to kept from a reduction of the grouping. If hpf_before_regression + #is used, the filtering will happen after the PCA. + # + # + # + + + #high_pass, low_pass: Filters to be applied as the last step in processing. + #set as False if you don't want to use them, otherwise set equal to the + #cutoff frequency + + # + #If any of the input parameters are set to True, they will be treated as if they were + #set to False, because True values wouldn't mean anything.... + # + # + # + ################################################################################################# + ################################################################################################# + ################################################################################################# + ################################################################################################# + ################################################################################################# + ################################################################################################# + + #Create an array with 1s for timepoints to use, and 0s for scrubbed timepointsx + good_timepoints = find_timepoints_to_scrub(parc_obj, scrub_criteria_dictionary) + + #Load the arrays with the data for both the clean and noise components to be used in regression + clean_comps_pre_filter = load_comps_dict(parc_obj, clean_comps_dict) + noise_comps_pre_filter = load_comps_dict(parc_obj, noise_comps_dict) + + #Apply an initial HPF to everything if necessary - this does not remove scrubbed timepoints, + #but does skips the first n_skip_vols (which will be set to 0 and not used in subsequent steps) + if hpf_before_regression != False: + + b, a = imaging_utils.construct_filter('highpass', [hpf_before_regression], parc_obj.TR, 6) + + #start with the clean comps matrix + if type(clean_comps_pre_filter) != type(False): + + clean_comps_post_filter = np.zeros(clean_comps_pre_filter.shape) + for clean_dim in range(clean_comps_pre_filter.shape[0]): + + clean_comps_post_filter[clean_dim, parc_obj.n_skip_vols:] = imaging_utils.apply_filter(b, a, clean_comps_pre_filter[clean_dim, parc_obj.n_skip_vols:]) + + #this option for both clean/noise indicates there is no input matrix to filter + else: + + clean_comps_post_filter = False + + #Move to the noise comps matrix + if type(noise_comps_pre_filter) != type(False): + + noise_comps_post_filter = np.zeros(noise_comps_pre_filter.shape) + for noise_dim in range(noise_comps_pre_filter.shape[0]): + + noise_comps_post_filter[noise_dim, parc_obj.n_skip_vols:] = imaging_utils.apply_filter(b, a, noise_comps_pre_filter[noise_dim, parc_obj.n_skip_vols:]) + + else: + + noise_comps_post_filter = False + + #then filter the original time signal + filtered_time_series = np.zeros(parc_obj.time_series.shape) + for original_ts_dim in range(parc_obj.time_series.shape[0]): + + filtered_time_series[original_ts_dim, parc_obj.n_skip_vols:] = imaging_utils.apply_filter(b, a, parc_obj.time_series[original_ts_dim, parc_obj.n_skip_vols:]) + + #If you don't want to apply the initial HPF, then + #just make a copy of the matrices of interest + else: + + clean_comps_post_filter = clean_comps_pre_filter + noise_comps_post_filter = noise_comps_pre_filter + filtered_time_series = parc_obj.time_series + + + + + #Now create the nuisance regression model. Only do this step if + #the noise_comps_post_filter isn't false. + good_timepoint_inds = np.where(good_timepoints == True)[0] + bad_timepoint_inds = np.where(good_timepoints == False)[0] + if type(noise_comps_post_filter) == type(False): + + regressed_time_signal = filtered_time_series + + else: + + + #Weird thing where I need to swap dimensions here...(implemented correctly) + + #First add constant/linear trend to the denoising model + constant = np.ones((1,filtered_time_series.shape[1])) + linear_trend = np.linspace(0,filtered_time_series.shape[1],num=filtered_time_series.shape[1]) + linear_trend = np.reshape(linear_trend, (1,filtered_time_series.shape[1]))[0] + noise_comps_post_filter = np.vstack((constant, linear_trend, noise_comps_post_filter)) + + regressed_time_signal = np.zeros(filtered_time_series.shape).transpose() + filtered_time_series_T = filtered_time_series.transpose() + + #If there aren't any clean components, + #do a "hard" or "agressive" denosing + if type(clean_comps_post_filter) == type(False): + + noise_comps_post_filter_T_to_be_used = noise_comps_post_filter[:,good_timepoint_inds].transpose() + XT_X_Neg1_XT = imaging_utils.calculate_XT_X_Neg1_XT(noise_comps_post_filter_T_to_be_used) + for temp_time_signal_dim in range(filtered_time_series.shape[0]): + regressed_time_signal[good_timepoint_inds,temp_time_signal_dim] = imaging_utils.partial_clean_fast(filtered_time_series_T[good_timepoint_inds,temp_time_signal_dim], XT_X_Neg1_XT, noise_comps_post_filter_T_to_be_used) + + + + #If there are clean components, then + #do a "soft" denoising... + + +########################################################################### +########################################################################### +####THIS CHUNK OF CODE IS THE ONLY THING TO BE CHANGED BETWEEN############# +####THE ORIGINAL FLEXIBLE DENOISE FUNC FUNCTION AND THIS ONE############### +########################################################################### +########################################################################### + + else: + + noise_comps_post_filter_T_to_be_used = noise_comps_post_filter[:,good_timepoint_inds].transpose() + clean_comps_post_filter_T_to_be_used = clean_comps_post_filter[:,good_timepoint_inds].transpose() + + orth_noise_comps_post_filter = np.zeros(noise_comps_post_filter.shape).transpose() + + initial_XT_X_Neg1_XT = imaging_utils.calculate_XT_X_Neg1_XT(clean_comps_post_filter_T_to_be_used) + for temp_time_signal_dim in range(orth_noise_comps_post_filter.shape[1]): + orth_noise_comps_post_filter[good_timepoint_inds,temp_time_signal_dim] = imaging_utils.partial_clean_fast(noise_comps_post_filter_T_to_be_used[:,temp_time_signal_dim], initial_XT_X_Neg1_XT, clean_comps_post_filter_T_to_be_used) + + noise_comps_post_filter_T_to_be_used = orth_noise_comps_post_filter[good_timepoint_inds,:] + XT_X_Neg1_XT = imaging_utils.calculate_XT_X_Neg1_XT(noise_comps_post_filter_T_to_be_used) + for temp_time_signal_dim in range(filtered_time_series.shape[0]): + regressed_time_signal[good_timepoint_inds,temp_time_signal_dim] = imaging_utils.partial_clean_fast(filtered_time_series_T[good_timepoint_inds,temp_time_signal_dim], XT_X_Neg1_XT, noise_comps_post_filter_T_to_be_used) + + #full_matrix_to_be_used = np.vstack((noise_comps_post_filter, clean_comps_post_filter))[:,good_timepoint_inds].transpose() + #noise_comps_post_filter_T_to_be_used = noise_comps_post_filter[:,good_timepoint_inds].transpose() + #XT_X_Neg1_XT = imaging_utils.calculate_XT_X_Neg1_XT(full_matrix_to_be_used) + + #for temp_time_signal_dim in range(filtered_time_series.shape[0]): + # regressed_time_signal[good_timepoint_inds,temp_time_signal_dim] = imaging_utils.partial_clean_fast(filtered_time_series_T[good_timepoint_inds,temp_time_signal_dim], XT_X_Neg1_XT, noise_comps_post_filter_T_to_be_used) + + + #Put back into original dimensions + regressed_time_signal = regressed_time_signal.transpose() + + +########################################################################### +########################################################################### +########################################################################### +########################################################################### +########################################################################### + + + #Now apply interpolation + interpolated_time_signal = np.zeros(regressed_time_signal.shape) + + if interpolation_method == 'spectral': + + interpolated_time_signal = spectral_interpolation_fast(good_timepoints, regressed_time_signal, parc_obj.TR) + + else: + for dim in range(regressed_time_signal.shape[0]): + interpolated_time_signal[dim,:] = interpolate(good_timepoints, regressed_time_signal[dim,:], interpolation_method, parc_obj.TR) + + #Now if necessary, apply additional filterign: + if high_pass == False and low_pass == False: + + filtered_time_signal = interpolated_time_signal + + else: + + if high_pass != False and low_pass == False: + + b, a = imaging_utils.construct_filter('highpass', [high_pass], parc_obj.TR, 6) + + elif high_pass == False and low_pass != False: + + b, a = imaging_utils.construct_filter('lowpass', [low_pass], parc_obj.TR, 6) + + elif high_pass != False and low_pass != False: + + b, a = imaging_utils.construct_filter('bandpass', [high_pass, low_pass], parc_obj.TR, 6) + + filtered_time_signal = np.zeros(regressed_time_signal.shape) + for dim in range(regressed_time_signal.shape[0]): + + filtered_time_signal[dim,:] = imaging_utils.apply_filter(b,a,regressed_time_signal[dim,:]) + + + #Now set all the undefined timepoints to Nan + cleaned_time_signal = filtered_time_signal + cleaned_time_signal[:,bad_timepoint_inds] = np.nan + + return cleaned_time_signal, good_timepoint_inds + + + +def spectral_interpolation_fast(timepoint_defined, signal, TR): + + + good_timepoint_inds = np.where(timepoint_defined == True)[0] + bad_timepoint_inds = np.where(timepoint_defined == False)[0] + num_timepoints = timepoint_defined.shape[0] + signal_copy = signal.copy() + + t = float(TR)*good_timepoint_inds + h = signal[:,good_timepoint_inds] + TH = np.linspace(0,(num_timepoints - 1)*TR,num=num_timepoints) + ofac = float(8) #Higher than this is slow without good quality improvements + hifac = float(1) + + N = timepoint_defined.shape[0] #Number of timepoints + T = np.max(t) - np.min(t) #Total observed timespan + + #Calculate sampling frequencies + f = np.linspace(1/(T*ofac), hifac*N/(2*T), num = int(((hifac*N/(2*T))/((1/(T*ofac))) + 1))) + + #angular frequencies and constant offsets + w = 2*np.pi*f + + t1 = np.reshape(t,((1,t.shape[0]))) + w1 = np.reshape(w,((w.shape[0],1))) + + tan_a = np.sum(np.sin(np.matmul(w1,t1*2)), axis=1) + tan_b = np.sum(np.cos(np.matmul(w1,t1*2)), axis=1) + tau = np.divide(np.arctan2(tan_a,tan_b),2*w) + + a1 = np.matmul(w1,t1) + b1 = np.asarray([np.multiply(w,tau)]*t.shape[0]).transpose() + cs_input = a1 - b1 + + #Calculate the spectral power sine and cosine terms + cterm = np.cos(cs_input) + sterm = np.sin(cs_input) + + cos_denominator = np.sum(np.power(cterm,2),axis=1) + sin_denominator = np.sum(np.power(sterm,2),axis=1) + + #The inverse function to re-construct the original time series pt. 1 + Time = TH + T_rep = np.asarray([Time]*w.shape[0]) + #already have w defined + prod = np.multiply(T_rep, w1) + sin_t = np.sin(prod) + cos_t = np.cos(prod) + + for i in range(h.shape[0]): + + ##C_final = (sum(Cmult,2).^2)./sum(Cterm.^2,2) + #This calculation is done speerately for the numerator, denominator, and the division + Cmult = np.multiply(cterm, h[i,:]) + numerator = np.sum(Cmult,axis=1) + + c = np.divide(numerator, cos_denominator) + + #Repeat the above for sine term + Smult = np.multiply(sterm,h[i,:]) + numerator = np.sum(Smult, axis=1) + s = np.divide(numerator,sin_denominator) + + #The inverse function to re-construct the original time series pt. 2 + sw_p = np.multiply(sin_t,np.reshape(s,(s.shape[0],1))) + cw_p = np.multiply(cos_t,np.reshape(c,(c.shape[0],1))) + + S = np.sum(sw_p,axis=0) + C = np.sum(cw_p,axis=0) + H = C + S + + #Normalize the reconstructed spectrum, needed when ofac > 1 + Std_H = np.std(H) + Std_h = np.std(h) + norm_fac = np.divide(Std_H,Std_h) + H = np.divide(H,norm_fac) + + signal_copy[i,bad_timepoint_inds] = H[bad_timepoint_inds] + + + return signal_copy + + diff --git a/build/lib/discovery_imaging_utils/imaging_utils.py b/build/lib/discovery_imaging_utils/imaging_utils.py new file mode 100644 index 0000000..877229e --- /dev/null +++ b/build/lib/discovery_imaging_utils/imaging_utils.py @@ -0,0 +1,780 @@ +#!/usr/bin/env python + +import sys +from nibabel import load as nib_load +import nibabel as nib +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +import pandas as pd +import statsmodels.api as sm +from scipy import signal +import os +from numpy import genfromtxt +from sklearn.decomposition import PCA + + + + +def load_gifti_func(path_to_file): + """ + #Wrapper function to load functional data from + #a gifti file using nibabel. Returns data in shape + # + """ + + gifti_img = nib_load(path_to_file) + gifti_list = [x.data for x in gifti_img.darrays] + gifti_data = np.vstack(gifti_list).transpose() + + return gifti_data + +def load_cifti_func(path_to_file): + + cifti_img = nib_load(path_to_file) + return np.asarray(cifti_img.dataobj).transpose() + + +def calc_fishers_icc(tp1, tp2): + + """ + #Calculate intraclass correlation coefficient + #from the equation on wikipedia describing + #fisher's formulation. tp1 and tp2 should + # be of shape (n,1) or (n,) where n is the + #number of samples + """ + + xhat = np.mean(np.vstack((tp1, tp2))) + sq_dif1 = np.power((tp1 - xhat),2) + sq_dif2 = np.power((tp2 - xhat),2) + s2 = np.mean(np.vstack((sq_dif1, sq_dif2))) + r = 1/(tp1.shape[0]*s2)*np.sum(np.multiply(tp1 - xhat, tp2 - xhat)) + + return r + + + + +def pre_post_carpet_plot(noisy_time_series, cleaned_time_series): + """ + #This function is for calculating a carpet plot figure, that + #will allow for comparison of the BOLD time series before and + #after denoising takes place. The two input matrices should have + #shape , and will ideally be from a + #parcellated time series and not whole hemisphere data (lots of points). + + #The script will demean and then normalize all regions' time signals, + #and then will display them side by side on grey-scale plots + """ + + + #Copy the data + noisy_data = np.copy(noisy_time_series) + clean_data = np.copy(cleaned_time_series) + + #Calculate means and standard deviations for all parcels + noisy_means = np.mean(noisy_data, axis = 1) + noisy_stds = np.std(noisy_data, axis = 1) + clean_means = np.mean(clean_data, axis = 1) + clean_stds = np.std(clean_data, axis = 1) + + #Empty matrices for demeaned and normalized data + dn_noisy_data = np.zeros(noisy_data.shape) + dn_clean_data = np.zeros(clean_data.shape) + + #Use the means and stds to mean and normalize all parcels' time signals + for i in range(0, clean_data.shape[0]): + dn_noisy_data[i,:] = (noisy_data[i,:] - noisy_means[i])/noisy_stds[i] + dn_clean_data[i,:] = (clean_data[i,:] - clean_means[i])/clean_stds[i] + + #Create a subplot + plot_obj = plt.subplot(1,2,1) + + #Plot the noisy data + img_plot = plt.imshow(dn_noisy_data, aspect = 'auto', cmap = 'binary') + plt.title('Noisy BOLD Data') + plt.xlabel('Timepoint #') + plt.ylabel('Region # (Arbritrary)') + plt.colorbar() + + #Plot the clean data + plt.subplot(1,2,2) + img_plot2 = plt.imshow(dn_clean_data, aspect = 'auto', cmap = 'binary') + plt.title('Clean BOLD Data') + plt.xlabel('Timepoint #') + plt.colorbar() + fig = plt.gcf() + fig.set_size_inches(15, 5) + + return plot_obj + + + +def parcellate_func_combine_hemis(lh_func, rh_func, lh_parcel_path, rh_parcel_path): + + """ + #Function that takes functional data in the form for + #both the left and right hemisphere, and averages the functional time series across + #all vertices defined in a given parcel, for every parcel, with the parcels identified + #by a annotation file specified at ?h_parcel_path. The function then returns a combined + #matrix of size and for the time series and + #parcel label names, respectively. The lh parcels will preceed the rh parcels in order. + + #NOTE: THIS ASSUMES THE FIRST PARCEL WILL BE MEDIAL WALL, AND DISREGARDS ANY VERTICES WITHIN + #THAT PARCEL. IF THIS IS NOT THE CASE FOR YOUR PARCELLATION, DO NOT USE THIS FUNCTION. + """ + + #Output will be tuple of format [labels, ctab, names] + lh_parcels = nib.freesurfer.io.read_annot(lh_parcel_path) + rh_parcels = nib.freesurfer.io.read_annot(rh_parcel_path) + + #Make array to store parcellated data with shape + lh_parcellated_data = np.zeros((len(lh_parcels[2]) - 1, lh_func.shape[1])) + rh_parcellated_data = np.zeros((len(rh_parcels[2]) - 1, rh_func.shape[1])) + + #Start with left hemisphere + for i in range(1,len(lh_parcels[2])): + + #Find the voxels for the current parcel + vois = np.where(lh_parcels[0] == i) + + #Take the mean of all voxels of interest + lh_parcellated_data[i-1, :] = np.mean(lh_func[vois[0],:], axis = 0) + + #Move to right hemisphere + for i in range(1,len(rh_parcels[2])): + + vois = np.where(rh_parcels[0] == i) + rh_parcellated_data[i-1, :] = np.mean(rh_func[vois[0],:], axis = 0) + + #Then concatenate parcel labels and parcel timeseries between the left and right hemisphere + #and drop the medial wall from label list + parcellated_data = np.vstack((lh_parcellated_data, rh_parcellated_data)) + parcel_labels = lh_parcels[2][1:] + rh_parcels[2][1:] + + #Try to convert the parcel labels from bytes to normal string + for i in range(0, len(parcel_labels)): + parcel_labels[i] = parcel_labels[i].decode("utf-8") + + return parcellated_data, parcel_labels + + + + +def net_mat_summary_stats(matrix_data, include_diagonals, parcel_labels): + """ + #Function that takes a network matrix of size + #and calculates summary statistics for each grouping of parcels within a + #given network combination (i.e. within DMN would be one grouping, between + #DMN and Control would be another grouping). If you would like to include + #the diagonals of the matrix set include_diagonals to true, otherwise, + #as is the case in conventional functional connectivity matrices, exclude + #the diagonal since it will most commonly be 1 or Inf. + + #This function only works on data formatted in the Schaeffer/Yeo 7 network + #configuration. + + #Parcel labels should be a list of strings that has the names of the different + #parcels in the parcellation. This is how the function knows what parcels + #belong to what networks. + """ + + + #The names of the different networks + network_names = ['Vis', 'SomMot', 'DorsAttn', 'SalVentAttn', 'Limbic', 'Cont', 'Default'] + + #Array to store network IDs (0-6, corresponding to order of network names) + network_ids = np.zeros((len(parcel_labels),1)) + + #Find which network each parcel belongs to + for i in range(0,len(parcel_labels)): + for j in range(0,len(network_names)): + + if network_names[j] in parcel_labels[i]: + network_ids[i] = j + + #Calculate the average stat for each network combination + network_stats = np.zeros((7,7)) + for i in range(0,7): + for j in range(0,7): + temp_stat = 0 + temp_stat_count = 0 + rel_inds_i = np.where(network_ids == i)[0] + rel_inds_j = np.where(network_ids == j)[0] + for inds_i in rel_inds_i: + for inds_j in rel_inds_j: + if inds_i == inds_j: + if include_diagonals == True: + temp_stat += matrix_data[inds_i, inds_j] + temp_stat_count += 1 + else: + temp_stat += matrix_data[inds_i, inds_j] + temp_stat_count += 1 + + network_stats[i,j] = temp_stat/temp_stat_count + + + return network_stats + + + +def net_summary_stats(parcel_data, parcel_labels): + """ + #Function that takes a statistic defined at a parcel level, and + #resamples that statistic to the network level. This function is a copy of + #net_mat_summary_stats only now defined to work on 1D instead of 2D data. + + #This function only works on data formatted in the Schaeffer/Yeo 7 network + #configuration. + + #Parcel labels should be a list of strings that has the names of the different + #parcels in the parcellation. This is how the function knows what parcels + #belong to what networks. + """ + + + #The names of the different networks + network_names = ['Vis', 'SomMot', 'DorsAttn', 'SalVentAttn', 'Limbic', 'Cont', 'Default'] + + #Array to store network IDs (0-6, corresponding to order of network names) + network_ids = np.zeros((len(parcel_labels),1)) + + #Find which network each parcel belongs to + for i in range(0,len(parcel_labels)): + for j in range(0,len(network_names)): + + if network_names[j] in parcel_labels[i]: + network_ids[i] = j + + #Calculate the average stat for each network combination + network_stats = np.zeros((7)) + for i in range(0,7): + temp_stat = 0 + temp_stat_count = 0 + rel_inds_i = np.where(network_ids == i)[0] + for inds_i in rel_inds_i: + temp_stat += parcel_data[inds_i] + temp_stat_count += 1 + + network_stats[i] = temp_stat/temp_stat_count + + + return network_stats + + +def plot_network_timeseries(parcel_data, parcel_labels): + + + #The names of the different networks + network_names = ['Vis', 'SomMot', 'DorsAttn', 'SalVentAttn', 'Limbic', 'Cont', 'Default'] + network_colors = [[121/255,3/255,136/255,1],[67/255,129/255,182/255,1],[0/255,150/255,0/255,1], \ + [198/255,41/255,254/255,1],[219/255,249/255,160/255,1], \ + [232/255,149/255,0/255,1], [207/255,60/255,74/255,1]] + + #Array to store network IDs (0-6, corresponding to order of network names) + network_ids = np.zeros((len(parcel_labels),1)) + + #Find which network each parcel belongs to + for i in range(0,len(parcel_labels)): + for j in range(0,len(network_names)): + + if network_names[j] in parcel_labels[i]: + network_ids[i] = j + + + + fig, ax = plt.subplots(7,1) + + for i in range(0,7): + in_network = np.where(network_ids == i)[0] + plt.sca(ax[i]) + + for j in range(0, in_network.shape[0]): + + plt.plot(parcel_data[in_network[j]], color=network_colors[i]) + + plt.ylabel('Signal Intensity') + plt.title('Time-Course For All ' + network_names[i] + ' Parcels') + + if i != 6: + plt.xticks([]) + + + plt.xlabel('Volume # (excluding high-motion volumes)') + fig.set_size_inches(15, 20) + return fig + + +def calc_norm_std(parcel_data, confound_path): + """ + #This script is used to calculate the normalized standard + #deviation of a cleaned fmri time signal. This is a metric + #representative of variability/amplitude in the BOLD signal. + #This is a particularly good option if you are working with + #scrubbed data such that the FFT for ALFF can no longer be + #properly calculated. + + #parcel_data has size . Confound + #path is the path to the confound file for the run of interest. + #The global signal will be taken from the confound file to calculate + #the median BOLD signal in the brain before pre-processing. This will then + #be used to normalize the standard deviation of the BOLD signal such that + #the output measure will be std(BOLD_Time_Series)/median_global_signal_intensity. + """ + + + + #Create a dataframe for nuisance variables in confounds + confound_df = pd.read_csv(confound_path, sep='\t') + global_signal = confound_df.global_signal.values + median_intensity = np.median(global_signal) + + parcel_std = np.zeros((parcel_data.shape[0])) + for i in range(0, parcel_data.shape[0]): + + parcel_std[i] = np.std(parcel_data[i,:])/median_intensity + + + return parcel_std + +def network_bar_chart(network_vals, ylabel): + + #The names of the different networks + network_names = ['Vis', 'SomMot', 'DorsAttn', 'SalVentAttn', 'Limbic', 'Cont', 'Default'] + network_colors = [[121/255,3/255,136/255,1],[67/255,129/255,182/255,1],[0/255,150/255,0/255,1], \ + [198/255,41/255,254/255,1],[219/255,249/255,160/255,1], \ + [232/255,149/255,0/255,1], [207/255,60/255,74/255,1]] + + x = [1, 2, 3, 4, 5, 6, 7] + fig = plt.bar(x, network_vals, color = network_colors, tick_label = network_names) + plt.ylabel(ylabel) + plt.xticks(rotation=45) + + + return fig + +def fs_anat_to_array(path_to_fs_subject, folder_for_output_files): + """ + #This function serves the function of collecting the aseg.stats file, + #lh.aparc.stats file, and rh.aparc.stats files from a freesurfer subject + #found at the path path_to_fs_subject, and grabs the volumes for all + #subcortical structures, along with volumes, thicknesses, and surface + #areas for all cortical structures, and saves them as .npy files under + #folder_for_output_files. Also saves a text file with the names of the + #regions (one for subcortical, and one for lh/rh) + """ + + aseg_path = os.path.join(path_to_fs_subject, 'stats', 'aseg.stats') + lh_path = os.path.join(path_to_fs_subject, 'stats', 'lh.aparc.stats') + rh_path = os.path.join(path_to_fs_subject, 'stats', 'rh.aparc.stats') + + + f = open(aseg_path, "r") + lines = f.readlines() + f.close() + header = '# ColHeaders Index SegId NVoxels Volume_mm3 StructName normMean normStdDev normMin normMax normRange' + subcort_names = ['Left-Lateral-Ventricle', 'Left-Inf-Lat-Vent', 'Left-Cerebellum-White-Matter', + 'Left-Cerebellum-Cortex', 'Left-Thalamus-Proper', 'Left-Caudate', 'Left-Putamen', + 'Left-Pallidum', '3rd-Ventricle', '4th-Ventricle', 'Brain-Stem', 'Left-Hippocampus', + 'Left-Amygdala', 'CSF' ,'Left-Accumbens-area', 'Left-VentralDC', 'Left-vessel', + 'Left-choroid-plexus', 'Right-Lateral-Ventricle', 'Right-Inf-Lat-Vent', + 'Right-Cerebellum-White-Matter','Right-Cerebellum-Cortex', 'Right-Thalamus-Proper', + 'Right-Caudate', 'Right-Putamen', 'Right-Pallidum', 'Right-Hippocampus', + 'Right-Amygdala', 'Right-Accumbens-area', 'Right-VentralDC', 'Right-vessel', + 'Right-choroid-plexus', '5th-Ventricle', 'WM-hypointensities', 'Left-WM-hypointensities', + 'Right-WM-hypointensities', 'non-WM-hypointensities', 'Left-non-WM-hypointensities', + 'Right-non-WM-hypointensities', 'Optic-Chiasm', 'CC_Posterior', 'CC_Mid_Posterior', + 'CC_Central', 'CC_Mid_Anterior', 'CC_Anterior'] + + aseg_vol = [] + header_found = 0 + for i in range(0,len(lines)): + + if header_found == 1: + split_line = lines[i].split() + if split_line[4] != subcort_names[i-header_found_ind]: + raise NameError('Error: anatomy names do not line up with expectation. Expected ' + + subcort_names[i-header_found_ind] + ' but found ' + split_line[4]) + aseg_vol.append(float(split_line[3])) + + + if header in lines[i]: + header_found = 1 + header_found_ind = i + 1 #actually add one for formatting.... + #This indicates that (1) the column headings should + #be correct, and that (2) this is where to start + #looking for anatomical stats + + + + lh_f = open(lh_path, "r") + lh_lines = lh_f.readlines() + lh_f.close() + + header = '# ColHeaders StructName NumVert SurfArea GrayVol ThickAvg ThickStd MeanCurv GausCurv FoldInd CurvInd' + cort_names = ['bankssts', 'caudalanteriorcingulate', 'caudalmiddlefrontal', 'cuneus', 'entorhinal', + 'fusiform', 'inferiorparietal', 'inferiortemporal', 'isthmuscingulate', 'lateraloccipital', + 'lateralorbitofrontal', 'lingual', 'medialorbitofrontal', 'middletemporal', 'parahippocampal', + 'paracentral', 'parsopercularis', 'parsorbitalis', 'parstriangularis', 'pericalcarine', + 'postcentral', 'posteriorcingulate', 'precentral', 'precuneus', 'rostralanteriorcingulate', + 'rostralmiddlefrontal', 'superiorfrontal', 'superiorparietal', 'superiortemporal', 'supramarginal', + 'frontalpole', 'temporalpole', 'transversetemporal', 'insula'] + + lh_surface_area = [] + lh_volume = [] + lh_thickness = [] + header_found = 0 + for i in range(0,len(lh_lines)): + + if header_found == 1: + split_line = lh_lines[i].split() + if split_line[0] != cort_names[i-header_found_ind]: + raise NameError('Error: anatomy names do not line up with expectation. Expected ' + + cort_names[i-header_found_ind] + ' but found ' + split_line[4]) + #then insert text to actually grab/save the data..... + + lh_surface_area.append(float(split_line[2])) + lh_volume.append(float(split_line[3])) + lh_thickness.append(float(split_line[4])) + + if header in lh_lines[i]: + header_found = 1 + header_found_ind = i + 1 #actually add one for formatting.... + #This indicates that (1) the column headings should + #be correct, and that (2) this is where to start + #looking for anatomical stats + + + + rh_f = open(rh_path, "r") + rh_lines = rh_f.readlines() + rh_f.close() + + rh_surface_area = [] + rh_volume = [] + rh_thickness = [] + header_found = 0 + for i in range(0,len(rh_lines)): + + if header_found == 1: + split_line = rh_lines[i].split() + if split_line[0] != cort_names[i-header_found_ind]: + raise NameError('Error: anatomy names do not line up with expectation. Expected ' + + cort_names[i-header_found_ind] + ' but found ' + split_line[4]) + #then insert text to actually grab/save the data..... + + rh_surface_area.append(float(split_line[2])) + rh_volume.append(float(split_line[3])) + rh_thickness.append(float(split_line[4])) + + if header in rh_lines[i]: + header_found = 1 + header_found_ind = i + 1 #actually add one for formatting.... + #This indicates that (1) the column headings should + #be correct, and that (2) this is where to start + #looking for anatomical stats + + if os.path.exists(folder_for_output_files) == False: + os.mkdir(folder_for_output_files) + + #Save the metrics as numpy files + np.save(os.path.join(folder_for_output_files, 'aseg_vols.npy'), np.asarray(aseg_vol)) + np.save(os.path.join(folder_for_output_files, 'lh_aseg_surface_areas.npy'), np.asarray(lh_surface_area)) + np.save(os.path.join(folder_for_output_files, 'lh_aseg_volumes.npy'), np.asarray(lh_volume)) + np.save(os.path.join(folder_for_output_files, 'lh_aseg_thicknesses.npy'), np.asarray(lh_thickness)) + np.save(os.path.join(folder_for_output_files, 'rh_aseg_surface_areas.npy'), np.asarray(rh_surface_area)) + np.save(os.path.join(folder_for_output_files, 'rh_aseg_volumes.npy'), np.asarray(rh_volume)) + np.save(os.path.join(folder_for_output_files, 'rh_aseg_thicknesses.npy'), np.asarray(rh_thickness)) + + #Calculate some bilateral metrics + left_vent = 0 + right_vent = 18 + total_lateral_vent = aseg_vol[left_vent] + aseg_vol[right_vent] + + left_hipp = 11 + right_hipp = 26 + total_hipp_vol = aseg_vol[left_hipp] + aseg_vol[right_hipp] + + left_thal = 4 + right_thal = 22 + total_thal_vol = aseg_vol[left_thal] + aseg_vol[right_thal] + + left_amyg = 12 + right_amyg = 27 + total_amyg_vol = aseg_vol[left_amyg] + aseg_vol[right_amyg] + + #Also calculate global thickness + numerator = np.sum(np.multiply(lh_surface_area,lh_thickness)) + np.sum(np.multiply(rh_surface_area,rh_thickness)) + denominator = np.sum(lh_surface_area) + np.sum(rh_surface_area) + whole_brain_ave_thick = numerator/denominator + + discovery_metric_array = [total_hipp_vol, total_amyg_vol, total_thal_vol, + total_lateral_vent, whole_brain_ave_thick] + + np.save(os.path.join(folder_for_output_files, 'discovery_anat_metrics.npy'), np.asarray(discovery_metric_array)) + discovery_anat_ids = ['bilateral_hipp_volume', 'bilateral_amyg_vol', 'bilateral_thal_vol', + 'bilateral_lateral_vent_vol', 'whole_brain_ave_thick'] + + #Then save a file with the region names + with open(os.path.join(folder_for_output_files, 'subcortical_region_names.txt'), 'w') as f: + for item in subcort_names: + f.write("%s\n" % item) + + with open(os.path.join(folder_for_output_files, 'cortical_region_names.txt'), 'w') as f: + for item in cort_names: + f.write("%s\n" % item) + + with open(os.path.join(folder_for_output_files, 'discovery_region_names.txt'), 'w') as f: + for item in discovery_anat_ids: + f.write("%s\n" % item) + + return + + + + +def calculate_XT_X_Neg1_XT(X): + + """ + #Calculate term that can be multiplied with + #Y to calculate the beta weights for least + #squares regression. X should be of shape + #(n x d) where n is the number of observations + #and d is the number of dimensions/predictors + #uses inverse transform + """ + + XT = X.transpose() + XT_X_Neg1 = np.linalg.pinv(np.matmul(XT,X)) + return np.matmul(XT_X_Neg1, XT) + +def partial_clean_fast(Y, XT_X_Neg1_XT, bad_regressors): + + """ + #Function to help in the denoising of time signal Y with shape + #(n,1) or (n,) where n is the number of timepoints. + #XT_X_Neg1_XT is ((X^T)*X)^-1*(X^T), where ^T represents transpose + #and ^-1 represents matrix inversions. X contains bad regressors including + #noise ICs, a constant component, and a linear trend (etc.), and good regressors + #containing non-motion related ICs. The Beta weights for the linear model + #will be solved by multiplying XT_X_Neg1_XT with Y, and then the beta weights + #determined for the bad regressors will be subtracted off from Y and the residuals + #from this operation will be returned. For this reason, it is important to + #put all bad regressors in front when doing matrix multiplication + """ + + B = np.matmul(XT_X_Neg1_XT, Y) + Y_noise = np.matmul(bad_regressors, B[:bad_regressors.shape[1]]) + return (Y - Y_noise) + + +from scipy.signal import butter, filtfilt +def construct_filter(btype, cutoff, TR, order): + + """ + #btype should be 'lowpass', 'highpass', or 'bandpass' and + #cutoff should be list (in Hz) with length 1 for low and high and + #2 for band. Order is the order of the filter + #which will be doubled since filtfilt will be used + #to remove phase distortion from the filter. Recommended + #order is 6. Will return filter coefficients b and a for + #the desired butterworth filter. + + #Constructs filter coefficients. Use apply_filter to use + #the coefficients to filter a signal. + + #Should have butter imported from scipy.signal + """ + + + nyq = 0.5 * (1/TR) + + if btype == 'lowpass': + if len(cutoff) != 1: + raise NameError('Error: lowpass type filter should have one cutoff values') + low = cutoff[0]/nyq + b, a = butter(order, low, btype='lowpass') + + elif btype == 'highpass': + if len(cutoff) != 1: + raise NameError('Error: highpass type filter should have one cutoff values') + high = cutoff[0]/nyq + b, a = butter(order, high, btype='highpass') + + elif btype == 'bandpass': + if len(cutoff) != 2: + raise NameError('Error: bandpass type filter should have two cutoff values') + low = min(cutoff)/nyq + high = max(cutoff)/nyq + b, a = butter(order, [low, high], btype='bandpass') + + else: + raise NameError('Error: filter type should by low, high, or band') + + + return b, a + + +######################################################################################## +######################################################################################## +######################################################################################## + +def apply_filter(b, a, signal): + + """ + #Wrapper function to apply the filter coefficients from + #construct_filter to a signal. + + #should have filtfilt imported from scipy.signal + """ + + filtered_signal = filtfilt(b, a, signal) + return filtered_signal + + +######################################################################################## +######################################################################################## +######################################################################################## + +def output_stats_figures_pa_ap_compare(cleaned_ap, cleaned_pa): + cleaned_ap_netmat = np.corrcoef(cleaned_ap) + cleaned_pa_netmat = np.corrcoef(cleaned_pa) + + plt.figure() + plt.imshow(cleaned_ap_netmat) + plt.colorbar() + plt.title('AP Conn Matrix') + plt.figure() + cleaned_ap.shape + + plt.imshow(cleaned_pa_netmat) + plt.colorbar() + plt.title('PA Conn Matrix') + plt.figure() + + corr_dif = cleaned_ap_netmat - cleaned_pa_netmat + plt.imshow(np.abs(corr_dif), vmin=0, vmax=0.1) + plt.title('abs(AP - PA)') + plt.colorbar() + plt.figure() + + plt.hist(np.abs(np.reshape(corr_dif, corr_dif.shape[0]**2)), bins = 20) + plt.title('abs(AP - PA) mean = ' + str(np.mean(np.abs(corr_dif)))) + + ap_arr = cleaned_ap_netmat[np.triu_indices(cleaned_ap_netmat.shape[0], k = 1)] + pa_arr = cleaned_pa_netmat[np.triu_indices(cleaned_pa_netmat.shape[0], k = 1)] + plt.figure() + plt.scatter(ap_arr, pa_arr) + plt.title('AP-PA corr: ' + str(np.corrcoef(ap_arr, pa_arr)[0,1])) + + + + + +def find_mean_fd(path_to_func): + + #For a functional path (must be pointing to fsaverage), + #and a list of confounds (from *desc-confounds_regressors.tsv). + #This function will make two matrices of shape (t x n), where + #t is the number of timepoints, and n the number of regressors. + #The first matrix will contain 'nuisance_vars' which will be + #a combination of the variables from list_of_confounds, and + #independent components identified as noise by ICA-AROMA. + #The second will contain the indpendent components not identified + #by ICA-AROMA, which are presumed to contain meaningful functional + #data + + confound_path = path_to_func[:-31] + 'desc-confounds_regressors.tsv' + + confound_df = pd.read_csv(confound_path, sep='\t') + partial_confounds = [] + temp = confound_df.loc[ : , 'framewise_displacement' ] + fd_arr = np.copy(temp.values) + + return np.mean(fd_arr[1:]) + + +def convert_to_upper_arr(np_square_matrix): + """ + #Function that takes a square matrix, + #and outputs its upper triangle without + #the diagonal as an array + """ + + + inds = np.triu_indices(np_square_matrix.shape[0], k = 1) + return np_square_matrix[inds] + + + + +def demedian_parcellate_func_combine_hemis(lh_func, rh_func, lh_parcel_path, rh_parcel_path): + + """ + #Function that takes functional data in the form for + #both the left and right hemisphere, and averages the functional time series across + #all vertices defined in a given parcel, for every parcel, with the parcels identified + #by a annotation file specified at ?h_parcel_path. The function then returns a combined + #matrix of size and for the time series and + #parcel label names, respectively. The lh parcels will preceed the rh parcels in order. + + #Prior to taking the average of all vertices, all vertices time signals are divided by their + #median signal intensity. The mean of all these medians within a given parcel is then + #exported with this function as the third argument + + #NOTE: THIS ASSUMES THE FIRST PARCEL WILL BE MEDIAL WALL, AND DISREGARDS ANY VERTICES WITHIN + #THAT PARCEL. IF THIS IS NOT THE CASE FOR YOUR PARCELLATION, DO NOT USE THIS FUNCTION. + """ + + #Output will be tuple of format [labels, ctab, names] + lh_parcels = nib.freesurfer.io.read_annot(lh_parcel_path) + rh_parcels = nib.freesurfer.io.read_annot(rh_parcel_path) + + #Make array to store parcellated data with shape + lh_parcellated_data = np.zeros((len(lh_parcels[2]) - 1, lh_func.shape[1])) + rh_parcellated_data = np.zeros((len(rh_parcels[2]) - 1, rh_func.shape[1])) + lh_parcel_medians = np.zeros(len(lh_parcels[2]) - 1) + rh_parcel_medians = np.zeros(len(rh_parcels[2]) - 1) + + + lh_vertex_medians = np.nanmedian(lh_func, axis=1) + rh_vertex_medians = np.nanmedian(rh_func, axis=1) + + lh_vertex_medians[np.where(lh_vertex_medians < 0.001)] = np.nan + rh_vertex_medians[np.where(rh_vertex_medians < 0.001)] = np.nan + + lh_adjusted_func = lh_func/lh_vertex_medians[:,None] + rh_adjusted_func = rh_func/rh_vertex_medians[:,None] + + + + + #Start with left hemisphere + for i in range(1,len(lh_parcels[2])): + + #Find the voxels for the current parcel + vois = np.where(lh_parcels[0] == i) + + #Take the mean of all voxels of interest + lh_parcellated_data[i-1, :] = np.nanmean(lh_adjusted_func[vois[0],:], axis = 0) + lh_parcel_medians[i-1] = np.nanmean(lh_vertex_medians[vois[0]]) + + #Move to right hemisphere + for i in range(1,len(rh_parcels[2])): + + vois = np.where(rh_parcels[0] == i) + rh_parcellated_data[i-1, :] = np.nanmean(rh_adjusted_func[vois[0],:], axis = 0) + rh_parcel_medians[i-1] = np.nanmean(rh_vertex_medians[vois[0]]) + + #Then concatenate parcel labels and parcel timeseries between the left and right hemisphere + #and drop the medial wall from label list + parcellated_data = np.vstack((lh_parcellated_data, rh_parcellated_data)) + parcel_labels = lh_parcels[2][1:] + rh_parcels[2][1:] + parcel_medians = np.hstack((lh_parcel_medians, rh_parcel_medians)) + + #Try to convert the parcel labels from bytes to normal string + for i in range(0, len(parcel_labels)): + parcel_labels[i] = parcel_labels[i].decode("utf-8") + + return parcellated_data, parcel_labels, parcel_medians diff --git a/build/lib/discovery_imaging_utils/imaging_visualizations.py b/build/lib/discovery_imaging_utils/imaging_visualizations.py new file mode 100644 index 0000000..3802a97 --- /dev/null +++ b/build/lib/discovery_imaging_utils/imaging_visualizations.py @@ -0,0 +1,305 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib +from mpl_toolkits.axes_grid1 import make_axes_locatable + +def imagesc_schaeffer_17(connectivity_matrix, parcel_labels, minmax, border_width=14, add_colorbar=True, dpi=200, + x_tick_labels=True, y_tick_labels=True, matplotlib_color_scheme='jet', + x_tick_font_size='xx-small',y_tick_font_size='xx-small', title=''): + + """This function can make a connectomic plot for the 17 network schaeffer parcellation + at any resolution. Needs to take a nxn numpy matrix, a length n list of parcel names (taken + directly from the Schaeffer/Yeo parcellation), and a two element list specifying the + minimum and maximum for the color scale (i.e. minmax = [0, 1]). You can choose how + wide you want the border coloring to be, whether or not to use a colorbar, figure resolution, + etc. (see kwargs above). tick font size and coloring schemes accept values that work with + matplotlib. + + + example usage: + imagesc_schaeffer_17(nxn_conn_mat_as_np_array, len_n_list_of_label_names, [-1, 1])""" + + + import numpy as np + import matplotlib.pyplot as plt + import matplotlib + from mpl_toolkits.axes_grid1 import make_axes_locatable + + #The names of the different networks for the visualization + network_names = ['Vis. A', 'Vis. B', 'SomMot. A', 'SomMot. B', 'Temp. Par.', 'Dors. Attn. A', + 'Dors. Attn. B', 'Sal. A', 'Sal. B', 'Cont. A', 'Cont. B', 'Cont. C', 'DMN A', + 'DMN B', 'DMN C', 'Limbic A', 'Limbic B'] + + #The name of different networks to pull out of the labels + network_identifiers = ['VisCent','VisPeri','SomMotA','SomMotB','TempPar', 'DorsAttnA', + 'DorsAttnB','SalVentAttnA','SalVentAttnB','ContA','ContB','ContC','DefaultA', + 'DefaultB','DefaultC','Limbic_OFC','Limbic_TempPole'] + + network_colors = [ + [97/255, 38/255, 107/255, 1], #vis. a + [195/255, 40/255, 39/255, 1], #vis. b + [79/255, 130/255, 165/255, 1], #sommot a + [82/255, 181/255, 140/255, 1], #sommat b + [53/255, 75/255, 159/255, 1], #temp par + [75/255, 147/255, 72/255, 1], #dors attn a + [50/255, 116/255, 62/255, 1], #dors attn b + [149/255, 77/255, 158/255, 1], #sal A + [222/255, 130/255, 177/255, 1], #sal B + [210/255, 135/255, 48/255, 1], #cont a + [132/255, 48/255, 73/255, 1], #cont b + [92/255, 107/255, 130/255, 1], #cont c + [217/255, 221/255, 72/255, 1], #dmn a + [176/255, 49/255, 69/255, 1], #dmn b + [41/255, 37/255, 99/255, 1], #dmn c + [75/255, 87/255, 61/255, 1], #limbic a + [149/255, 166/255, 110/255, 1] #limbic b + ] + + #[121,3,136,1] + #Array to store network IDs (0-6, corresponding to order of network names) + network_ids = np.zeros((len(parcel_labels),1)) + + #Find which network each parcel belongs to + for i in range(0,len(parcel_labels)): + for j in range(0,len(network_identifiers)): + + if network_identifiers[j] in parcel_labels[i]: + network_ids[i] = j + + + #Create arrays for the sorted network ids and also store the inds to + #obtain the sorted matrix + sorted_ids = np.sort(network_ids, axis = 0, kind = 'mergesort') + sorted_id_inds = np.argsort(network_ids, axis = 0, kind = 'mergesort') + + #Calculate where the center and edge of each network is for labeling + #different networks on netmat figures + network_edges = np.zeros((len(network_names),1)) + for i in range(0,len(network_names)): + for j in range(0,len(parcel_labels)): + + #if sorted_id_inds[j] == i: + if sorted_ids[j] == i: + + network_edges[i] = j + + network_centers = np.zeros((len(network_names),1)) + network_centers[0] = network_edges[0]/2.0 + for i in range(1,len(network_edges)): + network_centers[i] = (network_edges[i] + network_edges[i-1])/2.0 + + + #Sort the connectivity matrix to be aligned with networks + sorted_conn_matrix = np.zeros(connectivity_matrix.shape) + sorted_conn_matrix = np.reshape(connectivity_matrix[sorted_id_inds,:], connectivity_matrix.shape) + sorted_conn_matrix = np.reshape(sorted_conn_matrix[:,sorted_id_inds], connectivity_matrix.shape) + + + cmap = getattr(matplotlib.cm, matplotlib_color_scheme) + norm = matplotlib.colors.Normalize(vmin=minmax[0], vmax=minmax[1]) + + m = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap) + jet_conn_matrix = m.to_rgba(sorted_conn_matrix, norm=True) + + jet_conn_with_borders = np.zeros((jet_conn_matrix.shape[0] + border_width, jet_conn_matrix.shape[1] + border_width, \ + jet_conn_matrix.shape[2])) + jet_conn_with_borders[0:(-1*border_width),border_width:,:] = jet_conn_matrix + for i in range(0,sorted_ids.shape[0]): + jet_conn_with_borders[i,0:border_width,:] = network_colors[int(sorted_ids[i])] + jet_conn_with_borders[(-1*border_width):,i+border_width,:] = network_colors[int(sorted_ids[i])] + + +###################################################################################### +###################################################################################### +###################################################################################### + #Calculate where the center and edge of each network is for labeling + #different networks on netmat figures + network_edges = np.zeros((len(network_names),1)) + for i in range(0,len(network_names)): + for j in range(0,len(parcel_labels)): + + if sorted_ids[j] == i: + + network_edges[i] = j + + network_centers = np.zeros((len(network_names),1)) + network_centers[0] = network_edges[0]/2.0 + for i in range(1,len(network_edges)): + network_centers[i] = (network_edges[i] + network_edges[i-1])/2.0 + + + #Make and plot figure + fig = plt.figure(dpi=fig_dpi) + plot_obj = plt.imshow(jet_conn_with_borders) + if len(plot_title) > 0: + plt.title(plot_title) + + #Add lines to identify network borders + for i in network_edges[:-1]: + plt.axvline(x=i + border_width + 0.7,color='black', lw=1) + plt.axvline(x=border_width - 0.7, color='black', lw=1) + + for i in network_edges: + plt.axhline(y=i,color='black', lw=1) + +###################################################################################### +###################################################################################### +###################################################################################### + + #optionally add x tick labels + if x_tick_labels: + plt.xticks(ticks=network_centers + border_width,labels=network_names, rotation=90, fontsize=x_tick_font_size) + + #optionally add y tick labels + if y_tick_labels: + plt.yticks(ticks=network_centers,labels=network_names, fontsize=y_tick_font_size) + + #optionally add colorbar + if add_colorbar: + ax = plt.gca() + mappable = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap) + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", size="5%", pad=0.15) + plt.colorbar(mappable = mappable, cax = cax) + + + + return #fig + + + +def imagesc_schaeffer_7(connectivity_matrix, parcel_labels, minmax, border_width=14, add_colorbar=True, dpi=200, + x_tick_labels=True, y_tick_labels=True, matplotlib_color_scheme='jet', + x_tick_font_size='xx-small',y_tick_font_size='xx-small', title=''): + + """This function can make a connectomic plot for the 7 network schaeffer parcellation + at any resolution. Needs to take a nxn numpy matrix, a length n list of parcel names (taken + directly from the Schaeffer/Yeo parcellation), and a two element list specifying the + minimum and maximum for the color scale (i.e. minmax = [0, 1]). You can choose how + wide you want the border coloring to be, whether or not to use a colorbar, figure resolution, + etc. (see kwargs above). tick font size and coloring schemes accept values that work with + matplotlib.""" + + import numpy as np + import matplotlib.pyplot as plt + import matplotlib + from mpl_toolkits.axes_grid1 import make_axes_locatable + + #The names of the different networks + network_names = ['Vis', 'SomMot', 'DorsAttn', 'SalVentAttn', 'Limbic', 'Cont', 'Default'] + network_identifiers = ['Vis', 'SomMot', 'DorsAttn', 'SalVentAttn', 'Limbic', 'Cont', 'Default'] + network_colors = [[121/255,3/255,136/255,1],[67/255,129/255,182/255,1],[0/255,150/255,0/255,1], \ + [198/255,41/255,254/255,1],[219/255,249/255,160/255,1], \ + [232/255,149/255,0/255,1], [207/255,60/255,74/255,1]] + + + + #Array to store network IDs (0-6, corresponding to order of network names) + network_ids = np.zeros((len(parcel_labels),1)) + + #Find which network each parcel belongs to + for i in range(0,len(parcel_labels)): + for j in range(0,len(network_identifiers)): + + if network_identifiers[j] in parcel_labels[i]: + network_ids[i] = j + + + #Create arrays for the sorted network ids and also store the inds to + #obtain the sorted matrix + sorted_ids = np.sort(network_ids, axis = 0, kind = 'mergesort') + sorted_id_inds = np.argsort(network_ids, axis = 0, kind = 'mergesort') + + #Calculate where the center and edge of each network is for labeling + #different networks on netmat figures + network_edges = np.zeros((len(network_names),1)) + for i in range(0,len(network_names)): + for j in range(0,len(parcel_labels)): + + #if sorted_id_inds[j] == i: + if sorted_ids[j] == i: + + network_edges[i] = j + + network_centers = np.zeros((len(network_names),1)) + network_centers[0] = network_edges[0]/2.0 + for i in range(1,len(network_edges)): + network_centers[i] = (network_edges[i] + network_edges[i-1])/2.0 + + + #Sort the connectivity matrix to be aligned with networks + sorted_conn_matrix = np.zeros(connectivity_matrix.shape) + sorted_conn_matrix = np.reshape(connectivity_matrix[sorted_id_inds,:], connectivity_matrix.shape) + sorted_conn_matrix = np.reshape(sorted_conn_matrix[:,sorted_id_inds], connectivity_matrix.shape) + + + cmap = getattr(matplotlib.cm, matplotlib_color_scheme) + norm = matplotlib.colors.Normalize(vmin=minmax[0], vmax=minmax[1]) + + m = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap) + jet_conn_matrix = m.to_rgba(sorted_conn_matrix, norm=True) + + jet_conn_with_borders = np.zeros((jet_conn_matrix.shape[0] + border_width, jet_conn_matrix.shape[1] + border_width, \ + jet_conn_matrix.shape[2])) + jet_conn_with_borders[0:(-1*border_width),border_width:,:] = jet_conn_matrix + for i in range(0,sorted_ids.shape[0]): + jet_conn_with_borders[i,0:border_width,:] = network_colors[int(sorted_ids[i])] + jet_conn_with_borders[(-1*border_width):,i+border_width,:] = network_colors[int(sorted_ids[i])] + + +###################################################################################### +###################################################################################### +###################################################################################### + #Calculate where the center and edge of each network is for labeling + #different networks on netmat figures + network_edges = np.zeros((len(network_names),1)) + for i in range(0,len(network_names)): + for j in range(0,len(parcel_labels)): + + if sorted_ids[j] == i: + + network_edges[i] = j + + network_centers = np.zeros((len(network_names),1)) + network_centers[0] = network_edges[0]/2.0 + for i in range(1,len(network_edges)): + network_centers[i] = (network_edges[i] + network_edges[i-1])/2.0 + + + #Make and plot figure + fig = plt.figure(dpi=fig_dpi) + plot_obj = plt.imshow(jet_conn_with_borders) + if len(plot_title) > 0: + plt.title(plot_title) + + #Add lines to identify network borders + for i in network_edges[:-1]: + plt.axvline(x=i + border_width + 0.7,color='black', lw=1) + plt.axvline(x=border_width - 0.7, color='black', lw=1) + + for i in network_edges: + plt.axhline(y=i,color='black', lw=1) + +###################################################################################### +###################################################################################### +###################################################################################### + + #optionally add x tick labels + if x_tick_labels: + plt.xticks(ticks=network_centers + border_width,labels=network_names, rotation=90, fontsize=x_tick_font_size) + + #optionally add y tick labels + if y_tick_labels: + plt.yticks(ticks=network_centers,labels=network_names, fontsize=y_tick_font_size) + + #optionally add colorbar + if add_colorbar: + ax = plt.gca() + mappable = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap) + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", size="5%", pad=0.15) + plt.colorbar(mappable = mappable, cax = cax) + + + + return #fig diff --git a/build/lib/discovery_imaging_utils/nifti_utils.py b/build/lib/discovery_imaging_utils/nifti_utils.py new file mode 100644 index 0000000..ed13cb0 --- /dev/null +++ b/build/lib/discovery_imaging_utils/nifti_utils.py @@ -0,0 +1,94 @@ +import numpy as np +import nibabel as nib + + + + +def convert_spherical_roi_coords_to_nifti(template_nifti_path, spherical_coords, radius, output_nifti_path, spherical_labels=None): + """ + #Template_nifti_path should point to a nifti with the desired + #affine matrix/size, should be 3d, not a timeseries. Spherical_coords + #should be a list of RAS coordinates for the spheres, radius should be a list + #of radii for the different spheres, output_nifti_path is where the mask file + #will be saved. Spherical labels is an optional list that can specify the number + #assigned to values for different spheres. If this isn't set, spheres will be labeled + #1, 2, 3 ... etc. + """ + + template_nifti = nib.load(template_nifti_path) + affine = template_nifti.affine + mask_vol = np.zeros(template_nifti.get_fdata().shape) + + for i in range(mask_vol.shape[0]): + for j in range(mask_vol.shape[1]): + for k in range(mask_vol.shape[2]): + + temp_ras = np.matmul(affine,[i, j, k, 1])[0:3] + for l in range(len(spherical_coords)): + + distance = np.linalg.norm(spherical_coords[l] - temp_ras) + + if radius[l] <= distance: + + if type(spherical_labels) == None: + mask_vol[i,j,k] = l + 1 + else: + mask_vol[i,j,k] = spherical_labels[l] + break + + template_header = template_nifti.header + img = nib.nifti1Image(mask_vol, affine, header = template_header) + nib.save(img, output_nifti_path) + + + + + + +def nifti_rois_to_time_signals(input_timeseries_nii_path, input_mask_nii_path, demedian_before_averaging = True): + + """ + #Function that takes a 4d nifti file with path input_timeseries_nii_path, + #and a 3d mask registered to the 4d timeseries (input_mask_nii_path) who has, + #different values which specify different regions, and returns average time + #signals for the different regions. By default, each voxel in a given ROI will be + #divided by its median prior to averaging. The mean of all voxel medians will be + #provided as output. To turn off this normalization, set demedian_before_averaging + #to False. + # + #Output nifti_time_series - size n_regions, n_timepoints + #unique_mask_vals - size n_regions (specifying the ID for each mask) + #parc_mean_median_signal_intensities - size n_regions + """ + + input_ts_nii = nib.load(input_timeseries_nii_path) + input_mask_nii = nib.load(input_mask_nii_path) + + input_mask_matrix = input_mask_nii.get_fdata() + input_ts_matrix = input_ts_nii.get_fdata() + unique_mask_vals = np.unique(input_mask_matrix) + unique_mask_vals.sort() + unique_mask_vals = unique_mask_vals[1:] + + nifti_time_series = np.zeros((unique_mask_vals.shape[0], input_ts_matrix.shape[3])) + parc_mean_median_signal_intensities = np.zeros(unique_mask_vals.shape[0]) + + + + for i in range(len(unique_mask_vals)): + + inds = np.where(input_mask_matrix == unique_mask_vals[i]) + temp_timeseries = input_ts_matrix[inds] + + voxel_medians = np.nanmedian(temp_timeseries, axis=1) + voxel_medians[np.where(voxel_medians < 0.001)] = np.nan + + if demedian_before_averaging: + temp_timeseries = temp_timeseries/voxel_medians[:,None] + + + + nifti_time_series[i,:] = np.nanmean(temp_timeseries, axis=0) + parc_mean_median_signal_intensities[i] = np.nanmean(voxel_medians) + + return nifti_time_series, unique_mask_vals, parc_mean_median_signal_intensities diff --git a/build/lib/discovery_imaging_utils/parc_ts_dictionary.py b/build/lib/discovery_imaging_utils/parc_ts_dictionary.py new file mode 100644 index 0000000..ab94275 --- /dev/null +++ b/build/lib/discovery_imaging_utils/parc_ts_dictionary.py @@ -0,0 +1,344 @@ +import pandas as pd +import numpy as np +import os +import glob +from discovery_imaging_utils import imaging_utils +from discovery_imaging_utils import nifti_utils +import json + +#Run in this order: +#(1) file_paths_dict = generate_file_paths(....) +#(2) check if files present with all_file_paths_exist(...) +#(3) if files present, parc_ts_dict = populate_parc_dictionary(....) +#(4) then use save/load functions to store in directory structure + + + + +def all_file_paths_exist(file_path_dictionary): + """Takes a dictionary of file paths and checks if they exist. + + Takes a dictionary where each entry is a string representing a file + path, and iterates over all entries checking whether or not they each + point to a file. + + Parameters + ---------- + file_path_dictionary : dict + a dictionary where all entries are paths to files + + Returns + ------- + files_present : bool + a boolean saying whether or not all files in the dictionary were found + + """ + + #Check if all files exist, and if they don't + #return False + files_present = True + + for temp_field in file_path_dictionary: + + if os.path.exists(file_path_dictionary[temp_field]) == False: + print(file_path_dictionary[temp_field]) + files_present = False + + return files_present + +def generate_file_paths(lh_gii_path=None, lh_parcellation_path=None, nifti_ts_path=None, + nifti_parcellation_path=None, aroma_included=True): + """ + #This function gathers paths useful for imaging analyses... either a gifti or nifti + #or both must be specified along with accompanying parcellation paths, outputs a dictionary + #with relevant paths, output of this function can be used with "all_file_paths_exist", then + #with "populate_parc_dictionary", then can be saved with "save_dictionary" + """ + + path_dictionary = {} + prefix = '' + + #Gifti related paths + if type(lh_gii_path) != type(None): + + if type(lh_parcellation_path) == type(None): + + raise NameError('Error: need to specify lh parcellation path if you specify a lh_gii_path') + + path_dictionary['lh_func_path'] = lh_gii_path + path_dictionary['rh_func_path'] = lh_gii_path[:-10] + 'R.func.gii' + path_dictionary['lh_parcellation_path'] = lh_parcellation_path + + lh_parcel_end = lh_parcellation_path.split('/')[-1] + + path_dictionary['rh_parcellation_path'] = lh_parcellation_path[:-len(lh_parcel_end)] + 'r' + lh_parcellation_path.split('/')[-1][1:] + + #For finding aroma/nuisance files + prefix = '_'.join(lh_gii_path.split('_')[:-2]) + + #Nifti related paths + if type(nifti_ts_path) != type(None): + + if type(nifti_parcellation_path) == type(None): + + raise NameError('Error: need to specify nifti parcellation path if you specify a nifti_ts_path') + + path_dictionary['nifti_func_path'] = nifti_ts_path + path_dictionary['nifti_parcellation_path'] = nifti_parcellation_path + + nifti_parcellation_json = nifti_parcellation_path.split('.')[0] + '.json' + if os.path.exists(nifti_parcellation_json) == True: + path_dictionary['nifti_parcellation_json_path'] = nifti_parcellation_json + + if prefix != '': + if '_'.join(nifti_ts_path.split('_')[:-3]) != prefix: + raise NameError('Error: It doesnt look like the nifti and gifti timeseries point to the same run') + + prefix = '_'.join(nifti_ts_path.split('_')[:-3]) + + + #Aroma related paths + if aroma_included: + path_dictionary['melodic_mixing_path'] = prefix + '_desc-MELODIC_mixing.tsv' + path_dictionary['aroma_noise_ics_path'] = prefix + '_AROMAnoiseICs.csv' + + #Confounds path + path_dictionary['confounds_regressors_path'] = prefix + '_desc-confounds_regressors.tsv' + + return path_dictionary + + +def populate_parc_dictionary(file_path_dictionary, TR): + + parc_ts_dictionary = {} + has_gifti = False + has_nifti = False + + ############################################################################## + #Load the timeseries data and apply parcellation, saving also the parcel labels + + if 'lh_func_path' in file_path_dictionary.keys(): + + has_gifti = True + lh_data = imaging_utils.load_gifti_func(file_path_dictionary['lh_func_path']) + rh_data = imaging_utils.load_gifti_func(file_path_dictionary['rh_func_path']) + time_series_cortex, parcel_labels_cortex, parc_median_signal_intensities = imaging_utils.demedian_parcellate_func_combine_hemis(lh_data, rh_data, file_path_dictionary['lh_parcellation_path'], file_path_dictionary['rh_parcellation_path']) + + parc_ts_dictionary['surface_labels'] = parcel_labels_cortex + parc_ts_dictionary['surface_time_series'] = time_series_cortex + parc_ts_dictionary['surface_median_ts_intensities'] = parc_median_signal_intensities + + + + if 'nifti_func_path' in file_path_dictionary.keys(): + + + has_nifti = True + time_series_nifti, parcel_labels_nifti, parc_median_signal_intensities_nifti = nifti_utils.nifti_rois_to_time_signals(file_path_dictionary['nifti_func_path'], file_path_dictionary['nifti_parcellation_path'], demedian_before_averaging = True) + + + if 'nifti_parcellation_json_path' in file_path_dictionary.keys(): + + with open(file_path_dictionary['nifti_parcellation_json_path'], 'r') as temp_file: + + json_contents = temp_file.read() + json_dict = json.loads(json_contents) + + + parc_ts_dictionary['nifti_parcellation_info.json'] = json_dict + + output_labels = [] + for temp_label in parcel_labels_nifti: + + strint_label = str(int(temp_label)) + + if 'unique_region_identifier' in json_dict[strint_label].keys(): + + output_labels.append(json_dict[strint_label]['unique_region_identifier']) + + else: + + output_labels.append(strint_label) + + parcel_labels_nifti = output_labels + + parc_ts_dictionary['nifti_labels'] = parcel_labels_nifti + parc_ts_dictionary['nifti_time_series'] = time_series_nifti + parc_ts_dictionary['nifti_median_ts_intensities'] = parc_median_signal_intensities_nifti + + + if (has_nifti and has_gifti): + + parc_ts_dictionary['time_series'] = np.vstack((parc_ts_dictionary['surface_time_series'],parc_ts_dictionary['nifti_time_series'])) + parc_ts_dictionary['labels'] = parc_ts_dictionary['surface_labels'] + parc_ts_dictionary['nifti_labels'] + parc_ts_dictionary['median_ts_intensities'] = np.hstack((parc_ts_dictionary['surface_median_ts_intensities'],parc_ts_dictionary['nifti_median_ts_intensities'])) + + elif has_nifti: + + parc_ts_dictionary['time_series'] = parc_ts_dictionary['nifti_time_series'] + parc_ts_dictionary['labels'] = parc_ts_dictionary['nifti_labels'] + parc_ts_dictionary['median_ts_intensities'] = parc_ts_dictionary['nifti_median_ts_intensities'] + + elif has_gifti: + + parc_ts_dictionary['time_series'] = parc_ts_dictionary['surface_time_series'] + parc_ts_dictionary['labels'] = parc_ts_dictionary['surface_labels'] + parc_ts_dictionary['median_ts_intensities'] = parc_ts_dictionary['surface_median_ts_intensities'] + + else: + + raise NameError('Error: File Path Dictionary must either have gifti or nifti specified') + + #Set the TR + parc_ts_dictionary['TR'] = TR + + + #################################################### + #Get the variables from the confounds regressors file + #confound_df = pd.read_csv(self.confounds_regressors_path, sep='\t') + #for (columnName, columnData) in confound_df.iteritems(): + # setattr(self, columnName, columnData.as_matrix()) + parc_ts_dictionary['confounds'] = populate_confounds_dict(file_path_dictionary) + + parc_ts_dictionary['file_path_dictionary.json'] = file_path_dictionary + + ################################################### + #Calculate general info, such as number of high motion timepoints + #number of volumes that need to be skipped at the beginning of the + #scan, etc. + + parc_ts_dictionary['general_info.json'] = populate_general_info_dict(parc_ts_dictionary) + + + + return parc_ts_dictionary + + +def populate_general_info_dict(parc_ts_dictionary): + + general_info_dict = {} + + ################################################### + #Calculate the number of timepoints to skip at the beginning for this person. + #If equal to zero, we will actually call it one so that we don't run into any + #issues during denoising with derivatives + general_info_dict['n_skip_vols'] = len(np.where(np.absolute(parc_ts_dictionary['confounds']['a_comp_cor_00']) < 0.00001)[0]) + if general_info_dict['n_skip_vols'] == 0: + general_info_dict['n_skip_vols'] = 1 + + general_info_dict['mean_fd'] = np.mean(parc_ts_dictionary['confounds']['framewise_displacement'][general_info_dict['n_skip_vols']:]) + general_info_dict['mean_dvars'] = np.mean(parc_ts_dictionary['confounds']['dvars'][general_info_dict['n_skip_vols']:]) + general_info_dict['num_std_dvars_above_1p5'] = len(np.where(parc_ts_dictionary['confounds']['std_dvars'][general_info_dict['n_skip_vols']:] > 1.5)[0]) + general_info_dict['num_std_dvars_above_1p3'] = len(np.where(parc_ts_dictionary['confounds']['std_dvars'][general_info_dict['n_skip_vols']:] > 1.3)[0]) + general_info_dict['num_std_dvars_above_1p2'] = len(np.where(parc_ts_dictionary['confounds']['std_dvars'][general_info_dict['n_skip_vols']:] > 1.2)[0]) + + general_info_dict['num_fd_above_0p5_mm'] = len(np.where(parc_ts_dictionary['confounds']['framewise_displacement'][general_info_dict['n_skip_vols']:] > 0.5)[0]) + general_info_dict['num_fd_above_0p4_mm'] = len(np.where(parc_ts_dictionary['confounds']['framewise_displacement'][general_info_dict['n_skip_vols']:] > 0.4)[0]) + general_info_dict['num_fd_above_0p3_mm'] = len(np.where(parc_ts_dictionary['confounds']['framewise_displacement'][general_info_dict['n_skip_vols']:] > 0.3)[0]) + general_info_dict['num_fd_above_0p2_mm'] = len(np.where(parc_ts_dictionary['confounds']['framewise_displacement'][general_info_dict['n_skip_vols']:] > 0.2)[0]) + general_info_dict['labels'] = parc_ts_dictionary['labels'] + + + #Set TR + general_info_dict['TR'] = parc_ts_dictionary['TR'] + + #Find session/subject names + if 'lh_func_path' in parc_ts_dictionary['file_path_dictionary.json'].keys(): + + temp_path = parc_ts_dictionary['file_path_dictionary.json']['lh_func_path'].split('/')[-1] + split_end_path = temp_path.split('_') + + else: + + temp_path = parc_ts_dictionary['file_path_dictionary.json']['nifti_func_path'].split('/')[-1] + split_end_path = temp_path.split('_') + + general_info_dict['subject'] = split_end_path[0] + + if split_end_path[1][0:3] == 'ses': + general_info_dict['session'] = split_end_path[1] + else: + general_info_dict['session'] = [] + + return general_info_dict + + + +def populate_confounds_dict(file_path_dictionary): + + confounds_dictionary = {} + + confounds_regressors_tsv_path = file_path_dictionary['confounds_regressors_path'] + confound_df = pd.read_csv(confounds_regressors_tsv_path, sep='\t') + for (columnName, columnData) in confound_df.iteritems(): + confounds_dictionary[columnName] = columnData.as_matrix() + + + #For convenience, bunch together some commonly used nuisance components + + #Six motion realignment paramters + confounds_dictionary['motion_regs_six'] = np.vstack((confounds_dictionary['trans_x'], confounds_dictionary['trans_y'], confounds_dictionary['trans_z'], + confounds_dictionary['rot_x'], confounds_dictionary['rot_y'], confounds_dictionary['rot_z'])) + + #Six motion realignment parameters plus their temporal derivatives + confounds_dictionary['motion_regs_twelve'] = np.vstack((confounds_dictionary['trans_x'], confounds_dictionary['trans_y'], confounds_dictionary['trans_z'], + confounds_dictionary['rot_x'], confounds_dictionary['rot_y'], confounds_dictionary['rot_z'], + confounds_dictionary['trans_x_derivative1'], confounds_dictionary['trans_y_derivative1'], + confounds_dictionary['trans_z_derivative1'], confounds_dictionary['rot_x_derivative1'], + confounds_dictionary['rot_y_derivative1'], confounds_dictionary['rot_z_derivative1'])) + + #Six motion realignment parameters, their temporal derivatives, and + #the square of both + confounds_dictionary['motion_regs_twentyfour'] = np.vstack((confounds_dictionary['trans_x'], confounds_dictionary['trans_y'], confounds_dictionary['trans_z'], + confounds_dictionary['rot_x'], confounds_dictionary['rot_y'], confounds_dictionary['rot_z'], + confounds_dictionary['trans_x_derivative1'], confounds_dictionary['trans_y_derivative1'], + confounds_dictionary['trans_z_derivative1'], confounds_dictionary['rot_x_derivative1'], + confounds_dictionary['rot_y_derivative1'], confounds_dictionary['rot_z_derivative1'], + confounds_dictionary['trans_x_power2'], confounds_dictionary['trans_y_power2'], confounds_dictionary['trans_z_power2'], + confounds_dictionary['rot_x_power2'], confounds_dictionary['rot_y_power2'], confounds_dictionary['rot_z_power2'], + confounds_dictionary['trans_x_derivative1_power2'], confounds_dictionary['trans_y_derivative1_power2'], + confounds_dictionary['trans_z_derivative1_power2'], confounds_dictionary['rot_x_derivative1_power2'], + confounds_dictionary['rot_y_derivative1_power2'], confounds_dictionary['rot_z_derivative1_power2'])) + + #white matter, and csf + confounds_dictionary['wmcsf'] = np.vstack((confounds_dictionary['white_matter'], confounds_dictionary['csf'])) + + #white matter, csf, and their temporal derivatives + confounds_dictionary['wmcsf_derivs'] = np.vstack((confounds_dictionary['white_matter'], confounds_dictionary['csf'], + confounds_dictionary['white_matter_derivative1'], confounds_dictionary['csf_derivative1'])) + + #White matter, csf, and global signal + confounds_dictionary['wmcsfgsr'] = np.vstack((confounds_dictionary['white_matter'], confounds_dictionary['csf'], confounds_dictionary['global_signal'])) + + #White matter, csf, and global signal plus their temporal derivatives + confounds_dictionary['wmcsfgsr_derivs'] = np.vstack((confounds_dictionary['white_matter'], confounds_dictionary['csf'], confounds_dictionary['global_signal'], + confounds_dictionary['white_matter_derivative1'], confounds_dictionary['csf_derivative1'], + confounds_dictionary['global_signal_derivative1'])) + + #The first five anatomical comp cor components + confounds_dictionary['five_acompcors'] = np.vstack((confounds_dictionary['a_comp_cor_00'], confounds_dictionary['a_comp_cor_01'], + confounds_dictionary['a_comp_cor_02'], confounds_dictionary['a_comp_cor_03'], + confounds_dictionary['a_comp_cor_04'])) + + #################################################### + #Load the melodic IC time series + melodic_df = pd.read_csv(file_path_dictionary['melodic_mixing_path'], sep="\t", header=None) + + #################################################### + #Load the indices of the aroma ics + aroma_ics_df = pd.read_csv(file_path_dictionary['aroma_noise_ics_path'], header=None) + noise_comps = (aroma_ics_df.values - 1).reshape(-1,1) + + + #################################################### + #Gather the ICs identified as noise/clean by AROMA + all_ics = melodic_df.values + mask = np.zeros(all_ics.shape[1],dtype=bool) + mask[noise_comps] = True + confounds_dictionary['aroma_noise_ics'] = np.transpose(all_ics[:,~mask]) + confounds_dictionary['aroma_clean_ics'] = np.transpose(all_ics[:,mask]) + + return confounds_dictionary + + + diff --git a/build/lib/discovery_imaging_utils/triple_network_model.py b/build/lib/discovery_imaging_utils/triple_network_model.py new file mode 100644 index 0000000..b1bb743 --- /dev/null +++ b/build/lib/discovery_imaging_utils/triple_network_model.py @@ -0,0 +1,172 @@ +import sklearn +import os +import numpy as np +from discovery_imaging_utils import dictionary_utils +import matplotlib.pyplot as plt + +def calc_triple_network_model(parcellated_timeseries, parcel_ids): + + salience_ids = [] + control_ids = [] + dmn_ids = [] + + salience_identifier = 'SalVentAttnA' + executive_control_identifier = 'ContA' + dmn_identifier = 'DefaultA' + + for i, temp_label in enumerate(parcel_ids): + + if salience_identifier in temp_label: + + salience_ids.append(i) + + if executive_control_identifier in temp_label: + + control_ids.append(i) + + if dmn_identifier in temp_label: + + dmn_ids.append(i) + + + salience_signal = np.mean(parcellated_timeseries[salience_ids,:],axis=0) + control_signal = np.mean(parcellated_timeseries[control_ids,:], axis=0) + dmn_signal = np.mean(parcellated_timeseries[dmn_ids,:], axis=0) + + nii_mean, nii_std, nii_corr, z_corr_1, z_corr_2 = calc_network_interaction_index(control_signal, dmn_signal, salience_signal, 0.8) + + return nii_mean, nii_std, nii_corr + + + +def calc_network_interaction_index(timeseries_1, timeseries_2, timeseries_a, TR, window_length_seconds=40, slide_step_seconds=2, decay_constant=0.333): + + import numpy as np + + #This function calculates the network interaction index as defined + #by the paper Dysregulated Brain Dynamics in a Triple-Network Saliency + #Model of Schizophrenia and Its Relation to Psychosis published in Biological + #Psychiatry by Supekar et. al., which follows from Time-Resolved Resting-State + #Brain Networks published in PNAS by Zalesky et. al. + + #For the triple salience model, timeseries_1 should be a cleaned central executive + #network time signal, timeseries_2 should be a default mode time signal, and + #timeseries_a should be the salience signal. TR must be defined. The window length + #defaults to 40s, and slide step defaults to 2s. The decay constant for the window + #weights defaults to 0.333, and higher values make the weighting approach linearity. + + #The window length and slide step will be rounded to the nearest TR. + + #The function will output a nii_mean, nii_std, and nii_corr, where (without including + #notation for the decaying weights) the values are described as: + + # mean_over_sliding_windows(zcorr_i(timeseries_1, timeseries_a) - z_corr_i(timeseries_2, timeseries_a)) + # std_over_sliding_windows(zcorr_i(timeseries_1, timeseries_a) - z_corr_i(timeseries_2, timeseries_a)) + # corr_over_sliding_windows(z_corr_i(timeseries_1, timeseries_a), z_corr_i(timeseries_2, timeseries_a)) + + + #The function will calculate nii_mean, nii_std, and nii_corr which is the + + #Calculate window length and slide step length in number of TRs + window_length = int(window_length_seconds/TR) + slide_step_length = int(slide_step_seconds/TR) + + #Calculate the number of windows and make arrays + #to store the correlations within each window + num_steps = int(np.floor((len(timeseries_1) - window_length)/slide_step_length)) + corr_1 = np.zeros(num_steps) + corr_2 = np.zeros(num_steps) + + #Calculate the tapered weights for the sliding windows + weights = calc_tapered_weights(window_length, decay_constant) + + #Calculate the pearson product moment correlation + #for each window + for i in range(len(corr_1)): + + beginning = int(i*slide_step_length) + end = int(i*slide_step_length+window_length) + + corr_1[i] = calc_pearson_product_moment_correlation(timeseries_1[beginning:end], timeseries_a[beginning:end], weights) + corr_2[i] = calc_pearson_product_moment_correlation(timeseries_2[beginning:end], timeseries_a[beginning:end], weights) + + #Calculate fisher transformation + z_corr_1 = np.arctanh(corr_1) + z_corr_2 = np.arctanh(corr_2) + + #Calculate mean difference, std difference, and corr across all windows + nii_mean = np.mean(np.subtract(z_corr_1, z_corr_2)) + nii_std = np.std(np.subtract(z_corr_1, z_corr_2)) + nii_corr = np.corrcoef(z_corr_1, z_corr_2)[1,0] + + return nii_mean, nii_std, nii_corr, np.mean(z_corr_1), np.mean(z_corr_2) + + + +#This function calculates tapered weights for sliding-window +#analyses as described in "Time-Resolved Resting-State Brain +#Networks" by Zalesky et. al in PNAS 2014 +def calc_tapered_weights(window_length, decay_constant = 0.333): + + #Calculates tapered weights for sliding window connectivity + #analyses. Uses exponentially tapered window as defined in: + #"Time-Resolved Resting-State Brain Networks" by Zalesky et. al + #in PNAS 2014. Window length should be in number of TRs, and decay + #constant should be relative to window_length. Default decay_constant + #is 0.333, which is equivelant to one third the window_length. + #Returns an array with the weights for each timepoint. + + #Decay constants << 1 will be more non-linear, and decay constants + # >> 1 will approach linearity + + if decay_constant < 0: + + raise NameError('Error: Decay Constant must be positive') + + decay_constant = window_length*decay_constant + w0 = (1 - np.exp(-1/decay_constant))/(1 - np.exp(-1*window_length/decay_constant)) + window_indices = np.linspace(1, window_length, window_length, dtype=int) + weights = np.zeros(len(window_indices)) + for i in window_indices: + weights[i - 1] = w0*np.exp((i - len(weights))/decay_constant) + + return weights + + + + +#These functions implement the pearson product-moment correlation +#as described in "Time-Resolved Resting-State Brain Networks" by +#Zalesky et. al in PNAS 2014 +def calc_weighted_mean(time_signal, weights): + + #print('Weighted_Mean: ' + str(np.sum(np.multiply(time_signal, weights)))) + return np.sum(np.multiply(time_signal, weights))/np.sum(weights) + +def calc_weighted_cov(time_signal, weights): + + #Added a square term that is not listed in the supplement + #because it makes more sense and I get an imaginary number otherwise + + x_dif = np.subtract(time_signal, calc_weighted_mean(time_signal, weights)) + weighted_x_dif = np.multiply(weights, x_dif) + weighted_x_dif_sqr = np.power(weighted_x_dif, 2) + weighted_cov = weighted_x_dif_sqr/np.sum(weights) + #print('Weighted_std: ' + str(np.sqrt(np.sum(weighted_x_dif_sqr)))) + return weighted_cov + +def calc_weighted_cov(time_signal_1, time_signal_2, weights): + + partial_1 = time_signal_1 - calc_weighted_mean(time_signal_1, weights) + partial_2 = time_signal_2 - calc_weighted_mean(time_signal_2, weights) + product = np.multiply(partial_1, partial_2) + weighted_product = np.multiply(weights, product) + #print('Weighted_cov: ' + str(np.sum(weighted_product))) + return np.sum(weighted_product) + +def calc_pearson_product_moment_correlation(time_signal_1, time_signal_2, weights): + + numerator = calc_weighted_cov(time_signal_1, time_signal_2, weights) + denominator_sqrd = calc_weighted_cov(time_signal_1, time_signal_1, weights)*calc_weighted_cov(time_signal_2, time_signal_2, weights) + denominator = np.sqrt(denominator_sqrd) + return numerator/denominator diff --git a/discovery_imaging_utils.egg-info/PKG-INFO b/discovery_imaging_utils.egg-info/PKG-INFO new file mode 100644 index 0000000..f4815f5 --- /dev/null +++ b/discovery_imaging_utils.egg-info/PKG-INFO @@ -0,0 +1,24 @@ +Metadata-Version: 2.1 +Name: discovery-imaging-utils +Version: 0.1.1 +Summary: A package to aid in resting-state fMRI analysis +Home-page: https://github.com/erikglee/discovery_imaging_utils +Author: Erik Lee +Author-email: leex6144@umn.edu +License: UNKNOWN +Download-URL: https://github.com/erikglee/discovery_imaging_utils/archive/v0.1.1.tar.gz +Description: # discovery_imaging_utils + Imaging utilities for the Discovery Program at the University of Minnesota. Utilities assist with the analysis of resting-state fMRI data (primarily through fMRIPREP), along with other miscellaneous utilities for FreeSurfer and general imaging analysis. + + This package is undergoing active development and should be used with caution. + + The package can be installed through "pip install discovery-imaging-utils", and required dependancies can be installed by going to the requirements.txt file and running "pip install -r requirements.txt" + + TESTING + +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.6 +Description-Content-Type: text/markdown diff --git a/discovery_imaging_utils.egg-info/SOURCES.txt b/discovery_imaging_utils.egg-info/SOURCES.txt new file mode 100644 index 0000000..aa99147 --- /dev/null +++ b/discovery_imaging_utils.egg-info/SOURCES.txt @@ -0,0 +1,16 @@ +README.md +setup.cfg +setup.py +discovery_imaging_utils/__init__.py +discovery_imaging_utils/denoise_ts_dict.py +discovery_imaging_utils/dictionary_utils.py +discovery_imaging_utils/func_denoising.py +discovery_imaging_utils/imaging_utils.py +discovery_imaging_utils/imaging_visualizations.py +discovery_imaging_utils/nifti_utils.py +discovery_imaging_utils/parc_ts_dictionary.py +discovery_imaging_utils/triple_network_model.py +discovery_imaging_utils.egg-info/PKG-INFO +discovery_imaging_utils.egg-info/SOURCES.txt +discovery_imaging_utils.egg-info/dependency_links.txt +discovery_imaging_utils.egg-info/top_level.txt \ No newline at end of file diff --git a/discovery_imaging_utils.egg-info/dependency_links.txt b/discovery_imaging_utils.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/discovery_imaging_utils.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/discovery_imaging_utils.egg-info/top_level.txt b/discovery_imaging_utils.egg-info/top_level.txt new file mode 100644 index 0000000..cef3732 --- /dev/null +++ b/discovery_imaging_utils.egg-info/top_level.txt @@ -0,0 +1 @@ +discovery_imaging_utils diff --git a/dist/discovery_imaging_utils-0.1.0-py3-none-any.whl b/dist/discovery_imaging_utils-0.1.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..de67d3cfa95cd7ae7d90ac50f8e575ad340cf0eb GIT binary patch literal 42265 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

ooZ=47 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 a@!?9c&mfA`P*CWQ&jloY$ISWp@4oZ literal 0 HcmV?d00001 diff --git a/dist/discovery_imaging_utils-0.1.0.tar.gz b/dist/discovery_imaging_utils-0.1.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..8cdb59108a3e58e3340b2b1b9108615ad6b19c2f GIT binary patch literal 37421 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%0DqhV#%6{c0x$p7ZHB* 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 literal 0 HcmV?d00001 diff --git a/dist/discovery_imaging_utils-0.1.1-py3-none-any.whl b/dist/discovery_imaging_utils-0.1.1-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..470a865bdda5e925e41360691006e28b789ea79b GIT binary patch literal 42264 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

ooZ=47 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 a-DmgNeRiMSXZLx|&;J3oIPMVu_yPb1HFIkK literal 0 HcmV?d00001