From 206cc3a56fdb7967fce04937a057ffeb5585c529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20B=C3=A9nyei?= <33413678+kheki4@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:13:56 +0100 Subject: [PATCH] Initial commit to add files --- CALC_FILTERED_TEPR.m | 599 ++++++++++++++++++++++++++ CONFIG_PREPROC_DEFAULTS.m | 61 +++ CONFIG_PREPROC_NBACK.m | 283 ++++++++++++ CONFIG_TEPR_DEFAULTS.m | 203 +++++++++ CONFIG_TEPR_NBACK.m | 176 ++++++++ DQ_CalcInterpLossAnalogWhole.m | 32 ++ DQ_Decimate.m | 12 + DQ_RemoveBlinks.m | 25 ++ DQ_RemoveHiccups.m | 37 ++ DQ_RemoveNaNs.m | 12 + DQ_RemoveSamplesByConfidence.m | 44 ++ DQ_RemoveZeros.m | 42 ++ DQ_Resample.m | 20 + ERPD_SET_AMP_EQ.m | 13 + FiltSweepsOnBlink.m | 22 + FiltSweepsOnInterpol.m | 20 + FiltSweepsOnSD.m | 18 + FiltSweepsOnSaccade.m | 22 + GET_TRIALCHANGES.m | 65 +++ GetData.m | 27 ++ GetData_MapBehav_NBACKpython.m | 92 ++++ Misc/DemoPic.png | Bin 0 -> 124993 bytes PREPROC.m | 363 ++++++++++++++++ Parser_EyeLink.m | 169 ++++++++ Parser_Other.m | 15 + Parser_PupilEXT.m | 94 ++++ Parser_PupilEXT_determineDelimiter.m | 21 + Parser_PupilEXT_getHeaderColNrs.m | 36 ++ Parser_SMI.m | 130 ++++++ Parser_SMI_Blinks.m | 29 ++ Parser_SMI_Saccades.m | 38 ++ Parser_SMI_getFirstDataRow_Events.m | 29 ++ Parser_SMI_getFirstDataRow_Samples.m | 20 + Parser_SMI_getHeaderColNrs.m | 50 +++ Parser_SMI_getParamValue.m | 41 ++ README.MD | 21 + callable_behavfilt_NBACK.m | 71 +++ callable_initbehavfilt_NBACK.m | 88 ++++ log_d.m | 9 + log_e.m | 3 + log_i.m | 10 + log_w.m | 11 + support_CalcERADensity.m | 50 +++ support_DefineETDataSpecs.m | 22 + support_FindParticipantsByFiles.m | 26 ++ support_PerformHarFilt.m | 47 ++ support_PlotDynBLCorrMap.m | 181 ++++++++ support_PlotERA.m | 135 ++++++ support_PlotGrandERAConf.m | 132 ++++++ support_PlotGrandERADensity.m | 17 + support_PlotGrandTEPR.m | 174 ++++++++ support_PlotPupilData.m | 39 ++ support_PlotTEPR.m | 148 +++++++ support_SaveBaselineValues.m | 35 ++ support_SaveEveryTrial.m | 38 ++ support_SavePeakValues.m | 40 ++ support_SaveTEPREveryParticipant.m | 43 ++ support_SaveTrialExclusionSummary.m | 48 +++ support_SplitIntoSweeps.m | 67 +++ support_ZNormSweeps.m | 37 ++ support_calcInterpolRatios.m | 58 +++ support_createAlignedTriggerVecStim.m | 46 ++ support_createTriggerVecResp.m | 26 ++ support_displayStat.m | 25 ++ support_eventRelatedFunc.m | 35 ++ support_filterBlinkDelta.m | 24 ++ support_findBlinkDelta.m | 44 ++ support_findTrialChanges.m | 43 ++ support_restrictMaskToNumVals.m | 19 + 69 files changed, 4672 insertions(+) create mode 100644 CALC_FILTERED_TEPR.m create mode 100644 CONFIG_PREPROC_DEFAULTS.m create mode 100644 CONFIG_PREPROC_NBACK.m create mode 100644 CONFIG_TEPR_DEFAULTS.m create mode 100644 CONFIG_TEPR_NBACK.m create mode 100644 DQ_CalcInterpLossAnalogWhole.m create mode 100644 DQ_Decimate.m create mode 100644 DQ_RemoveBlinks.m create mode 100644 DQ_RemoveHiccups.m create mode 100644 DQ_RemoveNaNs.m create mode 100644 DQ_RemoveSamplesByConfidence.m create mode 100644 DQ_RemoveZeros.m create mode 100644 DQ_Resample.m create mode 100644 ERPD_SET_AMP_EQ.m create mode 100644 FiltSweepsOnBlink.m create mode 100644 FiltSweepsOnInterpol.m create mode 100644 FiltSweepsOnSD.m create mode 100644 FiltSweepsOnSaccade.m create mode 100644 GET_TRIALCHANGES.m create mode 100644 GetData.m create mode 100644 GetData_MapBehav_NBACKpython.m create mode 100644 Misc/DemoPic.png create mode 100644 PREPROC.m create mode 100644 Parser_EyeLink.m create mode 100644 Parser_Other.m create mode 100644 Parser_PupilEXT.m create mode 100644 Parser_PupilEXT_determineDelimiter.m create mode 100644 Parser_PupilEXT_getHeaderColNrs.m create mode 100644 Parser_SMI.m create mode 100644 Parser_SMI_Blinks.m create mode 100644 Parser_SMI_Saccades.m create mode 100644 Parser_SMI_getFirstDataRow_Events.m create mode 100644 Parser_SMI_getFirstDataRow_Samples.m create mode 100644 Parser_SMI_getHeaderColNrs.m create mode 100644 Parser_SMI_getParamValue.m create mode 100644 README.MD create mode 100644 callable_behavfilt_NBACK.m create mode 100644 callable_initbehavfilt_NBACK.m create mode 100644 log_d.m create mode 100644 log_e.m create mode 100644 log_i.m create mode 100644 log_w.m create mode 100644 support_CalcERADensity.m create mode 100644 support_DefineETDataSpecs.m create mode 100644 support_FindParticipantsByFiles.m create mode 100644 support_PerformHarFilt.m create mode 100644 support_PlotDynBLCorrMap.m create mode 100644 support_PlotERA.m create mode 100644 support_PlotGrandERAConf.m create mode 100644 support_PlotGrandERADensity.m create mode 100644 support_PlotGrandTEPR.m create mode 100644 support_PlotPupilData.m create mode 100644 support_PlotTEPR.m create mode 100644 support_SaveBaselineValues.m create mode 100644 support_SaveEveryTrial.m create mode 100644 support_SavePeakValues.m create mode 100644 support_SaveTEPREveryParticipant.m create mode 100644 support_SaveTrialExclusionSummary.m create mode 100644 support_SplitIntoSweeps.m create mode 100644 support_ZNormSweeps.m create mode 100644 support_calcInterpolRatios.m create mode 100644 support_createAlignedTriggerVecStim.m create mode 100644 support_createTriggerVecResp.m create mode 100644 support_displayStat.m create mode 100644 support_eventRelatedFunc.m create mode 100644 support_filterBlinkDelta.m create mode 100644 support_findBlinkDelta.m create mode 100644 support_findTrialChanges.m create mode 100644 support_restrictMaskToNumVals.m diff --git a/CALC_FILTERED_TEPR.m b/CALC_FILTERED_TEPR.m new file mode 100644 index 0000000..214d681 --- /dev/null +++ b/CALC_FILTERED_TEPR.m @@ -0,0 +1,599 @@ +if ~exist('FLAG_GRANDCALC', 'var') || ~FLAG_GRANDCALC + clear; % clear env + clc; % clear command window + Config.Plots.Layered = false; + Config.Plots.LayeredFigCounter = 1; +else + Config.Plots.Layered = true; +end +format compact; +format long; + +global LOGLEVEL +% LOGLEVEL = 1; % nothing +% LOGLEVEL = 2; % info only +% LOGLEVEL = 3; % info and warning too +LOGLEVEL = 4; % info, warning and debug too + +% DEVMODE = true; +DEVMODE = false; + +% -------------------------------------------------- +% CONFIG + +% Initializing defaults (must happen here) +CONFIG_TEPR_DEFAULTS + +% Config for the actual experiment (may be custom) +CONFIG_TEPR_NBACK + + +% -------------------------------------------------- +% PREPARE VARIABLES + +Meta = load(['~PREPDATA/' Config.ETDSDirName '/' 'Metafile' '.mat']); +if DEVMODE + Meta.RootDirTag = [Meta.RootDirTag '_DEV']; + disp('RUNNING IN DEVELOPER MODE'); +end + +Participants = support_FindParticipantsByFiles(Config, ['~PREPDATA/' Config.ETDSDirName], '.mat'); + +if isnan(Config.Plot.GrandTEPR.XLim) + Config.Plot.GrandTEPR.XLim = [Config.AnalyzeFromSec*1000 Config.AnalyzeToSec*1000]; +end +if Config.Plot.GrandTEPR.Make && sum(isnan(Config.Plot.GrandTEPR.YLim)) == 0 + log_w('No Y limit defined for Grand TEPR plot generation. Every participant will have different scaling accordingly.'); +end + +if isnan(Config.Plot.TEPR.XLim) + Config.Plot.TEPR.XLim = [Config.AnalyzeFromSec*1000 Config.AnalyzeToSec*1000]; +end +if Config.Plot.TEPR.Make && sum(isnan(Config.Plot.TEPR.YLim)) == 0 + log_w('No Y limit defined for TEPR plot generation. Every participant will have different scaling accordingly.'); +end + +% -------------------------------------------------- +% PREPARE EXPERIMENT-SPECIFIC VARIABLES + +NumParticipants = size(Participants,2); + +ERAEventDensity_grand = []; + +% To avoid cases when e.g. the stimulus presented software lagged a bit during a trial. But only when each trial was the same long. Can be disabled +if ~isnan(RejectTrialsOutsideLenSec) && length(RejectTrialsOutsideLenSec) == 2 && sum(isnan(RejectTrialsOutsideLenSec))==0 && RejectTrialsOutsideLenSec(1) >= 0.1 && RejectTrialsOutsideLenSec(2) <= 10*60 + RejectTrialsUnderSec = RejectTrialsOutsideLenSec(1); + RejectTrialsOverSec = RejectTrialsOutsideLenSec(2); +else + RejectTrialsUnderSec = Meta.ISISec -0.2; + RejectTrialsOverSec = Meta.ISISec +0.2; +end + +Config.SRate = Meta.NomSRate; +% Needed for calculations: +Config.FixBeforeStimSec = Meta.ISISec -Meta.StimOnScreenSec; +Config.AnalyzeLenSec = Config.AnalyzeToSec -Config.AnalyzeFromSec; + +% Relative to x=0 line depending on sign, can be negative (+1 for Matlab indexing): +Config.AnalyzeFromSample = round(Meta.NomSRate *Config.AnalyzeFromSec) +1; +Config.AnalyzeToSample = round(Meta.NomSRate *Config.AnalyzeToSec) +1; +Config.PeakFromSample = round(Meta.NomSRate *Config.PeakFromSec) +1; +Config.PeakToSample = round(Meta.NomSRate *Config.PeakToSec) +1; +Config.BaselineFromSample = round(Meta.NomSRate *Config.BaselineFromSec) +1; +Config.BaselineToSample = round(Meta.NomSRate *Config.BaselineToSec) +1; + +% Mapped sample values, can be used for indexing the event-related curve +Config.PeakFromSampleMapped = (-1*Config.AnalyzeFromSample) +Config.PeakFromSample +1; +Config.PeakToSampleMapped = (-1*Config.AnalyzeFromSample) +Config.PeakToSample; +Config.BaselineFromSampleMapped = (-1*Config.AnalyzeFromSample) +Config.BaselineFromSample +1; +Config.BaselineToSampleMapped = (-1*Config.AnalyzeFromSample) +Config.BaselineToSample; + +% Can only be positive: +Config.ISISec = Meta.StimOnScreenSec; +Config.ISISec = Config.FixBeforeStimSec; +Config.ISISec = Meta.ISISec; + +Config.StimOnScreenSample = round( Meta.NomSRate *Meta.StimOnScreenSec); +Config.FixBeforeStimSample = round( Meta.NomSRate *Config.FixBeforeStimSec); +Config.ISISample = round(Meta.NomSRate *Meta.ISISec); +Config.AnalyzeLenSample = round(Meta.NomSRate *Config.AnalyzeLenSec) +1; % We store data at 0th elem too + +NumTrials = Meta.FilterTrials(2); + +RejectTrialsUnderLen = floor(RejectTrialsUnderSec * Meta.NomSRate); +RejectTrialsOverLen = floor(RejectTrialsOverSec * Meta.NomSRate); +log_i([ 'Rejecting trials under typical length: ' num2str(RejectTrialsUnderSec) ' sec (= ' num2str(RejectTrialsUnderLen) ' data points)' ]); +log_i([ 'Rejecting trials over typical length: ' num2str(RejectTrialsOverSec) ' sec (= ' num2str(RejectTrialsOverLen) ' data points)' ]); + +Config.Filter.Behav.S = '~'; +Config.Filter.Behav.R = '~'; +Config.Filter.Behav.V = '~'; +Config.Filter.Behav.FriendlyName = 'All Trials'; + +if Config.Save.BaselineValues + BaselineValues = NaN(NumParticipants, 1); +end + +Filter.SD.PercentFiltered = zeros(NumParticipants, 1); +Config.Filter.Behav.PercentFiltered = zeros(NumParticipants, 1); +Filter.Interpol.PercentFiltered = zeros(NumParticipants, 1); +Filter.BaselineBlink.PercentFiltered = zeros(NumParticipants, 1); +Filter.BaselineSaccade.PercentFiltered = zeros(NumParticipants, 1); +Filter.SOIBlink.PercentFiltered = zeros(NumParticipants, 1); +Filter.SOISaccade.PercentFiltered = zeros(NumParticipants, 1); + +TEPRCurves = NaN(Config.AnalyzeLenSample, NumParticipants); +PeakValues = NaN(NumParticipants, 1); +TRIAL_EXCLUSIONS = NaN(NumParticipants, 9); +TEPREveryParticipant = NaN( NumParticipants, Config.AnalyzeLenSample ); +TIMECOURSE_BRUTE = NaN( NumParticipants, Config.AnalyzeLenSample ); + +% -------------------------------------------------- +% LOOP TO PROCESS PARTICIPANTS + +for ppnr = 1:NumParticipants + + Participant.ID = Participants{ppnr}; + Participant.Nr = ppnr; + + log_i('--------------------------------------------------'); + log_i(['Currently processing ' char(Participant.ID) ' at index ' num2str(Participant.Nr)]); + + % -------------------------------------------------- + % LOAD DATA + + load(['~PREPDATA/' Config.ETDSDirName '/' Participant.ID '.mat']); + + % Samples.Pupdil = Samples.Pupdil(randperm(length(Samples.Pupdil))); + + if round(Samples.SRate) ~= Meta.NomSRate + log_w(['~Samples file SRate does not equal to the nominal sampling rate stated in Metafile. Please check preprocessor code and ensure data consistency.']); + end + + % -------------------------------------------------- + % PREPARE SUBEJCT-SPECIFIC VARIABLES + + RejectedTrials = false(NumTrials, 1); + excludedTrials = false(NumTrials, 1); % this will contain the unified mask from all filters AND also the RejectedTrials mask + Filter.Interpol.ExcludedMask = false(NumTrials, 1); + Config.Filter.Behav.ExcludedMask = false(NumTrials, 1); + + % -------------------------------------------------- + % SELECTING TIMESTAMPS FOR INTER-TRIAL ALIGNMENT + + if Config.AlignToStimOrResp + TrigsForAlignment = Triggers.Stim.Ts; + else + % TODO: check if exists + TrigsForAlignment = Triggers.resp.Ts; + end + + % -------------------------------------------------- + % REJECT CERTAIN TRIALS FROM ANY PROCESSING + + for v = 1:NumTrials + + % E.g. when we are making a response-aligned analysis, and the subject has no response in a trial + if isnan(TrigsForAlignment(v)) + RejectedTrials(v) = true; + continue; + end + + % Skip first N trials if necessary (e.g. when there was no separate practice block) + if v <= Config.SkipFirstNtrials + RejectedTrials(v) = true; + continue; + end + + % NOTE: only whole trials are taken into averaging. Analytic time length setting affects this + if ( length(Samples.Pupdil) - find(Samples.Ts >= TrigsForAlignment(v), 1, 'first') ) < Config.AnalyzeLenSample + RejectedTrials(v) = true; + continue; + end + + if Config.AlignToStimOrResp == false && isnan(Behav.RT(v)) % EKKOR RESPONSE-HOZ IGAZÍTUNK. HA NINCS RESPONSE, REJECT TRIAL + RejectedTrials(v) = true; + continue; + end + + % reject trials that would later break the pipeline because their analyzed period begins earlier than the first sample, or later than the last + if ( length(TrigsForAlignment) > v && ... + ( find(Samples.Ts >= TrigsForAlignment(v), 1, 'first')) + Config.AnalyzeFromSample < 1 || ... + (find(Samples.Ts >= TrigsForAlignment(v), 1, 'first') + Config.AnalyzeLenSample > length(Samples.Ts) ) ) + + RejectedTrials(v) = true; + continue; + end + + % only in case we are in a stimulus-aligned analysis + % NOTE: can be buggy, disable if needed + if RejectTrialsOnTypicalLen && ... + Config.AlignToStimOrResp == true && ( length(TrigsForAlignment) > v && ... + ( (find(Samples.Ts >= TrigsForAlignment(v+1), 1, 'first') - find(Samples.Ts >= TrigsForAlignment(v), 1, 'first')) < RejectTrialsUnderLen || ... + (find(Samples.Ts >= TrigsForAlignment(v+1), 1, 'first') - find(Samples.Ts >= TrigsForAlignment(v), 1, 'first')) > RejectTrialsOverLen ) ) || ... + ( length(TrigsForAlignment) == v && (length(Samples.Ts) - find(Samples.Ts >= TrigsForAlignment(v), 1, 'first') < RejectTrialsUnderLen) ) + RejectedTrials(v) = true; + continue; + end + + end + + log_d('See list of rejected trials by trial number:') + log_d(num2str(transpose(find(RejectedTrials==1)))); + + % -------------------------------------------------- + % SPLIT SIGNAL INTO SEGMENTS (aka SWEEPS or TRIALS) + + [TrialsArray, ConfsArray] = support_SplitIntoSweeps(Samples, ~RejectedTrials, TrigsForAlignment, Config, Config.PerformTJC); + + % -------------------------------------------------- + % WITHIN-TRIAL PROCESSING + + % Z-NORMALIZATION + if Config.Z_norm_method ~= 0 + TrialsArray = support_ZNormSweeps(Samples, TrialsArray, ~RejectedTrials, Config.Z_norm_method); + else + log_i('Using no Z-normalization now'); + end + + % -------------------------------------------------- + % BETWEEN-TRIALS FILTERING + + % FILTERING ON STIMULUS//RESPONSE CATEGORY + if Config.Filter.Behav.Enabled + + if ~isfield(Config.Filter.Behav, 'CondComb') + Config.Filter.Behav.CondComb = 1; + + log_i('No Config.Filter.Behav.CondComb specified'); + end + + [Config.Filter.Behav, Config.Plot.GrandTEPR] = feval(Config.BehavInitFunction, Config.Filter.Behav, Config.Plot.GrandTEPR); + Config.Filter.Behav.ExcludedMask = feval(Config.BehavFiltFunction, NumTrials, Behav, Config.Filter.Behav); + + end + %---------------------------------------------------------------------------------------------------------------- + + + % MAKING EXCLUDED MASKS FOR COND-COMPUTED TEPR CURVE, FILTERING ON STIMULUS//RESPONSE CATEGORY + % e.g. when: (individual TEPR) = (TEPR of all trials) - (TEPR of false alarms) + % --------------------------------------------------------------------------------------------------------------- + if Config.CC.Enabled + + Config.CC.ExcludedMasks = false(NumTrials, length(Config.CC.Conds)); + + LocalFilterConfig.StimType.A = Config.Filter.Behav.StimType.A; + LocalFilterConfig.StimType.B = Config.Filter.Behav.StimType.B; + LocalFilterConfig.RespType.A = Config.Filter.Behav.RespType.A; + LocalFilterConfig.RespType.B = Config.Filter.Behav.RespType.B; + % TODO: A BETTER SOLUTION + + for cx=1:length(Config.CC.Conds) + LocalFilterConfig.CondComb = Config.CC.Conds(cx); + Config.CC.ExcludedMasks(:, cx) = callable_behavfilt_NBACK(NumTrials, Behav, LocalFilterConfig); + end + + end + %---------------------------------------------------------------------------------------------------------------- + + if Config.Filter.Interpol.Enabled + log_i(['Excluding trials whose interpolation ratio is greater than ' num2str(Config.Filter.Interpol.Threshold)]); + Filter.Interpol.ExcludedMask = FiltSweepsOnInterpol(Samples, ~RejectedTrials, TrigsForAlignment, Config.Filter.Interpol); + end + if Config.Filter.SD.Enabled + log_d(['SD of whole recording: ' num2str(std( Samples.Pupdil , 'omitnan'))]); + log_d(['SD of all existing trials: ' num2str(std( reshape(TrialsArray(:,1:NumTrials),1,[]) , 'omitnan'))]); + log_d(['SD of all non-rejected trials: ' num2str(std( reshape(TrialsArray(:,~RejectedTrials),1,[]) , 'omitnan'))]); + log_i(['Excluding trials whose SD is greater than ' num2str(Config.Filter.SD.LocalLimit)]); + Filter.SD.ExcludedMask = FiltSweepsOnSD(TrialsArray, ~RejectedTrials, Filter.Config.SD); + end + if Config.Filter.BaselineBlink.Enabled + log_i(['Excluding trials whose baseline-correction-period (BLP) would either contain a blink start, or blink end, or would completely fall within a blink']); + Filter.BaselineBlink.ExcludedMask = FiltSweepsOnBlink(Blinks, ~RejectedTrials, TrigsForAlignment, Config.Filter.BaselineBlink); + end + if Config.Filter.BaselineSaccade.Enabled + log_i(['Excluding trials whose baseline-correction-period (BLP) would contain a saccade']); + Filter.BaselineSaccade.ExcludedMask = FiltSweepsOnSaccade(Saccades, ~RejectedTrials, TrigsForAlignment, Config.Filter.BaselineSaccade); + end + if Config.Filter.SOIBlink.Enabled + log_i(['Excluding trials whose time-section-of-interest (SOI) would either contain a blink start, or blink end, or would completely fall within a blink']); + Filter.SOIBlink.ExcludedMask = FiltSweepsOnBlink(Blinks, ~RejectedTrials, TrigsForAlignment, Config.Filter.SOIBlink); + end + if Config.Filter.SOISaccade.Enabled + log_i(['Excluding trials whose time-section-of-interest (SOI) would contain a saccade']); + Filter.SOISaccade.ExcludedMask = FiltSweepsOnSaccade(Saccades, ~RejectedTrials, TrigsForAlignment, Config.Filter.SOISaccade); + end + + + + + % TODO: somehow put this in a loop? e.g. with function handles? + log_i([ 'Proportion of trials rejected: ' num2str( sum(RejectedTrials)/(NumTrials) *100 ) '%' ]); + if Config.Filter.Interpol.Enabled + Filter.Interpol.PercentFiltered(Participant.Nr, 1) = sum(Filter.Interpol.ExcludedMask)/(NumTrials) *100; + log_i([ 'Proportion of trials excluded on within-trial interpolation percentage: ' num2str( Filter.Interpol.PercentFiltered(Participant.Nr, 1) ) '%' ]); + end + if Config.Filter.BaselineBlink.Enabled + Filter.BaselineBlink.PercentFiltered(Participant.Nr, 1) = sum(Filter.BaselineBlink.ExcludedMask)/(NumTrials) *100; + log_i([ 'Proportion of trials excluded on blink-affected baseline interval: ' num2str( Filter.BaselineBlink.PercentFiltered(Participant.Nr, 1) ) '%' ]); + end + if Config.Filter.BaselineSaccade.Enabled + Filter.BaselineSaccade.PercentFiltered(Participant.Nr, 1) = sum(Filter.BaselineSaccade.ExcludedMask)/(NumTrials) *100; + log_i([ 'Proportion of trials excluded on saccade-affected baseline interval: ' num2str( Filter.BaselineSaccade.PercentFiltered(Participant.Nr, 1) ) '%' ]); + end + if Config.Filter.SOIBlink.Enabled + Filter.SOIBlink.PercentFiltered(Participant.Nr, 1) = sum(Filter.SOIBlink.ExcludedMask)/(NumTrials) *100; + log_i([ 'Proportion of trials excluded on blink-affected time-section of interest (SOI): ' num2str( Filter.SOIBlink.PercentFiltered(Participant.Nr, 1) ) '%' ]); + end + if Config.Filter.SOISaccade.Enabled + Filter.SOISaccade.PercentFiltered(Participant.Nr, 1) = sum(Filter.SOISaccade.ExcludedMask)/(NumTrials) *100; + log_i([ 'Proportion of trials excluded on saccade-affected time-section of interest (SOI): ' num2str( Filter.SOISaccade.PercentFiltered(Participant.Nr, 1) ) '%' ]); + end + if Config.Filter.SD.Enabled + Filter.SD.PercentFiltered(Participant.Nr, 1) = sum(Filter.SD.ExcludedMask)/(NumTrials) *100; + log_i([ 'Proportion of trials excluded on SD: ' num2str( Filter.SD.PercentFiltered(Participant.Nr, 1) ) '%' ]); + end + if Config.Filter.Behav.Enabled + Config.Filter.Behav.PercentFiltered(Participant.Nr, 1) = sum(Config.Filter.Behav.ExcludedMask)/(NumTrials) *100; + log_i([ 'Proportion of trials excluded on behav logfile (Stim or Resp category) (all components): ' num2str( Config.Filter.Behav.PercentFiltered(Participant.Nr, 1) ) '%' ]); + end + + % Unify binary masks of filters + % TODO: no loop (but then we cannot check for filters enabled flags) + for i = 1:NumTrials + + if RejectedTrials(i) || ... + (Config.Filter.Interpol.Enabled && Filter.Interpol.ExcludedMask(i)) || ... + (Config.Filter.BaselineBlink.Enabled && Filter.BaselineBlink.ExcludedMask(i)) || ... + (Config.Filter.BaselineSaccade.Enabled && Filter.BaselineSaccade.ExcludedMask(i)) || ... + (Config.Filter.SOIBlink.Enabled && Filter.SOIBlink.ExcludedMask(i)) || ... + (Config.Filter.SOISaccade.Enabled && Filter.SOISaccade.ExcludedMask(i)) || ... + (Config.Filter.SD.Enabled && Filter.SD.ExcludedMask(i)) || ... + (Config.Filter.Behav.Enabled && Config.Filter.Behav.ExcludedMask(i) ) + excludedTrials(i) = true; + end + + end + + % -------------------------------------------------- + % EVENT-RELATED ARTEFACTS CALCULATION + + if Config.ERA.Enabled % event-related artefacts (event-related blink curve & event-related saccades curve) + + % TODO: Put Sample, Blinks, Saccades in a structure, and keep them NaN if empty + % TODO: beutify + ERAEventDensity = support_CalcERADensity(Samples, Blinks, Saccades, ~RejectedTrials, TrigsForAlignment, Config); + + % TODO: + if ~isempty(ERAEventDensity) + ERAEventDensity_grand = [ERAEventDensity_grand; ERAEventDensity]; + end + end + + % -------------------------------- + + % DEV eventRelatedAmpEq + eventRelatedAmpEq = true; + + if ~Config.CC.Enabled && eventRelatedAmpEq + + if exist('eventRelatedAmpEq1_lastRun', 'var') + excludedTrials = support_restrictMaskToNumVals(excludedTrials, eventRelatedAmpEq1_lastRun, false, 2); + end + + Config.CC.TrialsArray_cond1 = TrialsArray; + Config.CC.TrialsArray_cond1(:, excludedTrials ) = NaN; + + eventRelatedAmpEq1(1,Participant.Nr) = sum(~isnan(mean(Config.CC.TrialsArray_cond1, 1, 'omitnan')),'omitnan'); + log_d(['Cond-Computed TEPR calc: Num.trials finally taken into averaging for Cond.1 = ' num2str(eventRelatedAmpEq1(1,Participant.Nr))]); + end + + % -------------------------------- + + log_i([ 'Proportion of all rejected & excluded trials: ' num2str( sum(excludedTrials)/(NumTrials) *100 ) '%' ]); + + log_d('See list of passed trials by trial number:') + log_d(num2str(transpose(find(excludedTrials==0)))); + + % record the number of exclusions for this participant + if Config.Filter.Interpol.Enabled + TRIAL_EXCLUSIONS(Participant.Nr, 1) = sum(Filter.Interpol.ExcludedMask); + end + if Config.Filter.BaselineBlink.Enabled + TRIAL_EXCLUSIONS(Participant.Nr, 2) = sum(Filter.BaselineBlink.ExcludedMask); + end + if Config.Filter.BaselineSaccade.Enabled + TRIAL_EXCLUSIONS(Participant.Nr, 3) = sum(Filter.BaselineSaccade.ExcludedMask); + end + if Config.Filter.SOIBlink.Enabled + TRIAL_EXCLUSIONS(Participant.Nr, 4) = sum(Filter.SOIBlink.ExcludedMask); + end + if Config.Filter.SOISaccade.Enabled + TRIAL_EXCLUSIONS(Participant.Nr, 5) = sum(Filter.SOISaccade.ExcludedMask); + end + if Config.Filter.SD.Enabled + TRIAL_EXCLUSIONS(Participant.Nr, 6) = sum(Filter.SD.ExcludedMask); + end + if Config.Filter.Behav.Enabled + TRIAL_EXCLUSIONS(Participant.Nr, 7) = sum(Config.Filter.Behav.ExcludedMask); + end + TRIAL_EXCLUSIONS(Participant.Nr, 8) = sum(RejectedTrials); % rejected + TRIAL_EXCLUSIONS(Participant.Nr, 9) = sum(~excludedTrials); % passed + + % filter using the final mask + TrialsArray(:, excludedTrials) = NaN; + ConfsArray(:, excludedTrials) = NaN; + + + % -------------------------------------------------- + % PEAK VALUES COMPUTATION + + if Config.BLCLocalOrGlobal == true + for b = 1:NumTrials + TrialsArray(:, b) = TrialsArray(:, b) - mean(TrialsArray(Config.BaselineFromSampleMapped:Config.BaselineToSampleMapped, b), 'omitnan'); + end + clear b; + end + + % DEV + save_EventRelated_WithinSubject = false; + + if save_EventRelated_WithinSubject + + header = cell( 1, NumTrials ); + for bfc = 1:NumTrials + % headerColStr = [ Meta.CfPrefix '_' 'Epoch' '_' num2str(bfc) ]; + headerColStr = [ 'Epoch' '_' num2str(bfc) ]; + header{1, bfc} = headerColStr; + end + + cols_sub_vals = cell(Config.AnalyzeLenSample+1, NumTrials); + cols_sub_vals(1, 1:NumTrials) = header; + + cols_sub_vals(2:(Config.AnalyzeLenSample+1), 1:NumTrials) = num2cell(TrialsArray); + outputMatrix = [cols_sub_vals]; + + OutFilePath = char([ ... + '~RESULTS/' Meta.RootDirTag '/' ... + 'TEPR csv WS' '/']); + + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = char([ ... + Participant.ID '_TEPR' ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ]); + writecell(outputMatrix ,[OutFilePath OutFileName '.csv'],'Delimiter',';'); + end + + % DEV: + % COND-COMPUTED TEPR CURVE NEEDS THIS + % e.g. when: (individual TEPR) = (TEPR of all trials) - (TEPR of false alarms) + if Config.CC.Enabled + % DEV NOTE: + % currently averaged TEPR only, and + % for 2 conditions only, and + % it is subtracted, cond 1 - cond 2 + + if exist('eventRelatedAmpEq1_lastRun', 'var') && exist('eventRelatedAmpEq2_lastRun', 'var') + Config.CC.ExcludedMasks(:,1) = support_restrictMaskToNumVals(Config.CC.ExcludedMasks(:,1), eventRelatedAmpEq1_lastRun, false, 2); + Config.CC.ExcludedMasks(:,2) = support_restrictMaskToNumVals(Config.CC.ExcludedMasks(:,2), eventRelatedAmpEq2_lastRun, false, 2); + end + + Config.CC.TrialsArray_cond1 = TrialsArray; + Config.CC.TrialsArray_cond1(:, Config.CC.ExcludedMasks(:,1) ) = NaN; + + Config.CC.TrialsArray_cond2 = TrialsArray; + Config.CC.TrialsArray_cond2(:, Config.CC.ExcludedMasks(:,2) ) = NaN; + + eventRelatedAmpEq1(1,Participant.Nr) = sum(~isnan(mean(Config.CC.TrialsArray_cond1, 1, 'omitnan')),'omitnan'); + eventRelatedAmpEq2(1,Participant.Nr) = sum(~isnan(mean(Config.CC.TrialsArray_cond2, 1, 'omitnan')),'omitnan'); + log_d(['Cond-Computed TEPR calc: Num.trials finally taken into averaging for Cond.1 = ' num2str(eventRelatedAmpEq1(1,Participant.Nr))]); + log_d(['Cond-Computed TEPR calc: Num.trials finally taken into averaging for Cond.2 = ' num2str(eventRelatedAmpEq2(1,Participant.Nr))]); + + end + + % ERA CONFIDENCE + if Config.ERA.Enabled + ERAConfCurves(:, Participant.Nr)= ... + support_eventRelatedFunc(ConfsArray, Config.EventRelatedMethod); + end + % TODO: save them to csv + + % -------------------------------------------------- + % Calculate and store TEPR + + if Config.CC.Enabled + TEPRCurves(:, Participant.Nr)= ... + support_eventRelatedFunc(Config.CC.TrialsArray_cond1, Config.EventRelatedMethod) - ... + support_eventRelatedFunc(Config.CC.TrialsArray_cond2, Config.EventRelatedMethod) ; + else + TEPRCurves(:, Participant.Nr)= ... + support_eventRelatedFunc(TrialsArray, Config.EventRelatedMethod); + end + + % Also keep the non-baseline-corrected data + TEPREveryParticipant(Participant.Nr,:) = TEPRCurves(:, Participant.Nr); + + if Config.BLCLocalOrGlobal == true + PeakValues(Participant.Nr, 1) = ... + mean(TEPRCurves(Config.PeakFromSampleMapped:Config.PeakToSampleMapped, Participant.Nr), 'omitnan'); + else + PeakValues(Participant.Nr, 1) = ... + mean(TEPRCurves(Config.PeakFromSampleMapped:Config.PeakToSampleMapped, Participant.Nr), 'omitnan') - ... + mean(TEPRCurves(Config.BaselineFromSampleMapped:Config.BaselineToSampleMapped, Participant.Nr), 'omitnan'); + end + + if Config.Save.BaselineValues + BaselineValues(Participant.Nr) = ... + mean(TEPRCurves(Config.BaselineFromSampleMapped:Config.BaselineToSampleMapped, Participant.Nr), 'omitnan'); + end + + if Config.Save.EveryTrial + % NOTE: This does not try to save the different versions according to "TEPR condComputed" behav. filter selection + % TODO: save according to any binary mask + % TODO: remove redundant baseline correction step + support_SaveEveryTrial(TrialsArray, Meta, Config, Participant); + end + + if Config.Plot.TEPR.Make + % TODO: test and beautify + support_PlotTEPR(TrialsArray, TEPRCurves, Config, Meta, Participant); + end + + if Config.ERA.Enabled && Config.Plot.ERA.Make + % TODO: test and beautify + support_PlotERA(ERAEventDensity, ERAConfCurves, Config, Meta, Participant); + end + +end +% PARTICIPANT LOOP END + +if Config.Plot.GrandTEPR.Make + + if ~isfield(Config.Plots, 'LayeredFigCounter') + Config.Plots.LayeredFigCounter = 1; + end + + % TODO: make it work with layered fig properly WHILE still keeping + % support for everyparticipant plot + grandTEPR(Config.Plots.LayeredFigCounter) = support_PlotGrandTEPR(TEPRCurves, Config, Meta); +end + + +% -------------------------------------------------- +% PLOTTING EVENT-RELATED ARTEFACTS + +% TODO: check isempty(ERAEventDensity_grand) +% TODO: save image, beautify code, extend configurability +if Config.ERA.Enabled && Config.Plot.GrandERA.Make + support_PlotGrandERADensity(ERAEventDensity_grand, Config, Meta); +end + +if Config.ERA.Enabled && Config.Plot.GrandERA.Make + support_PlotGrandERAConf(ERAConfCurves, Config, Meta); +end + +% TODO: not only plot, but save in csv +if Config.Plot.DynBLcorrMap.Make + support_PlotDynBLCorrMap(TEPREveryParticipant, Config, Meta); +end + +if Config.Save.BaselineValues + support_SaveBaselineValues(BaselineValues, Config, Meta, Participants); +end + +if Config.Save.PeakValues + support_SavePeakValues(PeakValues, Config, Meta, Participants); +end + +if Config.Save.TrialExclusionSummary + support_SaveTrialExclusionSummary(TRIAL_EXCLUSIONS, Config, Meta, Participants); +end + +if Config.Save.TEPREveryParticipant + support_SaveTEPREveryParticipant(TEPREveryParticipant, Config, Meta, Participants); +end diff --git a/CONFIG_PREPROC_DEFAULTS.m b/CONFIG_PREPROC_DEFAULTS.m new file mode 100644 index 0000000..bb166da --- /dev/null +++ b/CONFIG_PREPROC_DEFAULTS.m @@ -0,0 +1,61 @@ + +ISISec = NaN; +StimOnScreenSec = NaN; +Config.ExpDirName = '*'; +PerformGolayFiltering = false; + +Config.ETDataFormat = 'SMI'; +% Config.ETDataFormat = 'PupilEXT'; +% Config.ETDataFormat = 'EyeLink'; +% Config.ETDataFormat = 'Other'; + +% PupilEXT NOTE: ALWAYS USE THE SAME ALGORITHM FOR ONE ANALYSIS. CONFIDENCE CAN DIFFER + +% Config.PXorMM = true; +Config.PXorMM = false; + +% GolayWinSizeFactor = 1.5; % Too flat +GolayWinSizeFactor = 0.8; +% GolayWinSizeFactor = 0.3; % DEV + +% Config.HarFilt.Enabled = true; +Config.HarFilt.Enabled = false; + +% PupilEXT specific now +confidenceThreshold = 0.87; +outlineConfidenceThreshold = 0.87; + +% Also saves behav data alongside eye data +Config.MapBehav = false; + +SkipFirstNtrials = 0; + +Config.EveryWhichTrial = 1; +Config.BehavDir = '*'; +Config.BehavParserFunction = '*'; + +Config.SkipParticipants = '*'; + +Config.EncOrTest = true; +Config.EncOrTest = false; + +Config.FilterTrialsG = '*'; + +Config.PlotPupil.Enabled = false; +Config.PlotPupil.Mode = 0; +% 0 = none +% 1 = FFT before + after +% 2 = signal before + after + +Config.HarFilt.BaseFreq = NaN; +Config.HarFilt.FreqRadius = NaN; +Config.HarFilt.NumAddHarmonics = NaN; + +Config.OutputNomSRate = 50; % Hz + +% DEV: Manually correct trigger timestamps +% Config.ManShiftMs = 120; +% Config.ManShiftMs = 180; +Config.ManShiftMs = NaN; + + diff --git a/CONFIG_PREPROC_NBACK.m b/CONFIG_PREPROC_NBACK.m new file mode 100644 index 0000000..838f7fc --- /dev/null +++ b/CONFIG_PREPROC_NBACK.m @@ -0,0 +1,283 @@ + +% Config.ExpDirName = 'NBACK_ver1_s1_ALL75'; +% Config.ExpDirName = 'NBACK_ver1_s2_ALL75'; + +% Config.ExpDirName = 'NBACK_ver1_s1_MUTUAL'; +% Config.ExpDirName = 'NBACK_ver1_s2_MUTUAL'; +Config.ExpDirName = 'NBACK_ver1_s1_MUTUAL_BE'; +% Config.ExpDirName = 'NBACK_ver1_s2_MUTUAL_BE'; + +ISISec = 3.2; +StimOnScreenSec = 2.5; +PerformGolayFiltering = true; + +Config.ETDataFormat = 'SMI'; +% Config.ETDataFormat = 'PupilEXT'; +% Config.ETDataFormat = 'EyeLink'; +% Config.ETDataFormat = 'Other'; + +% Config.PXorMM = true; +Config.PXorMM = false; + +GolayWinSizeFactor = 0.8; + +Config.MapBehav = true; + +SkipFirstNtrials = 0; + +Config.EveryWhichTrial = 2; +Config.BehavDir = '~RAWDATA/BEHAV/NBACK_ver1_s1_+2023'; +% Config.BehavDir = '~RAWDATA/BEHAV/NBACK_ver1_s2_+2023'; + +Config.BehavParserFunction = 'GetData_MapBehav_NBACKpython'; + +Config.SkipParticipants = '*'; + + +Config.FilterTrialsG = { ... + + % NBACK, ver1 s1 (+ s1 plus) + % DE NOTE: eddig rossz volt, mert nem kell 5-205-ig mennie a számozásnak, maga a 205. trialváltás már csak a 204. (utolsĂł valid) trial vĂ©gĂ©t jelzi, az nem valid trial már + 'subject_2001_1647529685' [ 4 202 ]; ... % innen NBACK, ver1 s1 plus + 'subject_2002_1647594987' [ 6 204 ]; ... + 'subject_2003_1647610301' [ 6 204 ]; ... + 'subject_2004_1647615508' [ 6 204 ]; ... + 'subject_2005_1647875152' [ 6 204 ]; ... + 'subject_2006_1647882945' [ 6 204 ]; ... + 'subject_2007_1648112877' [ 6 204 ]; ... + 'subject_2008_1648119708' [ 6 204 ]; ... + 'subject_2009_1648133994' [ 6 204 ]; ... + 'subject_2010_1648207641' [ 6 204 ]; ... + 'subject_2011_1648213447' [ 6 204 ]; ... + 'subject_2012_1648477342' [ 6 204 ]; ... + 'subject_2013_1648483632' [ 6 204 ]; ... % kizárva amĂşgy + 'subject_2014_1648714287' [ 6 204 ]; ... + 'subject_2015_1648721763' [ 8 206 ]; ... + 'subject_2016_1648799833' [ 6 204 ]; ... + 'subject_2017_1648814509' [ 6 204 ]; ... % kizárva amĂşgy + 'subject_6001_1627027279' [ 6 204 ]; ... % innen NBACK, ver1 s1;; % kizárva amĂşgy + 'subject_6002_1627034871' [ 6 204 ]; ... + 'subject_6003_1627287687' [ 6 204 ]; ... + 'subject_6004_1627050264' [ 6 204 ]; ... % nem csekkoltam de gondolom jĂł ez is + 'subject_6005_1627295710' [ 6 204 ]; ... + 'subject_6006_1627301813' [ 6 204 ]; ... + 'subject_6007_1627308613' [ 6 204 ]; ... + 'subject_6008_1627374776' [ 6 204 ]; ... + 'subject_6009_1627381730' [ 6 204 ]; ... + 'subject_6010_1627388771' [ 6 204 ]; ... + 'subject_6011_1627395969' [ 6 204 ]; ... + 'subject_6012_1627457643' [ 35 233 ]; ... + 'subject_6013_1627477390' [ 6 204 ]; ... + 'subject_6014_1627485522' [ 10 208 ]; ... % kizárva amĂşgy + 'subject_6015_1627545854' [ 6 204 ]; ... + 'subject_6016_1627553996' [ 6 204 ]; ... + 'subject_6017_1627568126' [ 6 204 ]; ... + 'subject_6018_1627632596' [ 6 204 ]; ... + 'subject_6019_1627640359' [ 6 204 ]; ... % az elsĹ‘ 2 trial csak 1-1 sample mĂ©retű % JAVĂŤTVA. RÉGEBBEN: 'subject_6019_1627640359' [ 4 204 ]; ... + 'subject_6020_1627647354' [ 6 204 ]; ... + 'subject_6021_1627892543' [ 6 204 ]; ... % kizárva amĂşgy + 'subject_6022_1627900038' [ 6 204 ]; ... + 'subject_6023_1627916599' [ 6 204 ]; ... + 'subject_6024_1627996195' [ 10 208 ]; ... + 'subject_6025_1628004214' [ 6 204 ]; ... + 'subject_6026_1628150529' [ 6 204 ]; ... + 'subject_2701_1680507780' [6 204]; ... + 'subject_2702_1680617199' [6 204]; ... + 'subject_2703_1680712610' [6 204]; ... + 'subject_2704_1680791149' [6 204]; ... + 'subject_2705_1680799005' [6 204]; ... + 'subject_2706_1681221190' [6 204]; ... + 'subject_2707_1681303414' [6 204]; ... + 'subject_2708_1681310116' [6 204]; ... + 'subject_2709_1681317621' [6 204]; ... + 'subject_2710_1681380795' [6 204]; ... + 'subject_2711_1681389313' [6 204]; ... + 'subject_2712_1681396179' [6 204]; ... + 'subject_2713_1681725215' [6 204]; ... + 'subject_2714_1681735093' [6 204]; ... + 'subject_2715_1681741957' [6 204]; ... + 'subject_2716_1681748998' [6 204]; ... + 'subject_2717_1681826718' [6 204]; ... + 'subject_2718_1681922007' [6 204]; ... + 'subject_2719_1682430607' [6 204]; ... + 'subject_2720_1683132843' [6 204]; ... + + 'subject_7001_1687245440' [6 204]; ... + 'subject_7002_1687271701' [6 204]; ... + 'subject_7003_1687279161' [6 204]; ... + 'subject_7004_1687351746' [6 204]; ... + 'subject_7005_1687366367' [6 204]; ... + 'subject_7006_1687418460' [6 204]; ... + 'subject_7007_1687795664' [6 204]; ... + 'subject_7008_1687868111' [6 204]; ... + 'subject_7009_1687953925' [6 204]; ... + 'subject_7010_1687969951' [6 204]; ... + 'subject_7011_1688129738' [6 204]; ... + + 'subject_301_1687245440' [6 204]; ... + 'subject_302_1687271701' [6 204]; ... + 'subject_303_1687279161' [6 204]; ... + 'subject_304_1687351746' [6 204]; ... + 'subject_305_1687366367' [6 204]; ... + 'subject_306_1687418460' [6 204]; ... + 'subject_308_1687795664' [6 204]; ... + 'subject_309_1687868111' [6 204]; ... + 'subject_312_1687953925' [6 204]; ... + 'subject_314_1687969951' [6 204]; ... + 'subject_317_1688129738' [6 204]; ... + % PUPILEXT + 'subject_2701_1680507390-else' [6 204]; ... + 'subject_2702_1680616794-else' [6 204]; ... + 'subject_2703_1680712220-else' [6 204]; ... + 'subject_2704_1680790757-else' [6 204]; ... + 'subject_2705_1680798589-else' [6 204]; ... + 'subject_2706_1681220781-else' [6 204]; ... + 'subject_2707_1681303000-else' [6 204]; ... + 'subject_2708_1681309684-else' [6 204]; ... + 'subject_2709_1681317124-else' [6 204]; ... + 'subject_2710_1681380383-else' [6 204]; ... + 'subject_2711_1681388933-else' [6 204]; ... + 'subject_2712_1681395749-else' [6 204]; ... + 'subject_2713_1681724787-else' [6 204]; ... + 'subject_2714_1681734636-else' [6 204]; ... + 'subject_2715_1681741580-else' [6 204]; ... + 'subject_2716_1681748626-else' [6 204]; ... + 'subject_2717_1681826261-else' [6 204]; ... + 'subject_2718_1681921579-else' [6 204]; ... + 'subject_2719_1682430207-else' [6 204]; ... + 'subject_2720_1683132451-else' [6 204]; ... + + 'subject_301_1687245006-pure' [6 204]; ... + 'subject_302_1687271299-pure' [6 204]; ... + 'subject_303_1687278750-pure' [6 204]; ... + 'subject_304_1687351342-pure' [6 204]; ... + 'subject_305_1687365902-pure' [6 204]; ... + 'subject_306_1687418046-pure' [6 204]; ... + 'subject_309_1687867694-pure' [6 204]; ... + 'subject_312_1687953537-pure' [6 204]; ... + 'subject_314_1687969551-pure' [6 204]; ... + 'subject_317_1688129243-pure' [6 204]; ... + % 'subject_320_1689779453' [ 6 204 ]; ... + + + % NBACK, ver1 s2 (+ s2 plus) + 'subject_3000_1650551052' [6 204]; ... + 'subject_3001_1649339745' [6 204]; ... + 'subject_3002_1650528694' [6 204]; ... + 'subject_3003_1649686511' [6 204]; ... + 'subject_3004_1649692535' [6 204]; ... + 'subject_3005_1649939470' [6 204]; ... + 'subject_3006_1651241708' [6 204]; ... + 'subject_3007_1650901982' [6 204]; ... + 'subject_3008_1651140425' [6 204]; ... + 'subject_3009_1651500353' [6 204]; ... + 'subject_3010_1650895597' [6 204]; ... + 'subject_3011_1651148463' [6 204]; ... + 'subject_3012_1650615200' [6 204]; ... + 'subject_3013_1651508025' [6 204]; ... + 'subject_3014_1650556974' [6 204]; ... + 'subject_3015_1651759592' [6 204]; ... + 'subject_3016_1650542653' [6 204]; ... + 'subject_3017_1650636704' [6 204]; ... + 'subject_8002_1629188860' [6 204]; ... + 'subject_8001_1629382107' [6 204]; ... + 'subject_8003_1629885633' [6 204]; ... + 'subject_8005_1629107266' [6 204]; ... + 'subject_8006_1629117418' [6 204]; ... + 'subject_8007_1628777055' [6 204]; ... + 'subject_8008_1629277758' [6 204]; ... + 'subject_8009_1629815774' [6 204]; ... + 'subject_8010_1628849873' [6 204]; ... + 'subject_8011_1628763286' [6 204]; ... + 'subject_8012_1629112739' [6 204]; ... + 'subject_8014_1629123244' [6 204]; ... + 'subject_8015_1629205127' [6 204]; ... + 'subject_8016_1628862649' [6 204]; ... + 'subject_8017_1628602898' [6 204]; ... + 'subject_8018_1628843941' [6 204]; ... + 'subject_8019_1628755765' [6 204]; ... + 'subject_8020_1628770569' [6 204]; ... + 'subject_8021_1629268959' [6 204]; ... + 'subject_8022_1629375313' [6 204]; ... + 'subject_8023_1629733130' [6 204]; ... + 'subject_8024_1628856617' [6 204]; ... + 'subject_8025_1629799916' [6 204]; ... + 'subject_8026_1629299241' [6 204]; ... + 'subject_3801_1683034353' [6 204]; ... + 'subject_3802_1681831658' [6 204]; ... + 'subject_3803_1681998447' [6 204]; ... + 'subject_3804_1682603959' [6 204]; ... + 'subject_3805_1682327255' [6 204]; ... + 'subject_3806_1682585884' [6 204]; ... + 'subject_3807_1683201787' [6 204]; ... + 'subject_3808_1681992228' [6 204]; ... + 'subject_3809_1682437274' [6 204]; ... + 'subject_3810_1681905339' [6 204]; ... + 'subject_3811_1682352357' [6 204]; ... + 'subject_3812_1681974140' [6 204]; ... + 'subject_3813_1682345142' [6 204]; ... + 'subject_3814_1682337790' [6 204]; ... + 'subject_3815_1683209321' [6 204]; ... + 'subject_3816_1683216060' [6 204]; ... + 'subject_3817_1682523116' [6 204]; ... + 'subject_3818_1682610621' [6 204]; ... + 'subject_3819_1683044908' [6 204]; ... + 'subject_3820_1684417855' [6 204]; ... + + 'subject_9001_1688997282' [6 204]; ... + 'subject_9002_1689331585' [6 204]; ... + 'subject_9003_1689172996' [6 204]; ... + 'subject_9004_1688990247' [6 204]; ... + 'subject_9005_1689004374' [7 205]; ... % vmiert eggyel kesobb kezdodott + 'subject_9006_1689148557' [6 204]; ... + 'subject_9007_1689235807' [6 204]; ... + 'subject_9008_1689166266' [6 204]; ... + 'subject_9009_1689158997' [6 204]; ... + 'subject_9010_1689580813' [6 204]; ... + 'subject_9011_1689605699' [6 204]; ... + + 'subject_401_1688997282' [6 204]; ... + 'subject_402_1689331585' [6 204]; ... + 'subject_403_1689172996' [6 204]; ... + 'subject_404_1688990247' [6 204]; ... + 'subject_405_1689004374' [7 205]; ... + 'subject_406_1689148557' [6 204]; ... + 'subject_408_1689235807' [6 204]; ... + 'subject_409_1689166266' [6 204]; ... + 'subject_412_1689158997' [6 204]; ... + 'subject_414_1689580813' [6 204]; ... + 'subject_417_1689605699' [6 204]; ... + % PUPILEXT + 'subject_2801_1683033928-else' [6 204]; ... + 'subject_2802_1681831282-else' [6 204]; ... + 'subject_2803_1681998102-else' [6 204]; ... + 'subject_2804_1682603601-else' [6 204]; ... + 'subject_2805_1682326898-else' [6 204]; ... + 'subject_2806_1682585507-else' [6 204]; ... + 'subject_2807_1683201408-else' [6 204]; ... + 'subject_2808_1681991841-else' [6 204]; ... + 'subject_2809_1682436898-else' [6 204]; ... + % NOTE: 2810 missing + 'subject_2811_1682351958-else' [6 204]; ... + 'subject_2812_1681973726-else' [6 204]; ... + 'subject_2813_1682344761-else' [6 204]; ... + 'subject_2814_1682337389-else' [6 204]; ... + 'subject_2815_1683208960-else' [6 204]; ... + 'subject_2816_1683215698-else' [6 204]; ... + 'subject_2817_1682522743-else' [6 204]; ... + 'subject_2818_1682610266-else' [6 204]; ... + 'subject_2819_1683044558-else' [6 204]; ... + 'subject_2820_1684417489-else' [6 204]; ... + + 'subject_401_1688996914-pure' [6 204]; ... + 'subject_402_1689331216-pure' [6 204]; ... + 'subject_403_1689172621-pure' [6 204]; ... + 'subject_404_1688989869-pure' [6 204]; ... + 'subject_405_1689003916-pure' [6 204]; ... + 'subject_406_1689148192-pure' [6 204]; ... + 'subject_409_1689165879-pure' [6 204]; ... + 'subject_412_1689158624-pure' [6 204]; ... + 'subject_414_1689580432-pure' [6 204]; ... + 'subject_417_1689605340-pure' [6 204]; ... + +}; diff --git a/CONFIG_TEPR_DEFAULTS.m b/CONFIG_TEPR_DEFAULTS.m new file mode 100644 index 0000000..e419d67 --- /dev/null +++ b/CONFIG_TEPR_DEFAULTS.m @@ -0,0 +1,203 @@ + +% % NBACK + +Config.ETDSDirName = '*'; + +%-------------------- +Config.BehavInitFunction = '*'; +Config.BehavFiltFunction = '*'; + +Config.SkipFirstNtrials = NaN; + +%-------------------- +% !! Also for NBACK_2alk_2023 SMI & PupilEXT +Config.AnalyzeFromSec = NaN; +Config.AnalyzeToSec = NaN; +Config.PeakFromSec = NaN; +Config.PeakToSec = NaN; +Config.BaselineFromSec = NaN; +Config.BaselineToSec = NaN; + +%------------------- +Config.Filter.BaselineBlink.FromSec = NaN; +Config.Filter.BaselineBlink.ToSec = NaN; + +Config.Filter.BaselineSaccade.FromSec = NaN; +Config.Filter.BaselineSaccade.ToSec = NaN; + +Config.Filter.SOIBlink.FromSec = NaN; +Config.Filter.SOIBlink.ToSec = NaN; + +Config.Filter.SOISaccade.FromSec = NaN; +Config.Filter.SOISaccade.ToSec = NaN; + +Config.DynBLCorrMap.BehavDF = '*'; +Config.DynBLCorrMap.DVFrom = NaN; +Config.DynBLCorrMap.DVTo = NaN; + +Config.CC.Conds = []; + +%------------- +Config.Plot.TEPR.XLim = NaN; +Config.Plot.TEPR.YLim = NaN; + +Config.Plot.GrandTEPR.XLim = NaN; +Config.Plot.GrandTEPR.YLim = NaN; + +Config.Plot.GrandERA.YLim = [0.8, 1.0]; + + + + + +%------------------- az alábbiak ÁLTALÁNOSAK, voltak sokáig a tepr szkriptben +Config.AlignToStimOrResp = true; +% Config.AlignToStimOrResp = false; + +% NOTE: For testing only. If all computations are correct, both of +% these should produce the same results +Config.BLCLocalOrGlobal = true; +% Config.BLCLocalOrGlobal = false; + +% Config.Z_norm_method = 0; % No Z-normalization +Config.Z_norm_method = 1; % Reference to whole recording (advised) +% Config.Z_norm_method = 2; % Reference to each trial for its own +% Config.Z_norm_method = 3; % Reference to all existing trials +% Config.Z_norm_method = 4; % Reference to all non-rejected trials +% % % Config.Z_norm_method = 5; % TODO: Reference to nearby N seconds + +Config.Save.BaselineValues = false; +Config.Save.PeakValues = true; +Config.Save.TEPREveryParticipant = true; + +Config.Save.TrialExclusionSummary = true; + +% Config.Save.EveryTrial = false; % can be very slow if there are many excluded trials +Config.Save.EveryTrial = true; % can be very slow if there are many excluded trials + +% METHOD +Config.EventRelatedMethod = 1; % TEPR (Avg) +% Config.EventRelatedMethod = 2; % TEPR-SD +% Config.EventRelatedMethod = 3; % TEPR-Ku +% Config.EventRelatedMethod = 4; % TEPR-Sk +% Config.EventRelatedMethod = 5; % TEPR-MAD +% Config.EventRelatedMethod = 6; % TEPR-Min +% Config.EventRelatedMethod = 7; % TEPR-Max +% Config.EventRelatedMethod = 8; % TEPR-KMax +% Config.EventRelatedMethod = 9; % TEPR-KVal +% ... % TEPR-Sh - Shapiro + +% DYN BASELINE MAP +% Config.Plot.DynBLcorrMap.Make = true; +Config.Plot.DynBLcorrMap.Make = false; + +Config.DynBLCorrMap.SmallOrLarge = true; + +Config.DynBLCorrMap.CorrelMethod = 'Spearman'; + +FilterConfigs.SD.LocalLimit = 1.5; + + +% LOW FPS TRIGGER JITTER CORRECTION: + +% NOTE: This only makes sense if the sampling rate is very low, e.g. below 30hz +% so that the delay between a trigger timestamp of trial number increment and the first actual sample that belongs to the new trial +% can be high, like 50ms in case of 20hz eye data... so we could help a little with interpolation. +% BUT: this should never be used if there is higher raw eye data quality available! Or in other words: +% this is for a tiny correction of between-trial temporal alignment for better TEPR/TEPR averaging, and not for "enhancing" the +% preprocessed data if it was previously downsampled too much during preprocessing. (Then it would not improve anything.) + +% Config.PerformTJC = true; +Config.PerformTJC = false; + +RejectTrialsOnTypicalLen = false; +RejectTrialsOutsideLenSec = NaN; + +Config.Filter.Interpol.Threshold = 20; +Config.Filter.BaselineSaccade.Magnitude = 20; % deg? +Config.Filter.SOISaccade.Magnitude = 20; % deg? + +Config.Filter.Interpol.Enabled = true; +% Config.Filter.BaselineBlink.Enabled = true; %%%% +% Config.Filter.BaselineSaccade.Enabled = true; +% Config.Filter.SOIBlink.Enabled = true; +% Config.Filter.SOISaccade.Enabled = true; +% % % % % Filter.OnBaselineInterpol = true; +% Config.Filter.SD.Enabled = true; + +% Config.Filter.Interpol.Enabled = false; +Config.Filter.BaselineBlink.Enabled = false; %%%% +Config.Filter.BaselineSaccade.Enabled = false; +Config.Filter.SOIBlink.Enabled = false; +Config.Filter.SOISaccade.Enabled = false; +Config.Filter.SD.Enabled = false; + + +% Config.CC.Enabled = true; +Config.CC.Enabled = false; + +% Config.Filter.Behav.Enabled = true; +Config.Filter.Behav.Enabled = false; + +Config.ERA.Enabled = true; % event-related artefacts (event-related blink curve & event-related saccades curve) +% Config.ERA.Enabled = false; + +Config.ERA.EventOfInterest = 0; % blink start +% Config.ERA.EventOfInterest = 1; % blink end +% Config.ERA.EventOfInterest = 2; % saccade start +% Config.ERA.EventOfInterest = 3; % saccade end + +% ERA visualization only +% Config.Plot.ERA.VisualMethod = 0; % kernel density estimation +Config.Plot.ERA.VisualMethod = 1; % histogram +% +Config.Plot.ERA.KDEBandwidth = 200; +Config.Plot.ERA.HistBinWidth = 200; + +% Should we always close the existing figure on a new plot, or plot on it +% Config.Plots.Layered = true; +% Config.Plots.Layered = false; +% +% Config.Plots.LayeredFigCounter = 1; + +% Config.Plot.TEPR.Make = true; %%% %%% +Config.Plot.GrandTEPR.Make = true; +% Config.Plot.ERA.Make = true; +% Config.Plot.GrandERA.Make = true; + +% Config.Plot.TEPR.EveryTrial = true; %%%% %%% %%% +% Config.Plot.GrandTEPR.EveryParticipant = true; %%%% +% Config.Plot.GrandERA.EveryParticipant true; + +Config.Plot.TEPR.Make = false; %%% %%% +% Config.Plot.GrandTEPR.Make = false; %%% %%% +Config.Plot.ERA.Make = false; +Config.Plot.GrandERA.Make = false; + +Config.Plot.TEPR.EveryTrial = false; %%%% %%% %%% +Config.Plot.GrandTEPR.EveryParticipant = false; %%%% +Config.Plot.GrandERA.EveryParticipant = false; + + +Config.Plots.ScaleFactor = 0.4; +% Config.Plots.ScaleFactor = 0.6; +% Config.Plots.ScaleFactor = 1.0; + +% NOTE: adding plot markings when the analytic length is long is slow +% Config.Plots.Markings = false; +Config.Plots.Markings.Enabled = true; +Config.Plots.Markings.F = false; +Config.Plots.Markings.B = true; +Config.Plots.Markings.S = true; +Config.Plots.Markings.R = true; + +Config.Plots.Grid = true; +% Config.Plots.Grid = false; + +% Config.Plots.Markings.OnEdges = true; +Config.Plots.Markings.OnEdges = false; + + + + + diff --git a/CONFIG_TEPR_NBACK.m b/CONFIG_TEPR_NBACK.m new file mode 100644 index 0000000..8db81d8 --- /dev/null +++ b/CONFIG_TEPR_NBACK.m @@ -0,0 +1,176 @@ + +% % NBACK + +% +% Config.ETDSDirName = 'NBACK_ver1_s1_SESPEC_BE_DQ_MM_SMI'; +Config.ETDSDirName = 'NBACK_ver1_s1_MUTUAL_BE_DQ_MM_SMI'; %%%%%%%%%%% +% +% Config.ETDSDirName = 'NBACK_ver1_s2_SESPEC_BE_DQ_MM_SMI'; +% Config.ETDSDirName = 'NBACK_ver1_s2_MUTUAL_BE_DQ_MM_SMI'; %%%%%%%%%%% + +% Config.ETDSDirName = 'NBACK_ver1_s1_NEWONLY_DQ_MM_SMI'; +% Config.ETDSDirName = 'NBACK_ver1_s2_NEWONLY_DQ_MM_SMI'; + +% Config.ETDSDirName = 'NBACK_ver1_s1_NEWONLY_DQ_PX_PupilEXT'; +% Config.ETDSDirName = 'NBACK_ver1_s2_NEWONLY_DQ_PX_PupilEXT'; + +% Config.ETDSDirName = 'NBACK_ver1_s1_MUTUAL_BE_finderror_DQ_MM_SMI'; +% Config.ETDSDirName = 'NBACK_ver1_s2_MUTUAL_BE_finderror_DQ_MM_SMI'; + +% Config.ETDSDirName = 'NBACK_ver1_s1_MUTUAL_BE_2700S_DQ_MM_SMI'; +% Config.ETDSDirName = 'NBACK_ver1_s2_MUTUAL_BE_2700S_DQ_MM_SMI'; + +% Config.ETDSDirName = 'NBACK_ver1_s1_MUTUAL_BE_2700s_DQ_PX_PUPILEXT'; +% Config.ETDSDirName = 'NBACK_ver1_s2_MUTUAL_BE_2700s_DQ_PX_PUPILEXT'; + +% Config.ETDSDirName = 'NBACK_ver1_s1_OLD11_DQ_MM_SMI'; +% Config.ETDSDirName = 'NBACK_ver1_s2_OLD11_DQ_MM_SMI'; + +%-------------------- +Config.BehavInitFunction = 'callable_initbehavfilt_NBACK'; +Config.BehavFiltFunction = 'callable_behavfilt_NBACK'; + +Config.SkipFirstNtrials = 2; + +%-------------------- +% !! Also for NBACK_2alk_2023 SMI & PupilEXT +Config.AnalyzeFromSec = -0.7; +Config.AnalyzeToSec = 2.5; +Config.PeakFromSec = 0.0; +Config.PeakToSec = 2.5; +% Config.PeakFromSec = 1.0; +% Config.PeakToSec = 1.5; +% Config.BaselineFromSec = -0.7; +% Config.BaselineToSec = 0.0; +Config.BaselineFromSec = -0.2; +Config.BaselineToSec = 0.0; + +%------------------- +Config.Filter.BaselineBlink.FromSec = 1.4; +Config.Filter.BaselineBlink.ToSec = 2.2; + +Config.Filter.BaselineSaccade.FromSec = 1.4; +Config.Filter.BaselineSaccade.ToSec = 2.2; + +Config.Filter.SOIBlink.FromSec = 1.4; +Config.Filter.SOIBlink.ToSec = 2.2; + +Config.Filter.SOISaccade.FromSec = 1.4; +Config.Filter.SOISaccade.ToSec = 2.2; + +%------------------ behav cond comb +% +% Config.Filter.Behav.CondComb = 1; % S=Target, R=Yes, (V=Correct) %%%%%%%%%%%%%%%%%% % +% % Config.Filter.Behav.CondComb = 2; % S=Target, R=No, (V=Wrong) +% Config.Filter.Behav.CondComb = 3; % S=Nontarget, R=No, (V=Correct) %%%%%%%% % +% % Config.Filter.Behav.CondComb = 4; % S=Nontarget, R=Yes, (V=Wrong) +% +% Config.Filter.Behav.CondComb = 5; % V=Correct (CORRECT all) %%%%%%%%%%%%%%%%% +% Config.Filter.Behav.CondComb = 6; % V=Wrong (WRONG all) %%%%%%%%%%%%%%%%%%% +% +% Config.Filter.Behav.CondComb = 7; % all trials with key responses +% Config.Filter.Behav.CondComb = 8; % all trials without key response + +% -------------- behav filt config defs +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% NBACK python verzio +% RESPONSE: +% up = yes +% down = no +% STIMULUS: +% 1 = target (nback) +% 0 = nontarget +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% ver1 s1 es s2 +Config.Filter.Behav.StimType.A = 1; +Config.Filter.Behav.StimType.B = 0; +% +Config.Filter.Behav.RespType.A = 'up'; +Config.Filter.Behav.RespType.B = 'down'; +% +Config.Filter.Behav.StimType.A_friendly = 'Target'; +Config.Filter.Behav.StimType.B_friendly = 'Nontarget'; +% +Config.Filter.Behav.RespType.A_friendly = 'Yes'; +Config.Filter.Behav.RespType.B_friendly = 'No'; + +% % % % ver2 es NBACK elderly +% % % Config.Filter.Behav.StimType.A = 1; +% % % Config.Filter.Behav.StimType.B = 0; +% % % % +% % % Config.Filter.Behav.RespType.A = 'up'; +% % % Config.Filter.Behav.RespType.B = 'None'; +% % % % +% % % Config.Filter.Behav.StimType.A_friendly = 'Target'; +% % % Config.Filter.Behav.StimType.B_friendly = 'Nontarget'; +% % % % +% % % Config.Filter.Behav.RespType.A_friendly = 'Yes'; +% % % Config.Filter.Behav.RespType.B_friendly = 'Noresp'; + +% ------------------ +% NBACK ver1 uj adatokkal SMI, MUTUAL-BE +% Config.DynBLCorrMap.BehavDF = 'NBACK-ver1 behav N=11 OLD11.csv'; +% Config.DynBLCorrMap.BehavDF = 'NBACK-ver1 behav N=10 MUTUAL-BE HIT-FA NEWONLY (PupilEXT).csv'; +% Config.DynBLCorrMap.BehavDF = 'NBACK-ver1 behav N=11 MUTUAL-BE HIT-FA NEWONLY.csv'; +% Config.DynBLCorrMap.BehavDF = 'NBACK-ver1 behav N=46 MUTUAL-BE HIT-FA finderror.csv'; +% Config.DynBLCorrMap.BehavDF = 'NBACK-ver1 behav N=15 MUTUAL-BE HIT-FA 2700S.csv'; +Config.DynBLCorrMap.BehavDF = 'NBACK-ver1 behav N=55 MUTUAL-BE HIT-FA.csv'; +Config.DynBLCorrMap.DVFrom = 3; +Config.DynBLCorrMap.DVTo = 4; +% % +% % Config.DynBLCorrMap.DVFrom = 3; +% % Config.DynBLCorrMap.DVTo = 10; + +% % moindenfele valtozatra, 2alk nback esetĂ©n: +% Config.DynBLCorrMap.DVFrom = 2; +% Config.DynBLCorrMap.DVTo = 5; + + +% % NBACK 2alk 2023 s1 (SMI es PupilEXT) +% Config.DynBLCorrMap.BehavDF = 'NBACK-ver1_2alk_s1-SESPEC-BE behav N=18'; +% Config.DynBLCorrMap.DVFrom = 6; +% Config.DynBLCorrMap.DVTo = 6; + +% % NBACK 2alk 2023 s2 - SMI +% Config.DynBLCorrMap.BehavDF = 'NBACK-ver1_2alk_s2-SESPEC-BE behav N=17.csv'; +% % Config.DynBLCorrMap.DVFrom = 6; +% % Config.DynBLCorrMap.DVTo = 6; +% +% % NBACK 2alk 2023 s2 - PupilEXT +% Config.DynBLCorrMap.BehavDF = '+ NBACK_2alk_2023_s2/NBACK_2alk_2023_s2_behav-results_N=18.csv'; +% Config.DynBLCorrMap.DVFrom = 6; +% Config.DynBLCorrMap.DVTo = 6; + +Config.Plot.DynBLcorrMap.Make = true; + +%----------------- +% Config.CC.Conds = [ ... +% 1, ... % S=Target, R=Yes, (V=Correct) (HIT) (9-13 ilyen) +% 4, ... % S=Nontarget, R=Yes, (V=Wrong) (FA) (tul keves ilyen, 1-2) +% ]; + +% Config.CC.Conds = [ ... +% 3, ... % S=Nontarget, R=No, (V=Correct) (CR) (kb 70 ilyen) +% 4, ... % S=Nontarget, R=Yes, (V=Wrong) (FA) (tul keves ilyen, 1-2) +% ]; + +Config.CC.Conds = [ ... + 3, ... % S=Nontarget, R=No, (V=Correct) (CR) (kb 70 ilyen) + 1, ... % S=Target, R=Yes, (V=Correct) (HIT) (9-13 ilyen) +]; + +%------------- +Config.Plot.TEPR.XLim = NaN; +% Config.Plot.TEPR.YLim = [-3 3]; % Nback +Config.Plot.TEPR.YLim = [20 50]; % Nback + +Config.Plot.GrandTEPR.XLim = NaN; +Config.Plot.GrandTEPR.YLim = [-0.3 0.3]; % NBACK, baseline korr, TEPR +% % Config.Plot.GrandTEPR.YLim = [-0.3 0.5]; % NBACK 2023-as adatokkal is +% % Config.Plot.GrandTEPR.YLim = [-0.3 0.8]; % NBACK 2023-as adatokkal is, ujabb, 11 +% % % Config.Plot.GrandTEPR.YLim = [-0.5 2]; % NBACK, baseline korr, TEPR-SD + +Config.Plot.GrandERA.YLim = [0.85, 0.97]; + + + diff --git a/DQ_CalcInterpLossAnalogWhole.m b/DQ_CalcInterpLossAnalogWhole.m new file mode 100644 index 0000000..a1e9084 --- /dev/null +++ b/DQ_CalcInterpLossAnalogWhole.m @@ -0,0 +1,32 @@ +function [interpol_ratio] = DQ_CalcInterpLossAnalogWhole(timestamp, orig_srate, trig_trial, trig_timestamp, ISIsec) + + startTime = trig_timestamp(1); + if isnan(ISIsec) + log_w(['ISIsec parameter was found to be NaN. The end timestamp of the last trial could not be estimated accordingly.' newline() 'The last valid timestamp of all samples will be used instead.']) + endTime = timestamp(end); + else + endTime = trig_timestamp(end) + ISIsec*1000*1000; % microsec + end + recDuration = endTime-startTime; + + log_d(['Cleaned recording begin timestamp: ' sprintf('\t') num2str( min(timestamp) ) ]); + log_d(['Cleaned recording end timestamp: ' sprintf('\t') num2str( min(timestamp) ) ]); + % + log_d(['1 st trigger timestamp: ' sprintf('\t\t\t\t') num2str( trig_timestamp(1) ) ]); + log_d(['Last trigger timestamp: ' sprintf('\t\t\t\t') num2str( trig_timestamp(end) ) ]); + % + log_d(['1 st trial begin timestamp: ' sprintf('\t\t\t') num2str( trig_timestamp(1) ) ]); + log_d(['Last trial end timestamp: ' sprintf('\t\t\t') num2str( endTime ) ]); + % + log_d(['Duration of all trials, given the ISI considered (meaningful recording section): ' num2str( recDuration /1000 /1000) ' seconds' ]); + + + theoreticalNumSamples = ceil( recDuration / 1000 /1000 * orig_srate ); + + actualNumSamples = sum(timestamp>=startTime & timestamp<=endTime); + + interpol_ratio = (theoreticalNumSamples-actualNumSamples) / theoreticalNumSamples; % *100; + + log_i(['Interpolation percent of the meaningful recording section: ' num2str( interpol_ratio *100 ) ' %' ]); + +end \ No newline at end of file diff --git a/DQ_Decimate.m b/DQ_Decimate.m new file mode 100644 index 0000000..3560e0c --- /dev/null +++ b/DQ_Decimate.m @@ -0,0 +1,12 @@ +function [p_timestamp, p_pupdil, srate] = DQ_Decimate(timestamp, pupdil, orig_srate, factor) + + new_numPoints = ceil(length(timestamp)/factor); + + p_timestamp = transpose(linspace(min(timestamp), max(timestamp), new_numPoints)); + + p_pupdil = decimate(pupdil, factor); + %p_pupdil = decimate(pupdil, factor, 8); + %p_pupdil = decimate(pupdil, factor, 8, 'fir'); + + srate = orig_srate/factor; +end \ No newline at end of file diff --git a/DQ_RemoveBlinks.m b/DQ_RemoveBlinks.m new file mode 100644 index 0000000..e9a7d9d --- /dev/null +++ b/DQ_RemoveBlinks.m @@ -0,0 +1,25 @@ +function [p_timestamp, p_pupdil] = DQ_RemoveBlinks(timestamp, pupdil, b_start, b_end, cut_back, cut_forward) + + % [ms] to [microsec] + cut_back = cut_back *1000; + cut_forward = cut_forward *1000; + + b_index = 1; + j = 1; + while (j < size(timestamp,1) +1) && b_index < size(b_start,1) + if b_start(b_index)-cut_back < timestamp(j) && b_end(b_index)+cut_forward > timestamp(j) + timestamp(j) = NaN; + pupdil(j) = NaN; + end + if b_end(b_index) < timestamp(j) + b_index = b_index+1; + end + j = j+1; + end + + mask = ~isnan(timestamp); + + p_timestamp = timestamp(mask); + p_pupdil = pupdil(mask); + +end diff --git a/DQ_RemoveHiccups.m b/DQ_RemoveHiccups.m new file mode 100644 index 0000000..24e2c16 --- /dev/null +++ b/DQ_RemoveHiccups.m @@ -0,0 +1,37 @@ +function [p_timestamp, p_pupdil] = DQ_RemoveHiccups(timestamp, pupdil, watchedStepWindow, signifRange) + + mask = ~isnan(timestamp); +% DEBUG_counter = 0; + + j = watchedStepWindow +1; + while j < size(timestamp,1) +1 + movingRange = range(pupdil((j-watchedStepWindow):j)); % TODO: what if NaN + + if movingRange > signifRange + index_atFoundHiccup = j; + movingRange_inHiccup = 10000; + + seekIndex = index_atFoundHiccup; + while (seekIndex < size(timestamp,1)) && ( pupdil(seekIndex) > 0) && movingRange_inHiccup > signifRange + movingRange_inHiccup = range(pupdil((seekIndex-watchedStepWindow):seekIndex)); + seekIndex = seekIndex+1; + end + index_atEndOfHiccup = seekIndex; + + index_setNaN = index_atFoundHiccup; + while (index_setNaN < size(timestamp,1)) && index_setNaN < index_atEndOfHiccup +% DEBUG_counter = DEBUG_counter + 1; + mask(index_setNaN) = false; + index_setNaN = index_setNaN+1; + end + end + j = j+1; + end + +% disp(DEBUG_counter); +% disp( sum(isnan(pupdil)) ); + + p_timestamp = timestamp(mask); + p_pupdil = pupdil(mask); + +end diff --git a/DQ_RemoveNaNs.m b/DQ_RemoveNaNs.m new file mode 100644 index 0000000..e6ce931 --- /dev/null +++ b/DQ_RemoveNaNs.m @@ -0,0 +1,12 @@ +function [p_timestamp, p_pupdil] = DQ_RemoveNaNs(timestamp, pupdil) + + if isnan(timestamp(1)) + disp('Warning: The first element of timestamp array is NaN, which is removed now.'); + end + + mask = and(~isnan(timestamp), ~isnan(pupdil)); + + p_timestamp = timestamp(mask); + p_pupdil = pupdil(mask); + +end \ No newline at end of file diff --git a/DQ_RemoveSamplesByConfidence.m b/DQ_RemoveSamplesByConfidence.m new file mode 100644 index 0000000..b3ca6d6 --- /dev/null +++ b/DQ_RemoveSamplesByConfidence.m @@ -0,0 +1,44 @@ +function [p_timestamp, p_pupdil] = DQ_RemoveSamplesByConfidence(timestamp, pupdil, conf, outlineConf, confidenceThreshold, outlineConfidenceThreshold) + + mask = false(size(timestamp, 1), 1); + + regarded_c = true; + regarded_oc = true; + + uniq_c = unique(conf); + if length(uniq_c) == 1 && (isnan(uniq_c(1)) || uniq_c(1) == -1) + regarded_c = false; + log_w('Confidence values are invalid, thus we are not currently rejecting samples by confidence.') + end + + uniq_oc = unique(outlineConf); + if length(uniq_oc) == 1 && (isnan(uniq_oc(1)) || uniq_oc(1) == -1) + regarded_oc = false; + log_w('Outline confidence values are invalid, thus we are not currently rejecting samples by outline confidence.') + end + +% % % log_i('Using the AND logic: if a sample fulfils ALL criteria, it will pass.') +% % % for(j = 1:size(timestamp,1)) +% % % if regarded_c && conf(j) < confidenceThreshold +% % % continue; +% % % end +% % % if regarded_oc && outlineConf(j) < outlineConfidenceThreshold +% % % continue; +% % % end +% % % mask(j) = true; +% % % end + + log_i('Using the OR logic: if a sample fulfils ANY criteria, it will pass.') + for(j = 1:size(timestamp,1)) + if regarded_c && conf(j) >= confidenceThreshold + mask(j) = true; + end + if regarded_oc && outlineConf(j) >= outlineConfidenceThreshold + mask(j) = true; + end + end + + p_timestamp = timestamp(mask); + p_pupdil = pupdil(mask); + +end diff --git a/DQ_RemoveZeros.m b/DQ_RemoveZeros.m new file mode 100644 index 0000000..dc3cf59 --- /dev/null +++ b/DQ_RemoveZeros.m @@ -0,0 +1,42 @@ +function [p_timestamp, p_pupdil] = DQ_RemoveZeros(timestamp, pupdil, cut_back, cut_forward) + + % [ms] to [microsec] + cut_back = cut_back *1000; + cut_forward = cut_forward *1000; + + mask = true(size(timestamp)); + + j = 2; + while j <= length(timestamp) + if pupdil(j) == 0 && pupdil(j-1) ~= 0 + + % zero-section begins, we cut back + timestamp_atTarget = timestamp(j); + seekBi = j; + while seekBi > 1 && timestamp_atTarget - timestamp(seekBi) < cut_back + seekBi = seekBi-1; + end + mask(j:seekBi) = false; + + % during zero-section, we cut + while j < length(timestamp) && pupdil(j+1) == 0 + j = j + 1; + mask(j) = false; + end + + % end of zero-section, we cut forward + timestamp_atTarget = timestamp(j); + seekFi = j; + while seekFi < length(timestamp) && timestamp_atTarget + timestamp(seekFi) < cut_forward + seekFi = seekFi+1; + end + mask(j:seekFi) = false; + + end + j = j+1; + end + + p_timestamp = timestamp(mask); + p_pupdil = pupdil(mask); + +end diff --git a/DQ_Resample.m b/DQ_Resample.m new file mode 100644 index 0000000..2f4acce --- /dev/null +++ b/DQ_Resample.m @@ -0,0 +1,20 @@ +function [p_timestamp, p_pupdil, new_srate] = DQ_Resample(timestamp, pupdil, orig_srate) + + dataLengthTime = max(timestamp) - min(timestamp); %% [microsec] = 1/1000 [millisec] + +% orig_srate = 250; %% [1/sec] + orig_timeBetweenSamples = (1000000 / orig_srate); %% [microsec] = 1/1000 [millisec] + orig_numTheoreticalPoints = dataLengthTime / orig_timeBetweenSamples; + + new_numPoints = ceil(orig_numTheoreticalPoints); %% could be doubled to fully obey Shannon's law, but so high freq data is not important + new_srate = new_numPoints / orig_numTheoreticalPoints * orig_srate; + + + new_x = linspace (min(timestamp), max(timestamp), new_numPoints); + + % this avoids overshoot + new_y = interp1(timestamp, pupdil, new_x, 'pchip'); + + p_timestamp = transpose(new_x); %transzponálni kell, mert valamiért alapból nem oszlop hanem sor vektort csinál + p_pupdil = transpose(new_y); +end diff --git a/ERPD_SET_AMP_EQ.m b/ERPD_SET_AMP_EQ.m new file mode 100644 index 0000000..42b2b50 --- /dev/null +++ b/ERPD_SET_AMP_EQ.m @@ -0,0 +1,13 @@ + +FLAG_GRANDCALC = true; + +if exist('eventRelatedAmpEq1', 'var') + eventRelatedAmpEq1_lastRun = min(eventRelatedAmpEq1); +end + +if exist('eventRelatedAmpEq2', 'var') + eventRelatedAmpEq2_lastRun = floor(mean(eventRelatedAmpEq2)); +end + +% % eventRelatedAmpEq1(1,pp_id) = sum(~isnan(mean(trials_array_cond1, 1, 'omitnan')),'omitnan'); +% % eventRelatedAmpEq2(1,pp_id) = sum(~isnan(mean(trials_array_cond2, 1, 'omitnan')),'omitnan'); diff --git a/FiltSweepsOnBlink.m b/FiltSweepsOnBlink.m new file mode 100644 index 0000000..d6c0179 --- /dev/null +++ b/FiltSweepsOnBlink.m @@ -0,0 +1,22 @@ +function ExcludedMask = FiltSweepsOnBlink(Blinks, SearchBaseMask, TrigsForAlignment, FilterConfig) + + ExcludedMask = false(length(TrigsForAlignment), 1); + for i = 1:length(TrigsForAlignment) + if ~SearchBaseMask(i) + continue + end + + if sum(Blinks.startTs>=(TrigsForAlignment(i)+(FilterConfig.FromSec*1000*1000)) & Blinks.startTs<=(TrigsForAlignment(i)+(FilterConfig.ToSec*1000*1000))) > 0 || ... + sum(Blinks.endTs>=(TrigsForAlignment(i)+(FilterConfig.FromSec*1000*1000)) & Blinks.endTs<=(TrigsForAlignment(i)+(FilterConfig.ToSec*1000*1000))) > 0 || ... + sum(Blinks.startTs<(TrigsForAlignment(i)+(FilterConfig.FromSec*1000*1000)) & Blinks.endTs>(TrigsForAlignment(i)+(FilterConfig.ToSec*1000*1000))) > 0 + + % NOTE: MICROSEC + ExcludedMask(i) = true; + end + + if ExcludedMask(i) + log_d(['Excluded trial nr. ' num2str(i)]); + end + end + +end \ No newline at end of file diff --git a/FiltSweepsOnInterpol.m b/FiltSweepsOnInterpol.m new file mode 100644 index 0000000..44ff02d --- /dev/null +++ b/FiltSweepsOnInterpol.m @@ -0,0 +1,20 @@ +function ExcludedMask = FiltSweepsOnInterpol(Samples, SearchBaseMask, TrigsForAlignment, FilterConfig) + + ExcludedMask = false(length(TrigsForAlignment), 1); + InterpolRatios = support_calcInterpolRatios(Samples.Ts, Samples.OrigSamplesTs, TrigsForAlignment, Samples.SRate, Samples.OrigSRate); + + for i = 1:length(TrigsForAlignment) + if ~SearchBaseMask(i) + continue + end + + if InterpolRatios(i) > FilterConfig.Threshold + ExcludedMask(i) = true; + end + + if ExcludedMask(i) + log_d(['Excluded trial nr. ' num2str(i)]); + end + end + +end \ No newline at end of file diff --git a/FiltSweepsOnSD.m b/FiltSweepsOnSD.m new file mode 100644 index 0000000..4cb445e --- /dev/null +++ b/FiltSweepsOnSD.m @@ -0,0 +1,18 @@ +function ExcludedMask = FiltSweepsOnSD(TrialsArray, SearchBaseMask, FilterConfig) + + ExcludedMask = false(length(TrigsForAlignment), 1); + for i = 1:length(TrigsForAlignment) + if ~SearchBaseMask(i) + continue + end + + if std(TrialsArray(:,i), 'omitnan') > FilterConfig.LocalLimit + ExcludedMask(i) = true; + end + + if ExcludedMask(i) + log_d(['Excluded trial nr. ' num2str(i)]); + end + end + +end \ No newline at end of file diff --git a/FiltSweepsOnSaccade.m b/FiltSweepsOnSaccade.m new file mode 100644 index 0000000..21d43ae --- /dev/null +++ b/FiltSweepsOnSaccade.m @@ -0,0 +1,22 @@ +function ExcludedMask = FiltSweepsOnSaccade(Saccades, SearchBaseMask, TrigsForAlignment, FilterConfig) + + ExcludedMask = false(length(TrigsForAlignment), 1); + for i = 1:length(TrigsForAlignment) + if ~SearchBaseMask(i) + continue + end + + if sum(Saccades.magnitude(i) >= FilterConfig.Magnitude & Saccades.startTs>=(TrigsForAlignment(i)+(FilterConfig.FromSec*1000*1000)) & Saccades.startTs<=(TrigsForAlignment(i)+(FiltConfig.ToSec*1000*1000))) > 0 || ... + sum(Saccades.magnitude(i) >= FilterConfig.Magnitude & Saccades.endTs>=(TrigsForAlignment(i)+(FilterConfig.FromSec*1000*1000)) & Saccades.endTs<=(TrigsForAlignment(i)+(FiltConfig.ToSec*1000*1000))) > 0 || ... + sum(Saccades.magnitude(i) >= FilterConfig.Magnitude & Saccades.startTs<(TrigsForAlignment(i)+(FilterConfig.FromSec*1000*1000)) & Saccades.endTs>(TrigsForAlignment(i)+(FiltConfig.ToSec*1000*1000))) > 0 + % NOTE: MICROSEC + + ExcludedMask(i) = true; + end + + if ExcludedMask(i) + log_d(['Excluded trial nr. ' num2str(i)]); + end + end + +end \ No newline at end of file diff --git a/GET_TRIALCHANGES.m b/GET_TRIALCHANGES.m new file mode 100644 index 0000000..00d0397 --- /dev/null +++ b/GET_TRIALCHANGES.m @@ -0,0 +1,65 @@ +clear; %clear environment variables +clc; %clear command window +format compact; %tömören mutassa a parancssor kimenetét + +global LOGLEVEL +% LOGLEVEL = 1; % nothing +% LOGLEVEL = 2; % info only +% LOGLEVEL = 3; % info and warning too +LOGLEVEL = 4; % info, warning and debug too + +% -------------------------------------------------- +% CONFIG + +Config.ExpDirName = 'NBACK_ver1_s1_MUTUAL_BE'; +% Config.ExpDirName = 'NBACK_ver1_s2_MUTUAL_BE'; + + +Config.ETDataFormat = 'SMI'; +% Config.ETDataFormat = 'PupilEXT'; +% Config.ETDataFormat = 'EyeLink'; + + +% -------------------------------------------------- +% PREPARE VARIABLES + +Meta.RootDirTag = [strrep(Config.ExpDirName,' ','_')]; +Meta.CfPrefix = [strrep(Config.ExpDirName,' ','_') '_']; + +Meta.RootDirTag = [Meta.RootDirTag '_' Config.ETDataFormat]; +disp(['Using eye tracker data format: ' Config.ETDataFormat]); + +Config = support_DefineETDataSpecs(Config); +Participants = support_FindParticipantsByFiles(Config, ['~RAWDATA/' Config.ETDeviceDirName '/*' Config.ExpDirName], Config.ETDataFileNameEnding); + +% -------------------------------------------------- +% PROCESS AND SAVE + +for ppnr = 1:length(Participants) + + Participant.ID = Participants{ppnr}; + Participant.Nr = ppnr; + + disp('--------------------------------------------------'); + log_i(['Currently processing ' char(Participants(ppnr)) ' at index ' num2str(ppnr)]); + + Config.PXorMM = true; + ETData = GetData(['~RAWDATA/' Config.ETDeviceDirName '/' Config.ExpDirName], char(Participants(ppnr)), Config.ETDataFormat, Config.PXorMM); + timestamp = ETData.Samples.Ts; + pupdil = ETData.Samples.Pupdil; + + T = support_findTrialChanges(ETData.Triggers.Trial, ETData.Triggers.Ts); + + outFilePath = ['~RESULTS/' Meta.RootDirTag '/' 'Trial changes' '/' ]; + mkdir(outFilePath); + outFileName = [char(Participants(ppnr)) '_trial_changes' '_' Config.ETDataFormat '.xls']; % no prefix here + + writetable(T, [outFilePath outFileName]); + clearvars timestamp trial pupdil; + +end + + + + + diff --git a/GetData.m b/GetData.m new file mode 100644 index 0000000..b38227d --- /dev/null +++ b/GetData.m @@ -0,0 +1,27 @@ +function [ETData] = GetData(Directory, ParticipantName, ETDataFormat, PXorMM) + + % TODO: map fixations ? + % TODO: discard fixations shorter than 100ms ? + + ETData.QualityValues.Conf = NaN; + ETData.QualityValues.OutlineConf = NaN; + ETData.samples.Conf = NaN; + ETData.samples.OutlineConf = NaN; + + if(strcmp(ETDataFormat, 'SMI')) + [ETData.Samples.Ts, ETData.Samples.Pupdil, ETData.Samples.QualityValues.Conf, ETData.Triggers.Trial, ETData.Triggers.Ts, ETData.Blinks.Start, ETData.Blinks.End, ETData.Saccades.Start, ETData.Saccades.End, ETData.Saccades.StartX, ETData.Saccades.StartY, ETData.Saccades.EndX, ETData.Saccades.EndY, ETData.Saccades.Magnitude] = Parser_SMI(Directory, ParticipantName, PXorMM); + elseif(strcmp(ETDataFormat, 'PupilEXT')) + [ETData.Samples.Ts, ETData.Samples.Pupdil, ETData.Samples.QualityValues.Conf, ETData.Samples.QualityValues.OutlineConf, ETData.Triggers.Trial, ETData.Triggers.Ts, ETData.Blinks.Start, ETData.Blinks.End, ETData.Saccades.Start, ETData.Saccades.End, ETData.Saccades.StartX, ETData.Saccades.StartY, ETData.Saccades.EndX, ETData.Saccades.EndY, ETData.Saccades.Magnitude] = Parser_PupilEXT(Directory, ParticipantName, PXorMM); + elseif(strcmp(ETDataFormat, 'EyeLink')) + Filename = strcat(Directory, '/', ParticipantName,'.asc'); +% [coli_ts, coli_tr, coli_p, coli_c, coli_oc, StartRow] = support_EyeLink_GetHeaderColNrs(filename, colNamesToGet); + [ETData.Samples.Ts, ETData.Samples.Pupdil, ETData.Triggers.Trial, ETData.Triggers.Ts, ETData.Blinks.Start, ETData.Blinks.End, ETData.Saccades.Start, ETData.Saccades.End, ETData.Saccades.StartX, ETData.Saccades.StartY, ETData.Saccades.EndX, ETData.Saccades.EndY, ETData.Saccades.Magnitude] = Parser_EyeLink(Filename); + elseif(strcmp(ETDataFormat, 'Tobii')) + % TODO + log_e('Not yet supported'); + elseif(strcmp(ETDataFormat, 'Other')) + Filename = strcat(Directory, '/', ParticipantName,'.xlsx'); + [ETData.Samples.Ts, ETData.Samples.Pupdil] = Parser_Other(Filename, FilterTrials); + end + +end \ No newline at end of file diff --git a/GetData_MapBehav_NBACKpython.m b/GetData_MapBehav_NBACKpython.m new file mode 100644 index 0000000..c9c1b3b --- /dev/null +++ b/GetData_MapBehav_NBACKpython.m @@ -0,0 +1,92 @@ +function Behav = GetData_MapBehav_NBACKpython(Config, participantName) + +% Config.BehavDir = '~RT/NBACK_eye ver1 s1'; + + numTrials = ceil((Config.FilterTrials(2) - Config.FilterTrials(1) + 1 ) / Config.EveryWhichTrial); + disp(['numtrials @ behav mapping = ' num2str(numTrials)]); + + cn_RT = 'key_resp_2.rt'; + cn_key = 'key_resp_2.keys'; +% cn_corr = 'key_resp_3.corr'; + cn_tt = 'nback'; + + participantID_behav = extractBefore( extractAfter(participantName, '_'), '_'); + + % -------------------------------------------------- + + dfn = dir( [Config.BehavDir, '/', participantID_behav, '*.csv'] ); + listFound = {dfn.name}; + csvFile = char(listFound(1)); % TODO: warn user if there is more + clearvars dfn listFound; + + opts = detectImportOptions([Config.BehavDir '/' char(csvFile)]); + opts.VariableNamesLine = 1; + opts.PreserveVariableNames = true; +% opts.VariableNamingRule = 'preserve'; + T = readtable([Config.BehavDir '/' csvFile], opts); + + % detect column indices, because it can vary from subject to subject + ci_RT = find(strcmp(T.Properties.VariableNames, cn_RT), 1); + ci_key = find(strcmp(T.Properties.VariableNames, cn_key), 1); +% ci_corr = find(strcmp(T_test.Properties.VariableNames, cn_corr), 1); + ci_tt = find(strcmp(T.Properties.VariableNames, cn_tt), 1); %trial type + +% for hc = 1:length(ci_key) +% % ci_corr = find(strcmp(T_test.Properties.VariableNames, cn_corr), 1); +% end + + % get data out of the braces psychopy put them in (when multiple key + % responses are allowed) + ic = 1; + for hc = 1:numTrials +% ci_corr = find(strcmp(T_test.Properties.VariableNames, cn_corr), 1); + temp_cellarr = T{(hc+1), ci_RT}; + temp_str = temp_cellarr{1}; + if ~isempty(temp_str) + temp_str2 = extractBetween(temp_str,2,length(temp_str)-1); + Behav.RT(ic, 1) = str2double(temp_str2) * 1000; % to millisec + else + Behav.RT(ic, 1) = NaN; + end + + ic=ic+1; + end + +% Behav.RT = T{2:(numTrials+1), ci_RT}; + Behav.StimType = T{2:(numTrials+1), ci_tt}; % ez kivételesen egyszerű, 0 vagy 1 + + +% Behav.RespType = T{2:(numTrials+1), ci_key}; + ic = 1; + for hc = 1:numTrials +% ci_corr = find(strcmp(T_test.Properties.VariableNames, cn_corr), 1); + temp_cellarr = T{(hc+1), ci_key}; + temp_str = temp_cellarr{1}; + if ~strcmp(temp_str,'None') && length(temp_str)>1 + + multiAnswSeparatorIndices = strfind(temp_str,''''); + temp_str2 = extractBetween(temp_str, multiAnswSeparatorIndices(1)+1,multiAnswSeparatorIndices(2)-1); + + Behav.RespType(ic, 1) = temp_str2; + else + Behav.RespType(ic, 1) = cellstr(temp_str); % string to cell, erre jó a cellstr() + end + + ic=ic+1; + end +% Behav.RespVerid = T{2:(numTrials+1), ci_corr}; + + ic = 1; + for hc = 1:numTrials + if (Behav.StimType(hc) == 1 && strcmp(Behav.RespType(hc), 'up')) || ... + (Behav.StimType(hc) == 0 && strcmp(Behav.RespType(hc), 'down')) + Behav.RespVerid(ic, 1) = 1; % true + else + Behav.RespVerid(ic, 1) = 0; % false + end + ic=ic+1; + end + + Behav.Trial = transpose(1:numTrials); + +end \ No newline at end of file diff --git a/Misc/DemoPic.png b/Misc/DemoPic.png new file mode 100644 index 0000000000000000000000000000000000000000..c777f625de9149f86ee0628a97f72aa34479be99 GIT binary patch literal 124993 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%0D$QwZizG$YW(&e*e@4Un9C4jt5*R1x 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 ZzNhNZP4AE-svzV;DRKEL$(OG`{vT#f#K`~v literal 0 HcmV?d00001 diff --git a/PREPROC.m b/PREPROC.m new file mode 100644 index 0000000..0a39b5a --- /dev/null +++ b/PREPROC.m @@ -0,0 +1,363 @@ +clear; % clear environment variables +clc; % clear command window +format compact; % show terminal output in shorter version +format long; % show numbers normally, not scientific notation + +global LOGLEVEL +% LOGLEVEL = 1; % nothing +% LOGLEVEL = 2; % info only +% LOGLEVEL = 3; % info and warning too +LOGLEVEL = 4; % info, warning and debug too + +% DEVMODE = true; +DEVMODE = false; + +% -------------------------------------------------- +% CONFIG + +% Defaults, must come here +CONFIG_PREPROC_DEFAULTS + +% Config for the actual experiment (may be custom) +CONFIG_PREPROC_NBACK + +% -------------------------------------------------- +% PREPARE VARIABLES + +Meta.NomSRate = NaN; +Meta.FilterTrials = [NaN NaN]; + +Meta.ISISec = ISISec; +Meta.StimOnScreenSec = StimOnScreenSec; + +Meta.RootDirTag = [strrep(Config.ExpDirName,' ','_')]; +Meta.CfPrefix = regexprep(Config.ExpDirName,'[^a-zA-Z0-9_\s]',''); + +if Config.HarFilt.Enabled + Meta.RootDirTag = [Meta.RootDirTag '_BASE+' num2str(Config.HarFilt.NumAddHarmonics) 'HAR']; +else + Meta.RootDirTag = [Meta.RootDirTag '_DQ']; +end + +if Config.PXorMM + Meta.RootDirTag = [Meta.RootDirTag '_PX']; +else + Meta.RootDirTag = [Meta.RootDirTag '_MM']; +end + +% ADDITIONAL +% Meta.RootDirTag = [Meta.RootDirTag '_NOGOLAY']; +% Meta.RootDirTag = [Meta.RootDirTag '_MUTUAL']; +% Meta.RootDirTag = [Meta.RootDirTag '_FAST']; + +Meta.RootDirTag = [Meta.RootDirTag '_' Config.ETDataFormat]; +disp(['Using eye tracker data format: ' Config.ETDataFormat]); +Config = support_DefineETDataSpecs(Config); +Participants = support_FindParticipantsByFiles(Config, ['~RAWDATA/' Config.ETDeviceDirName '/*' Config.ExpDirName], Config.ETDataFileNameEnding); + +if DEVMODE + Meta.RootDirTag = [Meta.RootDirTag '_DEV']; + disp('RUNNING IN DEVELOPER MODE'); +end + +Meta.Flag_spectFiltered = Config.HarFilt.Enabled; +Meta.Flag_Config.PXorMM = Config.PXorMM; +Meta.Flag_BehavMapped = Config.MapBehav; +Meta.PreprocVersion = 0.006; +Meta.DataStructureVersion = 0.002; + +log_i(['Meta.Flag_spectFiltered = ' num2str(Meta.Flag_spectFiltered)]); +log_i(['Meta.Flag_Config.PXorMM = ' num2str(Meta.Flag_Config.PXorMM)]); +log_i(['Meta.Flag_BehavMapped = ' num2str(Meta.Flag_BehavMapped)]); +log_i(['Meta.PreprocVersion = ' num2str(Meta.PreprocVersion)]); + +% Preallocating vectors +MeanInterpolRatio = nan(length(Participants), 1); + +% Generates (target*harmonics)+-delta freq intervals, to be filtered later +interval_base = [ Config.HarFilt.BaseFreq-Config.HarFilt.FreqRadius Config.HarFilt.BaseFreq+Config.HarFilt.FreqRadius ]; +% +Config.HarFilt.TargetFreqRanges = [interval_base]; +for(fcs = 2:(Config.HarFilt.NumAddHarmonics+1)) + interval_har = [ (fcs*Config.HarFilt.BaseFreq)-Config.HarFilt.FreqRadius (fcs*Config.HarFilt.BaseFreq)+Config.HarFilt.FreqRadius ]; + Config.HarFilt.TargetFreqRanges = [Config.HarFilt.TargetFreqRanges ; interval_har]; +end +clearvars interval_base interval_har; + +% -------------------------------------------------- +% PROCESS AND SAVE + +for ppnr = 1:length(Participants) + + Participant.ID = Participants{ppnr}; + Participant.Nr = ppnr; + + disp('--------------------------------------------------'); + log_i(['Currently processing ' char(Participants(ppnr)) ' at index ' num2str(ppnr)]); + + Samples = struct(); + Behav = struct(); + Blinks = struct(); + Saccades = struct(); + Triggers = struct(); + + if(strcmp(Config.ETDataFormat, 'SMI')) + Samples.SRate = str2double(Parser_SMI_getParamValue(strcat(['~RAWDATA/' Config.ETDeviceDirName '/' Config.ExpDirName], '/', char(Participants(ppnr)), Config.ETDataFileNameEnding), 'Sample Rate')); + elseif(strcmp(Config.ETDataFormat, 'PupilEXT')) + Samples.SRate = 50; % TODO: not hardcoded + elseif(strcmp(Config.ETDataFormat, 'EyeLink')) + Samples.SRate = 1000; % TODO: not hardcoded + elseif(strcmp(Config.ETDataFormat, 'Other')) + Samples.SRate = 500; + end + Samples.OrigSRate = Samples.SRate; + log_i(['Sample Rate in eye data file: ' num2str(Samples.SRate)]); + + % TODO: check srate so that every input recording has the same srate + + % Config.FilterTrialsG check + clear Config.FilterTrials; + if size(Config.FilterTrialsG, 1) > 1 + for fts = 1:size(Config.FilterTrialsG, 1) + if strcmp( Config.FilterTrialsG{fts,1},char(Participants(ppnr)) ) + Config.FilterTrials = Config.FilterTrialsG{fts,2}; + break + end + end + if ~isfield(Config,'FilterTrials') + if(strcmp(Config.ETDataFormat, 'Other')) + log_w(['Config.FilterTrials set for dummy [1 1] as device is specified as Other, now at participant ' char(Participants(ppnr))]); + Config.FilterTrials = [1 1]; + else + log_e(['Config.FilterTrials not specified for participant ' char(Participants(ppnr))]); + end + end + log_i(['Filtering trials of each participant separately.']); + else + log_e(['Please check Config.FilterTrialsG in script configuration']); + end + + if ~isnan(SkipFirstNtrials) && isnumeric(SkipFirstNtrials) && SkipFirstNtrials>0 + log_w(['Skipping first ' num2str(SkipFirstNtrials) ' trials']); + Config.FilterTrials(1) = Config.FilterTrials(1) + SkipFirstNtrials; + end + + ETData = GetData(['~RAWDATA/' Config.ETDeviceDirName '/' Config.ExpDirName], char(Participants(ppnr)), Config.ETDataFormat, Config.PXorMM); + Samples.Ts = ETData.Samples.Ts; + Samples.Pupdil = ETData.Samples.Pupdil; + Samples.QualityValues = ETData.Samples.QualityValues; + Blinks.StartTs = ETData.Blinks.Start; + Blinks.EndTs = ETData.Blinks.End; + Saccades.StartTs = ETData.Saccades.Start; + Saccades.EndTs = ETData.Saccades.End; + Saccades.StartX = ETData.Saccades.StartX; + Saccades.StartY = ETData.Saccades.StartY; + Saccades.EndX = ETData.Saccades.EndX; + Saccades.EndY = ETData.Saccades.EndY; + Saccades.Magnitude = ETData.Saccades.Magnitude; +% qualityValues = ETData.QualityValues; + % NOTE: "end" is a keyword of Matlab language, need to avoid it + + % PERFORM TRIGGER "NUMBERING ALIGNMENT", based on Config.FilterTrials and everyWhich, on ONLY the trigger timestamp vector! + % So we only KEEP the needed trigger timestamps, that is the "renumbering" step + [Triggers.Stim.Trial, Triggers.Stim.Ts] = support_createAlignedTriggerVecStim(ETData.Triggers.Trial, ETData.Triggers.Ts, Config.FilterTrials, Config.EveryWhichTrial); + + + Samples.OrigRecLenSec = (Samples.Ts(length(Samples.Ts))-Samples.Ts(1))/1000/1000; + log_i(['Length of recording to be processed in seconds: ' num2str( (Samples.Ts(length(Samples.Ts))-Samples.Ts(1))/1000/1000 )]); + + if DEVMODE && ~isnan(Config.ManShiftMs) && isnumeric(Config.ManShiftMs) && Config.ManShiftMs~=0 + log_w('MANUALLY SHIFTED STIMULUS TIMESTAMPS'); + Triggers.Stim.Ts = Triggers.Stim.Ts + Config.ManShiftMs*1000; + end + + % -------------------------------------------------- + % MAP BEHAV DATA + + if Config.MapBehav + if size(Config.FilterTrials, 1) == 1 + clear(Config.BehavParserFunction); + Behav = feval(Config.BehavParserFunction, Config, Participant.ID); + else + log_e(['Please check Config.FilterTrials in script configuration']); + end + end + + if Config.MapBehav + [Triggers.Resp.Trial, Triggers.Resp.Ts] = support_createTriggerVecResp(Triggers.Stim.Trial, Triggers.Stim.Ts, Behav.Trial, Behav.RT); + + if DEVMODE && ~isnan(Config.ManShiftMs) && isnumeric(Config.ManShiftMs) && Config.ManShiftMs~=0 + log_w('MANUALLY SHIFTED RESPONSE TIMESTAMPS'); + Triggers.Resp.Ts = Triggers.Resp.Ts + Config.ManShiftMs*1000; + end + end + + % -------------------------------------------------- + % DATA QUALITY SECTION + + if(strcmp(Config.ETDataFormat, 'PupilEXT')) + log_i(['Rejecting Samples upon pupil detection confidence criteria: ']); + log_i([sprintf('\t') 'Confidence < ' num2str( confidenceThreshold )]); + log_i([sprintf('\t') 'Outline Confidence < ' num2str( outlineConfidenceThreshold )]); + [Samples.Ts, Samples.Pupdil] = DQ_RemoveSamplesByConfidence(Samples.Ts, Samples.Pupdil, Samples.QualityValues.Conf, Samples.QualityValues.OutlineConf, confidenceThreshold, outlineConfidenceThreshold); + end + + [Samples.Ts, Samples.Pupdil] = DQ_RemoveNaNs(Samples.Ts, Samples.Pupdil); + [Samples.Ts, Samples.Pupdil] = DQ_RemoveZeros(Samples.Ts, Samples.Pupdil, 0, 0); + if sum(isnan(Blinks.StartTs)) == 0 && sum(isnan(Blinks.EndTs)) == 0 + [Samples.Ts, Samples.Pupdil] = DQ_RemoveBlinks(Samples.Ts, Samples.Pupdil, Blinks.StartTs, Blinks.EndTs, 0, 0); + end + + if(strcmp(Config.ETDataFormat, 'PupilEXT')) + log_i(['Removing hiccups']); + [Samples.Ts, Samples.Pupdil] = DQ_RemoveHiccups(Samples.Ts, Samples.Pupdil, floor(Samples.SRate/4), 10); + end + + + % DEV TEST, 2023.10.30 + %{ + if(strcmp(Config.ETDataFormat, 'PupilEXT')) + log_i(['(DEV) Removing extreme values']); + refy = mode(Samples.Pupdil); + devlimsd = 3; + mask2 = Samples.Pupdil > refy + devlimsd*std(Samples.Pupdil,'omitnan'); + mask3 = Samples.Pupdil < refy - devlimsd*std(Samples.Pupdil,'omitnan'); + markedToRemove = mask2 | mask3; + Samples.Pupdil = Samples.Pupdil(~markedToRemove); + Samples.Ts = Samples.Ts(~markedToRemove); + end + + if(strcmp(Config.ETDataFormat, 'PupilEXT')) + log_i(['(DEV) Lowpass filtering to remove noise']); + Samples.Pupdil = lowpass(Samples.Pupdil, 3, Samples.SRate); + end + %} + + [Samples.interpol_ratio] = DQ_CalcInterpLossAnalogWhole(Samples.Ts, Samples.SRate, Triggers.Stim.Trial, Triggers.Stim.Ts, ISISec); + MeanInterpolRatio(ppnr, 1) = Samples.interpol_ratio; + + % ANALOG VERSION + % There is no "interpolation ratio" generated here, but it is generated + % on-demand, if needed for event-related processing + % That is why we have stored the timestamps of ground truth Samples used + % for interpolation in this vector: + Samples.OrigSamplesTs = Samples.Ts; + + [Samples.Ts, Samples.Pupdil, Samples.SRate] = DQ_Resample(Samples.Ts, Samples.Pupdil, Samples.SRate); + + if PerformGolayFiltering && ~isnan(GolayWinSizeFactor) && isnumeric(GolayWinSizeFactor) && GolayWinSizeFactor>0 + golay_winlen = floor(Samples.SRate*GolayWinSizeFactor); + if mod(golay_winlen, 2) == 0 + golay_winlen = golay_winlen + 1; + end + Samples.Pupdil = sgolayfilt(Samples.Pupdil, 5, golay_winlen); % 3, 11 + end + + % TODO: why here? + [Samples.Ts, Samples.Pupdil, Samples.SRate] = DQ_Resample(Samples.Ts, Samples.Pupdil, Config.OutputNomSRate); + + log_i(['Sample Rate after decimation in data quality step: ' num2str(Samples.SRate)]); + log_i(['Length of recording in seconds, after data quality step: ' num2str( (Samples.Ts(length(Samples.Ts))-Samples.Ts(1))/1000/1000 )]); + + if isnan(Meta.NomSRate) + log_i(['No predefined nominal sampling rate was defined for metafile. Using the one first found. Namely ' num2str(Samples.SRate)]); + Meta.NomSRate = round(Samples.SRate); + else + log_i(['Nominal sampling rate is already defined.']); + if round(Meta.NomSRate) == round(Samples.SRate) + log_i([' Current eye data file complies with it.']); + else + log_e([' Current eye data file does not comply with it.']); + end + end + + % ANALOG VERSION + % There is no "interpolation ratio" generated here, but it is generated + % on-demand, if needed for event-related processing + % That is why we have stored the timestamps of ground truth Samples used + % for interpolation + + Config.FilterTrialsRenum = Config.FilterTrials; +% [Samples.Trial] = renumberTrials(Samples.Trial, Config.FilterTrials, Config.EveryWhichTrial); + Config.FilterTrialsRenum(2) = ceil((Config.FilterTrialsRenum(2)-Config.FilterTrialsRenum(1) + 1)/Config.EveryWhichTrial); + Config.FilterTrialsRenum(1) = 1; + Config.FilterTrials_original = Config.FilterTrials; + if sum(isnan(Meta.FilterTrials)) > 0 + Meta.FilterTrials = Config.FilterTrialsRenum; + log_d(['Re-setting Meta.FilterTrials']); + end + if Meta.FilterTrials(1) ~= Config.FilterTrialsRenum(1) || Meta.FilterTrials(2) ~= Config.FilterTrialsRenum(2) + log_e(['Current and defined common meta (renumbered and aligned) Config.FilterTrials do not match.']); + end + + % -------------------------------------------------- + % CHECKS (TODO) + % ... + + % -------------------------------------------------- + % PLOTTING 1 (DEV, only for debug, not saved as image files) + if Config.PlotPupil.Enabled + support_PlotPupilData(Config.PlotPupil.Mode, Samples); + end + + % -------------------------------------------------- + % PERFORM SPECTRAL FILTERING + + if Config.HarFilt.Enabled + Samples.Pupdil = support_PerformHarFilt(Config, Samples); + end + + % -------------------------------------------------- + % PLOTTING 2 (DEV, only for debug, not saved as image files) + + % TODO: not simple before-after show figure, but plot them on one plot, + % and also save them in needed + if Config.PlotPupil.Enabled + support_PlotPupilData(Config.PlotPupil.Mode, Samples); + end + + % -------------------------------------------------- + % SAVING SPECIFIC .mat FILES + + OutFilePath = ['~PREPDATA/' Meta.RootDirTag '/']; + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + outFileName = char(Participants(ppnr)); + + % TODO: wipe folder contents if folder exists already + + % save them as v7 .mat files so that SciPy can open them if needed + save([OutFilePath outFileName '.mat'], 'Samples', 'Behav', 'Blinks', 'Saccades', 'Triggers', '-v7'); + % NOTE: it can happen that behav data (resp type, stim type, etc) are + % not saved as strings. We need conversion in this case. Take care + + clearvars Samples Behav Blinks Saccades Triggers; +end + +% -------------------------------------------------- +% SAVING COMMON METAFILE + +OutFilePath = ['~PREPDATA/' Meta.RootDirTag '/']; +if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); +end +outFileName = 'Metafile'; +save([OutFilePath outFileName '.mat'], '-struct', 'Meta' ); + +% -------------------------------------------------- +% SAVING SUMMARY STATISTICS + +col_participants = transpose([{ [Meta.CfPrefix '_' 'Participant'] } Participants]); +cols_sub_vals = cell(length(Participants)+1, 1); +cols_sub_vals(1, 1) = { [Meta.CfPrefix '_' 'Recording interpolation percentage [%]']}; +cols_sub_vals(2:length(Participants)+1, 1) = num2cell(MeanInterpolRatio*100); +outputMatrix = [col_participants cols_sub_vals]; +OutFilePath = ['~RESULTS/' Meta.RootDirTag '/']; +if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); +end +outFileName = [ Meta.CfPrefix '_' 'Preproc interpolation percentages' ]; + +writecell(outputMatrix,[OutFilePath outFileName '.csv'],'Delimiter',';'); + diff --git a/Parser_EyeLink.m b/Parser_EyeLink.m new file mode 100644 index 0000000..535f67a --- /dev/null +++ b/Parser_EyeLink.m @@ -0,0 +1,169 @@ +function [timestamp, pupdil, trig_trial, trig_trial_ts, blinks_start, blinks_end, saccades_start, saccades_end] = Parser_EyeLink(filename) + +% lettersOnly = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + numbersOnly = '0123456789'; + + fileID = fopen(filename,'r'); + % NOTE: fgetl is used (this will strip newline from the end of the lines) because + % textscan will behave unreliably when newline is left at lines end, even if newline is specified as eol character + % However, it will work fine when ther is no newline in the input. See this workaround below + tline = fgetl(fileID); + cMSG = 1; + % e.g. EBLINK -> E means END OF BLINK... only end events are necessary, as they also contain start timestamps + cEBLINK = 1; + cEFIX = 1; + cESACC = 1; + while ~feof(fileID) + if startsWith(tline,'MSG') && contains(tline,'TRIAL') +% usefulData{elemCounter} = tline; + usefulDataMSG{cMSG} = strrep(tline,'TRIAL ',''); + cMSG=cMSG+1; + + elseif startsWith(tline,'EBLINK L') + % means END OF BLINK + % LEFT ONLY + usefulDataEBLINK{cEBLINK} = tline; + cEBLINK=cEBLINK+1; + + elseif startsWith(tline,'EFIX L') + % LEFT EYE ONLY + % NOTE: newline() is added as a workaround because textscan gets jammed DIFFERENTLY when the last field is a number, + % as opposed to the case when it is a string ('.....' in our case) + usefulDataEFIX{cEFIX} = append(tline, newline()); + cEFIX=cEFIX+1; + + elseif startsWith(tline,'ESACC L') + % LEFT EYE ONLY + % NOTE: newline() is added as a workaround because textscan gets jammed DIFFERENTLY when the last field is a number, + % as opposed to the case when it is a string ('.....' in our case) + usefulDataESACC{cESACC} = append(tline, newline()); + cESACC=cESACC+1; + + end + tline = fgetl(fileID); + end + fclose(fileID); + + % beleive me this is WAY FASTER than dynamic allocation with string append + % NOTE: workaround: for whatever reason, here concatenation will put newlines after each elem, thats why we use fgetl above + + + % -------------------------------------------------- + % CONVERTING TO ARRAYS + % TRIAL INCREMENT EVENTS + + textData = char(horzcat(usefulDataMSG{:})); + formatSpec = '%s%f%f'; + dataArray = textscan(textData, formatSpec, 'Headerlines', 0, 'EndOfLine', newline(), 'Delimiter', sprintf('\t')); + + trig_trial_ts = dataArray{:,2}.*1000; % stick to microsec + trig_trial = dataArray{:,3}; + clear usefulDataMSG dataArray; + + + % -------------------------------------------------- + % CONVERTING TO ARRAYS + % BLINKS + + textData = char(horzcat(usefulDataEBLINK{:})); + formatSpec = '%s%f%f'; + dataArray = textscan(textData, formatSpec, 'Headerlines', 0, 'EndOfLine', newline(), 'Delimiter', sprintf('\t')); + + % NOTE: we need this workaround, because in the asc file format, in case of blink end events, the start timestamp is + % somehow not preceded by a tabulator delimiter, but instead ONE SPACE + spaghetti = dataArray{:,1}; + % NOTE: extractAfter SHOULD return the substring after the last occurence of the pattern (whitespace in our case) + % according to docs, but guess what: it does not. For an input like 'EBLINK L 9473914' it will return 'L 9473914' + % So we are now using ONLY THE LEFT EYE NOW + blinks_start = str2double(arrayfun(@(x) extractAfter(char(x),'L '),spaghetti,'un',0)).*1000; % microsec + blinks_end = dataArray{:,2}.*1000; % microsec + clear usefulDataEBLINK dataArray; + + + % -------------------------------------------------- + % CONVERTING TO ARRAYS + % FIXATIONS + + textData = char(horzcat(usefulDataEFIX{:})); +% formatSpec = '%s%f%f%s'; + formatSpec = '%s%f%f%f%f%f%f'; % 1 string + 6 numeric fields + dataArray = textscan(textData, formatSpec, 'Headerlines', 0, 'EndOfLine', sprintf('\r\n'), 'Delimiter', sprintf('\t')); + + % NOTE: we need this workaround, because in the asc file format, in case of fixation end events, the start timestamp is + % somehow not preceded by a tabulator delimiter, but instead THREE SPACES + spaghetti = dataArray{:,1}; + % NOTE: extractAfter SHOULD return the substring after the last occurence of the pattern (whitespace in our case) + % according to docs, but guess what: it does not. For an input like 'EFIX L 9473914' it will return 'L 9473914' + % So we are now using ONLY THE LEFT EYE NOW + fixations_start = str2double(arrayfun(@(x) extractAfter(char(x),'L '),spaghetti,'un',0)).*1000; % microsec + fixations_end = dataArray{:,2}.*1000; + % fixations_dur = dataArray{:,3}; + % fixations_xpos = dataArray{:,4}; + % fixations_ypos = dataArray{:,5}; + % TODO: add gaze vector or coordinate, etc + clear usefulDataEFIX dataArray; + + + % -------------------------------------------------- + % CONVERTING TO ARRAYS + % SACCADES + + textData = char(horzcat(usefulDataESACC{:})); + formatSpec = '%s%f%f%f%f%f%f%f%f%f'; % 1 string + 9 numeric fields + dataArray = textscan(textData, formatSpec, 'Headerlines', 0, 'EndOfLine', newline(), 'Delimiter', sprintf('\t')); + + % NOTE: we need this workaround, because in the asc file format, in case of fixation end events, the start timestamp is + % somehow not preceded by a tabulator delimiter, but instead TWO SPACES + spaghetti = dataArray{:,1}; + % NOTE: extractAfter SHOULD return the substring after the last occurence of the pattern (whitespace in our case) + % according to docs, but guess what: it does not. For an input like 'EFIX L 9473914' it will return 'L 9473914' + % So we are now using ONLY THE LEFT EYE NOW + saccades_start = str2double(arrayfun(@(x) extractAfter(char(x),'L '),spaghetti,'un',0)).*1000; % microsec +% saccades_start = dataArray{:,2}.*1000; + saccades_end = dataArray{:,2}.*1000; + clear usefulDataESACC dataArray; + + + % -------------------------------------------------- + % retrieve numeric data: timestamps and pupil diameter + + fileID = fopen(filename,'r'); + tline = fgets(fileID); % fgets keeps newline characters + elemCounter = 1; + while ~feof(fileID) +% if ~isempty(tline) && ~contains(lettersOnly,tline(1)) + if ~isempty(tline) && contains(numbersOnly,tline(1)) + usefulData{elemCounter} = tline; +% usefulData{elemCounter} = strrep(tline,tabChar,newDelimChar); +% usefulData{elemCounter} = strrep(strtrim(tline),tabChar,newDelimChar); + elemCounter=elemCounter+1; + end + tline = fgets(fileID); % fgets keep newline characters + end + + fclose(fileID); + + textData = char(horzcat(usefulData{:})); + +% HeaderFmt = '%s%s%s'; + DataFmt = '%f%s'; +% DataFmt = char(['%f%s%[^' sprintf('\t') ']']); % results in undefined behavior +% DataFmt = '%f%s%[^0123456789.]'; % doesnt work either, even though it should + + % The line below SHOULD NOT work, however, it does (it is a workaround) + % Textscan will not return meaningful array when used with tab as delimiter (which is the actual delimiter) + % But (against any sane logic) it does work when delimiter is the comma character + % Matlab R2022a + dataArray = textscan(textData, DataFmt, 'Headerlines', 0, 'EndOfLine', newline(), 'Delimiter', sprintf(',')); + + % test = arrayfun(@(x) extractBefore(x,' '),pupdil) + + % the asc format will contain a DOT CHARACTER where there was no reliable eye readout, so all numeric fields must be read as string first, and + % then parsed + timestamp = dataArray{:,1}; + + spaghetti = dataArray{:,2}; + pupdil = str2double(arrayfun(@(x) extractBefore(x,sprintf('\t')),spaghetti)); + clear usefulData dataArray; + +end \ No newline at end of file diff --git a/Parser_Other.m b/Parser_Other.m new file mode 100644 index 0000000..c276b20 --- /dev/null +++ b/Parser_Other.m @@ -0,0 +1,15 @@ +function [timestamp, tr, pupdil] = Parser_Other(filename, filterTrials) + + dataArray = readtable(filename); %(fileID, formatSpec, 'Delimiter', delimiter, 'EmptyValue' ,NaN,'HeaderLines' ,startRow-1, 'ReturnOnError', true); + + timestamp = str2double(dataArray{:,1}); + tr = ones(size(dataArray,1), 1); + +% if iscell(dataArray{newcoli_p}) +% dataArray(newcoli_p) = str2double(dataArray(newcoli_p)); +% end + pupdil = str2double(dataArray{:,5}); + + timestamp = timestamp.*1000; + +end \ No newline at end of file diff --git a/Parser_PupilEXT.m b/Parser_PupilEXT.m new file mode 100644 index 0000000..41e95e4 --- /dev/null +++ b/Parser_PupilEXT.m @@ -0,0 +1,94 @@ +function [timestamp, pupdil, conf, outlineConf, uniq_tr, ts_abs, b_start, b_end, s_start, s_end, s_startX, s_startY, s_endX, s_endY, s_magnitude] = Parser_PupilEXT(directory, participantName, PXorMM) + + % DEV + s_startX = NaN; + s_startY = NaN; + s_endX = NaN; + s_endY = NaN; + s_magnitude = NaN; + + filename = strcat(directory, '/', participantName,'.csv'); + + delimiter = Parser_PupilEXT_determineDelimiter(filename); + + % TODO: mm? + colNamesToGet = {'timestamp', 'trial', 'diameter_px', 'confidence', 'outlineConfidence'}; + [coli_ts, coli_tr, coli_p, coli_c, coli_oc, startRow] = Parser_PupilEXT_getHeaderColNrs(filename, colNamesToGet, delimiter); + + coli = [coli_ts coli_tr coli_p coli_c coli_oc]; + % TODO: make the formatSpec parameter for textscan() automatically + formatSpec = num2str(zeros(1, max(coli)+1), '%i'); + formatSpec(coli) = '1'; + formatSpec = strrep(formatSpec, '1', '%f'); + formatSpec = strrep(formatSpec, '0', '%*s'); + formatSpec = strcat(formatSpec, '%[^\n\r]'); + %TODO: find the column indices(of textscan output) that correspond to the requested columns(of textscan input) + newcoli_ts = 1; %coli(coli==coli_ts) + newcoli_tr = 5; %coli(coli==coli_tr) + newcoli_p = 2; %coli(coli==coli_p) + newcoli_c = 3; + newcoli_oc = 4; + + % -------------------------------------------------- + % SAMPLES + + fileID = fopen(filename,'r'); + dataArray = textscan(fileID, formatSpec, 'Delimiter', delimiter, 'EmptyValue' ,NaN,'HeaderLines' ,startRow-1, 'ReturnOnError', true); + fclose(fileID); + + % sometimes columns have different length, check for that + lengths = [length(dataArray{newcoli_ts}) length(dataArray{newcoli_tr}) length(dataArray{newcoli_p})]; + timestamp = dataArray{newcoli_ts}(1:min(lengths)); + tr = dataArray{newcoli_tr}(1:min(lengths)); + pupdil = dataArray{newcoli_p}(1:min(lengths)); + conf = dataArray{newcoli_c}(1:min(lengths)); + outlineConf = dataArray{newcoli_oc}(1:min(lengths)); + + % NOTE: TIMESTAMP VALUES are in ms in case of PupilEXT, as instead SMI + % has it in microsec, so we artificially increase temporal resolution + % by multiplying PupilEXT values by 1000 + timestamp = timestamp.*1000; + + % -------------------------------------------------- + % TRIALS BASED ON SAMPLES + + % TODO: READ EVENTS XML + + uniq_tr = unique(tr); + ts_abs = zeros(length(uniq_tr), 1); + + if length(timestamp) < 10 + log_e('The length of this recording is less than 10 samples. Please check your data') + end + + ts_abs(1) = timestamp(1); + tr_prev = tr(1); + + changeC = 2; + iterC = 1; + while(iterC < length(timestamp)) %% && changeC < 10) + tr_curr = tr(iterC); + + if tr_curr < tr_prev && tr_curr > tr(1) + log_e(['Trial numbering restarted during recording. Please fix the data file and try again']); + end + + if tr_curr > tr_prev + ts_abs(changeC) = timestamp(iterC); + changeC = changeC + 1; + end + iterC = iterC + 1; + tr_prev = tr_curr; + end + + % DUMMY VARIABLES: reserved for future + b_start = NaN; + b_end = NaN; + s_start = NaN; + s_end = NaN; + + % DEV + % timestamp(1:20) + % pupdil(1:20) + +end \ No newline at end of file diff --git a/Parser_PupilEXT_determineDelimiter.m b/Parser_PupilEXT_determineDelimiter.m new file mode 100644 index 0000000..835e52d --- /dev/null +++ b/Parser_PupilEXT_determineDelimiter.m @@ -0,0 +1,21 @@ +function delimiter = Parser_PupilEXT_determineDelimiter(filename) + + lineToInspect = 2; + possibleDelimiters = { sprintf('\t') sprintf(',') sprintf(';') }; + possibleDelimiterCounts = [ 0 0 0 ]; + + fileID = fopen(filename,'r'); + + for i=1:lineToInspect + tline = fgetl(fileID); + end + + for i=1:length(possibleDelimiters) + possibleDelimiterCounts(i) = sum(count(tline, possibleDelimiters{i})); + end + + delimiter = possibleDelimiters{find(possibleDelimiterCounts, max(possibleDelimiterCounts))}; + + fclose(fileID); + +end \ No newline at end of file diff --git a/Parser_PupilEXT_getHeaderColNrs.m b/Parser_PupilEXT_getHeaderColNrs.m new file mode 100644 index 0000000..edccec8 --- /dev/null +++ b/Parser_PupilEXT_getHeaderColNrs.m @@ -0,0 +1,36 @@ +function [coli_ts, coli_tr, coli_p, coli_c, coli_oc, startRow] = Parser_PupilEXT_getHeaderColNrs(filename, colNamesToGet, delimiter) + + % DEV TODO + +% delimiter = sprintf(';'); %'\t'; +% delimiter = sprintf(','); + + + fid = fopen(filename, 'r'); + + tableHeader = fgets(fid); + startRow = 2; + + %% + % {timestamp, trial, pupdil} ebben a sorrendben, mrtkegysg nlkl! teht [] jelek nem lehetnek benne + colNumbers = [0 0 0 0 0]; + charc = 1; + cidn = 1; + while(cidn <= length(colNumbers) && charc < length(tableHeader)) + foundIdx = cell2mat(regexp(tableHeader, colNamesToGet(cidn), 'once', 'ignorecase')); + %colNumbersToGet(cidn) = length(regexp(tableHeader(1:foundIdx), delimiter)) + 1; + colNumbers(cidn) = count(tableHeader(1:foundIdx), delimiter) + 1; + cidn = cidn + 1; + end + % colNumbersToGet will now contain the numbers (indexes) of columns that contain timestamp, trial and pupdil respectively + + fclose(fid); + + coli_ts = colNumbers(1); + coli_tr = colNumbers(2); + coli_p = colNumbers(3); + coli_c = colNumbers(4); + coli_oc = colNumbers(5); + % egyszerbb lenne mshogy, de j ez gy, hogy ltszik a fgv deklarlsbl, hogy melyik vltoz micsoda + +end \ No newline at end of file diff --git a/Parser_SMI.m b/Parser_SMI.m new file mode 100644 index 0000000..cdff71c --- /dev/null +++ b/Parser_SMI.m @@ -0,0 +1,130 @@ +function [timestamp, pupdil, conf, uniq_tr_fixed, ts_abs, b_start, b_end, s_start, s_end, s_startX, s_startY, s_endX, s_endY, s_magnitude] = Parser_SMI(directory, participantName, PXorMM) + + if PXorMM + colNamesToGet = {'Time', 'Trial', 'L Dia X', 'Pupil Confidence'}; + else + colNamesToGet = {'Time', 'Trial', 'L Mapped Diameter', 'Pupil Confidence'}; + end + filename = strcat(directory, '/', participantName,' Samples.txt'); + [coli_ts, coli_tr, coli_p, coli_conf, startRow] = Parser_SMI_getHeaderColNrs(filename, colNamesToGet); + + + % -------------------------------------------------- + % SAMPLES + + delimiter = '\t'; + + %tornyos (MST) => 1,3,4 + %remote (baseline, REV LEARN EXP1, testing effect) => 1,3,6 + %remote (REV LEARN EXP2a-b) => 1,3,10 + coli = [coli_ts coli_tr coli_p]; + + if coli_conf~=0 + coli(length(coli)+1) = coli_conf; + end + + %make the formatSpec parameter for textscan() automatically + formatSpec = num2str(zeros(1, max(coli)+1), '%i'); + formatSpec(coli) = '1'; + formatSpec = strrep(formatSpec, '1', '%f'); + formatSpec = strrep(formatSpec, '0', '%*s'); + formatSpec = strcat(formatSpec, '%[^\n\r]'); + %TODO: find the column indices(of textscan output) that correspond to the requested columns(of textscan input) + newcoli_ts = 1; %coli(coli==coli_ts) + newcoli_tr = 2; %coli(coli==coli_tr) + newcoli_p = 3; %coli(coli==coli_p) + + newcoli_conf = 4; + + + fileID = fopen(filename,'r'); + dataArray = textscan(fileID, formatSpec, 'Delimiter', delimiter, 'EmptyValue' ,NaN,'HeaderLines' ,startRow-1, 'CommentStyle','#', 'ReturnOnError', true); + fclose(fileID); + + % sometimes the last line is not read, and columns will have different + % length, we check them here accordingly, and leave only full rows + lengths = [length(dataArray{newcoli_ts}) length(dataArray{newcoli_tr}) length(dataArray{newcoli_p})]; + timestamp = dataArray{newcoli_ts}(1:min(lengths)); + trial = dataArray{newcoli_tr}(1:min(lengths)); + pupdil = dataArray{newcoli_p}(1:min(lengths)); + + if coli_conf~=0 + conf = dataArray{newcoli_conf}(1:min(lengths)); + else +% conf = ones(1,min(lengths)); + conf = NaN; + end + % NOTE: SMI gives only 0 or 1 on pupil confidence output, no float value at all + + + % -------------------------------------------------- + % TRIALS BASED ON SAMPLES + + uniq_tr = unique(trial); +% ts_abs = zeros(length(uniq_tr), 1); % can cause discrepancies when there is a hopped trial number + ts_abs = zeros(max(uniq_tr), 1); + + if length(uniq_tr) ~= max(uniq_tr) + log_w('At least one trial is missing (hopped over) in the eye data file. This is worked around in the current version of SMI txt file reader by adding tiny dummy trials to keep the trial-switch event numbering the same') +% return; + end + + if length(timestamp) < 10 + error('The length of this recording is less than 10 samples. Please check your data') +% return; + end + + ts_abs(1) = timestamp(1); + tr_prev = trial(1); + % workaround for cases when trials are hopped: they will have the same timestamp as the last known + changeC = 2; + iterC = 1; + while(iterC < length(timestamp)) %% && changeC < 10) + tr_curr = trial(iterC); + + if tr_curr ~= tr_prev && tr_curr > tr_prev+1 + fillN = tr_curr-tr_prev-1; + for nb = 1:fillN + ts_abs(changeC) = timestamp(iterC) +nb; % add a very tiny bit to make timestamps different + changeC = changeC + 1; + end +% iterC = iterC + 1; +% tr_prev = tr_curr; + end + + if tr_curr > tr_prev % actually equals this case: tr_curr = tr_prev+1 + ts_abs(changeC) = timestamp(iterC); + changeC = changeC + 1; + end + iterC = iterC + 1; + tr_prev = tr_curr; + end + + % also we need to fix the quniq_tr vector + uniq_tr_fixed = 1:max(uniq_tr); + + + % -------------------------------------------------- + % BLINKS AND SACCADES + + % TODO: FIXATIONS? + % TODO: NO TRIAL VEC + b_start= NaN; + b_end = NaN; + s_start= NaN; + s_end = NaN; + s_startX = NaN; + s_startY = NaN; + s_endX = NaN; + s_endY = NaN; + s_magnitude = NaN; + filename = strcat(directory, '/', participantName,' Events.txt'); + startRow = Parser_SMI_getFirstDataRow_Events(filename); + if ~isnan(startRow) + [b_start, b_end] = Parser_SMI_Blinks(filename, startRow); + [s_start, s_end, s_startX, s_startY, s_endX, s_endY, s_magnitude] = Parser_SMI_Saccades(filename, startRow); + else + log_w('Could not find or open Events.txt for this subject. Blink filtering will not be done, saccades and fixations will not be saved accordingly.') + end + +end \ No newline at end of file diff --git a/Parser_SMI_Blinks.m b/Parser_SMI_Blinks.m new file mode 100644 index 0000000..0f12702 --- /dev/null +++ b/Parser_SMI_Blinks.m @@ -0,0 +1,29 @@ +function [b_start, b_end] = Parser_SMI_Blinks(filename, startRow) + + delimiter = '\t'; + + %formatSpec = '%s%f%f%f%f%f%*s%[^\n\r]'; % *s%*s%*s%*s%*s%*s%*s%*s%*s%*s%*s% + formatSpec = '%s%f%f%f%f%f%*s%*s%*s%*s%*s%*s%*s%*s%*s%*s%*s%[^\n\r]'; + + fileID = fopen(filename,'r'); + %Todo: check, hogy minden sort beolvas-e + dataArray = textscan(fileID, formatSpec, 'Delimiter', delimiter, 'EmptyValue' ,NaN,'HeaderLines' ,startRow-1, 'CommentStyle','UserEvent', 'ReturnOnError', false); + fclose(fileID); + +% Table Header for Blinks: +% Event Type Trial Number Start End Duration + + %TODO: logikai indexelssel is taln meg lehetne oldani + mask = false(size(dataArray{1},1)); + for i=1:size(dataArray{1},1) + if (strcmp(char(dataArray{1}(i)),'Blink L')) + mask(i) = true; %TODO: logical + end + + end + + %b_num = dataArray{3}(mask); + b_start = dataArray{4}(mask); + b_end = dataArray{5}(mask); + %b_duration = dataArray{6}(mask); +end \ No newline at end of file diff --git a/Parser_SMI_Saccades.m b/Parser_SMI_Saccades.m new file mode 100644 index 0000000..a5fdf07 --- /dev/null +++ b/Parser_SMI_Saccades.m @@ -0,0 +1,38 @@ +function [s_start, s_end, s_startX, s_startY, s_endX, s_endY, s_magnitude] = Parser_SMI_Saccades(filename, startRow) + + delimiter = '\t'; + +% % % %formatSpec = '%s%f%f%f%f%f%*s%[^\n\r]'; % *s%*s%*s%*s%*s%*s%*s%*s%*s%*s%*s% +% formatSpec = '%s%f%f%f%f%f%*s%*s%*s%*s%*s%*s%*s%*s%*s%*s%*s%[^\n\r]'; + formatSpec = '%s%f%f%f%f%f%f%f%f%f%f%*s%*s%*s%*s%*s%*s%[^\n\r]'; + + fileID = fopen(filename,'r'); + %Todo: check, hogy minden sort beolvas-e + dataArray = textscan(fileID, formatSpec, 'Delimiter', delimiter, 'EmptyValue' ,NaN,'HeaderLines' ,startRow-1, 'CommentStyle','UserEvent', 'ReturnOnError', false); + fclose(fileID); + +% Table Header for Saccades: +% Event Type Trial Number Start End Duration ....stb + + %TODO: logikai indexelssel is taln meg lehetne oldani + mask = false(size(dataArray{1},1)); + for i=1:size(dataArray{1},1) + if (strcmp(char(dataArray{1}(i)),'Saccade L')) + mask(i) = true; %TODO: logical + end + + end + + %b_num = dataArray{3}(mask); + s_start = dataArray{4}(mask); + s_end = dataArray{5}(mask); + %b_duration = dataArray{6}(mask); + + + s_startX = dataArray{7}(mask); + s_startY = dataArray{8}(mask); + s_endX = dataArray{9}(mask); + s_endY = dataArray{10}(mask); + s_magnitude = dataArray{11}(mask); + +end \ No newline at end of file diff --git a/Parser_SMI_getFirstDataRow_Events.m b/Parser_SMI_getFirstDataRow_Events.m new file mode 100644 index 0000000..728ef15 --- /dev/null +++ b/Parser_SMI_getFirstDataRow_Events.m @@ -0,0 +1,29 @@ +function startRow = Parser_SMI_getFirstDataRow_Events(filename) + + if exist(filename, 'file') ~= 2 + log_w('Error: Events.txt file does not exist here'); + startRow = NaN; + return; + end + + fid = fopen(filename, 'r'); + + possibleEventNames = {'Fixation', 'Saccade', 'Blink', 'User Event'}; + eventNameLengthMax = max(strlength(possibleEventNames)); + + %% + linec = 1; + lastLineWasComment = true; + while(linec < 100 && lastLineWasComment == true) + line = fgets(fid); + if length(line) >= eventNameLengthMax && contains(line(1:eventNameLengthMax), possibleEventNames) == true + break; + end + linec = linec + 1; + end + + startRow = linec; + + fclose(fid); + +end \ No newline at end of file diff --git a/Parser_SMI_getFirstDataRow_Samples.m b/Parser_SMI_getFirstDataRow_Samples.m new file mode 100644 index 0000000..6f871a3 --- /dev/null +++ b/Parser_SMI_getFirstDataRow_Samples.m @@ -0,0 +1,20 @@ +function startRow = Parser_SMI_getFirstDataRow_Samples(filename) + + fid = fopen(filename, 'r'); + + %% + linec = 1; + lastLineWasComment = true; + while(linec < 100 && lastLineWasComment == true) + line = fgets(fid); + if contains(line(1:2), '##') == false + break; + end + linec = linec + 1; + end + % linec will now contain the nr of the line, where the table header is + startRow = linec + 1; %next one is the first data row + + fclose(fid); + +end \ No newline at end of file diff --git a/Parser_SMI_getHeaderColNrs.m b/Parser_SMI_getHeaderColNrs.m new file mode 100644 index 0000000..eadd222 --- /dev/null +++ b/Parser_SMI_getHeaderColNrs.m @@ -0,0 +1,50 @@ +function [coli_ts, coli_tr, coli_p, coli_conf, startRow] = Parser_SMI_getHeaderColNrs(filename, colNamesToGet) + delimiter = sprintf('\t'); %'\t'; + + fid = fopen(filename, 'r'); + + %% + linec = 1; + lastLineWasComment = true; + while(linec < 100 && lastLineWasComment == true) + line = fgets(fid); + if contains(line(1:2), '##') == false + break; + end + linec = linec + 1; + end + % linec will now contain the nr of the line, where the table header is + tableHeader = line; % Matlab copies the content (no need of c++ strcpy) + + startRow = linec + 1; + + %% + % {timestamp, trial, pupdil} ebben a sorrendben, mrtkegysg nlkl! teht [] jelek nem lehetnek benne + colNumbers = [0 0 0 0]; + charc = 1; + cidn = 1; + while(cidn <= length(colNumbers) && charc < length(tableHeader)) + + foundIdx = cell2mat(regexp(tableHeader, colNamesToGet(cidn), 'once', 'ignorecase')); + + if isempty(foundIdx) || isnan(foundIdx) || (~isempty(foundIdx) && isnan(foundIdx) && (foundIdx==0 || isnan(foundIdx))) + log_w(['Could not find confidence values calumn for this SMI data file']); +% colNumbers(cidn) = NaN; + else + %colNumbersToGet(cidn) = length(regexp(tableHeader(1:foundIdx), delimiter)) + 1; + colNumbers(cidn) = count(tableHeader(1:foundIdx), delimiter) + 1; + end + + cidn = cidn + 1; + end + % colNumbersToGet will now contain the numbers (indexes) of columns that contain timestamp, trial and pupdil respectively + + fclose(fid); + + coli_ts = colNumbers(1); + coli_tr = colNumbers(2); + coli_p = colNumbers(3); + coli_conf = colNumbers(4); + % egyszerbb lenne mshogy, de j ez gy, hogy ltszik a fgv deklarlsbl, hogy melyik vltoz micsoda + +end \ No newline at end of file diff --git a/Parser_SMI_getParamValue.m b/Parser_SMI_getParamValue.m new file mode 100644 index 0000000..924f449 --- /dev/null +++ b/Parser_SMI_getParamValue.m @@ -0,0 +1,41 @@ +function paramValue = Parser_SMI_getParamValue(filename, paramName) + %paramName = 'Sample Rate'; + bufSize = 150; + + data = fileread(filename); + + param_idx = regexp(data, ['## ', paramName, ':'], 'once', 'ignorecase') + length(paramName) + 5; %strfind(data, + if(isempty(param_idx)) + paramValue = NaN; + return; + end + param_lineChunk = data(param_idx:(param_idx+bufSize)); + + %param_lineChunk + + % nem képes arra hogy a {'\n' ' '} elemeinek bármelyikének legelső előfordulását kiírja, + % hanem helyette visszaad egy cella tömböt, aminek két eleme van, az első az első regexp + % keresési string első megtalálási helyét, a második a másodikét tárolja, ezeket double-é + % kell alakítani, ami eltűnteni az esetleges üres cell elemeket is, és azok minimumát veszi + %param_lineChunk_EOLidx = min(cell2mat(regexp(param_lineChunk, {'\n' ' '}, 'once')))-length('\n'); + + + param_lineChunk_EOLidx = regexp(param_lineChunk, '\n', 'once')-length('\n'); + param_lineChunk_SPACEidx = regexp(param_lineChunk, ' ', 'once')-1; + + %param_lineChunk_EOLidx + %param_lineChunk_SPACEidx + + possibilities = [param_lineChunk_EOLidx param_lineChunk_SPACEidx]; + possibilities = possibilities(isnumeric(possibilities)); + + cutEnd = min(possibilities); + + %cutEnd + + if(isempty(param_idx)) + paramValue = NaN; + return; + end + paramValue = param_lineChunk(1:cutEnd); +end \ No newline at end of file diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..eeb3af1 --- /dev/null +++ b/README.MD @@ -0,0 +1,21 @@ +## LCP3 - Lightweight Customisable Processing Pipeline for Pupillometry + +This is a little processing pipeline I have been building from scratch for analysing pupillometry data from cognitive science experiments. Recently I started to beautify it and decided to upload it to let other people use it. + +Proper documentation and examples will be uploaded in near future, as well as bug fixes, revisions. This is the initial version here. Many small changes will be coming in the next few months. + +Currently available functionality is in the following scripts: +PREPROC.m, +CALC_FILTERED_ERPD.m, +GET_TRIALCHANGES.m + +This is a little demo plot of one of my analyses made with this pipeline: +![](Misc/DemoPic.png) + +The scripts only work with Matlab yet, Octave will probably be supported in the future. + +Feel free to message me or open a discussion here if you have any suggestion or noticed a bug. + +Author: Gábor Bényei @ BUTE + +License: GNU GPL v3 diff --git a/callable_behavfilt_NBACK.m b/callable_behavfilt_NBACK.m new file mode 100644 index 0000000..3514410 --- /dev/null +++ b/callable_behavfilt_NBACK.m @@ -0,0 +1,71 @@ +function ExcludedMask = callable_behavfilt_NBACK(NumTrials, Behav, FilterConfig) + + for i = 1:NumTrials + ExcludedMask(i) = true; + + if strcmp(Behav.StimType(i), '*') || ... + strcmp(Behav.RespType(i), '*') || ... + isnan(Behav.RespVerid(i)) % || ... + log_w(['Mapped behav data seems to be invalid for trial ' num2str(i)]); +% behav.RT(i) == NaN + continue; + end + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + % TRIAL-SPECIFIC CODE + if FilterConfig.CondComb == 0 +% warning('Behav filter not specified'); + ExcludedMask(i) = false; + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + elseif FilterConfig.CondComb == 1 && ... % S=Target, R=Yes, (V=Correct) % target HIT + Behav.StimType(i) == FilterConfig.StimType.A && ... + strcmp(Behav.RespType(i), FilterConfig.RespType.A) + ExcludedMask(i) = false; + + elseif FilterConfig.CondComb == 2 && ... % S=Target, R=No, (V=Wrong) % target miss + Behav.StimType(i) == FilterConfig.StimType.A && ... + strcmp(Behav.RespType(i), FilterConfig.RespType.B) + ExcludedMask(i) = false; + + elseif FilterConfig.CondComb == 3 && ... % S=Nontarget, R=No, (V=Correct) % CR + Behav.StimType(i) == FilterConfig.StimType.B && ... + strcmp(Behav.RespType(i), FilterConfig.RespType.B) + ExcludedMask(i) = false; + + elseif FilterConfig.CondComb == 4 && ... % S=Nontarget, R=Yes, (V=Wrong) % FA + Behav.StimType(i) == FilterConfig.StimType.B && ... + strcmp(Behav.RespType(i), FilterConfig.RespType.A) + ExcludedMask(i) = false; + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + elseif FilterConfig.CondComb == 5 && ... % V=Correct (CORRECT all) + Behav.RespVerid(i) == 1 + ExcludedMask(i) = false; + + elseif FilterConfig.CondComb == 6 && ... % V=Wrong (WRONG all) + Behav.RespVerid(i) == 0 + ExcludedMask(i) = false; + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + elseif FilterConfig.CondComb == 7 && ... % all trials with key responses + ~strcmp(Behav.RespType(i), 'None') + ExcludedMask(i) = false; + + elseif FilterConfig.CondComb == 8 && ... % all trials without key response + strcmp(Behav.RespType(i), 'None') + ExcludedMask(i) = false; + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +% elseif filterConfig.CondComb < 0 || filterConfig.CondComb > 13 +% error('Invalid filter method specified'); +% % excludedMask(i) = true; + end + + end + +end \ No newline at end of file diff --git a/callable_initbehavfilt_NBACK.m b/callable_initbehavfilt_NBACK.m new file mode 100644 index 0000000..0f9da23 --- /dev/null +++ b/callable_initbehavfilt_NBACK.m @@ -0,0 +1,88 @@ +function [FilterConfig, PlotConfig] = callable_initbehavfilt_NBACK(FilterConfig, PlotConfig) + + + % BEHAV-FILTER-SPECIFIC CODE for NBACK + if FilterConfig.CondComb == 0 +% warning('Behav filter not specified'); + PlotConfig.LineStyle = '-'; +% lineColor = [0 1 1]; % light sky blue (EXP1) +% lineColor = [153/255 51/255 255/255]; % light purple (EXP2) + FilterConfig.S = '~'; + FilterConfig.R = '~'; + FilterConfig.V = '~'; + FilterConfig.FriendlyName = 'All Trials'; + + elseif FilterConfig.CondComb == 1 % S=Target, R=Yes, (V=Correct) % target HIT + PlotConfig.LineStyle = '-'; + PlotConfig.LineColor = [0 1 0]; % GREEN + FilterConfig.S = FilterConfig.StimType.A_friendly; + FilterConfig.R = FilterConfig.RespType.A_friendly; + FilterConfig.V = 'C'; + FilterConfig.FriendlyName = 'Target Hits'; + + elseif FilterConfig.CondComb == 2 % S=Target, R=No, (V=Wrong) % target miss + PlotConfig.LineStyle = '-'; + PlotConfig.LineColor = [1 0 0]; % RED + FilterConfig.S = FilterConfig.StimType.A_friendly; + FilterConfig.R = FilterConfig.RespType.B_friendly; + FilterConfig.V = 'W'; + FilterConfig.FriendlyName = 'Target Misses'; + + elseif FilterConfig.CondComb == 3 % S=Nontarget, R=No, (V=Correct) % CR + PlotConfig.LineStyle = '-'; + PlotConfig.LineColor = [0.4660 0.6740 0.1880]; % dark green + FilterConfig.S = FilterConfig.StimType.B_friendly; + FilterConfig.R = FilterConfig.RespType.B_friendly; + FilterConfig.V = 'C'; + FilterConfig.FriendlyName = 'Correct Rejections'; + + elseif FilterConfig.CondComb == 4 % S=Nontarget, R=Yes, (V=Wrong) % FA + PlotConfig.LineStyle = '-'; + PlotConfig.LineColor = [0.6350 0.0780 0.1840]; % dark red + FilterConfig.S = FilterConfig.StimType.B_friendly; + FilterConfig.R = FilterConfig.RespType.A_friendly; + FilterConfig.V = 'W'; + FilterConfig.FriendlyName = 'False Alarms'; + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + elseif FilterConfig.CondComb == 5 % V=Correct (CORRECT all) + PlotConfig.LineStyle = '--'; + PlotConfig.LineColor = [0 1 1]; % cyan + FilterConfig.S = '~'; + FilterConfig.R = '~'; + FilterConfig.V = 'C'; + FilterConfig.FriendlyName = 'Correct Responses'; + + elseif FilterConfig.CondComb == 6 % V=Wrong (WRONG all) + PlotConfig.LineStyle = '--'; + PlotConfig.LineColor = [1 1 0]; % yellow + FilterConfig.S = '~'; + FilterConfig.R = '~'; + FilterConfig.V = 'W'; + FilterConfig.FriendlyName = 'Wrong Responses'; + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + elseif FilterConfig.CondComb == 7 % all trials with response + PlotConfig.LineStyle = '--'; + PlotConfig.LineColor = [0.5 0.5 0.5]; % grey + FilterConfig.S = '~'; + FilterConfig.R = '*'; + FilterConfig.V = '~'; + FilterConfig.FriendlyName = 'Any key response'; + + elseif FilterConfig.CondComb == 8 % all trials without response + PlotConfig.LineStyle = '--'; + PlotConfig.LineColor = [0 0 0]; % black + FilterConfig.S = '~'; + FilterConfig.R = '∅'; + FilterConfig.V = '~'; + FilterConfig.FriendlyName = 'No key reponse'; + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + else + error('Invalid filter method specified'); +% excludedOnBehav(i) = true; + end + +end \ No newline at end of file diff --git a/log_d.m b/log_d.m new file mode 100644 index 0000000..f31f514 --- /dev/null +++ b/log_d.m @@ -0,0 +1,9 @@ +function log_d(str) + + global LOGLEVEL + + if LOGLEVEL >= 4 + disp(['Debug: ' str]) + end + +end \ No newline at end of file diff --git a/log_e.m b/log_e.m new file mode 100644 index 0000000..320c0b4 --- /dev/null +++ b/log_e.m @@ -0,0 +1,3 @@ +function log_e(str) + error(['Error: ' str]) +end \ No newline at end of file diff --git a/log_i.m b/log_i.m new file mode 100644 index 0000000..13cc5a6 --- /dev/null +++ b/log_i.m @@ -0,0 +1,10 @@ +function log_i(str) + + global LOGLEVEL + + if LOGLEVEL >= 2 + disp(['Info: ' str]) + end + +% disp(['Info: ' str]) +end \ No newline at end of file diff --git a/log_w.m b/log_w.m new file mode 100644 index 0000000..e127c49 --- /dev/null +++ b/log_w.m @@ -0,0 +1,11 @@ +function log_w(str) + + global LOGLEVEL + + if LOGLEVEL >= 3 +% warning(['Warning: ' str]) + warning([str]) + end + +% warning(['Warning: ' str]) +end \ No newline at end of file diff --git a/support_CalcERADensity.m b/support_CalcERADensity.m new file mode 100644 index 0000000..0a4f7dd --- /dev/null +++ b/support_CalcERADensity.m @@ -0,0 +1,50 @@ +function EventDensity = support_CalcERADensity(Samples, Blinks, Saccades, SearchBaseMask, TrigsForAlignment, Config) + +% % we allocate for the worst case, when all e.g. blinks can be +% % fit onto the event-related curve +% ERA_EventNum = length(blinks.startTs); +% ERA_EventDensity = nan(ERA_EventNum,1); % column vector + EventDensity = []; + + analyticLenUs = (Config.AnalyzeToSec-Config.AnalyzeFromSec)*1000*1000; + + % TODO: optimize computation + for i = 1:length(TrigsForAlignment) + if ~SearchBaseMask(i) + continue + end + + if ~Config.PerformTJC + beginAtTs = TrigsForAlignment(i) + Config.AnalyzeFromSec*1000*1000; + % beginAtSec = TrigsForAlignment(i)/1000/1000 + Config.AnalyzeFromSec; + else + beginAtTs = Samples.Ts(find(Samples.Ts >= TrigsForAlignment(i), 1, 'first')) + Config.AnalyzeFromSec*1000*1000; + % beginAtSec = Samples.Ts(find(Samples.Ts >= TrigsForAlignment(i), 1, 'first'))/1000/1000 + Config.AnalyzeFromSec; + end + + if Config.ERA.EventOfInterest == 0 + eventsTssUnderInterval = Blinks.StartTs(find(Blinks.StartTs >= beginAtTs & Blinks.StartTs < beginAtTs+analyticLenUs)); + elseif Config.ERA.EventOfInterest == 1 + eventsTssUnderInterval = Blinks.EndTs(find(Blinks.EndTs >= beginAtTs & Blinks.EndTs < beginAtTs+analyticLenUs)); + elseif Config.ERA.EventOfInterest == 2 + eventsTssUnderInterval = Saccades.StartTs(find(Saccades.StartTs >= beginAtTs & Saccades.StartTs < beginAtTs+analyticLenUs)); + elseif Config.ERA.EventOfInterest == 3 + eventsTssUnderInterval = Saccades.EndTs(find(Saccades.EndTs >= beginAtTs & Saccades.EndTs < beginAtTs+analyticLenUs)); + end + + % transform each event to a relative timepoint, counting from analyzeFrom timepoint, and in millisec + % 0 is the beginning of analyzed period now + eventsTssUnderInterval = (eventsTssUnderInterval-beginAtTs)/1000; + + % 0 is the trigger timepoint from now on (e.g. stimulus + % presentation, or response) + eventsTssUnderInterval = eventsTssUnderInterval + Config.AnalyzeFromSec*1000; + + if ~isempty(eventsTssUnderInterval) + % append any blink start timestamps found under the analyzed + % period (e.g. trial length) + EventDensity = [EventDensity; eventsTssUnderInterval]; + end + end + +end diff --git a/support_DefineETDataSpecs.m b/support_DefineETDataSpecs.m new file mode 100644 index 0000000..b6e5274 --- /dev/null +++ b/support_DefineETDataSpecs.m @@ -0,0 +1,22 @@ +function Config = support_DefineETDataSpecs(Config) + + Config.ETDeviceDirName = '*'; + Config.ETDataFileNameEnding = '*'; + if(strcmp(Config.ETDataFormat, 'SMI')) + Config.ETDeviceDirName = 'ET_SMI_TXT'; + Config.ETDataFileNameEnding = ' Samples.txt'; % the whitespace is needed + elseif(strcmp(Config.ETDataFormat, 'PupilEXT')) + Config.ETDeviceDirName = 'ET_PUPILEXT_CSV'; + Config.ETDataFileNameEnding = '.csv'; + + log_w('PupilEXT only supports PX data yet') + Config.PXorMM = true; + elseif(strcmp(Config.ETDataFormat, 'EyeLink')) + Config.ETDeviceDirName = 'ET_EYELINK_ASC'; + Config.ETDataFileNameEnding = '.asc'; + elseif(strcmp(Config.ETDataFormat, 'Other')) + Config.ETDeviceDirName = 'ET_OTHER_XLSX'; + Config.ETDataFileNameEnding = '.xlsx'; + end + +end \ No newline at end of file diff --git a/support_FindParticipantsByFiles.m b/support_FindParticipantsByFiles.m new file mode 100644 index 0000000..9665906 --- /dev/null +++ b/support_FindParticipantsByFiles.m @@ -0,0 +1,26 @@ +function Participants = support_FindParticipantsByFiles(Config, LookupDir, FileNameEnding) + + dfn = dir(char([LookupDir '/*' FileNameEnding])); + Participants = regexprep({dfn.name}, FileNameEnding, '', 'once'); + if length(Participants) < 1 + log_e(['The directory you specified does not contain any data file with this ending: ' FileNameEnding ' Do you have your eyetracker data format set correctly?']); + end + Participants(ismember(Participants, 'Metafile')) = []; + log_i('Automatically detected participant names in the order of processing:') + disp(Participants'); + clearvars dfn; + + % TODO: less C-like? + if isfield(Config, 'SkipParticipants') && length(Config.SkipParticipants) ~= 1 + for skp = 1:length(Config.SkipParticipants) + acp = 1; + while acp <= length(Participants) + if strcmp(Config.SkipParticipants(skp), Participants(acp)) + Participants(acp) = []; % delete the cell entry (because participants{acp} = []; would only change that cell to an empty one + end + acp = acp + 1; + end + end + end + +end \ No newline at end of file diff --git a/support_PerformHarFilt.m b/support_PerformHarFilt.m new file mode 100644 index 0000000..c21dae5 --- /dev/null +++ b/support_PerformHarFilt.m @@ -0,0 +1,47 @@ +function Pupdil = support_PerformHarFilt(Config, Samples) + + log_i(['Filtering for frequency bands']); + if size(Config.HarFilt.TargetFreqRanges, 1) == 1 + bandToFilter = [Config.HarFilt.TargetFreqRanges(1,1) Config.HarFilt.TargetFreqRanges(1,2)]; +% Samples.Pupdil = bandpass(Samples.Pupdil, bandToFilter, Samples.SRate); + Hd = f_chebyshev(bandToFilter(1), bandToFilter(2), Samples.SRate, 2000, 'bandpass'); + Pupdil = filtfilt(Hd.Numerator, 1, Samples.Pupdil); + + log_i(['Applied passband filter for base frequency only: ' num2str(bandToFilter(1)) '-' num2str(bandToFilter(2)) 'Hz']); + else + for fic = 1:(size(Config.HarFilt.TargetFreqRanges, 1)-1) % 1. koordináta = sor index + bandToFilter = [Config.HarFilt.TargetFreqRanges(fic,2) Config.HarFilt.TargetFreqRanges(fic+1,1)]; + Pupdil = bandstop(Samples.Pupdil, bandToFilter, Samples.SRate); +% % % Hd = f_chebyshev(bandToFilter(1), bandToFilter(2), Samples.SRate, 1000, 'stop'); +% method = 'stop'; +% order = 500; % Order +% flag = 'scale'; % Sampling Flag +% SidelobeAtten = 50; % Window Parameter +% win = chebwin(order+1, SidelobeAtten); +% b = fir1(order, bandToFilter/(Samples.SRate/2), method, win, flag); +% Hd = dfilt.dffir(b); +% Samples.Pupdil = filtfilt(Hd.Numerator, 1, Samples.Pupdil); + + log_i(['Applied stopband filter: ' num2str(fic) ' / ' num2str((size(Config.HarFilt.TargetFreqRanges, 1)-1)) ]); + end + bandToFilter = [Config.HarFilt.TargetFreqRanges(1,1) Config.HarFilt.TargetFreqRanges(size(Config.HarFilt.TargetFreqRanges, 1), 2)]; + Pupdil = bandpass(Samples.Pupdil, bandToFilter, Samples.SRate); + +% Samples.Pupdil = highpass(Samples.Pupdil, bandToFilter(1), Samples.SRate); +% Samples.Pupdil = lowpass(Samples.Pupdil, bandToFilter(2), Samples.SRate); + +% % % Hd = f_chebyshev(bandToFilter(1), bandToFilter(2), Samples.SRate, 1000, 'bandpass'); +% method = 'bandpass'; +% order = 500; % Order +% flag = 'scale'; % Sampling Flag +% SidelobeAtten = 50; % Window Parameter +% win = chebwin(order+1, SidelobeAtten); +% b = fir1(order, bandToFilter/(Samples.SRate/2), method, win, flag); +% Hd = dfilt.dffir(b); +% Samples.Pupdil = filtfilt(Hd.Numerator, 1, Samples.Pupdil); + + log_i(['Applied passband filter as finish']); + end + log_i(['Done :)']); + +end \ No newline at end of file diff --git a/support_PlotDynBLCorrMap.m b/support_PlotDynBLCorrMap.m new file mode 100644 index 0000000..3bac19d --- /dev/null +++ b/support_PlotDynBLCorrMap.m @@ -0,0 +1,181 @@ +function support_PlotDynBLCorrMap(TEPREveryParticipant, Config, Meta) + + % NOTE: Not intended to be used for p-hacking. This is for exporatory + % data-driven analyses, mainly to help develop better feature extraction + % methods for machine learning, for use in between-subjects regression + + dv_cols = [Config.DynBLCorrMap.DVFrom Config.DynBLCorrMap.DVTo]; + T = readtable(Config.DynBLCorrMap.BehavDF); + num_dv = (dv_cols(2)-dv_cols(1)+1); + + for dv = 1:num_dv % cycle through dependent variable + + ErrorsEnc = false; + + if Config.DynBLCorrMap.SmallOrLarge + hmat = Config.AnalyzeLenSample; + wmat = Config.AnalyzeLenSample; + blFrom = 1; + cTo = Config.AnalyzeLenSample; + else + hmat = ceil(1.5 *Config.AnalyzeLenSample); + wmat = ceil(1.5 *Config.AnalyzeLenSample); + blFrom = (1 -ceil(Config.AnalyzeLenSample/2)); + cTo = (Config.AnalyzeLenSample +ceil(Config.AnalyzeLenSample/2)); + end + + correl_map_rho = NaN(hmat, wmat); + correl_map_pval = NaN(hmat, wmat); + + for blAt = blFrom:Config.AnalyzeLenSample % loop to change baseline sample index +% for blAt = 1:(Config.AnalyzeLenSample +ceil(Config.AnalyzeLenSample/2)) % loop to change baseline sample index + + singleMapTic = tic; + + for cAt = 1:cTo + + % skip trivial (repeating) correlations of: + % lower left triangle + % upper left triangle + % upper right triangle + % lower right triangle + if (cAt < ceil(Config.AnalyzeLenSample/2) && blAt+cAt < 1 ) || ... + (blAt > cAt) || ... + (blAt > ceil(Config.AnalyzeLenSample/2) && blAt + cAt > 2*Config.AnalyzeLenSample) || ... + (cAt > blAt + Config.AnalyzeLenSample ) + + continue; + end + subtractFrom = cAt; + subtractVal = blAt; + + if subtractFrom < 1 + subtractFrom = subtractFrom + Config.AnalyzeLenSample; + end + if subtractVal < 1 + subtractVal = subtractVal + Config.AnalyzeLenSample; + end + + if subtractFrom > Config.AnalyzeLenSample + subtractFrom = subtractFrom - Config.AnalyzeLenSample; + end + if subtractVal > Config.AnalyzeLenSample + subtractVal = subtractVal - Config.AnalyzeLenSample; + end + + statAtDelta = ... + TEPREveryParticipant(:, subtractFrom ) - ... + TEPREveryParticipant(:, subtractVal ); + + try + [RHO,PVAL] = corr(T{:,(dv_cols(1)+dv-1)}, statAtDelta, 'Type', Config.DynBLCorrMap.CorrelMethod, 'rows','complete'); % omits NaN + catch ME + ErrorsEnc = true; + RHO = NaN; PVAL = NaN; + end + + correl_map_rho(blAt -blFrom+1, cAt) = RHO; + correl_map_pval(blAt -blFrom+1, cAt) = PVAL; + + end + clear statAtDelta; + + steptime=toc(singleMapTic); % step time + + if ( blAt == round(Config.AnalyzeLenSample/50) || mod(blAt, round(Config.AnalyzeLenSample/5) )==0 ) + log_i([ num2str(dv) '@ ' 'Processing at: ' num2str( round(blAt/Config.AnalyzeLenSample*100) ) '%, estimated time remaining: ' num2str( (Config.AnalyzeLenSample-blAt)*steptime ) ' seconds' ]); + end + + end + clear singleMapTic steptime; + + hck = pcolor(correl_map_rho); + % hck = pcolor(correl_map_pval); + + set(hck,'EdgeColor','none'); + set(gca, 'Layer', 'top'); + % ylim([-1 1]); + + xticks_desired = 0:0.2:( (Config.AnalyzeLenSample/Meta.NomSRate)-mod( (Config.AnalyzeLenSample/Meta.NomSRate), 0.2)); + xticks_sampleMapped = zeros(1, length(xticks_desired)); + for i=1:length(xticks_desired) + xticks_sampleMapped(i) = (xticks_desired(i)/(Config.AnalyzeLenSample/Meta.NomSRate) *Config.AnalyzeLenSample); + end + xticks_sampleMapped = xticks_sampleMapped + abs(xticks_sampleMapped(1)/2); + +% for i=1:length(xticks_sampleMapped) +% xp = [xticks_sampleMapped(i) xticks_sampleMapped(i)]; +% yp = [xticks_sampleMapped(i) Config.AnalyzeLenSample]; +% line(xp, yp, 'Color', [0.8 0.8 0.8], 'LineWidth', 1); +% % xticks_sampleMapped(i) = (xticks_desired(i)/(Config.AnalyzeLenSample/Meta.NomSRate) *Config.AnalyzeLenSample); +% end + + if Config.Plots.Markings.Enabled == true + grayB = 0.7; + for s = 0:(Config.AnalyzeLenSample-1) + if mod(s-Config.AnalyzeFromSample, Config.ISISample) == 0 + + if Config.AlignToStimOrResp == true % STIMULUS ALIGNED + xline(s, 'Color', [grayB grayB grayB]); + yline(s, 'Color', [grayB grayB grayB]); + text((s)-round(Config.AnalyzeLenSample/40),(s)+round(Config.AnalyzeLenSample/40),'S', 'Color', [grayB grayB grayB]) + + xline(s-Config.FixBeforeStimSample, 'Color', [grayB grayB grayB]); + yline(s-Config.FixBeforeStimSample, 'Color', [grayB grayB grayB]); + text((s-Config.FixBeforeStimSample)-round(Config.AnalyzeLenSample/40),(s-Config.FixBeforeStimSample)+round(Config.AnalyzeLenSample/40),'T', 'Color', [grayB grayB grayB]) + + else % RESPONSE ALIGNED + xline(s, 'Color', [grayB grayB grayB]); + yline(s, 'Color', [grayB grayB grayB]); + text((s)-round(Config.AnalyzeLenSample/40),(s)+round(Config.AnalyzeLenSample/40),'R', 'Color', [grayB grayB grayB]) + end + + + end + + end + end + + if ErrorsEnc + log_w(['Could not compute correlation in at least one point']); + end + + + xticks(xticks_sampleMapped); + xticklabels(xticks_desired); + xlabel(['Timepoint of value correlated [sec]']); + + yticks(xticks_sampleMapped); + yticklabels(xticks_desired); +% yline(stimPresentedAtSec *Meta.NomSRate); + ylabel(['Timepoint of baseline [sec]']); + + % if y axis is better on the right +% set(gca, 'YAxisLocation', 'right'); + + hcb = colorbar; + % if color bar on the left +% set(hcb, 'Location', 'westoutside'); + colormap(parula(256)); + caxis([-0.8 0.8]); + + hold on + visboundaries(correl_map_pval<=0.05, 'Color', [0.8500 0.3250 0.0980], 'EnhanceVisibility',false); + visboundaries(correl_map_pval<=0.01, 'Color', [0.4660 0.6740 0.1880], 'EnhanceVisibility',false); + visboundaries(correl_map_pval<=0.001, 'Color', [0.6350 0.0780 0.1840], 'EnhanceVisibility',false); + hold off + + title(strrep(['Baseline corrected TEPR value (' Config.Filter.Behav.FriendlyName ') x ' T.Properties.VariableNames{Config.DynBLCorrMap.DVFrom+dv-1}],'_','-')); + OutFilePath = ['~RESULTS/' Meta.RootDirTag '/' 'TEPR dyn baseline corr maps' ' Config.AlignToStimOrResp=' num2str(Config.AlignToStimOrResp) ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' '/' ]; + OutFileName = ['TEPR-corrmap' '-' Config.DynBLCorrMap.CorrelMethod '_' 'dep-var=' T.Properties.VariableNames{Config.DynBLCorrMap.DVFrom+dv-1} ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' '.png']; + + mkdir(OutFilePath); + +% set(gcf, 'Position', get(0, 'Screensize')); + set(gcf, 'Position', get(0, 'Screensize')*Config.Plots.ScaleFactor); + pbaspect([1 1 1]) % looks like a square, better readable + saveas(gcf,[OutFilePath OutFileName]); + + end + +end \ No newline at end of file diff --git a/support_PlotERA.m b/support_PlotERA.m new file mode 100644 index 0000000..4185e88 --- /dev/null +++ b/support_PlotERA.m @@ -0,0 +1,135 @@ +function support_PlotERA(EventDensity, ERAConfCurves, Config, Meta, Participant) + + if Config.Plot.ERA.VisualMethod == 0 % kernel density estimation + [ks_y, ks_x] = ksdensity(EventDensity, 'Bandwidth', Config.Plot.ERA.KDEBandwidth, 'Kernel', 'normal'); + + % normalize ks density output + ks_y = (ks_y - min(ks_y)) / ( max(ks_y) - min(ks_y) ); + + plot(ks_x, ks_y, 'LineWidth', 2) %graphLineStyle + else %if Config.Plot.ERA.VisualMethod == 1 + histogram(EventDensity, Config.Plot.ERA.HistBinWidth) + end + + xlim([Config.AnalyzeFromSec*1000, Config.AnalyzeToSec*1000]) + + pause(0.5) + + % ------------------------------------------------------- + % calculate confidence ERA + + close(gcf); + figure + hold on + + + ERAtoPlot = ERAConfCurves(:, Participant.Nr); + plot_time = 0:(length(ERAConfCurves(:, Participant.Nr))-1); + + % TODO: SHORTEN CODE! from now on, almost full of this section is + % same as TEPR code + + if ~exist('TEPR_lineStyle', 'var') %ide majd isfield kell (isfield(behav, 'stimType')) + TEPR_lineStyle = '-'; + TEPR_lineColor = 'g'; + TEPR_lineWidth = 2.0; + end + + currentPlot = plot(plot_time, ERAtoPlot, TEPR_lineStyle, 'Color', TEPR_lineColor, 'LineWidth', TEPR_lineWidth); + + % Transform to milliseconds + set(currentPlot, 'XData', (get(currentPlot, 'XData')-1) / Meta.NomSRate * 1000 + Config.AnalyzeFromSec*1000); + + if ~isnan(Config.Plot.TEPR.YLim) + ylim(Config.Plot.TEPR.YLim); + end + + if isnan(Config.Plot.TEPR.XLim) +% Config.Plot.TEPR.XLim = [round(Config.AnalyzeFromSec*1000) round(Config.AnalyzeToSec*1000)]; + Config.Plot.TEPR.XLim = [0 round(Config.AnalyzeToSec*1000)]; + end + xlim(Config.Plot.TEPR.XLim); + + % todo: analĂłgra át kell Ă­rni +% % % xline(round(stimPresentedAtSec*1000)); % stim prez + + if Config.Plots.Grid + grid on; + grid minor; + end + + if Config.Plots.Markings.Enabled == true + currylim = ylim; + colorB = [0.3 0.3 0.9]; + yDt = 5; + for t = (Config.AnalyzeFromSec*1000):(Config.AnalyzeToSec*1000) +% for t = Config.Plot.TEPR.XLim(1):Config.Plot.TEPR.XLim(2) + + if ~Config.Plots.Markings.OnEdges && (t==Config.Plot.TEPR.XLim(1) || t==Config.Plot.TEPR.XLim(2)) + continue; + end + + if Config.Plots.Markings.B && t~=0 && mod(t, Config.ISISec*1000) == Config.ISISec*1000 + (Config.BaselineFromSec+Config.BaselineToSec)/2*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) + end + if Config.Plots.Markings.S && mod(t, Config.ISISec*1000) == 0 + % todo: ONLY IF STIMULUS-ALIGNED + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'S', 'Color', colorB) + end + if Config.Plots.Markings.F && t~=0 && mod(t, Config.ISISec*1000) == Config.ISISec*1000 - Config.FixBeforeStimSec*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'F', 'Color', colorB) + end + +% if mod(t, Config.ISISec*1000) == 0 +% xline(t+ (Config.BaselineFromSec+Config.BaselineToSec)/2*1000, 'Color', colorB); +% text(t+ (Config.BaselineFromSec+Config.BaselineToSec)/2*1000, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) +% +% % todo: ONLY IF STIMULUS-ALIGNED +% xline(t, 'Color', colorB); +% text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'S', 'Color', colorB) +% +% xline(t -Config.FixBeforeStimSec*1000, 'Color', colorB); +% text(t -Config.FixBeforeStimSec*1000, currylim(2)-(currylim(2)-currylim(1))/yDt,'F', 'Color', colorB) +% +% end + end + end + + set(gcf, 'Position', get(0, 'Screensize')*Config.Plots.ScaleFactor); + % title(['TEPR curve averaged across all Participants']); + xlabel(['Time [ms]']); + ylabel(['Confidence']); + + + OutFilePath = char([ ... + '~RESULTS/' Meta.RootDirTag '/' ... + 'ERA each iteration' ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ... + ' (' Config.Filter.Behav.FriendlyName ')' ... + '/']); + + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = char([ ... + Participant.ID '_response_each-iter' ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ... + ' (' Config.Filter.Behav.FriendlyName ')' ... + '.png']); + + % set(gcf, 'Position', get(0, 'Screensize')); + set(gcf, 'Position', get(0, 'Screensize')*Config.Plots.ScaleFactor); + saveas(gcf,[OutFilePath OutFileName]); + + hold off; + pause(0.5) + +end \ No newline at end of file diff --git a/support_PlotGrandERAConf.m b/support_PlotGrandERAConf.m new file mode 100644 index 0000000..df4e144 --- /dev/null +++ b/support_PlotGrandERAConf.m @@ -0,0 +1,132 @@ +function FigHandle = support_PlotGrandERAConf(ERAConfCurves, Config, Meta) + + if Config.Plot.GrandERA.EveryParticipant + for p = 1:size(ERAConfCurves,2) + + yVals = ERAConfCurves(:, p); + +% plot_time = 1:length(yVals); + plot_time = 0:(length(yVals)-1); + + FigHandle = plot(plot_time, yVals, 'LineWidth', 2); + + % Transform to milliseconds + set(FigHandle, 'XData', (get(FigHandle, 'XData')-1) / Meta.NomSRate * 1000 + Config.AnalyzeFromSec*1000); + + end + + else + +% PUPSIZE_CURVE_GRAND = NaN(Config.AnalyzeLenSample, 1); +% PUPSIZE_CURVE_GRAND(1:Config.AnalyzeLenSample, 1) = mean(TEPRCurves, 2, 'omitnan'); + ERAConfCurves_GRAND = mean(ERAConfCurves, 2, 'omitnan'); + + yVals = ERAConfCurves_GRAND; +% plot_time = 1:length(yVals); + plot_time = 0:(length(yVals)-1); + + if ~isfield(Config.Plots, 'LayeredFigCounter') + Config.Plots.LayeredFigCounter = 1; + end + + FigHandle = plot(plot_time, ERAConfCurves_GRAND, 'LineWidth', 2) + + % Transform to milliseconds + set(FigHandle, 'XData', (get(FigHandle, 'XData')-1) / Meta.NomSRate * 1000 + Config.AnalyzeFromSec*1000); + + end + + if isnan(Config.Plot.GrandTEPR.XLim) + Config.Plot.GrandTEPR.XLim = [round(Config.AnalyzeFromSec*1000) round(Config.AnalyzeToSec*1000)]; +% Config.Plot.GrandTEPR.XLim = [0 round(Config.AnalyzeToSec*1000)]; + end + xlim(Config.Plot.GrandTEPR.XLim); + + if ~isnan(Config.Plot.GrandERA.YLim) + ylim([0.85,1]); + end + + if Config.Plots.Grid + grid on; + grid minor; + end + + + if Config.Plots.Markings.Enabled == true && Config.Plots.LayeredFigCounter < 2 + currylim = ylim; + colorB = [0.3 0.3 0.9]; + yDt = 5; + for t = (Config.AnalyzeFromSec*1000):(Config.AnalyzeToSec*1000) +% for t = Config.Plot.GrandTEPR.XLim(1):Config.Plot.GrandTEPR.XLim(2) + + if ~Config.Plots.Markings.OnEdges && (t==Config.Plot.GrandTEPR.XLim(1) || t==Config.Plot.GrandTEPR.XLim(2)) + continue; + end + + if Config.AlignToStimOrResp == true % STIMULUS-ALIGNED + if Config.Plots.Markings.B && t~=0 && mod(t, Config.ISISec*1000) == Config.ISISec*1000 + (Config.BaselineFromSec+Config.BaselineToSec)/2*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) + end + if Config.Plots.Markings.S && mod(t, Config.ISISec*1000) == 0 + % todo: ONLY IF STIMULUS-ALIGNED + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'S', 'Color', colorB) + end + if Config.Plots.Markings.F && t~=0 && mod(t, Config.ISISec*1000) == Config.ISISec*1000 - Config.FixBeforeStimSec*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'F', 'Color', colorB) + end + elseif Config.AlignToStimOrResp == false % RESPONSE-ALIGNED + if Config.Plots.Markings.B && t~=0 && t/(Config.ISISec*1000) == Config.ISISec*1000 + (Config.BaselineFromSec+Config.BaselineToSec)/2*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) + end + if Config.Plots.Markings.R && t/(Config.ISISec*1000) == 0 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'R', 'Color', colorB) + end + end + +% if mod(t, Config.ISISec*1000) == 0 +% xline(t+ (Config.BaselineFromSec+Config.BaselineToSec)/2*1000, 'Color', colorB); +% text(t+ (Config.BaselineFromSec+Config.BaselineToSec)/2*1000, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) +% +% xline(t, 'Color', colorB); +% text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'S', 'Color', colorB) +% +% xline(t -Config.FixBeforeStimSec*1000, 'Color', colorB); +% text(t -Config.FixBeforeStimSec*1000, currylim(2)-(currylim(2)-currylim(1))/yDt,'F', 'Color', colorB) +% +% end + end + end + + set(gcf, 'Position', get(0, 'Screensize')*Config.Plots.ScaleFactor); +% title(['ERA confidence averaged across all Participants']); + xlabel(['Time [ms]']); + ylabel(['Confidence']); + + OutFilePath = ['~RESULTS/' Meta.RootDirTag '/' ]; + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = ['ERA']; + OutFileName = [OutFileName ' alignSR=' num2str(Config.AlignToStimOrResp)]; + OutFileName = [OutFileName ' skipN=' num2str(Config.SkipFirstNtrials)]; + OutFileName = [OutFileName ' filt=' num2str(Config.Filter.Behav.Enabled)]; + OutFileName = [OutFileName ' (' Config.Filter.Behav.FriendlyName ')']; + if Config.Plot.GrandTEPR.EveryParticipant + OutFileName = [OutFileName '_EP']; + end + OutFileName = [OutFileName '.png']; + OutFileName = char(OutFileName); + + % set(gcf, 'Position', get(0, 'Screensize')); + set(gcf, 'Position', get(0, 'Screensize')*Config.Plots.ScaleFactor); + saveas(gcf,[OutFilePath OutFileName]); + hold off; + pause(0.5) + +end \ No newline at end of file diff --git a/support_PlotGrandERADensity.m b/support_PlotGrandERADensity.m new file mode 100644 index 0000000..f908591 --- /dev/null +++ b/support_PlotGrandERADensity.m @@ -0,0 +1,17 @@ +function support_PlotGrandERADensity(ERAEventDensity_grand, Config, Meta) + + if Config.Plot.ERA.VisualMethod == 0 % kernel density estimation + [ks_y, ks_x] = ksdensity(ERAEventDensity_grand, 'Bandwidth', Config.Plot.ERA.KDEBandwidth, 'Kernel', 'normal'); + + % normalize ks density output + ks_y = (ks_y - min(ks_y)) / ( max(ks_y) - min(ks_y) ); + + plot(ks_x, ks_y, 'LineWidth', 2) %graphLineStyle + else %if Config.Plot.ERA.VisualMethod == 1 + histogram(ERAEventDensity_grand, Config.Plot.ERA.HistBinWidth) + end + + xlim([Config.AnalyzeFromSec*1000, Config.AnalyzeToSec*1000]) + pause(0.5) + +end \ No newline at end of file diff --git a/support_PlotGrandTEPR.m b/support_PlotGrandTEPR.m new file mode 100644 index 0000000..d003009 --- /dev/null +++ b/support_PlotGrandTEPR.m @@ -0,0 +1,174 @@ +function FigHandle = support_PlotGrandTEPR(TEPRCurves, Config, Meta) + + if ~isfield(Config.Plots, 'Layered') || ~Config.Plots.Layered + close(gcf); + figure + hold on + end + + if ~isfield(Config.Plot.GrandTEPR, 'LineStyle') % ide majd ilyen kell isfield(behav, 'stimType') + Config.Plot.GrandTEPR.LineStyle = '-'; + end + if ~isfield(Config.Plot.GrandTEPR, 'LineColor') % ide majd ilyen kell isfield(behav, 'stimType') + Config.Plot.GrandTEPR.LineColor = 'g'; + end + Config.Plot.GrandTEPR.LineWidth = 2.0; + + + if Config.Plot.GrandTEPR.EveryParticipant + for p = 1:size(TEPRCurves,2) + + yVals = TEPRCurves(:, p); + + % kell ez ? + % if the curve should be baseline corrected + yVals = ... + yVals - ... + mean(yVals(Config.BaselineFromSampleMapped:Config.BaselineToSampleMapped, 1), 'omitnan'); + +% plot_time = 1:length(yVals); + plot_time = 0:(length(yVals)-1); + + FigHandle = plot(plot_time, yVals, Config.Plot.GrandTEPR.LineStyle, 'Color', Config.Plot.GrandTEPR.LineColor, 'LineWidth', Config.Plot.GrandTEPR.LineWidth); + + % Transform to milliseconds + set(FigHandle, 'XData', (get(FigHandle, 'XData')-1) / Meta.NomSRate * 1000 + Config.AnalyzeFromSec*1000); + + end + + else + + %---------------------------------------------- + % TODO: áttehetĹ‘ sima kĂłdba, ne itt a plottolĂłban legyen + +% TEPRCurves_GRAND = NaN(trial_length, 1); +% TEPRCurves_GRAND(1:trial_length, 1) = mean(TEPRCurves, 2, 'omitnan'); + TEPRCurves_GRAND = mean(TEPRCurves, 2, 'omitnan'); + + % if the curve should be baseline corrected + TEPRCurves_GRAND = ... + TEPRCurves_GRAND - ... + mean(TEPRCurves_GRAND(Config.BaselineFromSampleMapped:Config.BaselineToSampleMapped, 1), 'omitnan'); + %---------------------------------------------- + + yVals = TEPRCurves_GRAND; +% plot_time = 1:length(yVals); + plot_time = 0:(length(yVals)-1); + + % plot(plot_time, TEPRCurves_GRAND, grandTEPR_lineStyle) + FigHandle = plot(plot_time, TEPRCurves_GRAND, Config.Plot.GrandTEPR.LineStyle, 'Color', Config.Plot.GrandTEPR.LineColor, 'LineWidth', Config.Plot.GrandTEPR.LineWidth); +% grandTEPR(layeredFigCounter) = FigHandle; + + % Transform to milliseconds + set(FigHandle, 'XData', (get(FigHandle, 'XData')-1) / Meta.NomSRate * 1000 + Config.AnalyzeFromSec*1000); + + end + + if isnan(Config.Plot.GrandTEPR.XLim) + Config.Plot.GrandTEPR.XLim = [round(Config.AnalyzeFromSec*1000) round(Config.AnalyzeToSec*1000)]; +% Config.Plot.GrandTEPR.XLim = [0 round(TimeDefs.AnalyzeToSec*1000)]; + end + xlim(Config.Plot.GrandTEPR.XLim); + + + % TODO: analógra átirni +% % xline(round(stimPresentedAtSec*1000)); % stim prez + + + if ~isnan(Config.Plot.GrandTEPR.YLim) + ylim(Config.Plot.GrandTEPR.YLim); + end + + if Config.Plots.Grid + grid on; + grid minor; + end + + + if Config.Plots.Markings.Enabled == true && Config.Plots.LayeredFigCounter < 2 + currylim = ylim; + colorB = [0.3 0.3 0.9]; + yDt = 5; + for t = (Config.AnalyzeFromSec*1000):(Config.AnalyzeToSec*1000) +% for t = grandTEPR_xlim(1):grandTEPR_xlim(2) + + if ~Config.Plots.Markings.OnEdges && (t==Config.Plot.GrandTEPR.XLim(1) || t==Config.Plot.GrandTEPR.XLim(2)) + continue; + end + + if Config.AlignToStimOrResp == true % STIMULUS-ALIGNED + if Config.Plots.Markings.B && t~=0 && mod(t, Config.ISISec*1000) == Config.ISISec*1000 + (Config.BaselineFromSec+Config.BaselineToSec)/2*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) + end + if Config.Plots.Markings.S && mod(t, Config.ISISec*1000) == 0 + % todo: ONLY IF STIMULUS-ALIGNED + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'S', 'Color', colorB) + end + if Config.Plots.Markings.F && t~=0 && mod(t, Config.ISISec*1000) == Config.ISISec*1000 - fixBeforeStimSec*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'F', 'Color', colorB) + end + elseif Config.AlignToStimOrResp == false % RESPONSE-ALIGNED + if Config.Plots.Markings.B && t~=0 && t/(Config.ISISec*1000) == Config.ISISec*1000 + (Config.BaselineFromSec+Config.BaselineToSec)/2*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) + end + if Config.Plots.Markings.R && t/(Config.ISISec*1000) == 0 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'R', 'Color', colorB) + end + end + +% if mod(t, TimeDefs.ISISec*1000) == 0 +% xline(t+ (TimeDefs.BaselineFromSec+TimeDefs.BaselineToSec)/2*1000, 'Color', colorB); +% text(t+ (TimeDefs.BaselineFromSec+TimeDefs.BaselineToSec)/2*1000, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) +% +% xline(t, 'Color', colorB); +% text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'S', 'Color', colorB) +% +% xline(t -Config.FixBeforeStimSec*1000, 'Color', colorB); +% text(t -Config.FixBeforeStimSec*1000, currylim(2)-(currylim(2)-currylim(1))/yDt,'F', 'Color', colorB) +% +% end + end + end + + set(gcf, 'Position', get(0, 'Screensize')*Config.Plots.ScaleFactor); +% title(['TEPR curve averaged across all Participants']); + xlabel(['Time [ms]']); + if Config.Z_norm_method == 0 + if Meta.flag_PXorMM == 1 + ylabel(['Pupil size [px]']); + else + ylabel(['Pupil size [mm]']); + end + else + ylabel(['Pupil size [a.u.]']); + end + + OutFilePath = ['~RESULTS/' Meta.RootDirTag '/' ]; + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = ['TEPR']; + OutFileName = [OutFileName ' alignSR=' num2str(Config.AlignToStimOrResp)]; + OutFileName = [OutFileName ' skipN=' num2str(Config.SkipFirstNtrials)]; + OutFileName = [OutFileName ' filt=' num2str(Config.Filter.Behav.Enabled)]; + OutFileName = [OutFileName ' (' Config.Filter.Behav.FriendlyName ')']; + if Config.Plot.GrandTEPR.EveryParticipant + OutFileName = [OutFileName '_EP']; + end + OutFileName = [OutFileName '.png']; + OutFileName = char(OutFileName); + + % set(gcf, 'Position', get(0, 'Screensize')); + set(gcf, 'Position', get(0, 'Screensize')*Config.Plots.ScaleFactor); + saveas(gcf,[OutFilePath OutFileName]); + + hold off; + pause(0.5) + +end \ No newline at end of file diff --git a/support_PlotPupilData.m b/support_PlotPupilData.m new file mode 100644 index 0000000..87c112a --- /dev/null +++ b/support_PlotPupilData.m @@ -0,0 +1,39 @@ +function support_PlotPupilData(PlotMode, Samples) + + if PlotMode == 1 + fftIn = Samples.Pupdil-mean(Samples.Pupdil); + + spect_full = abs(fft(fftIn)); + spect_full = spect_full/(length(fftIn)/2); + spect_full = spect_full(1:floor(length(spect_full)/2)); + + % freq_points = (1:(length(Samples.Pupdil)/2))*(Samples.SRate/length(Samples.Pupdil)); % HIBĂ?T OKOZ, ELCSĂšSZĂ?ST + freq_points = (0:(length(fftIn)/2)-1)*(Samples.SRate/length(fftIn)); + + plot(freq_points , spect_full) + + freq_lim = [0 1]; + xlim(freq_lim) + xticks(linspace(freq_lim(1), freq_lim(2), 11)); + + disp(freq_points(spect_full==max(spect_full))) + + xlabel('Frequency [Hz]') + ylabel('Amplitude [px]') + + set(gcf, 'Position', get(0, 'Screensize') * 0.6); + hold on + + elseif PlotMode == 2 + xvec = Samples.Ts-Samples.Ts(1); + yvec = Samples.Pupdil-mean(Samples.Pupdil); + plot(xvec, yvec) + + xlabel('Time [-]') + ylabel('Pupil Size [px]') + + set(gcf, 'Position', get(0, 'Screensize') * 0.6); + hold on + end + +end \ No newline at end of file diff --git a/support_PlotTEPR.m b/support_PlotTEPR.m new file mode 100644 index 0000000..c2cfaa2 --- /dev/null +++ b/support_PlotTEPR.m @@ -0,0 +1,148 @@ +function support_PlotTEPR(TrialsArray, TEPRCurves, Config, Meta, Participant) + + close(gcf); + figure + hold on + + if Config.Plot.TEPR.EveryTrial + for p = 1:size(TrialsArray,2) + + plotVals = TrialsArray(:,p); +% plot_time = 1:length(plotVals); + plot_time = 0:(length(plotVals)-1); + + if excludedTrials(p) + graphLineStyle = '-b'; + else + graphLineStyle = '-g'; + end + +% if Filter.Phase.ExcludedMask(labelAt, p) || Filter.SD.ExcludedMask(1, p) +% graphLineStyle = '-b'; +% else +% graphLineStyle = '-g'; +% end + + currentPlot = plot(plot_time , plotVals, graphLineStyle); + + % Transform to milliseconds + set(currentPlot, 'XData', (get(currentPlot, 'XData')-1) / Meta.NomSRate * 1000 + Config.AnalyzeFromSec*1000); + + % DEV + ylim([-6 6]); + end + else + + TEPRtoPlot = TEPRCurves(:, Participant.Nr) - mean(TEPRCurves(Config.BaselineFromSampleMapped:Config.BaselineToSampleMapped, Participant.Nr), 'omitnan'); + plot_time = 0:(length(TEPRCurves(:, Participant.Nr))-1); + + if ~exist('TEPR_lineStyle', 'var') %ide majd isfield kell (isfield(behav, 'stimType')) + TEPR_lineStyle = '-'; + TEPR_lineColor = 'g'; + TEPR_lineWidth = 2.0; + end + + currentPlot = plot(plot_time, TEPRtoPlot, TEPR_lineStyle, 'Color', TEPR_lineColor, 'LineWidth', TEPR_lineWidth); + + % Transform to milliseconds + set(currentPlot, 'XData', (get(currentPlot, 'XData')-1) / Meta.NomSRate * 1000 + Config.AnalyzeFromSec*1000); + + end + + if exist('Config.Plot.TEPR.YLim', 'var') && ~isnan(Config.Plot.TEPR.YLim) + ylim(Config.Plot.TEPR.YLim); + end + + if isnan(Config.Plot.TEPR.XLim) +% Config.Plot.TEPR.XLim = [round(Config.AnalyzeFromSec*1000) round(Config.AnalyzeToSec*1000)]; + Config.Plot.TEPR.XLim = [0 round(Config.AnalyzeToSec*1000)]; + end + xlim(Config.Plot.TEPR.XLim); + + if Config.Plots.Grid + grid on; + grid minor; + end + + if Config.Plots.Markings.Enabled == true + currylim = ylim; + colorB = [0.3 0.3 0.9]; + yDt = 5; + for t = (Config.AnalyzeFromSec*1000):(Config.AnalyzeToSec*1000) +% for t = Config.Plot.TEPR.XLim(1):Config.Plot.TEPR.XLim(2) + + if ~Config.Plots.Markings.OnEdges && (t==Config.Plot.TEPR.XLim(1) || t==Config.Plot.TEPR.XLim(2)) + continue; + end + + if Config.Plots.Markings.B && t~=0 && mod(t, Config.ISISec*1000) == Config.ISISec*1000 + (Config.BaselineFromSec+Config.BaselineToSec)/2*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) + end + if Config.Plots.Markings.S && mod(t, Config.ISISec*1000) == 0 + % todo: ONLY IF STIMULUS-ALIGNED + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'S', 'Color', colorB) + end + if Config.Plots.Markings.F && t~=0 && mod(t, Config.ISISec*1000) == Config.ISISec*1000 - Config.FixBeforeStimSec*1000 + xline(t, 'Color', colorB); + text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'F', 'Color', colorB) + end + +% if mod(t, Config.ISISec*1000) == 0 +% xline(t+ (Config.BaselineFromSec+Config.BaselineToSec)/2*1000, 'Color', colorB); +% text(t+ (Config.BaselineFromSec+Config.BaselineToSec)/2*1000, currylim(2)-(currylim(2)-currylim(1))/yDt,'B', 'Color', colorB) +% +% % todo: ONLY IF STIMULUS-ALIGNED +% xline(t, 'Color', colorB); +% text(t, currylim(2)-(currylim(2)-currylim(1))/yDt,'S', 'Color', colorB) +% +% xline(t -Config.FixBeforeStimSec*1000, 'Color', colorB); +% text(t -Config.FixBeforeStimSec*1000, currylim(2)-(currylim(2)-currylim(1))/yDt,'F', 'Color', colorB) +% +% end + end + end + + set(gcf, 'Position', get(0, 'Screensize')*Config.Plots.ScaleFactor); +% title(['TEPR curve averaged across all Participants']); + xlabel(['Time [ms]']); + if Config.Z_norm_method == 0 + if Meta.flag_PXorMM == 1 + ylabel(['Pupil size [px]']); + else + ylabel(['Pupil size [mm]']); + end + else + ylabel(['Pupil size [a.u.]']); + end + + OutFilePath = char([ ... + '~RESULTS/' Meta.RootDirTag '/' ... + 'TEPR each iteration' ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ... + ' (' Config.Filter.Behav.FriendlyName ')' ... + '/']); + + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = char([ ... + Participant.ID '_response_each-iter' ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ... + ' (' Config.Filter.Behav.FriendlyName ')' ... + '.png']); + + % set(gcf, 'Position', get(0, 'Screensize')); + set(gcf, 'Position', get(0, 'Screensize')*Config.Plots.ScaleFactor); + saveas(gcf,[OutFilePath OutFileName]); + + hold off; + pause(0.5) + +end \ No newline at end of file diff --git a/support_SaveBaselineValues.m b/support_SaveBaselineValues.m new file mode 100644 index 0000000..5df1e37 --- /dev/null +++ b/support_SaveBaselineValues.m @@ -0,0 +1,35 @@ +function support_SaveBaselineValues(BaselineValues, Config, Meta, Participants) + + col_Participants = transpose([{ [Meta.CfPrefix '_' 'Participant'] } Participants]); + cols_sub_vals = cell(length(Participants)+1, 1); + cols_sub_vals(1, 1) = { [Meta.CfPrefix '_' 'PD baseline']}; + cols_sub_vals(2:length(Participants)+1, 1) = num2cell(BaselineValues); + outputMatrix = [col_Participants cols_sub_vals]; + + OutFilePath = char([ ... + '~RESULTS/' Meta.RootDirTag '/' ... + 'TEPR csv' ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ... + ' (' Config.Filter.Behav.FriendlyName ')'... + '/']); + + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = char([ ... + 'PD_Baseline-values' ... + ' BLfromSec=' num2str(Config.BaselineFromSec) ... + '; BLtoSec=' num2str(Config.BaselineToSec) ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' ... + ]); + +% OutFileName = [ Meta.CfPrefix '_' 'PD_Baseline-values' ' BLfromSec=' num2str(Config.BaselineFromSec) '; BLtoSec=' num2str(Config.BaselineToSec) ' Config.AlignToStimOrResp=' num2str(Config.AlignToStimOrResp) ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' ]; + + writecell(outputMatrix,[OutFilePath OutFileName '.csv'],'Delimiter',';'); % writematrix nem jĂł cell mátrixra + +end \ No newline at end of file diff --git a/support_SaveEveryTrial.m b/support_SaveEveryTrial.m new file mode 100644 index 0000000..2e893a5 --- /dev/null +++ b/support_SaveEveryTrial.m @@ -0,0 +1,38 @@ +function support_SaveEveryTrial(TrialsArray, Meta, Config, Participant) + + % TODO: remove this? + for ytr = 1:size(TrialsArray, 2) + TrialsArray(:, ytr) = TrialsArray(:, ytr) - mean(TrialsArray(Config.BaselineFromSampleMapped:Config.BaselineToSampleMapped, ytr), 'omitnan'); + end + + timepoints = (0:size(TrialsArray, 1)-1)/Meta.NomSRate *1000; + trnum = num2cell(1:size(TrialsArray, 2)); + + col_headerHoriz = [{ [''] } trnum]; + + % Sweeps (Trials) are columns + % Timepoints are rows (in ms) + + cols_sub_vals = cell(Config.AnalyzeLenSample, size(TrialsArray, 2)+1); + cols_sub_vals(1:Config.AnalyzeLenSample, 1) = num2cell(timepoints); + cols_sub_vals(1:Config.AnalyzeLenSample, 2:size(TrialsArray, 2)+1) = num2cell(TrialsArray); + outputMatrix = [col_headerHoriz; cols_sub_vals]; + + OutFilePath = char([ ... + '~RESULTS/' Meta.RootDirTag '/' ... + 'ERPD everyTrial csv' '/']); + + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = char([ ... + Participant.ID ' fromSmp=' num2str(1) ... + '; toSmp=' num2str(Config.AnalyzeLenSample) ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' ... + ]); + writecell(outputMatrix,[OutFilePath OutFileName '.csv'],'Delimiter',';'); + +end \ No newline at end of file diff --git a/support_SavePeakValues.m b/support_SavePeakValues.m new file mode 100644 index 0000000..1e9ce48 --- /dev/null +++ b/support_SavePeakValues.m @@ -0,0 +1,40 @@ +function support_SavePeakValues(PeakValues, Config, Meta, Participants) + + col_Participants = transpose([{ [Meta.CfPrefix '_' 'Participant'] } Participants]); + cols_sub_vals = cell(length(Participants)+1, 1); + cols_sub_vals(1, 1) = { [Meta.CfPrefix '_' 'Filtered-PD']}; + cols_sub_vals(2:length(Participants)+1, 1) = num2cell(PeakValues); + outputMatrix = [col_Participants cols_sub_vals]; + + OutFilePath = char([ ... + '~RESULTS/' Meta.RootDirTag '/' ... + 'TEPR csv' ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ... + ' (' Config.Filter.Behav.FriendlyName ')'... + '/']); + + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = char([ ... + 'Peaks' ... + ' PfromSec=' num2str(Config.PeakFromSec) ... + '; PtoSec=' num2str(Config.PeakToSec) ... + ' BLfromSec=' num2str(Config.BaselineFromSec) ... + '; BLtoSec=' num2str(Config.BaselineToSec) ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' ... + ]); + + % OutFilePath = ['~RESULTS/' Meta.RootDirTag '/' 'TEPR csv' ' Config.AlignToStimOrResp=' num2str(Config.AlignToStimOrResp) ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' '/' ]; + % % OutFilePath = ['~RESULTS/' Meta.RootDirTag '/' 'TEPR csv' '/' ]; + % mkdir(OutFilePath); + % OutFileName = [ 'peaks' ' PfromSec=' num2str(Config.PeakFromSec) '; PtoSec=' num2str(Config.PeakToSec) ' BLfromSec=' num2str(Config.BaselineFromSec) '; BLtoSec=' num2str(Config.BaselineToSec) ' alignSR=' num2str(Config.AlignToStimOrResp) ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' ]; + + writecell(outputMatrix,[OutFilePath OutFileName '.csv'],'Delimiter',';'); % writematrix nem jĂł cell mátrixra + +end \ No newline at end of file diff --git a/support_SaveTEPREveryParticipant.m b/support_SaveTEPREveryParticipant.m new file mode 100644 index 0000000..1bb5ef0 --- /dev/null +++ b/support_SaveTEPREveryParticipant.m @@ -0,0 +1,43 @@ +function support_SaveTEPREveryParticipant(TEPREveryParticipant, Config, Meta, Participants) + + col_Participants = transpose([{ [Meta.CfPrefix '_' 'Participant'] } Participants]); + + TEPREveryParticipantCSVheader = cell( 1, Config.AnalyzeLenSample ); + for bfc = 1:Config.AnalyzeLenSample + TEPREveryParticipantCSVthisCol = [ Meta.CfPrefix '_' 'Sample' ' ' num2str(bfc) ]; + TEPREveryParticipantCSVheader{1, bfc} = TEPREveryParticipantCSVthisCol; + end + + cols_sub_vals = cell(length(Participants)+1, Config.AnalyzeLenSample); + cols_sub_vals(1, 1:Config.AnalyzeLenSample) = TEPREveryParticipantCSVheader; + cols_sub_vals(2:length(Participants)+1, 1:Config.AnalyzeLenSample) = num2cell(TEPREveryParticipant); + outputMatrix = [col_Participants cols_sub_vals]; + % % OutFilePath = ['~RESULTS/' Meta.RootDirTag '/' 'TEPR csv' '/' ]; + % % mkdir(OutFilePath); + % OutFileName = [ 'TEPREveryParticipant' ' fromSmp=' num2str(1) '; toSmp=' num2str(Config.AnalyzeLenSample) ' alignSR=' num2str(Config.AlignToStimOrResp) ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' ]; + + OutFilePath = char([ ... + '~RESULTS/' Meta.RootDirTag '/' ... + 'TEPR csv' ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ... + ' (' Config.Filter.Behav.FriendlyName ')'... + '/']); + + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = char([ ... + 'TEPREveryParticipant' ... + 'fromSmp=' num2str(1) ... + '; toSmp=' num2str(Config.AnalyzeLenSample) ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' ... + ]); + + writecell(outputMatrix,[OutFilePath OutFileName '.csv'],'Delimiter',';'); + +end \ No newline at end of file diff --git a/support_SaveTrialExclusionSummary.m b/support_SaveTrialExclusionSummary.m new file mode 100644 index 0000000..51d7af7 --- /dev/null +++ b/support_SaveTrialExclusionSummary.m @@ -0,0 +1,48 @@ +function support_SaveTrialExclusionSummary(TRIAL_EXCLUSIONS, Config, Meta, Participants) + + col_Participants = transpose([{ [Meta.CfPrefix '_' 'Participant'] } Participants]); + cols_sub_vals = cell(length(Participants)+1, 1); + cols_sub_vals(1, 1:9) = { ... + ['Excl_OnInterpol'] ... + ['Excl_OnBaselineBlink'] ... + ['Excl_OnBaselineSaccade'] ... + ['Excl_OnSOIBlink'] ... + ['Excl_OnSOISaccade'] ... + ['Excl_OnSD'] ... + ['Excl_OnBehav'] ... + ['Rejected'] ... + ['Passed'] ... + }; + cols_sub_vals(2:length(Participants)+1, 1:9) = num2cell(TRIAL_EXCLUSIONS); + outputMatrix = [col_Participants cols_sub_vals]; + + if Config.CC.Enabled + + % TODO: better + outputMatrix = [outputMatrix ['Cond1_Passed'; num2cell(sum(~Config.CC.ExcludedMasks(1, :)))] ]; + outputMatrix = [outputMatrix ['Cond2_Passed'; num2cell(sum(~Config.CC.ExcludedMasks(2, :)))] ]; + + end + + OutFilePath = char([ ... + '~RESULTS/' Meta.RootDirTag '/' ... + ]); + + if ~exist(OutFilePath, 'dir') + mkdir(OutFilePath); + end + + OutFileName = char([ ... + 'Trial_excl' ... + ' PfromSec=' num2str(Config.PeakFromSec) ... + '; PtoSec=' num2str(Config.PeakToSec) ... + ' BLfromSec=' num2str(Config.BaselineFromSec) ... + '; BLtoSec=' num2str(Config.BaselineToSec) ... + ' alignSR=' num2str(Config.AlignToStimOrResp) ... + ' skipN=' num2str(Config.SkipFirstNtrials) ... + ' filt=' num2str(Config.Filter.Behav.Enabled) ' (' Config.Filter.Behav.FriendlyName ')' ... + ]); + + writecell(outputMatrix,[OutFilePath OutFileName '.csv'],'Delimiter',';'); + +end \ No newline at end of file diff --git a/support_SplitIntoSweeps.m b/support_SplitIntoSweeps.m new file mode 100644 index 0000000..224d514 --- /dev/null +++ b/support_SplitIntoSweeps.m @@ -0,0 +1,67 @@ +function [TrialsArray, ConfsArray] = support_SplitIntoSweeps(Samples, SearchBaseMask, TrigsForAlignment, TimeDefs, PerformTJC) + + % NOTE: first index is trial length in samples, second is number of trials + TrialsArray = NaN(TimeDefs.AnalyzeLenSample, length(TrigsForAlignment)); + ConfsArray = NaN(TimeDefs.AnalyzeLenSample, length(TrigsForAlignment)); + + for i = 1:length(TrigsForAlignment) + if ~SearchBaseMask(i) + continue + end + + BeginAtSample = find(Samples.Ts >= TrigsForAlignment(i), 1, 'first') + TimeDefs.AnalyzeFromSample; + + if ~PerformTJC + + TrialsArray(:,i) = Samples.Pupdil( BeginAtSample:(BeginAtSample+TimeDefs.AnalyzeLenSample-1) ); + + else + + % will work only if the eye data sampling rate is at least 10hz + PastConsideredSec = 0.1; + PastConsideredSample = ceil(PastConsideredSec * Samples.Srate); + + Mask = (BeginAtSample-PastConsideredSample):(BeginAtSample+analyzeLenSample-1); + + SweepSamples = Samples.Pupdil(Mask); + SweepTimestampsAbs = Samples.Ts(Mask); + SweepTimestampsRel = SweepTimestampsAbs - TrigsForAlignment(i) - TimeDefs.AnalyzeFromSample/Samples.Srate*1000*1000; % + pastConsideredSec*1000*1000; + JitterToRemove = SweepTimestampsRel(1) + PastConsideredSample/Samples.Srate*1000*1000; + + % this is needed because interp1 can only query points of a positive range when the interpolant was + % made on a positive ranging vector (idk why, but it appears to be like this) + ShiftInput = SweepTimestampsRel(1); + SweepTimestampsRel = SweepTimestampsRel - ShiftInput; + + NewX = linspace(0, TimeDefs.AnalyzeLenSample/Samples.Srate*1000*1000, TimeDefs.AnalyzeLenSample); % microsec + NewX = NewX - ShiftInput; + NewY = interp1(SweepTimestampsRel, SweepSamples, NewX, 'pchip'); + +% % NOTE: this is just left here if you want to inspect how it works +% plot(SweepTimestampsRel, SweepSamples, 'b-'); +% hold on +% xline(0 - ShiftInput + JitterToRemove, 'b-') +% plot(NewX, NewY, 'g-') +% xline(0 - ShiftInput, 'g-') +% hold off + + TrialsArray(:,i) = NewY; + end + + % ------------------------------------- + % also calculate confidence metric + % + % TODO: NOT ONLY AVERAGE BUT VAR/SD TOO !! ("which is the most + % variable part of the event related curve, regarding its pupil + % confidence") + % + % TODO: make lowFPS trigger jitter corrected version + + if ~isnan(Samples.QualityValues.Conf) + ConfsArray(:,i) = Samples.QualityValues.Conf( BeginAtSample:(BeginAtSample+TimeDefs.AnalyzeLenSample-1) ); + else + ConfsArray(:,i) = NaN; + end + end + +end \ No newline at end of file diff --git a/support_ZNormSweeps.m b/support_ZNormSweeps.m new file mode 100644 index 0000000..6b6daaf --- /dev/null +++ b/support_ZNormSweeps.m @@ -0,0 +1,37 @@ +function NewTrialsArray = support_ZNormSweeps(Samples, TrialsArray, SearchBaseMask, Z_norm_method) + + NewTrialsArray = NaN(size(TrialsArray)); + + if Z_norm_method == 1 + log_i('Using Z-normalization referenced to whole recording'); + elseif Z_norm_method == 2 + log_i('Using Z-normalization referenced to all each trial on its own'); + elseif Z_norm_method == 3 + log_i('Using Z-normalization referenced to all existing trials'); + elseif Z_norm_method == 4 + log_i('Using Z-normalization referenced to all non-rejected trials'); + else + log_e('Invalid Z-normalization method defined'); + end + + for i = 1:size(TrialsArray,2) + if ~SearchBaseMask(i) + continue + end + + if Z_norm_method == 1 + z_norm_reference = std(Samples.Pupdil, 'omitnan'); + elseif Z_norm_method == 2 + z_norm_reference = std(TrialsArray(:,i), 'omitnan'); + elseif Z_norm_method == 3 + z_norm_reference = std(reshape(TrialsArray(:,1:numTrials),1,[]), 'omitnan'); % convert matrix to row vector + elseif Z_norm_method == 4 + z_norm_reference = std(reshape(TrialsArray(~RejectedTrials,i),1,[]), 'omitnan'); % convert matrix to row vector + %else + % log_e('Invalid Z-normalization method defined'); + end + + NewTrialsArray(:,i) = (TrialsArray(:,i) - mean(z_norm_reference, 'omitnan'))./z_norm_reference; + end + +end \ No newline at end of file diff --git a/support_calcInterpolRatios.m b/support_calcInterpolRatios.m new file mode 100644 index 0000000..80f78d1 --- /dev/null +++ b/support_calcInterpolRatios.m @@ -0,0 +1,58 @@ +function interpolRatios = support_calcInterpolRatios(ts, origSamplesTs, triggersForAlignment, srate, origSrate) + + % TODO: calculate with better precision, also considering original srate? + + interpolRatios = nan(length(triggersForAlignment),1); + + for v = 1:length(triggersForAlignment) + + % E.g. when we are making a response-aligned analysis, and the subject has no response in a trial (or in the next trial !) + if isnan(triggersForAlignment(v)) || ... + (v < length(triggersForAlignment) && isnan(triggersForAlignment(v+1))) + interpolRatios(v) = NaN; + continue; + end + + + actualFromSample = find(ts >= triggersForAlignment(v), 1, 'first'); + if length(actualFromSample) ~= 1 + actualFromSample = NaN + end + + if v < length(triggersForAlignment) + actualToSample = find(ts <= triggersForAlignment(v+1), 1, 'last'); + + % invalid if the whole duration of this trial is before the + % beginning of the recording + if actualToSample == 1 + actualTosample = NaN; + end + else + actualToSample = length(ts); + end + +% % % actualFromSample +% % % actualToSample +% % % ~isnan(actualFromSample) +% % % ~isnan(actualToSample) + + if ~isnan(actualFromSample) && ~isnan(actualToSample) + actualLen = actualToSample - actualFromSample + 1; + + % IMPORTANT: we cannot beleive that the original timestamps and + % samples vectors really contained all the needed samples + + if v < length(triggersForAlignment) + theorLen = ceil( (triggersForAlignment(v+1) - triggersForAlignment(v) +1 ) / 1000 /1000 * srate ); + else + theorLen = ceil( (origSamplesTs(end) - triggersForAlignment(v) +1 ) / 1000 /1000 * srate ); + end + + interpolRatios(v) = (1-(actualLen / theorLen)) *100; + + end + end + +% check1 = length(ts) == find(ts >= triggersForAlignment(end), 1, 'last'); + +end \ No newline at end of file diff --git a/support_createAlignedTriggerVecStim.m b/support_createAlignedTriggerVecStim.m new file mode 100644 index 0000000..afe77dc --- /dev/null +++ b/support_createAlignedTriggerVecStim.m @@ -0,0 +1,46 @@ +function [trials, stimTs] = support_createAlignedTriggerVecStim(stim_trial, stim_timestamp, filterTrials, everyWhich) + +% numTriggers = ceil((filterTrials(2) - filterTrials(1)) /everyWhich + 1); + numTriggers = ceil((filterTrials(2) - filterTrials(1) + 1) /everyWhich); + trials = nan(numTriggers,1); + stimTs = nan(numTriggers,1); + + c = 1; + for i = 1:length(stim_timestamp) + +% % failsafe +% lastReadableStartTimestamp = stim_timestamp(i); +% lastTrialNrCandidate = c-1; + + if stim_trial(i) >= filterTrials(1) && mod(stim_trial(i)-filterTrials(1), everyWhich) == 0 + if isnan(stim_timestamp(i)) + stimTs(c) = NaN; + else + stimTs(c) = stim_timestamp(i); + end + trials(c) = c; % TODO: remove? + c = c+1; + end + end + + if isnan(trials(end)) + log_w(['We could not safely assign a trial number to the end of meaningful-trials section.' newline() 'This is likely because the number of trials to align and renumber is not divisible by the everyWhich parameter specified.' newline() 'Using a dummy value now.']) + stimTs(end) = stimTs(c-1); + trials(end) = c-1; + end + + interStimIntervals = zeros(1); % failsafe + for i = 1:(length(stimTs)-1) + interStimIntervals(i) = stimTs(i+1) - stimTs(i); +% log_d(['Length of renumbered trial nr. ' num2str(trials(i)) ' in seconds: ' num2str(interStimIntervals(i)/1000/1000)]); + end + + log_i(['Mean inter-stimulus-interval in seconds: ' num2str(mean(interStimIntervals, 'omitnan')/1000/1000)]); + log_i(['SD of inter-stimulus-interval in seconds: ' num2str(std(interStimIntervals, 'omitnan')/1000/1000)]); + + log_d(['Trial vector realigned to start from first relevant trial trigger, with respect to everyWhich param:']); + log_d([' Original numbering: ' sprintf('\t') '[' num2str(stim_trial(1)) ' ' num2str(stim_trial(end)) ']']); + log_d([' FilterTrials: ' sprintf('\t\t') '[' num2str(filterTrials(1)) ' ' num2str(filterTrials(2)) ']']); + log_d([' Realigned: ' sprintf('\t\t\t') '[' num2str(trials(1)) ' ' num2str(trials(end)) ']']); + +end \ No newline at end of file diff --git a/support_createTriggerVecResp.m b/support_createTriggerVecResp.m new file mode 100644 index 0000000..51c9dbe --- /dev/null +++ b/support_createTriggerVecResp.m @@ -0,0 +1,26 @@ +function [trials, respTs] = support_createTriggerVecResp(stim_trial, stim_timestamp, behav_trial, behav_RT) + + if length(behav_RT) ~= length(stim_timestamp) || length(behav_trial) ~= length(stim_trial) + log_e(['Behav_trial vector length is different than expected']) + end + +% trials = nan(length(stim_trial),1); + respTs = nan(length(stim_trial),1); + +% if length(respTs) > numStimuli || length(behav_trial) > numStimuli || min(behav_trial) < min(strim_trial) || max(behav_trial) > max(stim_trial) +% log_e(['There are more responses than stimuli in the input, or behav response numbering outranges that of stimuli. We cannot unequivocally map response times to stimuli accordingly.']) +% end + % TODO: possibly subtract a number from all behav_trial numbers to let it start from e.g. 1, in case stim_trial starts from 1 ? + + for i = 1:length(stim_trial) + respTs(i) = stim_timestamp(i) + behav_RT(i)*1000; % microsec + + end + + trials = stim_trial; + + % NOTE: NaNs (unanswered stimuli) are disregarded + + log_i('Mapped response times to stimuli.'); + +end \ No newline at end of file diff --git a/support_displayStat.m b/support_displayStat.m new file mode 100644 index 0000000..afcaffc --- /dev/null +++ b/support_displayStat.m @@ -0,0 +1,25 @@ +function support_displayStat(datavec) + + disp('--------------------------------------------------'); + + disp( ['N = ' num2str( length(datavec) ) ] ); + disp( ['min = ' num2str( min(datavec) ) ] ); + disp( ['max = ' num2str( max(datavec) ) ] ); + disp( ['M = ' num2str( mean(datavec, 'omitnan') ) ] ); + disp( ['SD = ' num2str( std(datavec, 'omitnan') ) ] ); + disp( ['Q1, Q2, Q3 = ' num2str( quantile(datavec, [0.25 0.50 0.75]) ) ] ); + + IQR = abs(quantile(datavec, 0.25) - quantile(datavec, 0.75)); + disp( ['IQR = ' num2str( IQR ) ] ); + + SEM = std(datavec, 'omitnan')/sqrt(length(datavec)); + ts = tinv([0.025 0.975],length(datavec)-1); % T-Score + CI = mean(datavec, 'omitnan') + ts*SEM; + disp( ['CI lower = ' num2str(CI(1)) ] ); + disp( ['CI upper = ' num2str(CI(2)) ] ); + + w = 1.5; + disp( ['Lower whisker = ' num2str( quantile(datavec,0.25)-w*IQR ) ] ); + disp( ['Upper whisker = ' num2str( quantile(datavec,0.75)+w*IQR ) ] ); + +end \ No newline at end of file diff --git a/support_eventRelatedFunc.m b/support_eventRelatedFunc.m new file mode 100644 index 0000000..0053f8d --- /dev/null +++ b/support_eventRelatedFunc.m @@ -0,0 +1,35 @@ +function result = support_eventRelatedFunc(trials_array, method) + + % This function does the "dimension reduction" by e.g. averaging all + % trials samplewise, but it can utilize other functions too, e.g. SD + % to be used for feature extraction purposes for ML, etc. DEV only + + result = NaN(size(trials_array, 1), 1); + + if method == 1 % ERPD + result = mean(trials_array,2,'omitnan'); + elseif method == 2 % ERPD-SD + result = std(trials_array, 0, 2,'omitnan'); + elseif method == 3 % ERPD-Ku + result = kurtosis(trials_array, 1, 2); + elseif method == 4 % ERPD-Sk + result = skewness(trials_array, 1, 2); + elseif method == 5 % ERPD-MAD + result = mad(trials_array, 1, 2); + elseif method == 6 % ERPD-Min + result = min(trials_array,[],2,'omitnan'); + elseif method == 7 % ERPD-Max + result = max(trials_array,[],2,'omitnan'); + elseif method == 8 % ERPD-KMax + for chu = 1:size(trials_array, 1) + [tempf, tempxi] = ksdensity(trials_array(chu,:)); + result(chu) = tempxi( tempf==max(tempf) ); + end + elseif method == 9 % ERPD-KVal + for chu = 1:size(trials_array, 1) + [tempf, tempxi] = ksdensity(trials_array(chu,:)); + result(chu) = max(tempf); + end + end + +end \ No newline at end of file diff --git a/support_filterBlinkDelta.m b/support_filterBlinkDelta.m new file mode 100644 index 0000000..9312d97 --- /dev/null +++ b/support_filterBlinkDelta.m @@ -0,0 +1,24 @@ +function T = support_filterBlinkDelta(T) + +% T = table(b_start, b_end, b_trial, b_duration, deltat_start, deltat_end, deltat_middle, deltat_between); +% header={'Start', 'End', 'Trial', 'Duration [ms]', 'Δt start [sec]', 'Δt end [sec]', 'Δt middle [sec]', 'Δt between [sec]'}; +% + + %mask = zeros(size(T,1), 1); + mask = (T{:,4}>1000); + T{mask,4} = NaN; + + mask = (T{:,5}>10); + T{mask,5} = NaN; + + mask = (T{:,6}>10); + T{mask,6} = NaN; + + mask = (T{:,7}>10); + T{mask,7} = NaN; + + mask = (T{:,8}>10); + T{mask,8} = NaN; + + +end \ No newline at end of file diff --git a/support_findBlinkDelta.m b/support_findBlinkDelta.m new file mode 100644 index 0000000..6cd28de --- /dev/null +++ b/support_findBlinkDelta.m @@ -0,0 +1,44 @@ +function T = support_findBlinkDelta(b_trial, b_start, b_end) + + %format longG; + + deltat_start = zeros(length(b_start), 1); + deltat_end = zeros(length(b_start), 1); + deltat_middle = zeros(length(b_start), 1); + deltat_between = zeros(length(b_start), 1); + + deltat_start(1) = NaN; + deltat_end(1) = NaN; + deltat_middle(1) = NaN; + deltat_between(1) = NaN; + + if length(b_start) < 2 + return; + end + + b_middle = zeros(length(b_start), 1); + for z = 1:length(b_start) + b_middle(z) = round((b_end(z) + b_start(z))/2); + end + + for p = 2:length(b_start) + deltat_start(p) = b_start(p) - b_start(p-1); + deltat_end(p) = b_end(p) - b_end(p-1); + deltat_middle(p) = b_middle(p) - b_middle(p-1); + deltat_between(p) = b_start(p) - b_end(p-1); + end + + deltat_start = deltat_start./1000./1000; + deltat_end = deltat_end./1000./1000; + deltat_middle = deltat_middle./1000./1000; + deltat_between = deltat_between./1000./1000; + %ts_rel_sec = round((ts_abs-timestamp(1))./1000./1000, 3); + + b_duration = (b_end-b_start)./1000; + + T = table(b_start, b_end, b_trial, b_duration, deltat_start, deltat_end, deltat_middle, deltat_between); + header={'Start', 'End', 'Trial', 'Duration [ms]', 'Δt start [sec]', 'Δt end [sec]', 'Δt middle [sec]', 'Δt between [sec]'}; + T.Properties.VariableNames = header; + %disp(T); + +end \ No newline at end of file diff --git a/support_findTrialChanges.m b/support_findTrialChanges.m new file mode 100644 index 0000000..8bdbc5a --- /dev/null +++ b/support_findTrialChanges.m @@ -0,0 +1,43 @@ +function T = support_findTrialChanges(tr, trial_ts) + + %format longG; + + uniq_tr = transpose(unique(tr)); +% uniq_tr = unique(tr); + + if uniq_tr ~= tr + log_e('Non-unique trial vector') + end + + ts_abs = zeros(length(uniq_tr), 1); + ts_end_abs = zeros(length(uniq_tr), 1); + deltat_ms = zeros(length(uniq_tr), 1); + + for k = 1:length(tr) + ts_abs(k) = trial_ts(k); + + if k < length(tr) + % todo: remove + ts_end_abs(k) = trial_ts(k+1); + + if k > 1 + deltat_ms(k) = (trial_ts(k) - trial_ts(k-1)) /1000; + end + end + end + + duration = round((ts_end_abs - ts_abs) ./1000, 3); + ts_rel_sec = round((ts_abs-ts_abs(1))./1000./1000, 3); + ts_rel_min = round(ts_rel_sec./60, 3); + deltat_sec = deltat_ms./1000; + + errT = zeros(length(uniq_tr), 1); + for hgg = 1:(length(duration)-1) + errT(hgg, 1) = round((ts_abs(hgg+1) - ts_end_abs(hgg)) /1000, 3); + end + + T = table(uniq_tr, ts_abs, ts_rel_sec, ts_rel_min, deltat_ms, deltat_sec, ts_end_abs, duration, errT); + header={'Trial', 'Start_abs_timestamp', 'Start_rel__sec', 'Start_rel__min', 'Start_deltat__ms', 'Start_deltat__sec', 'End_abs_timestamp', 'Start_to_end_duration_in_raw_data__ms', 'End_to_start_dead_time_of_raw_data__ms'}; + T.Properties.VariableNames = header; + +end \ No newline at end of file diff --git a/support_restrictMaskToNumVals.m b/support_restrictMaskToNumVals.m new file mode 100644 index 0000000..f976169 --- /dev/null +++ b/support_restrictMaskToNumVals.m @@ -0,0 +1,19 @@ +function outmask = support_restrictMaskToNumVals(inmask, numpass, valtopass, align) + + if valtopass == true + outmask = false(length(inmask),1); + else + outmask = true(length(inmask),1); + end + + wherevals = find(inmask==valtopass); + + if align == 1 + wherevals = wherevals(1:numpass,1); + else + wherevals = wherevals((length(wherevals)-numpass+1):length(wherevals),1); + end + + outmask(wherevals) = valtopass; + +end \ No newline at end of file