├── AR └── Closed_Loop_AR.m ├── ETP ├── Closed_Loop_ETP.m ├── Closed_Loop_ETP_train.m ├── ETP_AutoCorrect_edge.m ├── mypeakseek.m └── readme.md ├── FFT └── Closed_Loop_FFT.m ├── LICENSE ├── README.md └── Rest └── Closed_Loop_Rest.m /AR/Closed_Loop_AR.m: -------------------------------------------------------------------------------- 1 | function [allVec,allTs,allTs_marker,allTs_trigger] = Closed_Loop_AR() 2 | % Closed-loop algorithm using autoregressive forward prediction and hilbert 3 | % transform to detect the peak. 4 | % allVec: Raw EEG (channel*sample) 5 | % allTs: Timestamp of each sample 6 | % allTs_marker: Timestamp of event markers 7 | % allTs_trigger: Timestamp of the sample at which triggere was delivered 8 | %% Parameters 9 | TrigInt = 2; % Minimum interval between trials 10 | fnative = 10000; % Native sampling rate 11 | fs = 500; % Processing sampling rate 12 | win_length = fs/2; % Window length for online processing 13 | targetFreq = [8 13]; % Band of interest in Hz 14 | elec_interest = [47 13 14 16 17 44 45 46 48]; % ['Electrode of interest' 'Surrounding electrodes']; 15 | desired_phase = 0; % Targeted phase 16 | technical_delay = 8; % Technical delay in ms 17 | phase_tolereance = 0.1; % Phase tolerance in radians 18 | p = 30; % AR order 19 | edge = 32; % Number of samples to remove 20 | ForwardSamples = 64; % Number of samples to predict 21 | %% Initialization 22 | trig_timer = tic; % Used for timing between triggers 23 | adjusted_desired_phase = desired_phase-(2*pi*mean(targetFreq)*technical_delay)/1000; 24 | downsample = floor(fnative/fs); 25 | allVec = nan(64, 1000000); 26 | allTs = nan(1,1000000); 27 | chunk = zeros(fs,1); 28 | ft_defaults; 29 | %% Initialize LPT Port 30 | % initialize access to the inpoutx64 low-level I/O driver 31 | config_io; 32 | % optional step: verify that the inpoutx64 driver was successfully initialized 33 | global cogent; 34 | if( cogent.io.status ~= 0 ) 35 | error('inp/outp installation failed'); 36 | end 37 | % write a value to the LPT output port 38 | address = hex2dec('C020'); 39 | outp(address, 0); 40 | %% Close prieviously opened inlet streams in case it was not closed properly 41 | try 42 | inlet.close_stream(); 43 | inlet_marker.close_stream(); 44 | catch 45 | end 46 | %% instantiate the library 47 | disp('Loading the library...'); 48 | lib = lsl_loadlib(); 49 | 50 | % resolve a stream... 51 | disp('Resolving an EEG stream...'); 52 | result = {}; 53 | while isempty(result) 54 | result = lsl_resolve_byprop(lib,'type','EEG'); 55 | end 56 | 57 | result_marker = {}; 58 | while isempty(result_marker) 59 | result_marker = lsl_resolve_byprop(lib,'type','Markers'); 60 | end 61 | 62 | % create a new inlet 63 | disp('Opening an inlet...'); 64 | inlet = lsl_inlet(result{1}); 65 | inlet_marker = lsl_inlet(result_marker{1}); 66 | %% 67 | disp('Now receiving data...'); 68 | sample = 0; % Number of samples recieved 69 | downsample_idx = downsample; % Index used for downsampling 70 | allTs_trigger = []; 71 | allTs_marker = []; 72 | 73 | while 1 74 | % Get data from the inlet 75 | [vec,ts] = inlet.pull_sample(1); 76 | [~,ts_marker] = inlet_marker.pull_chunk(); 77 | allTs_marker = [allTs_marker ts_marker]; 78 | if isempty(vec) 79 | break; % End cycle if didn't receive data within certain time 80 | end 81 | if downsample_idx == downsample 82 | sample = sample+1; 83 | allVec(:,sample) = vec'; 84 | allTs(:,sample) = ts; 85 | downsample_idx = 1; 86 | if sample >= win_length && toc(trig_timer) > TrigInt 87 | if length(elec_interest) == 1 88 | chunk = allVec(elec_interest,sample-win_length+1:sample)-allVec(64,sample-win_length+1:sample); 89 | else 90 | ref = mean(allVec(elec_interest(2:end),sample-win_length+1:sample)); 91 | chunk = allVec(elec_interest(1),sample-win_length+1:sample)-ref; 92 | end 93 | chunk_filt = ft_preproc_bandpassfilter(chunk, fs, targetFreq, 128, 'fir','twopass'); 94 | coeffs = aryule(chunk_filt(edge+1:end-edge), p); % AR model (edges removed) 95 | coeffs = -coeffs(end:-1:2); 96 | nextValues = zeros(1,p+ForwardSamples); 97 | nextValues(1:p) = chunk_filt(end-p-edge+1:end-edge); 98 | for jj = 1:ForwardSamples 99 | nextValues(p+jj) = coeffs*nextValues(jj:p+jj-1)'; 100 | end 101 | phase = angle(hilbert(nextValues(p+1:end))); 102 | phase_now = phase(edge); 103 | 104 | if abs(phase_now-adjusted_desired_phase) <= phase_tolereance 105 | outp(address, 32); 106 | trig_timer = tic; % Reset timer after each trigger 107 | pause(0.015); 108 | outp(address, 0); 109 | allTs_trigger = [allTs_trigger ts]; 110 | disp('Stim'); 111 | end 112 | end 113 | else 114 | downsample_idx = downsample_idx + 1; 115 | end 116 | end 117 | 118 | inlet.close_stream(); 119 | inlet_marker.close_stream(); 120 | disp('Finished receiving'); 121 | clear cogent 122 | disp('Closed LPT Port'); 123 | end -------------------------------------------------------------------------------- /ETP/Closed_Loop_ETP.m: -------------------------------------------------------------------------------- 1 | function [allVec,allTs,allTs_marker,allTs_trigger] = Closed_Loop_ETP(fullCycle) 2 | % Closed-loop algorithm using ETP method to detect peak based on the 3 | % adjusted period 4 | % allVec: Raw EEG (channel*sample) 5 | % allTs: Timestamp of each sample 6 | % allTs_marker: Timestamp of event markers 7 | % allTs_trigger: Timestamp of the sample at which triggere was delivered 8 | %% Parameters 9 | elec_interest = [47 13 14 16 17 44 45 46 48]; % ['Electrode of interest' 'Surrounding electrodes']; 10 | TrigInt = 2; % Minimum interval between trials 11 | fnative = 10000; % Native sampling rate 12 | fs = 1000; % Processing sampling rate 13 | win_length = fs/2; % Window length for online processing 14 | targetFreq = [8 13]; % Band of interest in Hz 15 | edge = round(fs./(mean(targetFreq)*3)); % Number of samples to remove 16 | technical_delay = 8; % Technical delay in ms 17 | delay_tolerance = 1; % Delay tolerance in ms 18 | %% Initialization 19 | trig_timer = tic; % Used for timing between triggers 20 | downsample = floor(fnative/fs); 21 | allVec = nan(64, 1000000); 22 | allTs = nan(1,1000000); 23 | ft_defaults; 24 | %% Initialize LPT Port 25 | % initialize access to the inpoutx64 low-level I/O driver 26 | config_io; 27 | % optional step: verify that the inpoutx64 driver was successfully initialized 28 | global cogent; 29 | if( cogent.io.status ~= 0 ) 30 | error('inp/outp installation failed'); 31 | end 32 | % write a value to the LPT output port 33 | address = hex2dec('C020'); 34 | outp(address, 0); % sets all pins to 0 35 | %% Close prieviously opened inlet streams in case it was not closed properly 36 | try 37 | inlet.close_stream(); 38 | inlet_marker.close_stream(); 39 | catch 40 | end 41 | %% instantiate the library 42 | disp('Loading the library...'); 43 | lib = lsl_loadlib(); 44 | 45 | % resolve a stream... 46 | disp('Resolving an EEG stream...'); 47 | result_eeg = {}; 48 | while isempty(result_eeg) 49 | result_eeg = lsl_resolve_byprop(lib,'type','EEG'); 50 | end 51 | 52 | result_marker = {}; 53 | while isempty(result_marker) 54 | result_marker = lsl_resolve_byprop(lib,'type','Markers'); 55 | end 56 | 57 | % create a new inlet 58 | disp('Opening an inlet...'); 59 | inlet = lsl_inlet(result_eeg{1}); 60 | inlet_marker = lsl_inlet(result_marker{1}); 61 | %% 62 | disp('Now receiving data...'); 63 | sample = 0; % Number of samples recieved 64 | downsample_idx = 10; % Index used for downsampling 65 | allTs_trigger = []; 66 | allTs_marker = []; 67 | 68 | while 1 69 | [vec,ts] = inlet.pull_sample(1); 70 | [~,ts_marker] = inlet_marker.pull_chunk(); 71 | allTs_marker = [allTs_marker ts_marker]; 72 | if isempty(vec) 73 | break; % End cycle if didn't receive data within certain time 74 | end 75 | if downsample_idx == downsample 76 | sample = sample+1; 77 | allVec(:,sample) = vec'; 78 | allTs(:,sample) = ts; 79 | downsample_idx = 1; 80 | if sample >= win_length && toc(trig_timer) > TrigInt % Enough samples & enough time between triggers 81 | if length(elec_interest) == 1 82 | chunk = allVec(elec_interest,sample-win_length+1:sample)-allVec(64,sample-win_length+1:sample); 83 | else 84 | ref = mean(allVec(elec_interest(2:end),sample-win_length+1:sample)); 85 | chunk = allVec(elec_interest(1),sample-win_length+1:sample)-ref; 86 | end 87 | 88 | chunk_filt = ft_preproc_bandpassfilter(chunk, fs, targetFreq, [], 'brickwall','onepass'); 89 | 90 | locs_hi = mypeakseek(chunk_filt(1:end-edge),fs/(targetFreq(2)+1)); 91 | nextTarget = locs_hi(end) + fullCycle; 92 | 93 | if abs(nextTarget-win_length-round(technical_delay*fs/1000)) <= delay_tolerance 94 | outp(address, 32); % Sets pin 6 to 1 95 | trig_timer = tic;% Reset timer after each trigger 96 | pause(0.015); 97 | outp(address, 0); 98 | allTs_trigger = [allTs_trigger ts]; 99 | disp('Stim'); 100 | end 101 | end 102 | else 103 | downsample_idx = downsample_idx + 1; 104 | end 105 | end 106 | 107 | inlet.close_stream(); 108 | inlet_marker.close_stream(); 109 | disp('Finished receiving'); 110 | clear cogent 111 | disp('Closed LPT Port'); 112 | end -------------------------------------------------------------------------------- /ETP/Closed_Loop_ETP_train.m: -------------------------------------------------------------------------------- 1 | function [allVec_rest,allTs_rest,fullCycle] = Closed_Loop_ETP_train() 2 | % Closed-loop algorithm using ETP to train the prediction algorithm based 3 | % on a resting state recording to use in real time. 4 | % allVec_rest: Resting EEG (channel*sample) 5 | % allTs_rest: Timestamp of each sample 6 | % fullCycle: Adjusted period based on education 7 | %% Parameters 8 | fnative = 10000; % Native sampling rate 9 | fs = 1000; % Processing sampling rate 10 | rest_length_in_sec = 180; % Resting state length in s 11 | %% Initialization 12 | downsample = floor(fnative/fs); 13 | allVec_rest = nan(64, rest_length_in_sec*fs); 14 | allTs_rest = nan(1,rest_length_in_sec*fs); 15 | %% Close prieviously opened inlet streams in case it was not closed properly 16 | try 17 | inlet.close_stream(); 18 | inlet_marker.close_stream(); 19 | catch 20 | end 21 | %% instantiate the library 22 | disp('Loading the library...'); 23 | lib = lsl_loadlib(); 24 | 25 | % resolve a stream... 26 | disp('Resolving an EEG stream...'); 27 | result_eeg = {}; 28 | while isempty(result_eeg) 29 | result_eeg = lsl_resolve_byprop(lib,'type','EEG'); 30 | end 31 | 32 | % create a new inlet 33 | disp('Opening an inlet...'); 34 | inlet = lsl_inlet(result_eeg{1}); 35 | %% 36 | disp('Now receiving data...'); 37 | sample = 0; % Number of samples recieved 38 | downsample_idx = 10; % Index used for downsampling 39 | 40 | while 1 41 | [vec,ts] = inlet.pull_sample(1); 42 | if isempty(vec) 43 | break; % End cycle if didn't receive data within certain time 44 | end 45 | if downsample_idx == downsample 46 | sample = sample+1; 47 | allVec_rest(:,sample) = vec'; 48 | allTs_rest(:,sample) = ts; 49 | if mod(sample/fs,30) == 0 50 | disp(['time: ' num2str(sample/fs) 's']); 51 | end 52 | if sample == rest_length_in_sec*fs 53 | break 54 | end 55 | downsample_idx = 1; 56 | else 57 | downsample_idx = downsample_idx + 1; 58 | end 59 | end 60 | 61 | inlet.close_stream(); 62 | disp('Finished receiving'); 63 | 64 | % Calculate fullCycle 65 | % Change desired electrode and frequency band in the next function 66 | ft_defaults; 67 | [estimated_phase,fullCycle] = ETP_AutoCorrect_edge(allVec_rest); 68 | figure; 69 | polarhistogram(estimated_phase,36); 70 | end -------------------------------------------------------------------------------- /ETP/ETP_AutoCorrect_edge.m: -------------------------------------------------------------------------------- 1 | function [actual_phase,fullCycle] = ETP_AutoCorrect_edge(data) 2 | % This function adjusts for the bias in the algorithm and returns the best 3 | % fullCycle. Bias is a constant variable that is added to the typical cycle 4 | % length. 5 | bias = 0; 6 | bestBias = 0; 7 | minError = 100; % A large number to begin with 8 | while 1 9 | [actual_phase,~] = ETP_Bias(data,bias); 10 | % Check whether the new bias value yields better results 11 | if abs(mean(actual_phase)) < minError 12 | % If yes, set as best case and continue search 13 | minError = abs(mean(actual_phase)); 14 | bestBias = bias; 15 | else 16 | % If not, already found the optimum, stop the search 17 | break 18 | end 19 | if mean(actual_phase) < 0 20 | % If generally early 21 | bias = bias + 1; 22 | elseif mean(actual_phase) > 0 23 | % If generally late 24 | bias = bias - 1; 25 | end 26 | end 27 | % Run with the best bias found and return 28 | [actual_phase,fullCycle] = ETP_Bias(data,bestBias); 29 | end 30 | 31 | function [actual_phase,fullCycle] = ETP_Bias(data,bias) 32 | % Train and validate based on the resting data and given bias adjustment 33 | %% Parameters 34 | targetFreq = [8 13]; % Band of interest in Hz 35 | srate = 1000; % Sampling rate in Hz 36 | elec_interest = [47 13 14 16 17 44 45 46 48]; % ['Electrode of interest' 'Surrounding electrodes']; 37 | data_length_in_sec = 180; 38 | training_length_in_sec = 90; 39 | %% Train 40 | edge = round(srate./(targetFreq(1)*3)); % Ignore this many samples from the end 41 | 42 | mydata = ft_preproc_bandpassfilter(data, srate, targetFreq, [], 'fir','twopass'); 43 | 44 | if length(elec_interest) == 1 45 | myseq = mydata(elec_interest, 1:srate*data_length_in_sec)-mydata(64, 1:srate*data_length_in_sec); 46 | else 47 | ref = mean(mydata(elec_interest(2:end), 1:srate*data_length_in_sec)); 48 | myseq = mydata(elec_interest(1), 1:srate*data_length_in_sec)-ref; 49 | end 50 | 51 | alpha_phase = angle(hilbert(myseq)); 52 | 53 | locs_hi = mypeakseek(myseq(1:srate*training_length_in_sec),srate/(targetFreq(2)+3)); 54 | ipi = diff(locs_hi); 55 | 56 | pks2 = ipi; 57 | pks2(pks2>round(srate/6.7)) = nan; % Threshold (Not important, bias adjustes it anaway) 58 | fullCycle = round(exp(nanmean(log(pks2))))+bias; % Typical cycle length in samples 59 | 60 | trl_num = 255; 61 | nexttarget = nan(1,trl_num); 62 | delay = nan(1,trl_num); 63 | actual_phase = nan(1,trl_num); 64 | 65 | for i = 1:trl_num 66 | if length(elec_interest) == 1 67 | ref = data(64, srate*training_length_in_sec+i*350:srate*training_length_in_sec+i*350+(srate/2-1)); 68 | myseq2 = data(elec_interest, srate*training_length_in_sec+i*350:srate*training_length_in_sec+i*350+(srate/2-1))-ref; 69 | else 70 | ref = mean(data(elec_interest(2:end), srate*training_length_in_sec+i*350:srate*training_length_in_sec+i*350+(srate/2-1))); 71 | myseq2 = data(elec_interest(1), srate*training_length_in_sec+i*350:srate*training_length_in_sec+i*350+(srate/2-1))-ref; 72 | end 73 | 74 | myseq2 = ft_preproc_bandpassfilter(myseq2, srate, targetFreq, [], 'brickwall','onepass'); 75 | 76 | locs_hi = mypeakseek(myseq2(1:end-edge),srate/(targetFreq(2)+1)); 77 | 78 | nexttarget(i) = srate*training_length_in_sec+i*350 + locs_hi(end) + fullCycle; 79 | actual_phase(i) = alpha_phase(nexttarget(i)); 80 | end 81 | end -------------------------------------------------------------------------------- /ETP/mypeakseek.m: -------------------------------------------------------------------------------- 1 | function locs = mypeakseek(x,minpeakdist) 2 | % Alternative to the findpeaks function. This thing runs much much faster. 3 | % It really leaves findpeaks in the dust. It also can handle ties between 4 | % peaks. Findpeaks just erases both in a tie. Shame on findpeaks. 5 | % 6 | % x is a row vector input (generally a timecourse) 7 | % minpeakdist is the minimum desired distance between peaks (optional, defaults to 1) 8 | % minpeakh is the minimum height of a peak (optional) 9 | % 10 | % (c) 2010 11 | % Peter O'Connor 12 | % peteredoconnor .AT. gmailcom 13 | % 14 | % Modified by Sina Shirinpour (2019, shiri008umnedu) for 15 | % Shirinpour et al., Experimental Evaluation of Methods for Real-Time EEG 16 | % Phase-Specific Transcranial Magnetic Stimulation, 2019, bioRxiv 17 | 18 | 19 | % Find all maxima and ties 20 | locs=find(x(2:end-1)>=x(1:end-2) & x(2:end-1)>=x(3:end))+1; 21 | 22 | while 1 23 | del = diff(locs) 27 | deln=find(del); 28 | deln=[deln(mins == 1) deln(mins == 2)+1]; 29 | locs(deln) = []; 30 | end 31 | 32 | end -------------------------------------------------------------------------------- /ETP/readme.md: -------------------------------------------------------------------------------- 1 | # Educated Temporal Prediction (ETP) 2 | Educated Temporal Prediction (ETP) is a robust algorithm that learns statistical features of the individual EEG and predicts the ongoing EEG phase to deliver TMS pulse in real-time. 3 | 4 | The code conceptualized by Ivan Alekseichuk, implemented by Sina Shirinpour. 5 | 6 | ## Use 7 | 1) Run "[allVec_rest,allTs_rest,fullCycle] = Closed_Loop_ETP_train();" to record three minutes of resting EEG and train the algorithm. The fullCycle constant is the tranined parameter. 8 | 2) Run "[allVec,allTs,allTs_marker,allTs_trigger] = Closed_Loop_ETP(fullCycle);" to use fullCycle to deliver TMS pulse in real-time based on EEG phase. 9 | 10 | ## Parameters 11 | The following parameters need to be specified in the scripts, depending on the hardware, desired brain region, and the band of interest: 12 | 13 | ### In ETP_AutoCorrect_edge.m 14 | - targetFreq = [Bounds of the band of interest in Hz]; 15 | - srate = Sampling rate in Hz; 16 | - elec_interest = ['Electrode of interest' 'Surrounding electrodes']; 17 | 18 | ### In Closed_Loop_ETP.m 19 | - targetFreq = [Bounds of the band of interest in Hz]; 20 | - elec_interest = ['Electrode of interest' 'Surrounding electrodes']; 21 | - fnative = Acquisition sampling rate; 22 | - fs = processing sampling rate; 23 | - TrigInt = minimum interval between trials in seconds; 24 | - technical_delay = technical delay in ms; 25 | 26 | **Note: Make sure the corresponding parameters in the Closed_Loop_ETP.m are the same as in ETP_AutoCorrect_edge.m.** 27 | -------------------------------------------------------------------------------- /FFT/Closed_Loop_FFT.m: -------------------------------------------------------------------------------- 1 | function [allVec,allTs,allTs_marker,allTs_trigger] = Closed_Loop_FFT() 2 | % Closed-loop algorithm using fft method to detect peak 3 | % allVec: Raw EEG (channel*sample) 4 | % allTs: Timestamp of each sample 5 | % allTs_marker: Timestamp of event markers 6 | % allTs_trigger: Timestamp of the sample at which triggere was delivered 7 | %% Parameters 8 | elec_interest = [47 13 14 16 17 44 45 46 48]; % ['Electrode of interest' 'Surrounding electrodes']; 9 | fnative = 10000; % Native sampling rate 10 | fs = 1000; % Processing sampling rate 11 | TrigInt = 2; % Minimum interval between trials 12 | win_length = fs/2; % Window length for online processing 13 | targetFreq = [8 13]; % Band of interest in Hz 14 | desired_phase = 0; % Targeted phase 15 | technical_delay = 8; % Technical delay in ms 16 | delay_tolerance = 5; % Delay tolerance in ms 17 | %% Initialization 18 | trig_timer = tic; % Used for timing between triggers 19 | downsample = floor(fnative/fs); 20 | allVec = nan(64, 100000); 21 | allTs = nan(1,100000); 22 | ft_defaults; 23 | %% Initialize LPT Port 24 | % initialize access to the inpoutx64 low-level I/O driver 25 | config_io; 26 | % optional step: verify that the inpoutx64 driver was successfully initialized 27 | global cogent; 28 | if( cogent.io.status ~= 0 ) 29 | error('inp/outp installation failed'); 30 | end 31 | % write a value to the LPT output port 32 | address = hex2dec('C020'); 33 | outp(address, 0); 34 | %% Close prieviously opened inlet streams in case it was not closed properly 35 | try 36 | inlet.close_stream(); 37 | inlet_marker.close_stream(); 38 | catch 39 | end 40 | %% instantiate the library 41 | disp('Loading the library...'); 42 | lib = lsl_loadlib(); 43 | 44 | % resolve a stream... 45 | disp('Resolving an EEG stream...'); 46 | result_eeg = {}; 47 | while isempty(result_eeg) 48 | result_eeg = lsl_resolve_byprop(lib,'type','EEG'); 49 | end 50 | 51 | result_marker = {}; 52 | while isempty(result_marker) 53 | result_marker = lsl_resolve_byprop(lib,'type','Markers'); 54 | end 55 | 56 | % create a new inlet 57 | disp('Opening an inlet...'); 58 | inlet = lsl_inlet(result_eeg{1}); 59 | inlet_marker = lsl_inlet(result_marker{1}); 60 | %% 61 | disp('Now receiving data...'); 62 | sample = 0; % Number of samples recieved 63 | downsample_idx = 10; % Index used for downsampling 64 | allTs_trigger = []; 65 | allTs_marker = []; 66 | 67 | while 1 68 | [vec,ts] = inlet.pull_sample(1); 69 | [~,ts_marker] = inlet_marker.pull_chunk(); 70 | allTs_marker = [allTs_marker ts_marker]; 71 | if isempty(vec) 72 | break; % End cycle if didn't receive data within certain time 73 | end 74 | if downsample_idx == downsample 75 | sample = sample+1; 76 | allVec(:,sample) = vec'; 77 | allTs(:,sample) = ts; 78 | downsample_idx = 1; 79 | if sample >= win_length && toc(trig_timer) > TrigInt % Enough samples & enough time between triggers 80 | if length(elec_interest) == 1 81 | chunk = allVec(elec_interest,sample-win_length+1:sample)-allVec(64,sample-win_length+1:sample); 82 | else 83 | ref = mean(allVec(elec_interest(2:end),sample-win_length+1:sample)); 84 | chunk = allVec(elec_interest(1),sample-win_length+1:sample)-ref; 85 | end 86 | chunk_filt = ft_preproc_bandpassfilter(chunk, fs, targetFreq, [], 'fir','twopass'); 87 | 88 | Xf = fft(chunk_filt,4096); 89 | [~,idx] = max(abs(Xf)); 90 | f_est = (idx-1)*fs/length(Xf); % Estimated frequecy 91 | phase_est = angle(Xf(idx)); % Estimated phase at the beggining of winddow 92 | phase = mod(2*pi*f_est*(win_length-1)/fs+phase_est,2*pi); % Current sample phase 93 | phase = wrapToPi(phase); 94 | 95 | if abs((desired_phase-phase)*fs/f_est/2/pi-technical_delay) <= delay_tolerance 96 | outp(address, 32); 97 | trig_timer = tic; % Reset timer after each trigger 98 | pause(0.015); 99 | outp(address, 0); 100 | allTs_trigger = [allTs_trigger ts]; 101 | disp('Stim'); 102 | end 103 | end 104 | else 105 | downsample_idx = downsample_idx + 1; 106 | end 107 | end 108 | 109 | inlet.close_stream(); 110 | inlet_marker.close_stream(); 111 | disp('Finished receiving'); 112 | clear cogent 113 | disp('Closed LPT Port'); 114 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 OpitzLab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Real-time phase detection for closed-loop EEG-TMS 2 | Matlab Codes for resting-state EEG recording and 3 methods of real-time EEG phase estimation for closed-loop EEG-TMS: 3 | 1) Autoregression-based prediction (AR), 4 | 2) FFT-based prediction (FFT), and 5 | 3) Educated Temporal Prediction (ETP). 6 | 7 | Real-time EEG data acquisition is done using LSL (https://github.com/sccn/labstreaminglayer). 8 | Triggers to the TMS machine are delivered from the parallel port on the PC using InpOutx64 DLL (http://www.highrez.co.uk/downloads/inpout32/). 9 | The code runs in MATLAB (MathWorks) 2014b+ and requires the FieldTrip toolbox (https://github.com/fieldtrip/fieldtrip). 10 | 11 | For more information, refer to Shirinpour et al., Experimental Evaluation of Methods for Real-Time EEG 12 | Phase-Specific Transcranial Magnetic Stimulation, 2020, Journal of Neural Engineering, doi: https://doi.org/10.1088/1741-2552/ab9dba 13 | 14 | Correspondence: shiri008 -at- umn.edu (Sina Shirinpour), aopitz -at- umn.edu (Alex Opitz) 15 | 16 | For a further implementations of the approch by our lab see: 17 | Miles Wischnewski, Zachary J. Haigh, Sina Shirinpour, Ivan Alekseichuk, Alexander Opitz, The phase of sensorimotor mu and beta oscillations has the opposite effect on corticospinal excitability, Brain Stimulation, 2022. 18 | https://www.sciencedirect.com/science/article/pii/S1935861X22001838 19 | -------------------------------------------------------------------------------- /Rest/Closed_Loop_Rest.m: -------------------------------------------------------------------------------- 1 | function [allVec_rest,allTs_rest] = Closed_Loop_Rest() 2 | % Record resting state EEG 3 | % allVec_rest: Resting EEG (channel*sample) 4 | % allTs_rest: Timestamp of each sample 5 | %% Parameters 6 | fnative = 10000; % Native sampling rate 7 | fs = 1000; % Processing sampling rate 8 | %% Initialization 9 | downsample = floor(fnative/fs); 10 | allVec_rest = nan(64, 100000); 11 | allTs_rest = nan(1,100000); 12 | %% Close prieviously opened inlet streams in case it was not closed properly 13 | try 14 | inlet.close_stream(); 15 | catch 16 | end 17 | %% instantiate the library 18 | disp('Loading the library...'); 19 | lib = lsl_loadlib(); 20 | 21 | % resolve a stream... 22 | disp('Resolving an EEG stream...'); 23 | result_eeg = {}; 24 | while isempty(result_eeg) 25 | result_eeg = lsl_resolve_byprop(lib,'type','EEG'); 26 | end 27 | 28 | % create a new inlet 29 | disp('Opening an inlet...'); 30 | inlet = lsl_inlet(result_eeg{1}); 31 | %% 32 | disp('Now receiving data...'); 33 | sample = 0; % Number of samples recieved 34 | downsample_idx = 10; % Index used for downsampling 35 | 36 | while 1 37 | [vec,ts] = inlet.pull_sample(1); 38 | if isempty(vec) 39 | break; % End cycle if didn't receive data within certain time 40 | end 41 | if downsample_idx == downsample 42 | sample = sample+1; 43 | allVec_rest(:,sample) = vec'; 44 | allTs_rest(:,sample) = ts; 45 | downsample_idx = 1; 46 | else 47 | downsample_idx = downsample_idx + 1; 48 | end 49 | end 50 | 51 | inlet.close_stream(); 52 | disp('Finished receiving'); 53 | end --------------------------------------------------------------------------------