calculates summary statistics for physio data This function calculates a variety of summary statistics for epochs of physiological data signals, matches those statistics with responses from presentation logfiles (optional), saves these data to disk (signals and peaks) in a form amenable to import into SAS (optional), and also saves waveform graphics that may be used in assessing the validity of the summary statistics that have been calculated, as well as the processing steps taken during those calculations. This script has the option of either completing everything in one step, or using a two-step process whereby epochs and peaks are extracted in the first step, and then a second step takes the epoched and peaked data and calculates summary statistics from them. This allows manual intervention between steps to visually inspect and remove bad peaks or waveform artifacts from the data. NOTE: this script assumes that pre-processed physio data have been saved into eeglab .set files, and that presentation log files are available to guide signal epoching REQUIRES indata - cell array of ensemble data structs 'sinfo' - data struct, containing sinfo data for all (but only) those subjects that you wish to analyze 'paths' - ensemble data struct, output of ensemble_fmri_init, containing 'physio_outdir', which is used as the output location of all figures that are constructed 'physio_paths' - output of ensemble_physio, ensemble data struct containing one row per subject/session/run, with paths to pre-processed physio data files for each run 'pres_paths' - output of ensemble_parse_present, an ensemble data struct containing one row per subject/session/run, with paths to pre-processed presentation response and event data LOADING PHYSIO DATA FILES physio files are loaded on a per-subject, per-session, per-run basis, from locations identified in 'physio_paths', for those subjects that exist in 'sinfo'. By default, ensemble_physio (which generates physio_paths) generates a non-filtered raw data file for each subject/session/run, and saves these to physio_paths, and given filtering criteria, also saves a filtered data file to disk in the same location as the raw data file, and saves the path to the filtered data file into physio_paths, with '_filtered' appended to the end of the filename, before the extension. If the following parameter is set to 1, ensemble_physio_summ will search for and attempt to load the filtered data file as opposed to the raw data file. If set to 0, it will attempt to load the raw, unfiltered data file. defs.physio_summ.use_filtered (default 1) EPOCHING ensemble_physio_summ is intended to calculate summary statistics on epochs of physiological data signals. These epochs may, or may not, have responses or experimental classes associated with them. Epochs can currently be defined in terms of events from a presentation log file, or vectors of epoch onsets and offsets in seconds (calculated from the beginning of the provided preprocessed physiological signal) defs.physio_summ.epoch_start (default 0) - if a struct, with sub-field 'filter', this is treated as a filter criteria for the included presentation data, to filter for events to be treated as epoch onsets. If a numeric vector, treated as a vector of epochs onsets in seconds. If zero, no epoching will be conducted. defs.physio_summ.epoch_end (default 0) - if single-length numeric, this is treated as the epoch length in seconds. If a numeric vector of the same length as epoch_start or the onset vector returned by epoch_start, this is treated as a vector of epoch offsets in seconds. If a struct, with sub-field 'filter', this is treated as filter criteria for the included presentation data, to filter for events to be treated as epoch offsets. By default, epoch_starts will be matched with epoch_ends, and epoching will only continue if an equal number of starts and ends is found, with all matching ends occuring after their supposed matched start. By default, the max epoch length will be used if a vector is found as the result of a filter. If a filter criterion is used and the field 'minimum_end' exists and is set to = 1, the minimum epoch time (offset-onset) will be used for all epochs. If this variable is set to 0 and defs.physio_summ.epoch_start is not set to 0, epoch lengths will be set to the maximum interval between epoch starts. NOTE: this will cause overlapping signal between some epochs where there is a variance in the inter-start interval. If this is a problem, you should set 'minimum_end' = 1, so that the epoch length gets set to the minimum inter-start interval. defs.physio_summ.baseline (default 2) - pre-epoch baseline to be added to epoch when calculating and extracting epochs. this value is assumed to be in 'seconds'. FIXME: In the case that there is not sufficient room for the given baseline period between the first epoch onset and the very beginning of the signal acquisition (for instance, first epoch onset is at 1s, but the baseline period is 2s), the first epoch will be not be returned by eeglab. This is because eeglab requires an equal number of datapoints for all epochs extracted from a given signal, and this case creates an out-of-bounds epoch definition that is dropped by eeglab. FIXME: pad the signal with 0s to provide enough data points to allow for a baseline for the first epoch, and then adjust timings of all other events? this would necessarily require special calculation of the baseline of the first epoch, limited to real-data only, when subtracting the baseline from the signal of interest, so as not to bias the baseline with artifically added 0s. SUMMARY DATA CALCULATION The current analyses calculate summary statistics for both pulse and skin conductance data. For pulse, mean heart rate within an epoch, as well as heart rate variance, is calculated. For scr, a number of statistics are calculated to investigate both individual transient skin conductance responses (SCRs), and change in overall skin conductance level (SCL). For SCRs, individual responses are defined as the highest peak within a range of +/- 'proxlim' seconds (default 0.6) within an epoch, whose height is greater than height_thresh (default 0.02). A standard minimum SCR height, as defined by Dawson et al 2000, is 0.02 microsiemens, therefore if the incoming signal is defined in microsiemens (as is biopac data when both high pass filters on the GSR100 are set to DC), the default height_thresh should be used. Total number of SCRs within an epoch (npeaks), mean peak height, peak height variance, first peak height, and first peak time are calculated. If preprocessed data are provided in microsiemens, then heights and variance will be returned in microsiemens. For SCLs, epoched skin conductance data are standardized within-subject, across epochs, to control for individual differences in SCL variability and range (Dawson et al 2000, p209; also Ben-Shakhar 1985). Specific integral and mean SCL over the expected period of a specific SCR (spc_integral.being, spc_integral.end, default 2s to 6s) are calculated, as well as integral and mean SCL over the entire epoch (not including the baseline period). NOTE: if entering these data into mixed effects multi-level regression models, certain effects such as different rates of habituation between individuals will be somewhat controlled for by pooling variance within subjects, but if not, additional measures should be considered if you want to control for individual differences in (at least) rate of habituation when using repeated stimuli or chronic stimuli. Different rates of habituation may effect mean peak height and peak height variance, and may also have some effect on integral. defs.physio_summ.channels - channel names, as they appear in your eeglab chanlocs labels, that you wish to calculate summary information for. defs.physio_summ.scr - settings for SCR/GSR data summary .spc_integral.begin (default 2) - time, in seconds, to start calculation of the specific SCR integral .spc_integral.end (default 6) - time, in seconds, to end calculation of the specific SCR integral .find_peaks - the following parameters are sent to find_peaks: .thresh (default 0) - proportion of total signal range below which peaks will not be considered. for example, for .thresh = 0.5, peaks will only be returned if their height is more than half of the total signal range. This is passed on to find_peaks, but is always set to 0, since we want to be able to find peaks across the entire range of signal. .peak_heights.calcFrom (default 'left') - which side of a peak to find the trough used to calculate peak height .peak_heights.transformation (default 'none') - the transformation, if any, performed on peak height values before returning them. .peak_height_window (default 0.6) - sliding window in which only the highest peak is retained. for each peak found, if there are other peaks within +/- seconds of that peak, all but the highest peak will be discarded. .peak_height_thresh (default 0.02) - peak heights below this value will be rejected .keep_baseline (default: 1) - keep the baseline period in the final signal that is returned (1), or remove it (0)? either way, the baseline period (if greater than 0) mean is removed from the signal of interest for each epoch, and peaks during the baseline period are removed after peak finding defs.physio_summ.pulse - settings for pulse ox. data summary .find_peaks.thresh (default 0.5) - proportion of total signal range below which peaks will not be considered. for example, for .thresh = 0.5, peaks will only be returned if their height is more than half of the total signal range RESPONSE MATCHING FOR EPOCHS if there are responses expected and present within a presentation logfile that can be matched in a 1 to 1 relationship with epochs that are extracted or defined, these responses can be included in any data files that are exported, matched to each epoch and the summary statistics calculated for each epoch. Also, overlays of all epochs corresponding to each unique response within a response set, and average waveform for each level of each response, can be calculated and output. presentation data must be included, in the form of a pres_paths data struct (created by presentation_parse) containing paths to presentation data that has been parsed and saved to disk for the given participant/session. defs.physio_summ.responses (default 0) - struct array, each struct identifying filtering criteria to be applied to the given presentation data, to extract events containing response data that should be mapped to each trial. This response data will be exported along with summary information calculated for each trial. .type - string, name of the given response. this string will be used as the variable name for the given response data in the resulting output data struct .event - struct, assumed to include either an .include or .exclude fieldname, and subsequent ensemble_filter criteria for the given response class DATA EXTRACTION VS SUMMARY STATISTIC CALCULATION the default is to extract epochs, extract peaks, and calculate summary statistics in one step. You can control the execution of each step with the following parameters: defs.physio_summ.EXTRACT_DATA (default: 1) - extract epochs and peaks from provided waveforms. These data are returned in 'gsr_epochs' and 'cardiac_epochs' datasets. REQUIRES: physio_paths and pres_paths input datasets defs.physio_summ.CALCULATE_STATS (default: 1) - calculate summary statistics on extracted epochs and peaks. REQUIRES: either defs.physio_summ.EXTRACT_DATA, physio_paths, and pres_paths, OR 'gsr_epochs' and 'cardiac_epochs' input data structures to use the two step process, you can first execute this script with EXTRACT_DATA=1, CALCULATE_STATS=0, then import the results from that step into another script (possibly manual_adjust_signal_data.m) to manually edit the waveforms and peak information, and then return those edited datasets to this script with EXTRACT_DATA=0, CALCULATE_STATS=1 to complete the summary statistic calculation. DATA OUTPUT defs.physio_summ.GRAPH_EPOCHS (default 0) defs.physio_summ.EPOCHS_BY_RESP (default 0) defs.physio_summ.SAVE_EPOCHS (default 1) defs.physio_summ.SAVE_DATA (default 0) defs.physio_summ.export (default 0) defs.physio_summ.sas (default 0) defs.physio_summ.export_stub (default '') FIXME - should be re-written, to include a mechanism by which channels to analyze are identified by EEG.chanlocs name as sub-fields of the params struct, and under that, sub-fields of each channel should be named exactly as the names of functions used to either filter or report the data. Then, sub-fields of those fields should be params that get sent to those functions as the second argument, the first argument being the data output of the previous function (in may or all cases, an eeglab struct? ... OR a cell array of structs containing a series of processing jobs, params for those jobs, function handles/references, etc ...? CITATIONS Dawson ME, Schell AM, & Filion DL (2000). "The Electrodermal System." In JT Cacioppo, LG Tassinary & GG Berntson (Eds.) "Handbook of Psychophysiology" (pp 201-223). Cambridge: Cambridge Univ. Press. Ben-Shakhar (1985). "Standardization within individuals: Simple method to neutralize individual differences in skin conductance." Psychophysiology, 22:292-9. FB 2009.04.21 FB 2009.05.04 - adding documentation, now zscores signal for SCL calculation but leaves signal for SCR calculation in raw form. FB 2009.06.18 - changed "params.lead" to "params.baseline", and checked the script for hard-coded reliance on anything fmri-related, with aims to generalize from ensemble_fmri_physio_summ to ensemble_physio_summ
0001 function outdata = ensemble_physio_summ(indata,defs) 0002 0003 % calculates summary statistics for physio data 0004 % 0005 % This function calculates a variety of summary statistics for epochs of 0006 % physiological data signals, matches those statistics with responses from 0007 % presentation logfiles (optional), saves these data to disk (signals and 0008 % peaks) in a form amenable to import into SAS (optional), and also saves 0009 % waveform graphics that may be used in assessing the validity of the 0010 % summary statistics that have been calculated, as well as the processing 0011 % steps taken during those calculations. 0012 % 0013 % This script has the option of either completing everything in one step, 0014 % or using a two-step process whereby epochs and peaks are extracted in the 0015 % first step, and then a second step takes the epoched and peaked data and 0016 % calculates summary statistics from them. This allows manual intervention 0017 % between steps to visually inspect and remove bad peaks or waveform 0018 % artifacts from the data. 0019 % 0020 % NOTE: this script assumes that pre-processed physio data have been saved 0021 % into eeglab .set files, and that presentation log files are available to 0022 % guide signal epoching 0023 % 0024 % REQUIRES 0025 % 0026 % indata - cell array of ensemble data structs 0027 % 'sinfo' - data struct, containing sinfo data for all (but only) 0028 % those subjects that you wish to analyze 0029 % 'paths' - ensemble data struct, output of ensemble_fmri_init, 0030 % containing 'physio_outdir', which is used as the output 0031 % location of all figures that are constructed 0032 % 'physio_paths' - output of ensemble_physio, ensemble data 0033 % struct containing one row per subject/session/run, with paths 0034 % to pre-processed physio data files for each run 0035 % 'pres_paths' - output of ensemble_parse_present, an ensemble 0036 % data struct containing one row per subject/session/run, with 0037 % paths to pre-processed presentation response and event data 0038 % 0039 % LOADING PHYSIO DATA FILES 0040 % 0041 % physio files are loaded on a per-subject, per-session, per-run basis, 0042 % from locations identified in 'physio_paths', for those subjects that 0043 % exist in 'sinfo'. By default, ensemble_physio (which generates 0044 % physio_paths) generates a non-filtered raw data file for each 0045 % subject/session/run, and saves these to physio_paths, and given 0046 % filtering criteria, also saves a filtered data file to disk in the same 0047 % location as the raw data file, and saves the path to the filtered data 0048 % file into physio_paths, with '_filtered' appended to the end of the 0049 % filename, before the extension. If the following parameter is set to 1, 0050 % ensemble_physio_summ will search for and attempt to load the 0051 % filtered data file as opposed to the raw data file. If set to 0, it 0052 % will attempt to load the raw, unfiltered data file. 0053 % 0054 % defs.physio_summ.use_filtered (default 1) 0055 % 0056 % EPOCHING 0057 % 0058 % ensemble_physio_summ is intended to calculate summary statistics 0059 % on epochs of physiological data signals. These epochs may, or may not, 0060 % have responses or experimental classes associated with them. Epochs can 0061 % currently be defined in terms of events from a presentation log file, 0062 % or vectors of epoch onsets and offsets in seconds (calculated from the 0063 % beginning of the provided preprocessed physiological signal) 0064 % 0065 % defs.physio_summ.epoch_start (default 0) - if a struct, with sub-field 0066 % 'filter', this is treated as a filter criteria for the included 0067 % presentation data, to filter for events to be treated as epoch 0068 % onsets. If a numeric vector, treated as a vector of epochs onsets 0069 % in seconds. If zero, no epoching will be conducted. 0070 % defs.physio_summ.epoch_end (default 0) - if single-length numeric, this 0071 % is treated as the epoch length in seconds. If a numeric vector of 0072 % the same length as epoch_start or the onset vector returned by 0073 % epoch_start, this is treated as a vector of epoch offsets in 0074 % seconds. If a struct, with sub-field 'filter', this is treated as 0075 % filter criteria for the included presentation data, to filter for 0076 % events to be treated as epoch offsets. By default, epoch_starts 0077 % will be matched with epoch_ends, and epoching will only continue if 0078 % an equal number of starts and ends is found, with all matching ends 0079 % occuring after their supposed matched start. By default, the max 0080 % epoch length will be used if a vector is found as the result of a 0081 % filter. If a filter criterion is used and the field 'minimum_end' 0082 % exists and is set to = 1, the minimum epoch time (offset-onset) 0083 % will be used for all epochs. If this variable is set to 0 and 0084 % defs.physio_summ.epoch_start is not set to 0, epoch lengths will be 0085 % set to the maximum interval between epoch starts. NOTE: this will 0086 % cause overlapping signal between some epochs where there is a 0087 % variance in the inter-start interval. If this is a problem, you 0088 % should set 'minimum_end' = 1, so that the epoch length gets set to 0089 % the minimum inter-start interval. 0090 % defs.physio_summ.baseline (default 2) - pre-epoch baseline to be added to 0091 % epoch when calculating and extracting epochs. this value is assumed 0092 % to be in 'seconds'. 0093 % 0094 % FIXME: In the case that there is not sufficient room 0095 % for the given baseline period between the first epoch onset and the 0096 % very beginning of the signal acquisition (for instance, first epoch 0097 % onset is at 1s, but the baseline period is 2s), the first epoch 0098 % will be not be returned by eeglab. This is because eeglab 0099 % requires an equal number of datapoints for all epochs extracted 0100 % from a given signal, and this case creates an out-of-bounds epoch 0101 % definition that is dropped by eeglab. 0102 % 0103 % FIXME: pad the signal with 0s to provide enough data points to 0104 % allow for a baseline for the first epoch, and then adjust timings 0105 % of all other events? this would necessarily require special 0106 % calculation of the baseline of the first epoch, limited to 0107 % real-data only, when subtracting the baseline from the signal of 0108 % interest, so as not to bias the baseline with artifically added 0s. 0109 % 0110 % SUMMARY DATA CALCULATION 0111 % 0112 % The current analyses calculate summary statistics for both pulse and 0113 % skin conductance data. For pulse, mean heart rate within an epoch, as 0114 % well as heart rate variance, is calculated. For scr, a number of 0115 % statistics are calculated to investigate both individual transient skin 0116 % conductance responses (SCRs), and change in overall skin conductance 0117 % level (SCL). 0118 % 0119 % For SCRs, individual responses are defined as the highest peak within a 0120 % range of +/- 'proxlim' seconds (default 0.6) within an epoch, whose 0121 % height is greater than height_thresh (default 0.02). A standard minimum 0122 % SCR height, as defined by Dawson et al 2000, is 0.02 microsiemens, 0123 % therefore if the incoming signal is defined in microsiemens (as is 0124 % biopac data when both high pass filters on the GSR100 are set to DC), 0125 % the default height_thresh should be used. Total number of SCRs within 0126 % an epoch (npeaks), mean peak height, peak height variance, first peak 0127 % height, and first peak time are calculated. If preprocessed data are 0128 % provided in microsiemens, then heights and variance will be returned in 0129 % microsiemens. 0130 % 0131 % For SCLs, epoched skin conductance data are standardized 0132 % within-subject, across epochs, to control for individual differences in 0133 % SCL variability and range (Dawson et al 2000, p209; also Ben-Shakhar 0134 % 1985). Specific integral and mean SCL over the expected period of a 0135 % specific SCR (spc_integral.being, spc_integral.end, default 2s to 6s) 0136 % are calculated, as well as integral and mean SCL over the entire epoch 0137 % (not including the baseline period). 0138 % 0139 % NOTE: if entering these data into mixed effects multi-level regression 0140 % models, certain effects such as different rates of habituation between 0141 % individuals will be somewhat controlled for by pooling variance within 0142 % subjects, but if not, additional measures should be considered if you 0143 % want to control for individual differences in (at least) rate of 0144 % habituation when using repeated stimuli or chronic stimuli. Different 0145 % rates of habituation may effect mean peak height and peak height 0146 % variance, and may also have some effect on integral. 0147 % 0148 % defs.physio_summ.channels - channel names, as they appear in your 0149 % eeglab chanlocs labels, that you wish to calculate summary 0150 % information for. 0151 % defs.physio_summ.scr - settings for SCR/GSR data summary 0152 % .spc_integral.begin (default 2) - time, in seconds, to start 0153 % calculation of the specific SCR integral 0154 % .spc_integral.end (default 6) - time, in seconds, to end 0155 % calculation of the specific SCR integral 0156 % .find_peaks - the following parameters are sent to find_peaks: 0157 % .thresh (default 0) - proportion of total signal 0158 % range below which peaks will not be considered. for 0159 % example, for .thresh = 0.5, peaks will only be returned if 0160 % their height is more than half of the total signal range. 0161 % This is passed on to find_peaks, but is always set to 0, 0162 % since we want to be able to find peaks across the entire 0163 % range of signal. 0164 % .peak_heights.calcFrom (default 'left') - which side of a peak 0165 % to find the trough used to calculate peak height 0166 % .peak_heights.transformation (default 'none') - the 0167 % transformation, if any, performed on peak height values 0168 % before returning them. 0169 % .peak_height_window (default 0.6) - sliding window in 0170 % which only the highest peak is retained. for each peak 0171 % found, if there are other peaks within +/- seconds of that 0172 % peak, all but the highest peak will be discarded. 0173 % .peak_height_thresh (default 0.02) - peak heights below 0174 % this value will be rejected 0175 % .keep_baseline (default: 1) - keep the baseline period in the 0176 % final signal that is returned (1), or remove it (0)? either 0177 % way, the baseline period (if greater than 0) mean is removed 0178 % from the signal of interest for each epoch, and peaks during 0179 % the baseline period are removed after peak finding 0180 % defs.physio_summ.pulse - settings for pulse ox. data summary 0181 % .find_peaks.thresh (default 0.5) - proportion of total signal 0182 % range below which peaks will not be considered. for example, 0183 % for .thresh = 0.5, peaks will only be returned if their height 0184 % is more than half of the total signal range 0185 % 0186 % RESPONSE MATCHING FOR EPOCHS 0187 % 0188 % if there are responses expected and present within a presentation 0189 % logfile that can be matched in a 1 to 1 relationship with epochs that 0190 % are extracted or defined, these responses can be included in any data 0191 % files that are exported, matched to each epoch and the summary 0192 % statistics calculated for each epoch. Also, overlays of all epochs 0193 % corresponding to each unique response within a response set, and 0194 % average waveform for each level of each response, can be calculated and 0195 % output. presentation data must be included, in the form of a pres_paths 0196 % data struct (created by presentation_parse) containing paths to 0197 % presentation data that has been parsed and saved to disk for the given 0198 % participant/session. 0199 % 0200 % defs.physio_summ.responses (default 0) - struct array, each struct 0201 % identifying filtering criteria to be applied to the given 0202 % presentation data, to extract events containing response data that 0203 % should be mapped to each trial. This response data will be exported 0204 % along with summary information calculated for each trial. 0205 % .type - string, name of the given response. this string will be 0206 % used as the variable name for the given response data in the 0207 % resulting output data struct 0208 % .event - struct, assumed to include either an .include or .exclude 0209 % fieldname, and subsequent ensemble_filter criteria for the 0210 % given response class 0211 % 0212 % DATA EXTRACTION VS SUMMARY STATISTIC CALCULATION 0213 % 0214 % the default is to extract epochs, extract peaks, and calculate summary 0215 % statistics in one step. You can control the execution of each step with 0216 % the following parameters: 0217 % 0218 % defs.physio_summ.EXTRACT_DATA (default: 1) - extract epochs and peaks 0219 % from provided waveforms. These data are returned in 'gsr_epochs' 0220 % and 'cardiac_epochs' datasets. REQUIRES: physio_paths and 0221 % pres_paths input datasets 0222 % defs.physio_summ.CALCULATE_STATS (default: 1) - calculate summary 0223 % statistics on extracted epochs and peaks. REQUIRES: either 0224 % defs.physio_summ.EXTRACT_DATA, physio_paths, and pres_paths, OR 0225 % 'gsr_epochs' and 'cardiac_epochs' input data structures 0226 % 0227 % to use the two step process, you can first execute this script with 0228 % EXTRACT_DATA=1, CALCULATE_STATS=0, then import the results from that 0229 % step into another script (possibly manual_adjust_signal_data.m) to 0230 % manually edit the waveforms and peak information, and then return those 0231 % edited datasets to this script with EXTRACT_DATA=0, CALCULATE_STATS=1 0232 % to complete the summary statistic calculation. 0233 % 0234 % DATA OUTPUT 0235 % 0236 % defs.physio_summ.GRAPH_EPOCHS (default 0) 0237 % defs.physio_summ.EPOCHS_BY_RESP (default 0) 0238 % defs.physio_summ.SAVE_EPOCHS (default 1) 0239 % defs.physio_summ.SAVE_DATA (default 0) 0240 % defs.physio_summ.export (default 0) 0241 % defs.physio_summ.sas (default 0) 0242 % defs.physio_summ.export_stub (default '') 0243 % 0244 % FIXME - should be re-written, to include a mechanism by which channels to 0245 % analyze are identified by EEG.chanlocs name as sub-fields of the params 0246 % struct, and under that, sub-fields of each channel should be named 0247 % exactly as the names of functions used to either filter or report the 0248 % data. Then, sub-fields of those fields should be params that get sent to 0249 % those functions as the second argument, the first argument being the 0250 % data output of the previous function (in may or all cases, an eeglab 0251 % struct? ... OR a cell array of structs containing a series of processing 0252 % jobs, params for those jobs, function handles/references, etc ...? 0253 % 0254 % CITATIONS 0255 % Dawson ME, Schell AM, & Filion DL (2000). "The Electrodermal System." 0256 % In JT Cacioppo, LG Tassinary & GG Berntson (Eds.) "Handbook of 0257 % Psychophysiology" (pp 201-223). Cambridge: Cambridge Univ. Press. 0258 % Ben-Shakhar (1985). "Standardization within individuals: Simple method 0259 % to neutralize individual differences in skin conductance." 0260 % Psychophysiology, 22:292-9. 0261 % 0262 % FB 2009.04.21 0263 % FB 2009.05.04 - adding documentation, now zscores signal for SCL 0264 % calculation but leaves signal for SCR calculation in raw form. 0265 % FB 2009.06.18 - changed "params.lead" to "params.baseline", and checked 0266 % the script for hard-coded reliance on anything fmri-related, with aims to 0267 % generalize from ensemble_fmri_physio_summ to ensemble_physio_summ 0268 0269 outdata = ensemble_init_data_struct(); 0270 0271 global r 0272 0273 r = init_results_struct; 0274 0275 r.type = 'physio_summ'; % Identify the type of this reporting instance 0276 r.report_on_fly = 1; 0277 0278 % % load colormap 0279 % load new_seismic; 0280 % colormap(new_seismic); 0281 0282 % Parse out the input data 0283 for idata = 1:length(indata) 0284 if isfield(indata{idata},'type') 0285 switch indata{idata}.type 0286 case {'paths'} 0287 pathdata = indata{idata}; 0288 pcol = set_var_col_const(pathdata.vars); 0289 case {'physio_paths'} 0290 physio_paths = indata{idata}; 0291 physcol = set_var_col_const(physio_paths.vars); 0292 case {'pres_paths'} 0293 pres_paths = indata{idata}; 0294 prescol = set_var_col_const(pres_paths.vars); 0295 case {'sinfo'} 0296 sinfo = indata{idata}; 0297 sinfo = sinfo.data; 0298 proc_subs = {sinfo(:).id}; 0299 nsub_proc = length(proc_subs); 0300 case {'gsr_epochs'} 0301 gsr_epochs = indata{idata}; 0302 gecol = set_var_col_const(gsr_epochs.vars); 0303 case {'cardiac_epochs'} 0304 cardiac_epochs = indata{idata}; 0305 cecol = set_var_col_const(cardiac_epochs.vars); 0306 end 0307 end 0308 end 0309 0310 % check for required vars 0311 check_vars = {'sinfo','pathdata'}; 0312 check_required_vars; 0313 0314 if (iscell(indata) && ~isempty(indata) && isfield(indata{1},'task') && ... 0315 ~isempty(strmatch('return_outdir',indata{1}.task))) || ... 0316 (isstruct(indata) && isfield(indata,'task') && ... 0317 ~isempty(strmatch('return_outdir',indata.task))) 0318 if exist('pathdata','var') && ~isempty(pathdata.data{1}) 0319 if exist('nsub_proc','var') && length(nsub_proc) == 1 && ... 0320 exist('proc_subs','var') 0321 pfilt = struct(); 0322 pfilt.include.all.subject_id = proc_subs; 0323 lpathdata = ensemble_filter(pathdata,pfilt); 0324 if ~isempty(lpathdata.data{1}) 0325 sfilt = pfilt; 0326 sfilt.include.all.path_type = {'physio_outdir'}; 0327 spathdata = ensemble_filter(lpathdata,sfilt); 0328 if length(spathdata.data{1}) == 1 0329 % one epi outdir, save outdata = epi_outdir 0330 outdata = spathdata.data{pcol.path}{1}; 0331 else 0332 sfilt = pfilt; 0333 sfilt.include.all.path_type = {'sess_outdir'}; 0334 spathdata = ensemble_filter(lpathdata,sfilt); 0335 if length(spathdata.data{1}) == 1; 0336 outdata = spathdata.data{pcol.path}{1}; 0337 else 0338 sfilt = pfilt; 0339 sfilt.include.all.path_type = {'sub_outdir'}; 0340 spathdata = ensemble_filter(lpathdata,sfilt); 0341 if length(spathdata.data{1}) == 1; 0342 outdata = spathdata.data{pcol.path}{1}; 0343 end 0344 end 0345 end 0346 end 0347 end 0348 end 0349 if ~exist('outdata','var') || ~exist(outdata,'dir'), outdata = ''; end 0350 return 0351 end 0352 0353 % initialize outdata struct for sinfo 0354 outdata.vars = [outdata.vars 'sinfo']; 0355 sinfo_idx = length(outdata.vars); 0356 outdata.data{sinfo_idx} = ensemble_init_data_struct(); 0357 outdata.data{sinfo_idx}.type = 'sinfo'; 0358 outdata.data{sinfo_idx}.data = sinfo; 0359 0360 % check for physio_summ parameters within defs 0361 if isfield(defs,'physio_summ') 0362 ps = defs.physio_summ; 0363 else 0364 error('please provide params through defs.physio_summ\n'); 0365 end 0366 0367 % get parameters 0368 try chans_req = ps.channels; catch error('must provide channel names\n'); end 0369 try GRAPH_EPOCHS = ps.GRAPH_EPOCHS; catch GRAPH_EPOCHS = 0; end 0370 try EPOCH_BY_RESP = ps.EPOCH_BY_RESP; catch EPOCH_BY_RESP = 0; end 0371 try SAVE_EPOCHS = ps.SAVE_EPOCHS; catch SAVE_EPOCHS = 0; end 0372 try SAVE_DATA = ps.SAVE_DATA; catch SAVE_DATA = 0; end 0373 try EXTRACT_DATA = ps.EXTRACT_DATA; catch EXTRACT_DATA = 1; end 0374 try CALCULATE_STATS = ps.CALCULATE_STATS; catch CALCULATE_STATS = 1; end 0375 try export = ps.export; catch export.write2file = 0; end 0376 try sas = ps.sas; catch sas.libsave = 0; end 0377 try epoch_start = ps.epoch_start; catch epoch_start = ''; end 0378 try epoch_end = ps.epoch_end; catch epoch_end = ''; end 0379 try responses = ps.responses; catch responses = 0; end 0380 try baseline = ps.baseline; catch baseline = 2; end % in seconds 0381 try use_filtered = ps.use_filtered; catch use_filtered = 0; end 0382 try ep_per_fig = ps.ep_per_fig; catch ep_per_fig = 3; end 0383 try pargs = ps.pargs; catch pargs = {'-dpsc','-append'}; end 0384 try export_stub = ps.export_stub; catch export_stub = ''; end 0385 0386 % check required datasets, based upon steps taken 0387 if EXTRACT_DATA 0388 check_vars = {'physio_paths','pres_paths'}; 0389 check_required_vars; 0390 end 0391 if CALCULATE_STATS && ~EXTRACT_DATA 0392 check_vars = {{'gsr_epochs','cardiac_epochs'}}; 0393 check_required_vars; 0394 end 0395 0396 % get scr analysis parameters 0397 try scr.zscore = ps.scr.zscore; catch scr.zscore = ''; end 0398 try scr.int_begin = ps.scr.int_begin; catch scr.int_begin = 2; end % in sec 0399 try scr.int_end = ps.scr.int_end; catch scr.int_end = 6; end % in seconds 0400 try scr.pk.thresh = ps.scr.find_peaks.thresh; catch scr.pk.thresh = 0; end 0401 try scr.pk.peak_heights = ps.find_peaks.peak_heights; 0402 catch scr.pk.peak_heights.calcFrom = 'left'; 0403 scr.pk.peak_heights.transformation = 'none'; 0404 end 0405 try scr.pk.peak_height_window = ps.scr.find_peaks.peak_height_window; 0406 catch scr.pk.peak_height_window = 0.6; end % in seconds 0407 try scr.pk.peak_height_thresh = ps.scr.find_peaks.peak_height_thresh; 0408 catch scr.pk.peak_height_thresh = 0.02; end 0409 try keep_baseline = ps.scr.keep_baseline; catch keep_baseline = 1; end 0410 0411 % verify integral beginning and ending points 0412 if scr.int_begin > scr.int_end 0413 error(['must provide valid integral beginning and ending points, in '... 0414 'seconds\n']); 0415 end 0416 0417 % get pulse analysis parameters 0418 try pulse.pk = ps.pulse.find_peaks; 0419 catch pulse.pk.thresh = 0.5; pulse.pk.peak_height_window = 0.3; end 0420 0421 % get types of responses to include in output data 0422 if isstruct(responses) && (isnumeric(epoch_start) && ... 0423 (length(epoch_start) < 1 || epoch_start)) || ... 0424 (isstruct(epoch_start) && isfield(epoch_start,'filter')) 0425 resp_names = {responses.type}; 0426 nresp = length(resp_names); 0427 else 0428 resp_names = {}; 0429 nresp = 0; 0430 end 0431 0432 % check channels against what is supported by this script 0433 supported = {'scr','gsr','cardiac','pulse'}; 0434 nchanreq = length(chans_req); 0435 channels = {}; 0436 0437 % weed out unsupported channels 0438 for ic=1:nchanreq 0439 if ~isempty(strmatch(chans_req{ic},supported)) 0440 channels = [channels chans_req{ic}]; 0441 else 0442 warning('channel %s not supported, not being used\n',chans_req{ic}); 0443 end 0444 end 0445 0446 % get number of supported channels that have been requested 0447 nchan = length(channels); 0448 if ~nchan, error('no supported channels provided'), end 0449 0450 %eeglab('initpaths'); 0451 % Add only the eeglab paths we need 0452 eeglab_root = fileparts(which('eeglab')); 0453 addpath(fullfile(eeglab_root,'functions/popfunc')); 0454 0455 % init default vars used in extracted datasets 0456 xvars = {'subject_id','session','ensemble_id','run','trial',... 0457 'signal','srate','peakidxs'}; 0458 0459 if EXTRACT_DATA 0460 lvars = {xvars{:},resp_names{:}}; 0461 0462 if (~isempty(strmatch('gsr',channels)) || ... 0463 ~isempty(strmatch('scr',channels))) 0464 % initialize a gsr summary data output structure 0465 outdata.vars = [outdata.vars 'gsr_epochs']; 0466 gsre_idx = length(outdata.vars); 0467 outdata.data{gsre_idx} = ensemble_init_data_struct(); 0468 outdata.data{gsre_idx}.type = 'gsr_epochs'; 0469 outdata.data{gsre_idx}.vars = lvars; 0470 gsre_cols = set_var_col_const(outdata.data{gsre_idx}.vars); 0471 outdata.data{gsre_idx}.data{gsre_cols.subject_id} = {}; 0472 outdata.data{gsre_idx}.data{gsre_cols.session} = []; 0473 outdata.data{gsre_idx}.data{gsre_cols.ensemble_id} = []; 0474 outdata.data{gsre_idx}.data{gsre_cols.run} = []; 0475 outdata.data{gsre_idx}.data{gsre_cols.trial} = []; 0476 outdata.data{gsre_idx}.data{gsre_cols.signal} = {}; 0477 outdata.data{gsre_idx}.data{gsre_cols.srate} = []; 0478 outdata.data{gsre_idx}.data{gsre_cols.peakidxs} = {}; 0479 for in=1:length(resp_names) 0480 outdata.data{gsre_idx}.data{gsre_cols.(resp_names{in})} = {}; 0481 end 0482 end 0483 0484 if ~isempty(strmatch('cardiac',channels)) || ... 0485 ~isempty(strmatch('pulse',channels)) 0486 % initialize a cardiac summary data output structure 0487 outdata.vars = [outdata.vars 'cardiac_epochs']; 0488 carde_idx = length(outdata.vars); 0489 outdata.data{carde_idx} = ensemble_init_data_struct(); 0490 outdata.data{carde_idx}.type = 'cardiac_epochs'; 0491 outdata.data{carde_idx}.vars = lvars; 0492 carde_cols = set_var_col_const(outdata.data{carde_idx}.vars); 0493 outdata.data{carde_idx}.data{carde_cols.subject_id} = {}; 0494 outdata.data{carde_idx}.data{carde_cols.session} = []; 0495 outdata.data{carde_idx}.data{carde_cols.ensemble_id} = []; 0496 outdata.data{carde_idx}.data{carde_cols.run} = []; 0497 outdata.data{carde_idx}.data{carde_cols.trial} = []; 0498 outdata.data{carde_idx}.data{carde_cols.signal} = {}; 0499 outdata.data{carde_idx}.data{carde_cols.srate} = []; 0500 outdata.data{carde_idx}.data{carde_cols.peakidxs} = {}; 0501 for in=1:length(resp_names) 0502 outdata.data{carde_idx}.data{carde_cols.(resp_names{in})} = {}; 0503 end 0504 end 0505 end 0506 0507 % % % extract data? 0508 if EXTRACT_DATA 0509 0510 % 0511 % START OF THE SUBJECT LOOP 0512 % 0513 0514 for isub=1:nsub_proc 0515 subid = sinfo(isub).id; 0516 msg = sprintf('\t\tPROCESSING SUBJECT (%d/%d): %s\n', isub, nsub_proc,subid); 0517 r = update_report(r,msg); 0518 0519 % Determine number of sessions for this subject 0520 nsess = length(sinfo(isub).sessinfo); 0521 0522 % 0523 % START OF THE SESSION LOOP 0524 % 0525 0526 for isess = 1:nsess 0527 sess = sinfo(isub).sessinfo(isess); 0528 0529 if isfield(sess,'use_session') && ~sess.use_session 0530 msg = sprintf('\t\t\tSkipping session %s\n',sess.id); 0531 r = update_report(r,msg); 0532 continue 0533 elseif ~isfield(sess,'physio') || ~isstruct(sess.physio) 0534 msg = sprintf(['\t\t\tNo Physio sinfo for session %s, '... 0535 'SKIPPING!\n'],sess.id); 0536 r = update_report(r,msg); 0537 continue 0538 end 0539 0540 pfilt.include.all.subject_id = {subid}; 0541 pfilt.include.all.session = {sess.id}; 0542 pdata = ensemble_filter(pres_paths,pfilt); 0543 0544 if isempty(pdata.data{prescol.path}) || ... 0545 ~exist(pdata.data{prescol.path}{1},'file') 0546 error('can not find presentation data for subject %s, session %s',... 0547 subid,sess.id); 0548 else 0549 presdata = load(pdata.data{prescol.path}{1}); 0550 presdcol = set_var_col_const(presdata.vars); 0551 end 0552 0553 nruns = length(sess.use_epi_runs); 0554 for irun=1:nruns 0555 0556 rnum = sess.use_epi_runs(irun); 0557 msg = sprintf('\t\tPROCESSING RUN %d (%d/%d)\n',rnum,irun,nruns); 0558 r = update_report(r,msg); 0559 0560 lpresfilt.include.all.SUB_ID = {subid}; 0561 lpresfilt.include.all.RUN = rnum; 0562 0563 % get presentation data for this run 0564 lpres = ensemble_filter(presdata,lpresfilt); 0565 0566 lresp = cell(nresp,1); 0567 for ir=1:nresp 0568 lrespfilt = responses(ir).event; 0569 lrespfilt.include.all.RUN = rnum; 0570 lrespdata = ensemble_filter(lpres,lrespfilt); 0571 0572 nlresp = length(lrespdata.data{presdcol.RESP_CODE}); 0573 lresp{ir} = cell(nlresp,1); 0574 for iresp=1:nlresp 0575 if ir > length(lresp) || iresp > length(lresp{ir}) || ... 0576 iresp > length(lrespdata.data{presdcol.RESP_CODE}) || ... 0577 isempty(lrespdata.data{presdcol.RESP_CODE}{iresp}) 0578 fprintf(1,'error?\n'); 0579 end % if ir > length(lresp) || iresp > lresp{ir 0580 lresp{ir}{iresp} = lrespdata.data{presdcol.RESP_CODE}{iresp}{1}; 0581 end 0582 end 0583 0584 lphysfilt = pfilt; 0585 lphysfilt.include.all.run = rnum; 0586 if use_filtered 0587 lphysfilt.include.all.path = {'.*filtered.*'}; 0588 end 0589 0590 lphys = ensemble_filter(physio_paths,lphysfilt); 0591 if isempty(lphys.data{physcol.path}) || ... 0592 ~exist(lphys.data{physcol.path}{1},'file') 0593 warning('no physio file found for %s, %s, run %d\n',... 0594 subid,sess.id,rnum); 0595 continue 0596 end 0597 0598 % load data 0599 [fp,fn,fx] = fileparts(lphys.data{physcol.path}{1}); 0600 EEG = pop_loadset('filename',sprintf('%s%s',fn,fx),'filepath',fp); 0601 0602 % set up epochs using presentation data 0603 if (isnumeric(epoch_start) && (length(epoch_start) > 1 || ... 0604 epoch_start)) || ... 0605 (isstruct(epoch_start) && isfield(epoch_start,'filter')) 0606 0607 %%%% find beginnings of epochs 0608 % FIXME: add ability to handle multiple epoch definitions, for 0609 % instance a def for rest period and a def for task period 0610 if isnumeric(epoch_start) && length(epoch_start) > 1 0611 % epoch times provided in a vector, assumed in seconds 0612 estimes = epoch_start; 0613 ns = length(estimes); 0614 elseif isstruct(epoch_start) && isfield(epoch_start,'filter') 0615 % filter criteria for presentation data provided 0616 % events matching these criteria will be taken as epoch starts 0617 esf = epoch_start.filter; 0618 esdata = ensemble_filter(lpres,esf); 0619 if isempty(esdata.data{presdcol.EVENT_TYPE}) 0620 warning(['epoch start data missing, subject %s, session '... 0621 '%s, run %d\n'],subid,sess.id,rnum); 0622 continue 0623 end 0624 estimes = esdata.data{presdcol.RUN_REL_TIME}/1000; 0625 ns = length(estimes); 0626 else 0627 warning(['no epoch start information, skipping run %d, sub '... 0628 '%s, session %s\n'],rnum,subid,sess.id); 0629 continue 0630 end 0631 0632 %%%% find ends of epochs 0633 if isnumeric(epoch_end) && length(epoch_end) == 1 0634 % epoch time provided, assumed in seconds 0635 eetimes = estimes + epoch_end; 0636 ne = length(eetimes); 0637 elseif isnumeric(epoch_end) && length(epoch_end) > 1 0638 % epoch end times provided in a vector, assumed in seconds 0639 % will later be compared to epoch start times 0640 eetimes = epoch_end; 0641 ne = length(eetimes); 0642 elseif isstruct(epoch_end) && isfield(epoch_end,'filter') 0643 % filter criteria for presentation data provided 0644 % events matching these criteria will be taken as epoch ends 0645 eef = epoch_end.filter; 0646 eedata = ensemble_filter(lpres,eef); 0647 if isempty(eedata.data{presdcol.EVENT_TYPE}) 0648 warning(['epoch end data missing, subject %s, session '... 0649 '%s, run %d\n'],subid,sess.id,rnum); 0650 continue 0651 end 0652 eetimes = eedata.data{presdcol.RUN_REL_TIME}/1000; 0653 ne = length(eetimes); 0654 else 0655 warning(['no epoch end information provided for sub %s, sess '... 0656 '%s, run %d, calculating as minimum distance between '... 0657 'epoch onsets\n'],subid,sess.id,rnum); 0658 etime = min(diff(estimes)); 0659 end 0660 0661 % account for raw signal baseline period 0662 estimes = estimes - EEG.xmin; 0663 eetimes = eetimes - EEG.xmin; 0664 0665 if ~exist('etime','var') && ne ~= ns || any (estimes > eetimes) 0666 % end time not specified, 0667 warning(['outlier starting or ending events, or events not '... 0668 'aligned, sub %s, session %s, run %d\n'],subid,sess.id,rnum); 0669 continue 0670 % FIXME: should have some logic here to at least attempt to 0671 % reconcile estimes and eetimes ... rules such as "no end time 0672 % before the first start, no start time before the first end", as 0673 % well as "line up each start/end, 1 to 1, pick out outliers, and 0674 % reconcile pairs whose end is before it's given start 0675 end 0676 0677 % find epoch length, if not explicitly provided 0678 if ~exist('etime','var') 0679 % make sure to remove epochs that exceed the samples in EEG 0680 ee_end_idxs = eetimes > (EEG.pnts/EEG.srate); 0681 if any(ee_end_idxs) 0682 warning('%d epochs out of bounds, removing\n',length(ee_end_idxs)); 0683 end 0684 eetimes(ee_end_idxs) = []; % remove exceeding epochs from eetimes 0685 estimes(ee_end_idxs) = []; % remove exceeding epochs from estimes 0686 dtimes = eetimes - estimes; 0687 if isstruct(epoch_end) && isfield(epoch_end,'minimum_end') ... 0688 && epoch_end.minimum_end 0689 etime = min(dtimes); 0690 else 0691 etime = max(dtimes); 0692 end 0693 end 0694 0695 % double check that all epochs end before the end of the data set 0696 badtimes = (estimes+etime) > (EEG.pnts/EEG.srate); 0697 if any(badtimes) 0698 ee_end_idxs = find(badtimes); 0699 warning('%d epochs out of bounds, removing\n',length(ee_end_idxs)); 0700 eetimes(ee_end_idxs) = []; % remove exceeding epochs from eetimes 0701 estimes(ee_end_idxs) = []; % remove exceeding epochs from estimes 0702 end 0703 ns = length(estimes); 0704 ne = length(eetimes); 0705 0706 % import event onsets 0707 EEG = pop_importevent(EEG,'append','no','event',[ones(ns,1) ... 0708 estimes],'fields',{'type','latency'},'timeunit',1); 0709 0710 % epoch the data based upon event onsets and the largest event duration 0711 EEG = pop_epoch(EEG,[],[-baseline etime],'eventindices',1:ns); 0712 0713 if EEG.trials ~= ns 0714 error('one or more epochs was lost, sub %s, %s, run %d\n',... 0715 subid,sess.id,rnum); 0716 end 0717 0718 else 0719 ns = 1; 0720 end % if (isnumeric(epoch_start 0721 0722 % set sampling rate 0723 scr.pk.Fs = EEG.srate; 0724 pulse.pk.Fs = EEG.srate; 0725 0726 % set up a figure file for tsub/sess/run 0727 if GRAPH_EPOCHS && SAVE_EPOCHS 0728 figfname = fullfile(fp,sprintf('%s_epochs.ps',fn)); 0729 end 0730 0731 for ic=1:nchan 0732 c = channels{ic}; 0733 0734 baseline_end = (baseline*EEG.srate)+1; % next sample after end of baseline 0735 0736 % extract channel location 0737 cidx = strmatch(c,{EEG.chanlocs.labels}); 0738 if isempty(cidx) 0739 warning(['no channel label found for %s, skipping for '... 0740 'sub %s, session %s, run %d'],c,subid,sess.id,rnum); 0741 continue 0742 end 0743 0744 switch c 0745 case {'gsr','scr'} 0746 0747 % extract all epochs for this channel 0748 epochs = squeeze(EEG.data(cidx,:,:)); 0749 0750 sepoch = size(epochs); 0751 if (sepoch(1) == ns && sepoch(2) ~= ns) 0752 epochs = epochs'; 0753 sepoch = size(epochs); 0754 end 0755 0756 % subtract baseline for each epoch 0757 for iep=1:ns 0758 if baseline 0759 mbase = mean(epochs(1:(baseline*EEG.srate),iep)); 0760 pksig = epochs(:,iep) - mbase; 0761 if ~keep_baseline 0762 pksig(1:baseline*EEG.srate) = []; 0763 end 0764 else 0765 pksig = epochs(:,iep); 0766 end 0767 0768 % get peaks 0769 [pidxs,heights,bidxs] = find_peaks(pksig,scr.pk); 0770 0771 if baseline && keep_baseline 0772 % remove peaks whose troughs occur before 1 sec after 0773 % stimulus onset 0774 rmbase = bidxs < (baseline+1)*EEG.srate; 0775 pidxs(rmbase) = []; 0776 heights(rmbase) = []; 0777 bidxs(rmbase) = []; 0778 end 0779 0780 % graph?? 0781 if GRAPH_EPOCHS 0782 m = mod(iep-1,ep_per_fig); 0783 if m == 0, figure(); end 0784 subplot(ep_per_fig,1,m+1); 0785 plot(pksig); 0786 title(sprintf(['%s, %s, run %d, signal %s, epoch'... 0787 ' %d'],subid,sess.id,rnum,c,iep)); 0788 hold on; 0789 for ipk = 1:length(pidxs) 0790 plot(pidxs(ipk),pksig(pidxs(ipk)),'g*'); 0791 end 0792 hold off; 0793 0794 if SAVE_EPOCHS && (m+1) == ep_per_fig 0795 % save graphed epochs to a file 0796 print(pargs{:},figfname); 0797 end 0798 end 0799 0800 % save signal out to gsr_epochs 0801 outdata.data{gsre_idx} = ensemble_add_data_struct_row(... 0802 outdata.data{gsre_idx},'subject_id',subid,'session',... 0803 sess.id,'ensemble_id',sess.ensemble_id,'run',rnum,... 0804 'trial',iep,'signal',{pksig},'srate',EEG.srate,... 0805 'peakidxs',pidxs); 0806 end % for iep=1:ns 0807 0808 % save final figure to file if not already saved 0809 if GRAPH_EPOCHS && SAVE_EPOCHS && (m+1) ~= ep_per_fig 0810 print(pargs{:},figfname); 0811 end 0812 0813 case {'cardiac','pulse'} 0814 0815 % iterate over epochs 0816 for iep=1:ns 0817 % find peaks 0818 pksig = EEG.data(cidx,baseline_end:end,iep); 0819 pidxs = find_peaks(pksig,pulse.pk); 0820 0821 % save signal out to cardiac_epochs 0822 outdata.data{carde_idx} = ensemble_add_data_struct_row(... 0823 outdata.data{carde_idx},'subject_id',subid,'session',... 0824 sess.id,'ensemble_id',sess.ensemble_id,'run',rnum,... 0825 'trial',iep,'signal',{pksig'},'srate',EEG.srate,... 0826 'peakidxs',pidxs); 0827 0828 % graph?? 0829 if GRAPH_EPOCHS 0830 m = mod(iep-1,ep_per_fig); 0831 if m == 0, figure(); end 0832 subplot(ep_per_fig,1,m+1); 0833 plot(pksig); 0834 title(sprintf(['%s, %s, run %d, signal %s, epoch'... 0835 ' %d'],subid,sess.id,rnum,c,iep)); 0836 hold on; 0837 for ipk = 1:length(pidxs) 0838 plot(pidxs(ipk),pksig(pidxs(ipk)),'g*'); 0839 end 0840 hold off; 0841 0842 if SAVE_EPOCHS && (m+1) == ep_per_fig 0843 % save graphed epochs to a file 0844 print(pargs{:},figfname); 0845 end 0846 end 0847 end % for iep=1:ns 0848 0849 % save final figure to file if not already saved 0850 if GRAPH_EPOCHS && SAVE_EPOCHS && (m+1) ~= ep_per_fig 0851 print(pargs{:},figfname); 0852 end 0853 0854 otherwise 0855 warning('unknown channel %s',c); 0856 0857 end % switch c 0858 0859 % add response information 0860 for iresp = 1:length(resp_names) 0861 % get all responses 0862 rname = resp_names{iresp}; 0863 respdata = lresp{iresp}; 0864 nrespd = length(respdata); 0865 % check against length of events (ns) 0866 if nrespd < ns 0867 warning(['data for response %s of different length (%d) '... 0868 'than number of epochs (%d), these are not being '... 0869 'added to the output\n'],rname,nrespd,ns); 0870 continue 0871 elseif ns < nrespd 0872 warning(['more responses (%d) than epochs (%d), assume '... 0873 'that epochs were dropped from the end, and dropping '... 0874 'extra responses'],nrespd,ns); 0875 respdata(ns+1:end) = []; 0876 end 0877 0878 % get indices for this channel and this response 0879 switch c 0880 case {'gsr','scr'} 0881 oidx = gsre_idx; 0882 ridx = gsre_cols.(rname); 0883 case {'cardiac','pulse'} 0884 oidx = carde_idx; 0885 ridx = carde_cols.(rname); 0886 otherwise 0887 warning('unknown channel type (%s)\n',c); 0888 continue 0889 end 0890 0891 % save into outdata 0892 outdata.data{oidx}.data{ridx}(end-ns+1:end) = respdata; 0893 0894 end % for iresp=1:length(resp_names 0895 end % for ic=1:nchan 0896 end % for irun=1: 0897 end % for isess = 1: 0898 0899 if (GRAPH_EPOCHS && EPOCH_BY_RESP) 0900 pfilt = struct(); 0901 pfilt.include.all.subject_id = {subid}; 0902 pfilt.include.all.path_type = {'physio_outdir'}; 0903 lpathdata = ensemble_filter(pathdata,pfilt); 0904 if ~isempty(lpathdata.data{1}) 0905 physdir = lpathdata.data{pcol.path}{1}; 0906 check_dir(physdir); 0907 else 0908 warning(['could not find physio_outdir in pathdata, not graphing '... 0909 'epochs by response for subject %s'],subid); 0910 continue 0911 end 0912 0913 if GRAPH_EPOCHS && EPOCH_BY_RESP 0914 figfname = fullfile(physdir,sprintf('%s_epochs_by_resp.ps',subid)); 0915 % graph epochs by response type 0916 for iresp=1:length(resp_names) 0917 rname = resp_names{iresp}; 0918 for ic=1:nchan 0919 c = channels{ic}; 0920 % get channel and response indexes 0921 switch c 0922 case {'gsr','scr'} 0923 eidx = gsre_idx; 0924 ridx = gsre_cols.(rname); 0925 sidx = gsre_cols.signal; 0926 case {'cardiac','pulse'} 0927 eidx = carde_idx; 0928 ridx = carde_cols.(rname); 0929 sidx = carde_cols.signal; 0930 otherwise 0931 warning('unknown channel type (%s)\n',c); 0932 continue 0933 end % switch c 0934 0935 % get mask matrix 0936 if iscell(outdata.data{eidx}.data{ridx}) && ... 0937 all(cellfun(@isempty,outdata.data{eidx}.data{ridx})) 0938 warning('no responses for response_name %s\n',rname); 0939 continue 0940 end 0941 0942 [rm,ur] = make_mask_mtx(outdata.data{eidx}.data{ridx}); 0943 0944 for ir=1:length(ur) 0945 figure(); 0946 allsig = []; 0947 hold on; 0948 lidxs = find(rm(:,ir)); 0949 for i=1:length(lidxs) 0950 li=lidxs(i); 0951 sig=outdata.data{eidx}.data{sidx}{li}; 0952 if isempty(sig), continue, end 0953 if ~isempty(strmatch(c,{'cardiac','pulse'})) 0954 pidxs=find_peaks(sig,pulse.pk); 0955 if isempty(pidxs), continue, end 0956 sig=60./(diff(pidxs)/EEG.srate); 0957 end 0958 if size(sig,1) > size(sig,2), sig = sig'; end 0959 plot(sig,'g'); 0960 ssdiff = size(allsig,2) - size(sig,2); 0961 if ssdiff > 0 0962 % allsig is longer, extend sig with nans at the end 0963 sig = [sig nan(1,ssdiff)]; 0964 elseif ssdiff < 0 0965 % sig is longer, extend allsig with nans at the end 0966 allsig = [allsig nan(size(allsig,1),-ssdiff)]; 0967 end 0968 allsig = [allsig; sig]; 0969 end % for i=1:length(lidxs 0970 plot(nanmean(allsig),'k'); 0971 hold off; 0972 lr = ur{ir}; 0973 if isnumeric(lresp), lresp = num2str(lresp); end 0974 title(sprintf('signal: %s, response: %s, %s',c,rname,lr)); 0975 0976 if SAVE_EPOCHS, print(pargs{:},figfname); end 0977 end % for ir=1:length(ur 0978 end % for iresp=1:length(resp_names 0979 end % for ic=1:nchan 0980 end % if GRAPH_EPOCHS && EPOCH_BY_RESP 0981 end % if (GRAPH_EPOCHS && EPOCH_BY_RESP) || 0982 0983 end % for isub=1: 0984 0985 end % if EXTRACT_DATA 0986 0987 if CALCULATE_STATS 0988 0989 % get gsr data? 0990 if exist('gsr_epochs','var') 0991 gdata = gsr_epochs; 0992 elseif exist('gsre_idx','var') 0993 gdata = outdata.data{gsre_idx}; 0994 else 0995 warning('no gsr epoched data found'); 0996 end 0997 0998 % get cardiac data? 0999 if exist('cardiac_epochs','var') 1000 cdata = cardiac_epochs; 1001 elseif exist('carde_idx','var') 1002 cdata = outdata.data{carde_idx}; 1003 else 1004 warning('no cardiac epoched data found'); 1005 end 1006 1007 if exist('gdata','var') 1008 1009 % add response information 1010 ridxs = ~ismember(gdata.vars,xvars); 1011 rvars = {gdata.vars{ridxs}}; 1012 if ~iscell(rvars), rvars = {rvars}; end 1013 1014 % initialize a gsr summary data output structure 1015 outdata.vars = [outdata.vars 'gsr_summ']; 1016 gsr_idx = length(outdata.vars); 1017 outdata.data{gsr_idx} = ensemble_init_data_struct(); 1018 outdata.data{gsr_idx}.type = 'gsr_summ'; 1019 outdata.data{gsr_idx}.vars = {'subject_id','session','ensemble_id',... 1020 'run','trial','integral','scl','sclidx',... 1021 'ttl_integral','ttl_scl','ttl_sclidx',... 1022 'npeaks','mean_peak_height','peak_height_variance',... 1023 'first_peak_time','first_peak_height',rvars{:}}; 1024 gsr_cols = set_var_col_const(outdata.data{gsr_idx}.vars); 1025 outdata.data{gsr_idx}.data{gsr_cols.subject_id} = {}; 1026 outdata.data{gsr_idx}.data{gsr_cols.session} = []; 1027 outdata.data{gsr_idx}.data{gsr_cols.ensemble_id} = []; 1028 outdata.data{gsr_idx}.data{gsr_cols.run} = []; 1029 outdata.data{gsr_idx}.data{gsr_cols.trial} = []; 1030 outdata.data{gsr_idx}.data{gsr_cols.integral} = []; 1031 outdata.data{gsr_idx}.data{gsr_cols.scl} = []; 1032 outdata.data{gsr_idx}.data{gsr_cols.sclidx} = []; 1033 outdata.data{gsr_idx}.data{gsr_cols.ttl_integral} = []; 1034 outdata.data{gsr_idx}.data{gsr_cols.ttl_scl} = []; 1035 outdata.data{gsr_idx}.data{gsr_cols.ttl_sclidx} = []; 1036 outdata.data{gsr_idx}.data{gsr_cols.npeaks} = []; 1037 outdata.data{gsr_idx}.data{gsr_cols.mean_peak_height} = []; 1038 outdata.data{gsr_idx}.data{gsr_cols.peak_height_variance} = []; 1039 outdata.data{gsr_idx}.data{gsr_cols.first_peak_time} = []; 1040 outdata.data{gsr_idx}.data{gsr_cols.first_peak_height} = []; 1041 for in=1:length(rvars) 1042 outdata.data{gsr_idx}.data{gsr_cols.(rvars{in})} = {}; 1043 end 1044 1045 % initialize gsr vars 1046 gcol = set_var_col_const(gdata.vars); 1047 ng = length(gdata.data{1}); 1048 1049 % calculate stats for each epoch 1050 for ie=1:ng 1051 sig = gdata.data{gcol.signal}{ie}; 1052 srate = gdata.data{gcol.srate}(ie); 1053 pidxs = gdata.data{gcol.peakidxs}{ie}; 1054 if iscell(pidxs), pidxs = pidxs{1}; end 1055 if ~isempty(epoch_end) && isnumeric(epoch_end) 1056 end_samp = epoch_end*srate; 1057 sig = sig(1:end_samp); 1058 pidxs(pidxs > end_samp) = []; 1059 end 1060 if isempty(pidxs) 1061 pidxs = []; 1062 hghts = []; 1063 mpkhght = NaN; 1064 pkhghtv = NaN; 1065 fpktime = NaN; 1066 fpkhght = NaN; 1067 else 1068 hghts = peak_heights('signal',sig,'params',scr.pk.peak_heights,... 1069 'peakIdxs',pidxs); 1070 mpkhght = mean(hghts); 1071 pkhghtv = var(hghts); 1072 fpktime = pidxs(1)/srate; 1073 fpkhght = hghts(1); 1074 end 1075 npeaks = length(pidxs); 1076 1077 baseline_end = baseline*srate; 1078 1079 % calculate integral within integral window defined by 1080 % int_begin and int_end 1081 startint = baseline_end + scr.int_begin*srate + 1; 1082 endint = min([(baseline_end + scr.int_end*srate) length(sig)]); 1083 1084 integral = trapz(sig(startint:endint)); 1085 mscl = mean(sig(startint:endint)); 1086 sclidx = trapz(abs(sig(startint:endint)))/(endint-startint); 1087 ttl_int = trapz(sig(baseline_end+1:end)); 1088 ttlmscl = mean(sig(baseline_end+1:end)); 1089 ttlsclidx = trapz(abs(sig(baseline_end+1:end)))/(length(sig)-baseline_end); 1090 1091 % save to outdata 1092 outdata.data{gsr_idx} = ensemble_add_data_struct_row(... 1093 outdata.data{gsr_idx},... 1094 'subject_id',gdata.data{gcol.subject_id}{ie},... 1095 'session',gdata.data{gcol.session}(ie),... 1096 'ensemble_id',gdata.data{gcol.ensemble_id}(ie),... 1097 'run',gdata.data{gcol.run}(ie),... 1098 'trial',gdata.data{gcol.trial}(ie),... 1099 'integral',integral(:),'scl',mscl(:),'sclidx',sclidx(:),... 1100 'ttl_integral',ttl_int(:),'ttl_scl',ttlmscl(:),... 1101 'ttl_sclidx',ttlsclidx(:),... 1102 'npeaks',npeaks,'mean_peak_height',mpkhght,... 1103 'peak_height_variance',pkhghtv,... 1104 'first_peak_time',fpktime,'first_peak_height',fpkhght); 1105 1106 end % for ie=1:ng 1107 1108 % pass on response information 1109 for ir = 1:length(rvars) 1110 % get column indices 1111 ri = gcol.(rvars{ir}); 1112 ro = gsr_cols.(rvars{ir}); 1113 1114 % save into outdata 1115 outdata.data{gsr_idx}.data{ro} = gdata.data{ri}; 1116 end 1117 1118 end % if exist('gdata 1119 1120 if exist('cdata','var') 1121 1122 % add response information 1123 ridxs = ~ismember(cdata.vars,xvars); 1124 rvars = {cdata.vars{ridxs}}; 1125 if ~iscell(rvars), rvars = {rvars}; end 1126 1127 % initialize a cardiac summary data output structure 1128 outdata.vars = [outdata.vars 'cardiac_summ']; 1129 card_idx = length(outdata.vars); 1130 outdata.data{card_idx} = ensemble_init_data_struct(); 1131 outdata.data{card_idx}.type = 'cardiac_summ'; 1132 outdata.data{card_idx}.vars = {'subject_id','session','ensemble_id',... 1133 'run','trial','heart_rate','hr_std','hr_rmssd','hr_slope',rvars{:}}; 1134 card_cols = set_var_col_const(outdata.data{card_idx}.vars); 1135 outdata.data{card_idx}.data{card_cols.subject_id} = {}; 1136 outdata.data{card_idx}.data{card_cols.session} = []; 1137 outdata.data{card_idx}.data{card_cols.ensemble_id} = []; 1138 outdata.data{card_idx}.data{card_cols.run} = []; 1139 outdata.data{card_idx}.data{card_cols.trial} = []; 1140 outdata.data{card_idx}.data{card_cols.heart_rate} = []; 1141 outdata.data{card_idx}.data{card_cols.hr_rmssd} = []; 1142 outdata.data{card_idx}.data{card_cols.hr_std} = []; 1143 outdata.data{card_idx}.data{card_cols.hr_slope} = []; 1144 for in=1:length(rvars) 1145 outdata.data{card_idx}.data{card_cols.(rvars{in})} = {}; 1146 end 1147 1148 % initialize cardiac vars 1149 ccol = set_var_col_const(cdata.vars); 1150 nc = length(cdata.data{1}); 1151 1152 % iterate over epochs 1153 for iep=1:nc 1154 srate = cdata.data{ccol.srate}(iep); 1155 pidxs = cdata.data{ccol.peakidxs}{iep}; 1156 if iscell(pidxs), pidxs = pidxs{1}; end 1157 if ~isempty(epoch_end) && isnumeric(epoch_end) 1158 end_samp = epoch_end*srate; 1159 sig = sig(1:end_samp); 1160 pidxs(pidxs > end_samp) = []; 1161 end 1162 if isempty(pidxs), pidxs = nan; end 1163 1164 % get beat-to-beat intervals in ms 1165 dtimes = diff(pidxs)/srate*1000; 1166 dbak = dtimes; 1167 1168 % remove out-of-bounds values, such that 300ms <= dtimes <= 2000ms 1169 % Timonen et al 2006, in Li et al 2009 IntJPsychophysiology 1170 dbak(dbak < 300) = []; 1171 dbak(dbak > 2000) = []; 1172 dtimes(dtimes > mean(dtimes)+std(dtimes)*3) = []; 1173 dtimes(dtimes < mean(dtimes)-std(dtimes)*3) = []; 1174 dtimes(dtimes < 300) = []; 1175 dtimes(dtimes > 2000) = []; 1176 1177 % heart rate (in beats per minute) and heart rate variance 1178 hr_bpm = 1/(mean(dtimes)/1000)*60; 1179 hr_std = std(dtimes); 1180 hr_rmssd = sqrt(mean(diff(dtimes).^2)); 1181 1182 % slope of change in heart rate 1183 zlength = zscore(1:length(dtimes))'; 1184 ztimes = zscore(dtimes); 1185 hr_slope = regress(ztimes,zlength); 1186 1187 % save to outdata 1188 outdata.data{card_idx} = ensemble_add_data_struct_row(... 1189 outdata.data{card_idx},... 1190 'subject_id', cdata.data{ccol.subject_id}{iep},... 1191 'session', cdata.data{ccol.session}(iep),... 1192 'ensemble_id',cdata.data{ccol.ensemble_id}(iep),... 1193 'run', cdata.data{ccol.run}(iep),... 1194 'trial', cdata.data{ccol.trial}(iep),... 1195 'heart_rate',hr_bpm,'hr_rmssd',hr_rmssd,... 1196 'hr_std',hr_std,'hr_slope',hr_slope); 1197 1198 end % for iep=1:nc 1199 1200 1201 % pass on response information 1202 for ir = 1:length(rvars) 1203 % get column indices 1204 ri = ccol.(rvars{ir}); 1205 ro = card_cols.(rvars{ir}); 1206 1207 % save into outdata 1208 outdata.data{card_idx}.data{ro} = cdata.data{ri}; 1209 end 1210 1211 end % if exist('cdata 1212 1213 if SAVE_DATA 1214 for ic=1:nchan 1215 c = channels{ic}; 1216 % get channel idxs 1217 switch c 1218 case {'gsr','scr'} 1219 oidx = gsr_idx; 1220 ocol = gsr_cols; 1221 case {'cardiac','pulse'} 1222 oidx = card_idx; 1223 ocol = card_cols; 1224 otherwise 1225 warning('unknown channel type (%s)\n',c); 1226 end % switch c 1227 1228 % find unique subjects in calculated data 1229 us = unique(outdata.data{oidx}.data{ocol.subject_id}); 1230 ns = length(us); 1231 1232 % iterate over subjects 1233 for is=1:ns 1234 subid = us{is}; 1235 1236 % get output data for this subject 1237 pfilt = struct(); 1238 pfilt.include.all.subject_id = {subid}; 1239 odata = ensemble_filter(outdata.data{oidx},pfilt); 1240 1241 % get path data for this subject 1242 pfilt.include.all.path_type = {'physio_outdir'}; 1243 lpathdata = ensemble_filter(pathdata,pfilt); 1244 if isempty(lpathdata.data{1}) 1245 warning(['could not find physio_outdir in pathdata, not '... 1246 'saving data for subject %s'],subid); 1247 elseif isempty(odata.data{1}) 1248 warning('could not find output data for subject %s, SKIPPING',... 1249 subid); 1250 else 1251 physdir = lpathdata.data{pcol.path}{1}; 1252 check_dir(physdir); 1253 1254 xsp.export = export; 1255 xsp.export.fname = fullfile(physdir,... 1256 sprintf('%s_%s_%sexport.txt',subid,odata.type,export_stub)); 1257 xsp.sas = sas; 1258 xsp.sas.fname = fullfile(physdir,sprintf('%s_%s_%sexport.sas',... 1259 subid,odata.type,export_stub)); 1260 xsp.sas.libname=sprintf('s%s_%s%s',subid,odata.type,export_stub); 1261 ensemble_export_sastxt(odata,xsp); 1262 end % if ~isempty(lpathdata.data{1 1263 end % for is=1:ns 1264 end % for ic=1:nchan 1265 end % if SAVE_DATA 1266 1267 end % if CALCULATE_STATS