From 1480bf60ca1d98752effc644f8d16ffdfcc88d97 Mon Sep 17 00:00:00 2001 From: adamr Date: Wed, 31 May 2023 10:51:21 +0200 Subject: [PATCH] possibility to modify plot length added --- EStiMo_GUI_0123.py | 41 +++++++++++++------------ FirstWindow.py | 8 +++-- Functions.py | 11 ++++++- RDA.py | 28 +++++++++++++++-- TMS_protocol.txt | 1 + __pycache__/FirstWindow.cpython-38.pyc | Bin 32636 -> 32827 bytes __pycache__/Functions.cpython-38.pyc | Bin 5311 -> 5508 bytes __pycache__/RDA.cpython-38.pyc | Bin 5689 -> 6233 bytes 8 files changed, 64 insertions(+), 25 deletions(-) diff --git a/EStiMo_GUI_0123.py b/EStiMo_GUI_0123.py index 9af10d4..6118651 100644 --- a/EStiMo_GUI_0123.py +++ b/EStiMo_GUI_0123.py @@ -146,7 +146,7 @@ class NeurOneOffline(): sendqueue=False,ringbuf_factor=2,dump=False,avgPackets=1): self.ringbuffersize = ringbuffersize tmp_path = '/mnt/projects/P_BCT_EEG/DLPFCM1_iTBS/DLPFC/nobeep/subj_8/X47851_iTBS.vhdr'# - #'/mnt/projects/P_BCT_EEG/DLPFCM1_iTBS/DLPFC/beep/subj_14/X13193_adam.vhdr' #'/mnt/projects/P_BCT_EEG/DLPFCM1_iTBS/DLPFC/beep/subj_6/X77384_iTBS.vhdr' + #'/mnt/projects/P_BCT_EEG/DLPFCM1_iTBS/DLPFC/beep/subj_14/X13193_adam.vhdr' #'/mnt/projects/P_BCT_EEG/DLPFCM1_iTBS/DLPFC/beep/subj_6/X7738_iTBS.vhdr' num_electr = 18 eeg_chn = np.arange(0,num_electr,1) hdr = mne.io.read_raw_brainvision(tmp_path) @@ -215,7 +215,7 @@ def acquire_data(q, size, run, speed, downsample, sleep_time, ip = '192.168.200. run: Value class from multiprocessing library. That value can be changed in main process downsample: boolean value. Says if data will be downsampled to 1000 Hz sleep_time: int, set how often function should refresh. Usually it takes a bit more that that""" - offline = 'offline' + # offline = 'offline' #import NeurOne_v3 if offline=="offline": NO = NeurOneOffline() @@ -247,7 +247,7 @@ class AppForm(QMainWindow): def __init__(self, passed_params = None, parent=None): super().__init__() #self.setStyleSheet("background: whitesmoke") - # self.offline = 'NeurOne' #"BrainProducts"#False + self.offline = "offline" #'NeurOne' #"BrainProducts"#False self.time_start = time.time() QMainWindow.__init__(self, parent) self.montage_file_path = 'montage_18ch.csv' @@ -416,11 +416,12 @@ class AppForm(QMainWindow): self.remove_outliers = passed_params['remove_outliers'] self.ip = passed_params['ip'] self.port = passed_params['port'] - self.offline = passed_params['offline'] + self.offline = self.offline if not None else passed_params['offline'] self.exp_trig = passed_params['exp_trig'] self.exp_time = passed_params['exp_time'] self.if_percentage = passed_params['percentages'] self.received_thr_values = passed_params['thr_values'] + self.plot_len = passed_params['plot_len'] else: self.montage_file_path = 'montage_18ch.csv' self.time_between_bursts = int(settings_file[settings_file[0]=='time_between_trains'].values[0][1]) @@ -442,6 +443,7 @@ class AppForm(QMainWindow): self.ip = '192.168.200.201' self.port = 50000 self.offline = False + self.plot_len = 4 #length of data to plot (last seconds of data array) self.unit_label = np.array(self.unit_label)[self.used_features] self.log_file_writer.writerow(['time', 'state', self.used_features]) @@ -490,7 +492,6 @@ class AppForm(QMainWindow): self.theta_band= json.loads(settings_file[settings_file[0]=='theta_range'].values[0][1]) self.colors = ['b', 'm', 'r', 'k', 'c', ] #colors used for lines if less that 6 of them self.data_len = 30*self.Fs #length of the data array in seconds - self.plot_len = 4 #length of data to plot (last seconds of data array) self.plot_dividing_factor = 100 self.previous_state = np.zeros(6) if self.num_of_lines>5: #if more than 5 lines then colors of them from colormap @@ -513,9 +514,9 @@ class AppForm(QMainWindow): self.create_main_frame() #create plots, buttons, figures etc... #create data array - self.loaded = np.zeros([self.num_of_ch,self.data_len]) - self.loaded_full = np.zeros([self.num_of_ch+1,self.data_len]) - self.data = np.random.rand(self.num_of_ch,self.data_len) + self.loaded = np.full([self.num_of_ch,self.data_len], None) + self.loaded_full = np.full([self.num_of_ch+1,self.data_len], None) + self.data = np.full((self.num_of_ch,self.data_len), None) self.trigg_data = np.zeros(self.data_len) #array to keep trigger data in self.num=0 self.doit=0 #to count number of seconds after last stimuli in train @@ -742,7 +743,7 @@ class AppForm(QMainWindow): self.timer.setInterval(int(self.speed_general*1.03)) times = time.time() - self.offline='offline' #remove this! + # self.offline='offline' #remove this! if self.offline=="offline": incl = [0,2,6,7,8,10,13,16,18,22,25,28,31,34,41,43,-3,-2,-1] # For offline only loaded_temp = self.q.get()[incl]/10 # Load data @@ -761,7 +762,6 @@ class AppForm(QMainWindow): except ValueError: print("ValueError, wait...") return 0 - self.loaded_noeye = self.loaded.copy() step1 = time.time()-time_start @@ -787,7 +787,7 @@ class AppForm(QMainWindow): # [A,B] = ss.butter(2, 0.1/(self.Fs/2), 'highpass') # self.loaded[:self.num_of_ch,-4*self.Fs:] = ss.filtfilt(A, B, self.loaded[:self.num_of_ch, -4*self.Fs:]) # self.loaded[:self.num_of_ch,-4*self.Fs:] = self.loaded[:self.num_of_ch,-4*self.Fs:] - np.mean(self.loaded[:self.num_of_ch,-4*self.Fs:],1, keepdims=True) - self.loaded[:self.num_of_ch,-4*self.Fs:] = ss.detrend(self.loaded[:self.num_of_ch,-4*self.Fs:]) + self.loaded[:self.num_of_ch,-self.plot_len*self.Fs:] = ss.detrend(self.loaded[:self.num_of_ch,-self.plot_len*self.Fs:]) except ZeroDivisionError: # This error means that buffer is still not full if self.qmbx == None: self.qmbx = Waiting_window() # Small window with a message to wait @@ -826,9 +826,10 @@ class AppForm(QMainWindow): # print(len(self.loaded)) # Interpolation - pretty long line, but basically it chooses ranges and # assign boundary value as a baseline and does that in (I guess) more optimal way than using loops - self.loaded[:, od+ind-int(int_from*self.Fs):od+ind+int(int_to*self.Fs)] = np.outer( - self.loaded[:,min(od+ind+int(int_to*self.Fs), 30000-1)], np.ones(min(size, int((int_from+int_to)*self.Fs)))) - + # self.loaded[:, od+ind-int(int_from*self.Fs):od+ind+int(int_to*self.Fs)] = np.outer( + # self.loaded[:,min(od+ind+int(int_to*self.Fs), 30000-1)], np.ones(min(size, int((int_from+int_to)*self.Fs)))) + # this way is even easier... + self.loaded[:, od+ind-int(int_from*self.Fs):od+ind+int(int_to*self.Fs)] = self.loaded[:,min(od+ind+int(int_to*self.Fs), 30000-1)].reshape(-1, 1) # for i in range(self.loaded.shape[0]): # self.loaded[i, od+ind-int(int_from*self.Fs):od+ind+int(int_to*self.Fs)] = np.linspace( # self.loaded[i,min(od+ind-int(int_from*self.Fs),30000-1)], @@ -934,6 +935,7 @@ class AppForm(QMainWindow): #set data, last plot_len seconds plot_data = self.data[i,self.data_len-self.plot_len* self.Fs:self.data_len] + plot_data = plot_data[::self.plot_len] #lets speed up plotting by downsampling #EMG has higher amplitude usually. A special case to make it smaller #self.emg_ch+1 because self.num_of_ch doesn't include trigger if i==np.arange(self.num_of_ch)[self.emg_ch+1] and self.emg_ch!='': @@ -945,12 +947,12 @@ class AppForm(QMainWindow): else: plot_data = plot_data/self.plot_dividing_factor + self.num_of_ch - i # plot_data = plot_data/(3.5*np.max(np.abs(plot_data))) + self.num_of_ch - i - self.line[i].set_data(np.arange(0,self.plot_len*self.Fs), plot_data) + self.line[i].set_data(np.arange(0,self.Fs), plot_data) #self.plot_len* self.axes.set_ylim(0, self.num_of_ch+1) #self.axes.set_ylim(0,np.max(self.data[:,-self.plot_len*self.Fs:])+dif) #set ylim to fit everything on the plot if len(stim)>0: for ind in stim: - self.axes.axvline(ind) #plot vertical line for each trigger + self.axes.axvline(int(ind/self.plot_len)) #plot vertical line for each trigger self.num = len(stim) # plt.figure() # plt.plot(ss.detrend(self.data[10,self.data_len-self.plot_len* @@ -1248,7 +1250,7 @@ class AppForm(QMainWindow): self.axes.set_yticks(np.arange(1, (self.num_of_ch)*1.01, 1)) self.axes.set_yticklabels(self.ch_names[::-1]) - self.axes.set_xticks(np.arange(self.Fs,self.plot_len*self.Fs, self.Fs)) + self.axes.set_xticks(np.arange(int(self.Fs/self.plot_len), self.Fs, int(self.Fs/self.plot_len))) #self.plot_len* self.axes.grid(True) self.canvas = FigureCanvas(self.fig) @@ -1279,7 +1281,7 @@ class AppForm(QMainWindow): self.line[i], = self.axes.plot([] , color = 'black', linewidth=0.4) else: self.line[i], = self.axes.plot([] , color = 'silver', linewidth=0.3) - self.axes.set_xlim(0, self.plot_len*self.Fs) + self.axes.set_xlim(0, self.Fs) #self.plot_len* self.axes.set_ylim(0, (self.num_of_ch+1)*1) #self.axes.axvspan((self.plot_len-1)*self.Fs, # self.plot_len*self.size_of_up, alpha=0.3, color='lightcoral') @@ -1405,9 +1407,10 @@ class Ui(QMainWindow): if __name__ == '__main__': app = QApplication(sys.argv) + app.setQuitOnLastWindowClosed(True) form = First_window(AppForm) #AppForm() form.show() - app.exec_() + sys.exit(app.exec_()) # cut time different from both sides # some deafult settings. Maybe remember last configuration? diff --git a/FirstWindow.py b/FirstWindow.py index e84ffd7..58c5ebe 100644 --- a/FirstWindow.py +++ b/FirstWindow.py @@ -286,6 +286,7 @@ class First_window(QMainWindow): self.emg_ch_loaded = int(settings_file[settings_file[0]=='emg_channel'].values[0][1]) self.exp_trig_loaded = int(settings_file[settings_file[0]=='expected_triggers'].values[0][1]) self.exp_time_loaded = int(settings_file[settings_file[0]=='expected_time'].values[0][1]) + self.plot_len_loaded = int(settings_file[settings_file[0]=='plot_len'].values[0][1]) self.BoxChecked = False except Exception as e: ex_type, ex_value, ex_traceback = sys.exc_info() @@ -646,7 +647,8 @@ class First_window(QMainWindow): self.eog_ch_lab, self.line_eog_ch, eog_ch_layout = add_thing(self, "EOG channel number:", self.eog_ch_loaded) self.emg_ch_lab, self.line_emg_ch, emg_ch_layout = add_thing(self, "EMG channel number:", self.emg_ch_loaded) self.exp_trig_lab, self.line_exp_trig, exp_trig_layout = add_thing(self, "Number of bursts within the train:", self.exp_trig_loaded) - self.exp_time_lab, self.line_exp_time, exp_time_layout = add_thing(self, "Expected time of a single train:", self.exp_time_loaded) + self.exp_time_lab, self.line_exp_time, exp_time_layout = add_thing(self, "Expected time of a single train:", self.exp_time_loaded) + self.plot_len_lab, self.plot_len_time, plot_len_layout = add_thing(self, "Plot width [s]:", self.plot_len_loaded) # You can add feature name if function was added to the function "features" in the main file features_names = ['None', 'Theta FFT Power', 'Alpha FFT Power', 'Beta FFT Power', @@ -754,6 +756,7 @@ class First_window(QMainWindow): vbox.addLayout(emg_ch_layout) vbox.addLayout(exp_trig_layout) vbox.addLayout(exp_time_layout) + vbox.addLayout(plot_len_layout) scroll = QScrollArea() scroll.setWidget(text_last_ch) @@ -986,7 +989,8 @@ class First_window(QMainWindow): 'exp_trig': int(self.line_exp_trig.text()), 'exp_time': int(self.line_exp_time.text()), 'percentages': percentages, - 'thr_values': values + 'thr_values': values, + 'plot_len': int(self.plot_len_time.text()) } print(self.params_to_pass) diff --git a/Functions.py b/Functions.py index efa0ee9..4509a0a 100644 --- a/Functions.py +++ b/Functions.py @@ -75,6 +75,14 @@ def connect_sig(data1, data2, fs): fs: int sampling rate """ + print(data1.shape, data2.shape) + if all(data1[:, -fs] == None): + print(data2[:, -fs]) + data_ret = data1.copy() + data_ret[:, :-fs] = data2[:, -fs].reshape(-1, 1) + data_ret[:,-fs:] = data2[:, -fs:] + return data_ret, 800 + print(data2.shape) data2 = data2 startt = time.time() @@ -83,6 +91,7 @@ def connect_sig(data1, data2, fs): pts = list() data_ret = np.zeros(data1.shape) data_ret[:, :-fs] = data1[:,fs:] + if data2.shape[0]==0 or data2.shape[1]==0: return data2 if fs<2000: @@ -93,7 +102,7 @@ def connect_sig(data1, data2, fs): print("ARBEJDE IKKEEE") # data_ret = np.concatenate((data1, data2[:,-int(size):]),1) data_ret[:,-fs:] = data2[:, -fs:] - return data_ret, None + return data_ret, 800 #print('hehe', time.time()-startt) most_fr = most_frequent(np.array(pts)) #print('hehe', time.time()-startt) diff --git a/RDA.py b/RDA.py index 6edb91d..ac08dd1 100644 --- a/RDA.py +++ b/RDA.py @@ -15,6 +15,23 @@ import threading import queue import time +"""Packets are received every 20 ms in the size that it fits the sampling rate + +e.g.: + for 1000 Hz packet size will be 20, because 20*50=1000 + for 2500 Hz packet size will be 50, because 50*50=2500 + for 50 kHz it will be 1000, because 1000*50=50000 + """ +def average(arr, n, mode='mean'): + if mode=='max': + end = n * int(len(arr)/n) + return np.max(arr[:end].reshape(-1, n), 1) + arr = arr.T + data_raw_new = np.zeros((arr.shape[0], int(arr.shape[1]/n))) + for i in range(arr.shape[0]): + a = arr[i] + data_raw_new[i,:] = a.reshape(-1, n).mean(1) + return data_raw_new.T # Marker class for storing marker information class Marker: @@ -146,7 +163,6 @@ def sampleLoop(obj): elif msgtype == 4: # Data message, extract data and markers (block, points, markerCount, data, markers) = GetData(rawdata, channelCount) - if block!=0: ds=block-oldblock if ds!=1: @@ -174,7 +190,13 @@ def sampleLoop(obj): # Put data at the end of actual buffer data_array = data1s.reshape([int(len(data1s)/channelCount), channelCount]) * np.array(resolutions) data_array = np.vstack([data_array.T, marker_sig]).T #isn't that too slow? - obj.updateRingBuffer(data_array,block) + if obj.avgPackets: + resampling_coef = int((len(data)/channelCount)/20) + data1=average(data_array, resampling_coef, 'mean') + data1[:,-1]=average(data_array[:,-1], resampling_coef, 'max') + obj.updateRingBuffer(data1,block) + else: + obj.updateRingBuffer(data_array,block) data1s = [] @@ -194,7 +216,7 @@ def sampleLoop(obj): class RDA(): def __init__(self,ip='127.0.0.1', port=51244, buffersize=2**10, sendqueue=False, - si=1/1000, ringbuffersize = 2**12, avgPackets=False): + si=1/1000, ringbuffersize = 2**12, avgPackets=True): # Create a tcpip socket #con = socket(AF_INET, SOCK_STREAM) # Connect to recorder host via 32Bit RDA-port diff --git a/TMS_protocol.txt b/TMS_protocol.txt index dca2794..a6a5d5e 100644 --- a/TMS_protocol.txt +++ b/TMS_protocol.txt @@ -12,3 +12,4 @@ theta_range: [4,8] threshold_parameter: 2 expected_triggers: 10 expected_time: 2000 +plot_len: 4 diff --git a/__pycache__/FirstWindow.cpython-38.pyc b/__pycache__/FirstWindow.cpython-38.pyc index f1c07ac9cfa09674c6a5efa84377db3571e9d5c7..5791e54b781b8038e93c0e51576583d7011da636 100644 GIT binary patch delta 4982 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 TyRF&8kYjr-j*aMhw$t_pgGCFa delta 1143 zcma)4&r4KM6h7y^H*em1GxKJg8BOh%35uhWxoDB;N~|_%hDsUOq(03bq;=0#G=QYlyU$Fc4)?fs&zM9(`F2DxLlYyno zHSCA+m6E-ZS5`SnBLrRn_*S)*U12ejxK|nr ztNK<+8|j@97L0+Q>%(x7r~;RY#*|f|sg#@m0z(mLIj)=vD$|cP!h)%sH9-^G6~6Vi z9BODwIlkBj%;MYC*(#$R!n^3NR)^;(M^VzhRQ7UL&L5Yf%9#-~IsBhKLk5@UvX{vV zbwvFbQ~Ac`nB9&UiS$M!Jq#F}+m4cHfPU&6_vV@mfLub_95(egXI!6i>)lJXKM_n6 zSpxIqu|*qZg(nVRGudg;j_J^6(=L1TS9cxr zTE^P2TR)98XNtsFFwA@Jfok-TN;)*>V?rbv3TfNR9naa{Uv^@FHeCT z^V;;pV$P8K`elNln}IJ?A<0%h!F~FFmr7w1%6|PKF^W6-XX0SZ21hLdpjP5S4sEu3 zBbe$WhonK#R2cI5Ve$~3*RPYOjyxtuv5o*l9Swy4IHtlj^4O2Epfjm^>0yfJB65tj z9dmMSvACmOrCRaX=7*FJ@s7|vwRC-%s6VIcuuFeWpTL*8D^rV$`gG=#ca~GEU{+2S zXXRzxkuBgm{UE#laKOxBMAq<*r*p>O;9w5rmGCH+^!se*nNd2(eGHQfDTWq?c7`5? jK867X9+DhlC@{=1EHVW8WNlCKm~FW=wngZjFmmnx5M)oe^x#W-NNzc#Gt1bng*<}4ot>ST z-JP9}ho?S1UHoaGkhSppfdu~Nh5h0T``iB4f4t5oc#5a*vTFW{#eJT+Yw^q-yISB` zp1W&Ri#*Q@IF@*kmv9{7Lwp#=VLrk~aU4OdF+Pr3Ri4;<4?v^b{yfk;p*1$qz&%V$4uJj)Z++mljUo7^$#7exf)lVg<$0iRl zXp|*x;&I&Wp53sQt*t0=n;u@pyQZCXaO=?*_&s8HWFV+ywG&2CJ1r4uJL&@6Os7G_ zn?YA-wywSJ-xkn`wKw2+5JVdS=<=1cb^-v+BJ@y)3+;%A>pTx+P=jEn7Kt6rLd}AA zEhwwc*ozD4U|T@2AOG*j*2NqCQ~fj{wOJ;0RLM zuhqgRl(pJPb=H1m@wq3f&&t6JF3@6ZBlrwv)O&V)Y&%tPQ8sJI-%LA%hDQA1XM&FhNc&x+}+ElITM=b8FpJ zDAy!}(Z;fR%Pq4d^>_F4Nuzff6|~pIH3>&ac}YF=emQ%{MB$2%*P+uDAVU$qqRys{ zvX>vcmiiuJXVst5NB7Kmvfz=0Cw7e|V3v4u1oH$-1ZJmWNL7mP5;4vppl@28`b{IN zbL{|b3Rvm%Fic!}+`-+xV+6N@R!`{kKql5qo%4r}SADtL60K>~2UffnKKC>B zUG?kS_31HOHko6x>=6}|s=FpT-O7y8xg&e0kfGg}TyNg*Ws-l9m`*GK$Xz-mZb?it z$kW8b^b2;OL~>LSO*9Xlom@G1U3zxy%CIe<&4e$Nu8-abDm>q&!~YL)be~9J>gPX`aQf zm{3=X6}GN=#gEvUdbKo_hSzaJ_=$>3=h&+HXK8ii1TnS<06}Nbhv@4##A2He_YHzy z5g65H%K%VS(+@5V{gttd`eb-~>NZJIObmj-C=TINl}097dnBC>>RY`K1nknx_;DwU zcxMMUB(7*l6cP(ED+EG8IQnQMA0$e9fGG1X(;Mv5F&i6+T+s&qAG0A zUB}cP#!v21`_W$1YoiCv`;r@Ao8UEqX##3=aEf4wz|;+@7RF7+{8#O({bg+1y_UFO eqijrd!u2T){KvmcwvaDo{Gwm-C;eIf_5T6hhA_SW delta 1838 zcmZuxU2GIp6rQ^~GdsK8o&D)7I$x?Ci6VQy>)A{z_UDutIxZ5 z9@io-a39waFY*$uWz;J33TjnQtHSLGtI>V)w@}?ntvmPs#Yau2hwa-Il+;SOKBGKuVh}4EXy6uSp z($(Gt(Oeu2WUy2HW7k=)syWwd_ZjJW&_faq1ktR3y1MO*^lKLW&jn+lYogr><%EPV znjKWWyN&Hv2i?ExJ5U+=jfFOzNyrmpzZ%ZG+qcDu!f7GLA#Mwhp-6_+-OO$_bmx!E z>x>;xz1iJs+flWV1BWa;wrk>sEb*vB&_%GHz}!?os^LR}7y}5HiB{a4H?q2GA%J-S zBXK8^+U6KP>?6S!jo@O?>Ij|Llu0yGKV-{$wI`R`f{UcNH3J!RpHg^UJ;=VW>jau; zQUV`6{YX%WM(|6<)Nt-7JEh{>Ew<-Qx$7i5w(;C*-)j0+idnPPHSg-cYLQpxQ!pSi zymo^zi?eIHR;@XEW7{o@SyuD`ZK@NrMy5!r3#nXVf9X7d@YR(ZBNL!-RRX> zDt!PDENgeVGn0fX+D`BYdW}><*)S;`A~OexnNB1CMW9{rz7!GHsi>`;O(V{VfEa%z zDrTKjOerYOn;Fkq$k4V77chAsk}E-5Xm>G@L33WSiz@JYS-)EKcb1RP4Y`>R5^0K} z)p!2H(Z{8NL9%&>;3bmGwt_^SdR#p?N+&N999M^n)z@Ajp_DwVb5zNxhat~k^CVZc z7Auzo3+C&G0PNrKV;|4fT2H_vharr!vhribPZDTp}bTChi~|EY6N& zPJ~QgM153#m5r*O$`?k86wy3Ei*$2Xvl#(cHUW#oT_NZY%n>}@L_p)=mphj$U$Y_8 zE=)U|{@5`v-Ry%Tr8f!A5Rf75m?Ej&)t z5{t)+#G!#|4R_%Q{a-E$O@|4L{T76TyHoQc-?!_x00uF)MNVCCkvC$>?$lL1J zr^xfwrJmNYQ9AvE)ao8T+~%^uV8o5OL4w$$P#H9&@*b(i)|EX2Ii5+_qAkx;AKUX|sU_vp3#tmk`0 L*Q