function [ripples] = findRipples(varargin) %findRipples - Find hippocampal ripples (100~200Hz oscillations). % % USAGE % [ripples] = findRipples(lfp.data,lfp.timestamps,) % OR % [ripples] = findRipples(basepath,channel,) % % Ripples are detected using the normalized squared signal (NSS) by % thresholding the baseline, merging neighboring events, thresholding % the peaks, and discarding events with excessive duration. % Thresholds are computed as multiples of the standard deviation of % the NSS. Alternatively, one can use explicit values, typically obtained % from a previous call. The estimated EMG can be used as an additional % exclusion criteria % % INPUTS - note these are NOT name-value pairs... just raw values % lfp unfiltered LFP structure. % optional list of property-value pairs (see tables below) % % OR % % channel Ripple channel to use for detection (0-indexed, a la neuroscope) % % OR (legacy) % % lfp unfiltered LFP (one channel) to use % timestamps timestamps to match filtered variable % % OR % % basepath path to a single session to run findRipples on % channel Ripple channel to use for detection (1-indexed) % % ========================================================================= % Properties Values % ------------------------------------------------------------------------- % 'thresholds' thresholds for ripple beginning/end and peak, in multiples % of the stdev (default = [2 5]); must be integer values % 'durations' min inter-ripple interval and max ripple duration, in ms % (default = [30 100]). % 'minDuration' min ripple duration. Keeping this input nomenclature for backwards % compatibility % 'restrict' interval used to compute normalization (default = all) % 'frequency' sampling rate (in Hz) (default = 1250Hz) % 'stdev' reuse previously computed stdev % 'show' plot results (default = 'off') % 'noise' noisy unfiltered channel used to exclude ripple- % like noise (events also present on this channel are % discarded) % 'passband' N x 2 matrix of frequencies to filter for ripple detection % (default = [130 200]) % 'EMGThresh' 0-1 threshold of EMG to exclude noise % 'saveMat' logical (default=false) to save in buzcode format % 'plotType' 1=original version (several plots); 2=only raw lfp % 'basepath' Path to a single session to run findRipples, default % pwd % ========================================================================= % % OUTPUT % % ripples buzcode format .event. struct with the following fields % .timestamps Nx2 matrix of start/stop times for % each ripple % .detectorName string ID for detector function used % .peaks Nx1 matrix of peak power timestamps % .stdev standard dev used as threshold % .noise candidate ripples that were % identified as noise and removed % .peakNormedPower Nx1 matrix of peak power values % .detectorParams struct with input parameters given % to the detector % SEE ALSO % % Copyright (C) 2004-2011 by Michaƫl Zugaro, initial algorithm by Hajime Hirase % edited by David Tingley, 2017 % Manu Valero 2022 (adapt to 1-index, remove sessionInfo dependency and update inputs) % % This program is free software; you can redistribute it and/or modify % it under the terms of the GNU General Public License as published by % the Free Software Foundation; either version 3 of the License, or % (at your option) any later version. warning('this function is under development and may not work... yet') % Default values p = inputParser; addParameter(p,'thresholds',[2 5],@isnumeric) addParameter(p,'durations',[30 100],@isnumeric) addParameter(p,'restrict',[],@isnumeric) addParameter(p,'frequency',1250,@isnumeric) addParameter(p,'stdev',[],@isnumeric) addParameter(p,'show','off',@isstr) %changed by Anna, default was off addParameter(p,'noise',[],@ismatrix) addParameter(p,'passband',[130 200],@isnumeric) addParameter(p,'EMGThresh',.9,@isnumeric); addParameter(p,'saveMat',false,@islogical); addParameter(p,'minDuration',20,@isnumeric); addParameter(p,'plotType',2,@isnumeric); addParameter(p,'basepath',pwd,@isfolder); prevPath = pwd; if isstr(varargin{1}) % if first arg is basepath addRequired(p,'basepath',@isstr) addRequired(p,'channel',@isnumeric) parse(p,varargin{:}) basename = bz_BasenameFromBasepath(p.Results.basepath); basepath = p.Results.basepath; passband = p.Results.passband; %EMGThresh = p.Results.EMGThresh; lfp = getLFP(p.Results.channel,'basepath',p.Results.basepath,'basename',basename);%currently cannot take path inputs signal = bz_Filter(double(lfp.data),'filter','butter','passband',passband,'order', 3); timestamps = lfp.timestamps; elseif isnumeric(varargin{1}) && length(varargin{1})>2 % if first arg is filtered LFP addRequired(p,'lfp',@isnumeric) addRequired(p,'timestamps',@isnumeric) parse(p,varargin{:}) passband = p.Results.passband; EMGThresh = p.Results.EMGThresh; signal = bz_Filter(double(p.Results.lfp),'filter','butter','passband',passband,'order', 3); timestamps = p.Results.timestamps; basepath = pwd; basename = basenameFromBasepath(basepath); elseif isstruct(varargin{1}) % Added by Pablo Abad to manage when first input if lfp sctruct, not % filtered. This lfp channel should have addRequired(p,'lfp',@isstruct); parse(p,varargin{:}) passband = p.Results.passband; EMGThresh = p.Results.EMGThresh; timestamps = p.Results.lfp.timestamps; EMGThres = p.Results.EMGThresh; lfp = p.Results.lfp; basepath = pwd; if length(lfp.channels)>1 error('lfp structure must not have more than 1 channel!'); else signal = bz_Filter(double(lfp.data),'filter','butter','passband',passband,'order',3); basename = basenameFromBasepath(basepath); end elseif isnumeric(varargin{1}) addRequired(p,'channel',@isnumeric) parse(p,varargin{:}); passband = p.Results.passband; EMGThresh = p.Results.EMGThresh; basepath = pwd; cd(basepath); basename = basenameFromBasepath(p.Results.basepath); lfp = getLFP(p.Results.channel,'basepath',p.Results.basepath,'basename',basename); signal = bz_Filter(lfp,'filter','butter','passband',passband,'order',3); timestamps = signal.timestamps; signal = signal.data; end % assign parameters (either defaults or given) frequency = p.Results.frequency; show = p.Results.show; restrict = p.Results.restrict; sd = p.Results.stdev; noise = p.Results.noise; lowThresholdFactor = p.Results.thresholds(1); highThresholdFactor = p.Results.thresholds(2); minInterRippleInterval = p.Results.durations(1); maxRippleDuration = p.Results.durations(2); minRippleDuration = p.Results.minDuration; plotType = p.Results.plotType; %% filter and calculate noise % Parameters windowLength = frequency/frequency*11; % Square and normalize signal squaredSignal = signal.^2; % squaredSignal = abs(opsignal); window = ones(windowLength,1)/windowLength; keep = []; if ~isempty(restrict) for i=1:size(restrict,1) keep = InIntervals(timestamps,restrict); end end keep = logical(keep); [normalizedSquaredSignal,sd] = unity(Filter0(window,sum(squaredSignal,2)),sd,keep); % Detect ripple periods by thresholding normalized squared signal thresholded = normalizedSquaredSignal > lowThresholdFactor; start = find(diff(thresholded)>0); stop = find(diff(thresholded)<0); % Exclude last ripple if it is incomplete if length(stop) == length(start)-1 start = start(1:end-1); end % Exclude first ripple if it is incomplete if length(stop)-1 == length(start) stop = stop(2:end); end % Correct special case when both first and last ripples are incomplete if start(1) > stop(1) stop(1) = []; start(end) = []; end firstPass = [start,stop]; if isempty(firstPass) disp('Detection by thresholding failed'); return else disp(['After detection by thresholding: ' num2str(length(firstPass)) ' events.']); end % Merge ripples if inter-ripple period is too short minInterRippleSamples = minInterRippleInterval/1000*frequency; secondPass = []; ripple = firstPass(1,:); for i = 2:size(firstPass,1) if firstPass(i,1) - ripple(2) < minInterRippleSamples % Merge ripple = [ripple(1) firstPass(i,2)]; else secondPass = [secondPass ; ripple]; ripple = firstPass(i,:); end end secondPass = [secondPass ; ripple]; if isempty(secondPass) disp('Ripple merge failed'); return else disp(['After ripple merge: ' num2str(length(secondPass)) ' events.']); end % Discard ripples with a peak power < highThresholdFactor thirdPass = []; peakNormalizedPower = []; for i = 1:size(secondPass,1) [maxValue,maxIndex] = max(normalizedSquaredSignal([secondPass(i,1):secondPass(i,2)])); if maxValue > highThresholdFactor thirdPass = [thirdPass ; secondPass(i,:)]; peakNormalizedPower = [peakNormalizedPower ; maxValue]; end end if isempty(thirdPass), disp('Peak thresholding failed.'); return else disp(['After peak thresholding: ' num2str(length(thirdPass)) ' events.']); end % Detect negative peak position for each ripple peakPosition = zeros(size(thirdPass,1),1); for i=1:size(thirdPass,1) [minValue,minIndex] = min(signal(thirdPass(i,1):thirdPass(i,2))); peakPosition(i) = minIndex + thirdPass(i,1) - 1; end % Discard ripples that are way too long ripples = [timestamps(thirdPass(:,1)) timestamps(peakPosition) ... timestamps(thirdPass(:,2)) peakNormalizedPower]; duration = ripples(:,3)-ripples(:,1); ripples(duration>maxRippleDuration/1000,:) = NaN; %disp(['After duration test: ' num2str(size(ripples,1)) ' events.']); % Discard ripples that are too short ripples(durationhighThresholdFactor) excluded(i) = 1; end end bad = ripples(excluded,:); ripples = ripples(~excluded,:); disp(['After ripple-band noise removal: ' num2str(size(ripples,1)) ' events.']); end %% lets try to also remove EMG artifact? if EMGThresh basename = basenameFromBasepath(basepath); EMGfilename = fullfile(basepath,[basename '.EMGFromLFP.LFP.mat']); if exist(EMGfilename) load(EMGfilename) %should use a bz_load script here else [EMGFromLFP] = bz_EMGFromLFP(basepath,'samplingFrequency',10,'savemat',false,'noPrompts',true); end excluded = logical(zeros(size(ripples,1),1)); for i = 1:size(ripples,1) [a ts] = min(abs(ripples(i,1)-EMGFromLFP.timestamps)); % get closest sample if EMGFromLFP.data(ts) > EMGThresh excluded(i) = 1; end end bad = sortrows([bad; ripples(excluded,:)]); ripples = ripples(~excluded,:); disp(['After EMG noise removal: ' num2str(size(ripples,1)) ' events.']); end % Optionally, plot results if strcmp(show,'on') if plotType == 1 figure; if ~isempty(noise) MultiPlotXY([timestamps signal],[timestamps squaredSignal],... [timestamps normalizedSquaredSignal],[timestamps squaredNoise],... [timestamps squaredNoise],[timestamps normalizedSquaredNoise]); nPlots = 6; subplot(nPlots,1,3); ylim([0 highThresholdFactor*1.1]); subplot(nPlots,1,6); ylim([0 highThresholdFactor*1.1]); else MultiPlotXY([timestamps signal],[timestamps squaredSignal],[timestamps normalizedSquaredSignal]); % MultiPlotXY(time,signal,time,squaredSignal,time,normalizedSquaredSignal); nPlots = 3; subplot(nPlots,1,3); ylim([0 highThresholdFactor*1.1]); end for i = 1:nPlots subplot(nPlots,1,i); hold on; yLim = ylim; for j=1:size(ripples,1) plot([ripples(j,1) ripples(j,1)],yLim,'g-'); plot([ripples(j,2) ripples(j,2)],yLim,'k-'); plot([ripples(j,3) ripples(j,3)],yLim,'r-'); if i == 3 plot([ripples(j,1) ripples(j,3)],[ripples(j,4) ripples(j,4)],'k-'); end end for j=1:size(bad,1) plot([bad(j,1) bad(j,1)],yLim,'k-'); plot([bad(j,2) bad(j,2)],yLim,'k-'); plot([bad(j,3) bad(j,3)],yLim,'k-'); if i == 3 plot([bad(j,1) bad(j,3)],[bad(j,4) bad(j,4)],'k-'); end end if mod(i,3) == 0 plot(xlim,[lowThresholdFactor lowThresholdFactor],'k','linestyle','--'); plot(xlim,[highThresholdFactor highThresholdFactor],'k-'); end end elseif plotType == 2 lfpPlot = bz_Filter(double(lfp.data),'filter','butter','passband',[50 300],'order', 3); plot(timestamps,lfpPlot);hold on; yLim = ylim; for j=1:size(ripples,1) plot([ripples(j,1) ripples(j,1)],yLim,'g-'); plot([ripples(j,2) ripples(j,2)],yLim,'k-'); plot([ripples(j,3) ripples(j,3)],yLim,'r-'); if i == 3 plot([ripples(j,1) ripples(j,3)],[ripples(j,4) ripples(j,4)],'k-'); end end for j=1:size(bad,1) plot([bad(j,1) bad(j,1)],yLim,'k-'); plot([bad(j,2) bad(j,2)],yLim,'k-'); plot([bad(j,3) bad(j,3)],yLim,'k-'); if i == 3 plot([bad(j,1) bad(j,3)],[bad(j,4) bad(j,4)],'k-'); end end if mod(i,3) == 0 plot(xlim,[lowThresholdFactor lowThresholdFactor],'k','linestyle','--'); plot(xlim,[highThresholdFactor highThresholdFactor],'k-'); end end end %% BUZCODE Struct Output rips = ripples; clear ripples ripples.lfp = lfp; ripples.signal = signal; % low-pass signal ripples.timestamps = rips(:,[1 3]); ripples.peaks = rips(:,2); %peaktimes? could also do these as timestamps and then ripples.ints for start/stops? ripples.peakNormedPower = rips(:,4); %amplitudes? ripples.stdev = sd; if ~isempty(bad) ripples.noise.times = bad(:,[1 3]); ripples.noise.peaks = bad(:,[2]); ripples.noise.peakNormedPower = bad(:,[4]); else ripples.noise.times = []; ripples.noise.peaks = []; ripples.noise.peakNormedPower = []; end %The detectorinto substructure detectorinfo.detectorname = 'findRipples'; detectorinfo.detectiondate = date; detectorinfo.detectionintervals = restrict; detectorinfo.detectionparms = p.Results; detectorinfo.detectionparms = rmfield(detectorinfo.detectionparms,'noise'); if isfield(detectorinfo.detectionparms,'timestamps') detectorinfo.detectionparms = rmfield(detectorinfo.detectionparms,'timestamps'); end if isfield(p.Results,'channel') detectorinfo.detectionchannel = p.Results.channel; end if ~isempty(noise) detectorinfo.noisechannel = noise; else detectorinfo.noisechannel = nan; end %Put it into the ripples structure ripples.detectorinfo = detectorinfo; cd(prevPath); %Save if p.Results.saveMat save(fullfile(basepath, [basename '.ripples.events.mat']),'ripples') end function y = Filter0(b,x) if size(x,1) == 1 x = x(:); end if mod(length(b),2)~=1 error('filter order should be odd'); end shift = (length(b)-1)/2; [y0 z] = filter(b,1,x); y = [y0(shift+1:end,:) ; z(1:shift,:)]; function [U,stdA] = unity(A,sd,restrict) if ~isempty(restrict), meanA = mean(A(restrict)); stdA = std(A(restrict)); else meanA = mean(A); stdA = std(A); end if ~isempty(sd), stdA = sd; end U = (A - meanA)/stdA;