-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPyVibAnalyze.py
1115 lines (974 loc) · 50.4 KB
/
PyVibAnalyze.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""
Created on Thu Jul 14 14:31:59 2016
This program analyzes the data sets added in by the user. All data sets of MScan are imported from the folder chosen by the user, and it
graphs the current chosen data set's magnitude, phase, and gain, as well as the average of all data sets' magnitude, phase, and gain. The
average graph also calculates and graphs the standard error of mean. The data sets can be filtered through the category comboboxes, and the
data sets calculated in the averages can also be filtered depending on the category chosen in the average category comboboxes. The project is
saved as a pickle (*.p) file. The Characteristic Frequency and Q10dB are calculated and displayed just above the Magnitude graph. If the data set
does not mention whether a mouse is dead or alive, a message box will pop up to ask the user the mouse's status, then calculate CF. The Q10dB is
also displayed as a dashed red line on the Magnitude graph. The noise floor/threshold can also be set, and it will show up on the Magnitude
graph as a dashed black line. All data points below the noise floor are NaNs. The phase shift is also calculated for each data set, and done so
before the average phase is calculated. The phase shift can be unchecked anytime to see the phase before it has been shifted. All data sets can
also be exported to Excel, which includes each individual data set's magnitude, shifted phase, and gain, as well as the averages and standard
error for each average. The averages are split into all possible different combinations based on the categories listed in the combo boxes. A
dialog box will pop up to ask what the user would like to name the Excel files containing the averages. The individual data sets will be named
simply based on the data set name. All graphs are labelled, for both x and y axis, and title. Also, each graph can be exported as an image by
right-clicking the desired graph, choosing export, then choosing image, and hitting the export button.
@author: OHNS, Jacqueline Yau, John Oghalai, Patrick Raphael
"""
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from collections import defaultdict
from scipy import interpolate, stats
import os
import sys
import traceback
import numpy as np
import pickle
import math
import pyqtgraph as pg
import xlsxwriter
# for Windows uncomment this line
#sys.path.append("C:\\PyVib\\src")
# for MacOS X uncomment this line
sys.path.append('/Volumes/NO NAME/OCT Mscan analysis program/PyVib src')
import MScan
#Set to a white background
pg.setConfigOption('background', 'w')
#form_class = uic.loadUiType(os.path.join("..", "ui", "PyOCT.ui"))[0] # Load the UI
form_class = uic.loadUiType("pyvibanalyze.ui")[0] # Load the UI
class PyVibAnalyzeWindowClass(QtGui.QMainWindow, form_class):
def __init__(self, parent=None):
QtGui.QMainWindow.__init__(self, parent)
try:
self.setupUi(self)
except Exception as ex:
print(format(ex))
self.addDataSets_pushBiutton.clicked.connect(self.addDataSets)
self.excludeDataSet_checkBox.setChecked(False)
self.phaseShift_checkBox.setChecked(True)
self.logX_checkBox.setChecked(False)
self.lineEdit.setText("0.0")
self.CF_lineEdit.setText("0.0")
self.CF_lineEdit.setReadOnly(True)
self.Q10db_lineEdit.setText("0.0")
self.Q10db_lineEdit.setReadOnly(True)
self.N_lineEdit.setText("0")
self.N_lineEdit.setReadOnly(True)
self.apply_pushButton.clicked.connect(self.newCategories)
self.excludeDataSet_checkBox.stateChanged.connect(self.excludeData)
self.remove_pushButton.clicked.connect(self.removeCategories)
self.dataSet_comboBox.currentIndexChanged.connect(self.dataSetChanged)
self.dataSet_category_comboBox.currentIndexChanged.connect(self.filterData)
self.dataSet_category_comboBox_2.currentIndexChanged.connect(self.filterData)
self.dataSet_category_comboBox_3.currentIndexChanged.connect(self.filterData)
self.avg_category_comboBox.currentIndexChanged.connect(self.filterAverage)
self.avg_category_comboBox_2.currentIndexChanged.connect(self.filterAverage)
self.newProject_pushBiutton.clicked.connect(self.createNewProject)
self.saveProject_pushBiutton.clicked.connect(self.saveProject)
self.openProject_pushBiutton.clicked.connect(self.openProject)
self.lineEdit.returnPressed.connect(self.setThreshold)
self.phaseShift_checkBox.stateChanged.connect(self.phaseShiftPrep)
self.logX_checkBox.stateChanged.connect(self.phaseShiftPrep)
self.excel_pushButton.clicked.connect(self.exportToExcel)
# self.magPlot.mouseReleaseEvent = self.mouseCFCalculation
self.newProject()
self.setComboBoxDefaults()
#Each new project's data is contained in these data structures
def newProject(self):
self.dataSets = []
self.categories = []
self.dataSetNameDict = {}
#Keeps track of the data sets that are included in the average calculation
self.avgData = {}
#Keeps track of the chosen threshold for each data set
self.threshold = {}
#Keeps track of mouse status if there are data sets that did not label whether a mouse is dead or live
self.mouseStatus = {}
#Sets the default comboBox categories for data set comboBox and average comboBox
def setComboBoxDefaults(self):
self.categories.append("")
self.categories.append("dead")
self.categories.append("live")
self.categories.append("BM")
self.categories.append("TM")
self.categories.append("RL")
for name in self.categories:
self.dataSet_category_comboBox.addItem(name)
self.dataSet_category_comboBox_2.addItem(name)
self.dataSet_category_comboBox_3.addItem(name)
self.avg_category_comboBox.addItem(name)
self.avg_category_comboBox_2.addItem(name)
#Creates a new project and clears all plots and previous project data
def createNewProject(self):
self.newProject()
self.magPlot.clear()
self.phasePlot.clear()
self.gainPlot.clear()
self.avgMag_Plot.clear()
self.avgPhase_Plot.clear()
self.avgGain_Plot.clear()
self.dataSet_comboBox.clear()
self.dataSet_category_comboBox.clear()
self.dataSet_category_comboBox_2.clear()
self.dataSet_category_comboBox_3.clear()
self.avg_category_comboBox.clear()
self.avg_category_comboBox_2.clear()
self.setComboBoxDefaults()
self.categories_textEdit.clear()
self.excludeDataSet_checkBox.setChecked(False)
self.phaseShift_checkBox.setChecked(False)
self.lineEdit.setText("0.0")
self.CF_lineEdit.setText("0.0")
self.CF_lineEdit.setReadOnly(True)
self.Q10db_lineEdit.setText("0.0")
self.Q10db_lineEdit.setReadOnly(True)
self.N_lineEdit.setText("0")
self.N_lineEdit.setReadOnly(True)
#Saves the current project in a pickle file, allows user to choose directory to save in
#Files end with .p as pickle file
def saveProject(self):
#Keep track of all data in a large list
save = [self.dataSets, self.categories, self.dataSetNameDict, self.avgData, self.threshold, self.mouseStatus]
caption = "Save file"
fil = "Pickles (*.p)"
path = '.'
dataDir = QtGui.QFileDialog.getSaveFileName(self, caption, path, fil)
try:
pickle.dump(save, open(dataDir, "wb"))
except:
traceback.print_exc()
print('Could not save project at %s' % dataDir)
#Opens a previous project from a pickle file, user chooses from a file dialog box
#Can only open projects that are .p pickle files
def openProject(self):
caption = "Open file"
fil = "Pickles (*.p)"
path = '.'
dataDir = QtGui.QFileDialog.getOpenFileName(self, caption, path, fil)
try:
#Load in the data from the pickle file of the saved project
prev = pickle.load(open(dataDir, "rb"))
self.dataSets = prev[0]
self.categories = prev[1]
self.dataSetNameDict = prev[2]
self.avgData = prev[3]
self.threshold = prev[4]
self.mouseStatus = prev[5]
#Set N for number of data sets in average calculation
self.N_lineEdit.setText(str(len(self.avgData)))
self.dataSet_comboBox.clear()
for n in self.dataSetNameDict:
self.dataSet_comboBox.addItem(n)
self.dataSet_category_comboBox.clear()
self.dataSet_category_comboBox_2.clear()
self.dataSet_category_comboBox_3.clear()
#Sets the category comboBoxes to have the right categories
for s in self.categories:
if self.dataSet_category_comboBox.findText(s) == -1:
self.dataSet_category_comboBox.addItem(s)
self.dataSet_category_comboBox_2.addItem(s)
self.dataSet_category_comboBox_3.addItem(s)
self.categories_textEdit.clear()
self.averagePlot()
except:
traceback.print_exc()
print('Could not open project at %s' % dataDir)
#Sets the noise floor/threshold whenever user inputs a new number into the Line Edit
def setThreshold(self):
current = self.dataSet_comboBox.currentText()
floor = self.lineEdit.text()
num = float(floor)
self.threshold[current] = num
self.plotDataSet(self.dataSetNameDict[current], current)
self.averagePlot()
#Filters out the data sets to be averaged together
def filterAverage(self):
self.avgData.clear()
curText1 = self.avg_category_comboBox.currentText()
curText2 = self.avg_category_comboBox_2.currentText()
if curText1 != "" or curText2 != "":
for name in self.dataSetNameDict:
check = True
#Case insensitive by turning the strings to lower-case first
temp = name.lower()
text1 = curText1.lower()
text2 = curText2.lower()
if (temp.find(text1) == -1) or (temp.find(text2) == -1):
check = False
if check == True:
self.avgData[name] = self.dataSetNameDict[name]
#If there are no filters chosen
else:
for name in self.dataSetNameDict:
self.avgData[name] = self.dataSetNameDict[name]
self.N_lineEdit.setText(str(len(self.avgData)))
self.averagePlot()
#Exclude data from average by removing it from the avgData dictionary, includes again if checkbox is unchecked
def excludeData(self):
if bool(self.dataSetNameDict) == True:
if self.excludeDataSet_checkBox.isChecked() == True:
curData = self.dataSet_comboBox.currentText()
if curData in self.avgData:
del self.avgData[curData]
else:
curData = self.dataSet_comboBox.currentText()
if curData not in self.avgData:
self.avgData[curData] = self.dataSetNameDict[curData]
self.N_lineEdit.setText(str(len(self.avgData)))
self.averagePlot()
#Adds new categories to the combo boxes for data set and average
def newCategories(self):
string = self.categories_textEdit.toPlainText()
cat = str(string)
for words in cat.split():
if (words not in self.categories):
self.categories.append(words)
for s in self.categories:
if self.dataSet_category_comboBox.findText(s) == -1:
self.dataSet_category_comboBox.addItem(s)
self.dataSet_category_comboBox_2.addItem(s)
self.dataSet_category_comboBox_3.addItem(s)
self.avg_category_comboBox.addItem(s)
self.avg_category_comboBox_2.addItem(s)
#Allows for filtering for multiple categories exactly, and search is case-insensitive
def filterData(self):
self.dataSet_comboBox.clear()
curText1 = self.dataSet_category_comboBox.currentText()
curText2 = self.dataSet_category_comboBox_2.currentText()
curText3 = self.dataSet_category_comboBox_3.currentText()
if curText1 != "" or curText2 != "" or curText3 != "":
#Need to iterate from the map, not combobox because combobox is being modified
for name in self.dataSetNameDict:
check = True
#Case insensitive by turning the strings to lower-case first
temp = name.lower()
text1 = curText1.lower()
text2 = curText2.lower()
text3 = curText3.lower()
if (temp.find(text1) == -1) or (temp.find(text2) == -1) or (temp.find(text3) == -1):
check = False
if check == True:
self.dataSet_comboBox.addItem(name)
#If there are no filters chosen
else:
for name in self.dataSetNameDict:
self.dataSet_comboBox.addItem(name)
#Removes categories from the category combo boxes for data set and average
def removeCategories(self):
string = self.categories_textEdit.toPlainText()
rem = str(string)
for words in rem.split():
if (words in self.categories):
self.categories.remove(words)
index = self.dataSet_category_comboBox.findText(words)
self.dataSet_category_comboBox.removeItem(index)
self.dataSet_category_comboBox_2.removeItem(index)
self.dataSet_category_comboBox_3.removeItem(index)
self.avg_category_comboBox.removeItem(index)
self.avg_category_comboBox_2.removeItem(index)
else:
print("This category does not exist")
#Prepares the correct data set that needs phase shifting
def phaseShiftPrep(self):
cur = self.dataSet_comboBox.currentText()
self.plotDataSet(self.dataSetNameDict[cur], cur)
#Rounds down to the nearest multiple of mul
def round_down(self, dif, mul):
return math.floor(dif / mul) * mul
#Shifts the phase so all phase is within 2*PI of the 80dB curve
def phaseShift(self, magResp, phase, freq, amp):
resultPhase = np.zeros((len(freq), len(amp)))
resultPhase[:, :] = np.NaN
check = False
for i in range(len(freq)):
for j in range(len(amp)):
#Curve of the 80dB phase
if j != len(amp) - 1:
index = len(amp) - 1
#If current phase curve has NaN
if np.isnan(phase[i][j]) == True:
resultPhase[i][j] = phase[i][j]
#Case when the 80dB curve has NaN at a point
elif np.isnan(phase[i][len(amp) - 1]) == True:
#Iterate through all curves to find one that is not NaN
for k in range(len(amp) - 1):
if (np.isnan(phase[i][len(amp) - (k + 2)]) == False):
index = len(amp) - (k + 2)
check = True
break
#All curves have NaN at this point, so this point is NaN
if check == False:
index = np.NaN
resultPhase[i][j] = np.NaN
#Phase shifting/adjusting
if np.isnan(index) == False:
dif = phase[i][j] - phase[i][index]
if dif > (2 * np.pi):
adjust = self.round_down(dif, 2 * np.pi)
result = phase[i][j] - adjust
resultPhase[i][j] = result
elif dif < -(2 * np.pi):
adjust = self.round_down(math.fabs(dif), 2 * np.pi)
result = phase[i][j] + adjust
resultPhase[i][j] = result
else:
resultPhase[i][j] = phase[i][j]
#Keep the original data points of the 80db curve
else:
resultPhase[i][j] = phase[i][j]
return resultPhase
#Takes in the a data set's magResp, phase, freq, and amp, finds the NaNs, and returns
#the corrected magResp and phase
def findNaNs(self, magResp, phase, freq, amp, setName):
resultMag = np.zeros((len(freq), len(amp)))
resultMag[:, :] = np.NaN
resultPhase = np.zeros((len(freq), len(amp)))
resultPhase[:, :] = np.NaN
thres = self.threshold[setName]
for i in range(len(freq)):
for j in range(len(amp)):
if magResp[i][j] >= thres:
resultMag[i][j] = magResp[i][j]
resultPhase[i][j] = phase[i][j]
else :
resultMag[i][j] = np.NaN
resultPhase[i][j] = np.NaN
return resultMag, resultPhase
#Plots the selected data set in the combo box
def plotDataSet(self, dataSet, setName):
audioParams = dataSet.audioParams
magResp = dataSet.magResp
freq = audioParams.freq[0, :]
amp = audioParams.amp
#Unwrapping the phase data along the frequency axis
phase = dataSet.phaseResp
self.phasePlot.clear()
unWrapThreshold = np.pi
phase = np.unwrap(phase, discont=unWrapThreshold, axis=0)
self.magPlot.clear()
#If threshold/noise floor is set, finds the NaNs in the magResp and applies to phase as well
magRespNew, phase = self.findNaNs(magResp, phase, freq, amp, setName)
#Shifts the phase if check box is checked
if self.phaseShift_checkBox.isChecked() == True:
phase = self.phaseShift(magRespNew, phase, freq, amp)
for a_i in range(len(amp)):
factor = 200 / len(amp)
red = 200 - a_i * factor
green = 200 - a_i * factor
#Creates a different shade of blue for each curve
coloredPen = pg.mkPen(color=(red, green, 255), width=2)
self.magPlot.plot(freq, magResp[:, a_i], pen=coloredPen)
self.phasePlot.plot(freq, phase[:, a_i], pen=coloredPen)
self.magPlot.setLogMode(x=False, y=True)
self.phasePlot.setLogMode(x=False)
self.gainPlot.setLogMode(x=False)
#Checks if the user wants x-axis to be plotted in log as well
if self.logX_checkBox.isChecked() == True:
self.magPlot.setLogMode(x=True)
self.phasePlot.setLogMode(x=True)
self.gainPlot.setLogMode(x=True)
#Calculates the CF
self.calculateCF(magRespNew, freq, amp, setName)
#Plots the gain graph
self.plotGain(magRespNew, freq, amp)
#If a threshold is set, graph the threshold line, in a dashed black line
if self.threshold[setName] != 0:
#Create the threshold line
line = []
for i in range(len(freq)):
line.append(self.threshold[setName])
#Plot the threshold line
pen = pg.mkPen('k', style=QtCore.Qt.DashLine)
self.magPlot.plot(freq, line, pen=pen)
#Setting the labels on axes
self.magPlot.setTitle('Magnitude Response at Stim Frequency')
self.magPlot.setLabel('left', 'Response', units='nm')
self.magPlot.setLabel('bottom', 'Stim Frequency', units='kHz')
self.phasePlot.setTitle('Phase at Stim Frequency')
self.phasePlot.setLabel('bottom', 'Stim Frequency', units='kHz')
self.phasePlot.setLabel('left', 'Phase', units='rad')
#Plots the gain graph of the current data set
def plotGain(self, magResp, freq, amp):
self.gainPlot.clear()
gain = np.zeros((len(freq), len(amp)))
gain[:, :] = np.NaN
#Calculate the gain for each data point
for j in range(len(amp)):
pascal = (2 * math.pow(10,-5)) * math.pow(10, (amp[j] / 20))
for i in range(len(freq)):
gain[i][j] = magResp[i][j] / pascal
for k in range(len(amp)):
factor = 200 / len(amp)
red = 200 - k * factor
green = 200 - k * factor
#Creates a different shade of blue for each curve
coloredPen = pg.mkPen(color=(red, green, 255), width=2)
self.gainPlot.plot(freq, gain[:, k], pen=coloredPen)
self.gainPlot.setLogMode(y=True)
#Setting the labels on gain graph
self.gainPlot.setTitle('Gain at Stim Frequency')
self.gainPlot.setLabel('left', 'Gain', units='nm/Pa')
self.gainPlot.setLabel('bottom', 'Stim Frequency', units='kHz')
#Calculates the gain from the magnitude
def calculateGain(self, magResp, freq, amp):
gain = np.zeros((len(freq), len(amp)))
gain[:, :] = np.NaN
#Calculate the gain for each data point
for j in range(len(amp)):
pascal = (2 * math.pow(10,-5)) * math.pow(10, (amp[j] / 20))
for i in range(len(freq)):
gain[i][j] = magResp[i][j] / pascal
return gain
#Plots the average graphs of all the data
def averagePlot(self):
freqlen = 0
amplen = 0
frequency = []
amplitude = []
#A 2D array where each element holds a list for all the data points of that respective position
allArraysMag = defaultdict(lambda : defaultdict(list))
allArraysPhase = defaultdict(lambda : defaultdict(list))
allArraysPhaseError = defaultdict(lambda : defaultdict(list))
allArraysMagError = defaultdict(lambda : defaultdict(list))
allArraysGain = defaultdict(lambda : defaultdict(list))
allArraysGainError = defaultdict(lambda : defaultdict(list))
if bool(self.avgData) == False:
self.avgMag_Plot.clear()
self.avgPhase_Plot.clear()
self.avgGain_Plot.clear()
else:
#Iterate through the data sets in the avgData dictionary, also unwrapping phase
for d in self.avgData:
dataSet = self.avgData[d]
audioParams = dataSet.audioParams
magResp = dataSet.magResp
phase = dataSet.phaseResp
unWrapThreshold = np.pi
phase = np.unwrap(phase, discont=unWrapThreshold, axis=0)
freq = audioParams.freq[0, :]
amp = audioParams.amp
freqlen = len(freq)
amplen = len(amp)
frequency = freq
amplitude = amp
#Calculate the gain
gain = self.calculateGain(magResp, freq, amp)
#Phase shift the phase first before averaging
phase = self.phaseShift(magResp, phase, freq, amp)
thres = self.threshold[d]
#Adds each data point from each data set to its corresponding list
#Need the error list because the stats.sem 0.16.1 version can't handle NaNs
for i in range(len(freq)):
for j in range(len(amp)):
if magResp[i][j] >= thres:
allArraysMag[i][j].append(magResp[i][j])
allArraysPhase[i][j].append(phase[i][j])
allArraysGain[i][j].append(gain[i][j])
if np.isnan(phase[i][j]) == False:
allArraysPhaseError[i][j].append(phase[i][j])
if np.isnan(magResp[i][j]) == False:
allArraysMagError[i][j].append(magResp[i][j])
if np.isnan(gain[i][j]) == False:
allArraysGainError[i][j].append(gain[i][j])
else:
allArraysMag[i][j].append(np.NaN)
allArraysPhase[i][j].append(np.NaN)
allArraysGain[i][j].append(np.NaN)
#Hold the resulting averages of each point
resultMag = np.zeros((freqlen, amplen))
resultMag[:, :] = np.NaN
resultPhase = np.zeros((freqlen, amplen))
resultPhase[:, :] = np.NaN
resultGain = np.zeros((freqlen, amplen))
resultGain[:, :] = np.NaN
#Hold the standard errors of mean for each data point
stdErrorMag = np.zeros((freqlen, amplen))
stdErrorMag[:, :] = np.NaN
stdErrorGain = np.zeros((freqlen, amplen))
stdErrorGain[:, :] = np.NaN
stdErrorPhase = np.zeros((freqlen, amplen))
stdErrorPhase[:, :] = np.NaN
#Average each list in the 2D array, to find the average for that corresponding point
for k in range(freqlen):
for z in range(amplen):
resultMag[k][z] = np.nanmean(allArraysMag[k][z])
resultPhase[k][z] = np.nanmean(allArraysPhase[k][z])
resultGain[k][z] = np.nanmean(allArraysGain[k][z])
#Calculates the standard error of mean with degree of freedom = 0
stdErrorMag[k][z] = stats.sem(allArraysMagError[k][z], ddof=0)
stdErrorGain[k][z] = stats.sem(allArraysGainError[k][z], ddof=0)
stdErrorPhase[k][z] = stats.sem(allArraysPhaseError[k][z], ddof=0)
#Plots the average magnitude and phase graphs
self.avgMag_Plot.clear()
self.avgPhase_Plot.clear()
self.avgGain_Plot.clear()
for i in range(amplen):
factor = 200 / amplen
red = 200 - i * factor
green = 200 - i * factor
coloredPen = pg.mkPen(color=(red, green, 255), width=2)
self.avgMag_Plot.plot(frequency, resultMag[:, i], pen=coloredPen)
self.avgPhase_Plot.plot(frequency, resultPhase[:, i], pen=coloredPen)
self.avgGain_Plot.plot(frequency, resultGain[:, i], pen=coloredPen)
self.avgMag_Plot.setLogMode(y=True)
self.avgGain_Plot.setLogMode(y=True)
#Set the axes labels
self.avgMag_Plot.setTitle('Avg. Magnitude Response at Stim Frequency')
self.avgMag_Plot.setLabel('left', 'Average Response', units='nm')
self.avgMag_Plot.setLabel('bottom', 'Stim Frequency', units='kHz')
self.avgPhase_Plot.setTitle('Average Phase at Stim Frequency')
self.avgPhase_Plot.setLabel('left', 'Average Phase (shifted)', units='rad')
self.avgPhase_Plot.setLabel('bottom', 'Stim Frequency', units='kHz')
self.avgGain_Plot.setTitle('Average Gain at Stim Frequency')
self.avgGain_Plot.setLabel('left', 'Average Gain', units='nm/Pa')
self.avgGain_Plot.setLabel('bottom', 'Stim Frequency', units='kHz')
#Plot the standard error of each mean
self.plotStandardError(resultMag, stdErrorMag, freqlen, amplen, frequency, "mag")
self.plotStandardError(resultGain, stdErrorGain, freqlen, amplen, frequency, "gain")
self.plotStandardError(resultPhase, stdErrorPhase, freqlen, amplen, frequency, "phase")
#Plots the standard error for the averages
def plotStandardError(self, result, stdError, freqlen, amplen, frequency, kind):
for x in range(freqlen):
for y in range(amplen):
ind = []
dep = []
ind.append(frequency[x])
ind.append(frequency[x])
below = result[x][y] - stdError[x][y]
above = result[x][y] + stdError[x][y]
dep.append(below)
dep.append(above)
adjust = pg.mkPen(0.5)
if kind == "mag":
self.avgMag_Plot.plot(ind, dep, pen=adjust)
elif kind == "gain":
self.avgGain_Plot.plot(ind, dep, pen=adjust)
elif kind == "phase":
self.avgPhase_Plot.plot(ind, dep, pen=adjust)
#Incomplete, need to fix the mapping of points from graph to the scene to compare against the mouse position, or vice versa
#Determines the position of the mouse when pressed in the widget to determine position, and find closest point to it from the graph
def mouseCFCalculation(self, QMouseEvent):
#Get mouse position
x = QMouseEvent.x()
y = QMouseEvent.y()
#Get current data set
setName = self.dataSet_comboBox.currentText()
dataSet = self.dataSetNameDict[setName]
audioParams = dataSet.audioParams
magResp = dataSet.magResp
freq = audioParams.freq[0, :]
amp = audioParams.amp
#Unwrapping the phase data along the frequency axis
phase = dataSet.phaseResp
self.phasePlot.clear()
unWrapThreshold = np.pi
phase = np.unwrap(phase, discont=unWrapThreshold, axis=0)
#If threshold/noise floor is set, finds the NaNs in the magResp and applies to phase as well
magResp, phase = self.findNaNs(magResp, phase, freq, amp, setName)
distance = 10000
index = 0
cf = 0.0
for i in range(len(freq)):
if np.isnan(magResp[i][0]) == False:
pt = QPoint(freq[i], magResp[i][0])
updated_pt = self.magPlot.mapToScene(pt)
check_x = updated_pt.x()
check_y = updated_pt.y()
dist1 = x - check_x
dist2 = y - check_y
dist = math.sqrt(math.pow(dist1, 2) + math.pow(dist2, 2))
print("updatedx: ", check_x)
print("updatedy: ", check_y)
if dist < distance:
cf = magResp[i][0]
index = freq[i]
distance = dist
print("x: ", x)
print("y: ", y)
print("Mouse called")
print("cf: ", cf)
self.CF_lineEdit.setText(str(cf))
self.calculateQ10db(magResp, freq, amp, cf, index)
#Calculates the Characteristic Frequency of the lowest stimulus, 10dB
def calculateCF(self, magResp, freq, amp, setName):
cf = 0.0
#Finds the x-coordinate of the CF
index = 0
#Keeps track of whether the mouse is dead or alive
result = ""
#Determine from the data set name if the mouse is live or dead
lowerCase = setName.lower()
if lowerCase.find("live") != -1:
result = "live"
elif lowerCase.find("dead") != -1:
result = "dead"
else:
if setName in self.mouseStatus:
result = self.mouseStatus[setName]
else:
#QWidget is the base class of all user interfaces objects in PyQt4
w = QWidget()
#Creates a question message box to ask the state of the mouse
msg = QMessageBox.question(w, "Mouse Status", "Is this a live mouse?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if msg == QMessageBox.Yes:
result = "live"
else:
result = "dead"
self.mouseStatus[setName] = result
#Determine the CF
if result == "live":
for i in range(len(freq)):
#Ensures that the CF found should not be noise for live mouse
if np.isnan(magResp[i][0]) == False and magResp[i][0] > cf and i > ((len(freq) / 2) + 2) and i < len(freq) - 1:
cf = magResp[i][0]
index = freq[i]
elif result == "dead":
for i in range(len(freq)):
#Filters out the noise for CF of dead mouse
if np.isnan(magResp[i][0]) == False and magResp[i][0] > cf and i < ((len(freq) / 2) + 2) and i > 0:
cf = magResp[i][0]
index = freq[i]
self.CF_lineEdit.setText(str(cf))
self.calculateQ10db(magResp, freq, amp, cf, index)
#Calculates the Q10dB of the 10dB curve, set to 0.0 by default and N/A Q10dB can't be calculated
def calculateQ10db(self, magResp, freq, amp, cf, index):
#Determine the shift for 10dB
shift = cf / math.pow(10, 0.5)
#CF subtracted by 10dB
limit = cf - shift
y = []
#Get the data points for the 10dB curve
for i in range(len(freq)):
y.append(magResp[i][0])
#Use interpolation to deal with NaNs first before calculating Q10dB
modify = np.array(y)
not_nan = np.logical_not(np.isnan(modify))
indices = np.arange(len(modify))
#Interpolate for the NaNs that occur, so Q10dB can be calculated
interp = np.interp(indices, indices[not_nan], modify[not_nan])
changed = interp[indices]
f = interpolate.UnivariateSpline(freq, changed, s=0)
yreduced = np.array(changed) - limit
freduced = interpolate.UnivariateSpline(freq, yreduced, s=0)
#Calculates the roots of intersection between the curve and the horizontal line y = limit
Q = freduced.roots()
#Finds the closest two roots to to CF calculate bandwidth, protect against when there are multiple roots that cross the y = limit line
if len(Q) > 2:
less = 0
more = freq[len(freq) - 1]
for num in Q:
if num >= less and num < index:
less = num
if num <= more and num > index:
more = num
bandwidth = more - less
#Calculates Q10dB
result = cf / bandwidth
rounded = round(result, 4)
self.Q10db_lineEdit.setText(str(rounded))
#Graphs the Q10Db, in a dotted red line
x = []
x.append(less)
x.append(more)
z = []
z.append(limit)
z.append(limit)
pen = pg.mkPen('r', width=1, style=QtCore.Qt.DashLine)
self.magPlot.plot(x, z, pen=pen)
elif len(Q) == 2:
#Case when there are exactly 2 roots
bandwidth = Q[1] - Q[0]
result = cf / bandwidth
rounded = round(result, 4)
self.Q10db_lineEdit.setText(str(rounded))
#Graphs the Q10Db, in a dotted red line
x = []
x.append(Q[len(Q) - 1])
x.append(Q[len(Q) - 2])
z = []
z.append(limit)
z.append(limit)
pen = pg.mkPen('r', width=1, style=QtCore.Qt.DashLine)
self.magPlot.plot(x, z, pen=pen)
else:
self.Q10db_lineEdit.setText("N/A")
#Exports the magnitude response, shifted phase, and gain of every data set to Excel. Also exports the averages and standard errors in a separate workbook in Excel
def exportToExcel(self):
#Iterate through to each data set
for data in self.dataSetNameDict:
filename = data + ".xlsx"
#Create a new Excel file
workbook = xlsxwriter.Workbook(filename)
#Write the magnitude response to Excel
self.magRespToExcel(self.dataSetNameDict[data], workbook, data)
#Close the workbook after done writing to it
workbook.close()
#Ask user for average Excel file name when exported
text, ok = QInputDialog.getText(self, 'Project Average Excel File Name', 'Name of Average Excel File: ')
if ok:
filename = str(text)
#Write the averages of the data sets to new workbook in Excel
self.avgToExcelPrep(filename)
#Writes the Magnitude Response of a data set to Excel
def magRespToExcel(self, dataSet, workbook, setName):
#Add a worksheet
worksheet = workbook.add_worksheet('Mag')
#Get the Magnitude Response from data set
audioParams = dataSet.audioParams
magResp = dataSet.magResp
freq = audioParams.freq[0, :]
amp = audioParams.amp
#Get the phase of data set
phase = dataSet.phaseResp
unWrapThreshold = np.pi
phase = np.unwrap(phase, discont=unWrapThreshold, axis=0)
magResp, phase = self.findNaNs(magResp, phase, freq, amp, setName)
worksheet.write('A1', 'Stim Mag Resp')
for i in range(len(freq)):
worksheet.write(i+2, 0, freq[i])
for j in range(len(amp)):
worksheet.write(1, j+1, amp[j])
for k in range(len(freq)):
for z in range(len(amp)):
if np.isnan(magResp[k][z]) == False:
worksheet.write(k+2, z+1, magResp[k][z])
#Write the shifted phase to Excel
self.phaseShiftToExcel(magResp, phase, workbook, freq, amp)
#Write the gain to Excel
self.gainToExcel(magResp, workbook, freq, amp)
#Writes the Phase Unwrapped and shifted of data set to Excel
def phaseShiftToExcel(self, magResp, phase, workbook, freq, amp):
#Add a worksheet
worksheet = workbook.add_worksheet('Phase')
#Phase shifting
phase = self.phaseShift(magResp, phase, freq, amp)
worksheet.write('A1', 'Phase Resp Unwrapped and Shifted')
for i in range(len(freq)):
worksheet.write(i+2, 0, freq[i])
for j in range(len(amp)):
worksheet.write(1, j+1, amp[j])
for k in range(len(freq)):
for z in range(len(amp)):
if np.isnan(phase[k][z]) == False:
worksheet.write(k+2, z+1, phase[k][z])
#Writes the Gain of data set to Excel
def gainToExcel(self, magResp, workbook, freq, amp):
#Add a worksheet
worksheet = workbook.add_worksheet('Gain')
#Get the gain of data set
gain = self.calculateGain(magResp, freq, amp)
worksheet.write('A1', 'Gain')
for i in range(len(freq)):
worksheet.write(i+2, 0, freq[i])
for j in range(len(amp)):
worksheet.write(1, j+1, amp[j])
for k in range(len(freq)):
for z in range(len(amp)):
if np.isnan(gain[k][z]) == False:
worksheet.write(k+2, z+1, gain[k][z])
#Prepare the data sets to be averaged together based on combos for exporting to Excel
def avgToExcelPrep(self, filename):
excelAvg = {}
curText1 = self.categories
curText2 = self.categories
for i in range(len(curText1)):
for j in range(i, len(curText2)):
excelAvg.clear()
cate1 = curText1[i]
cate2 = curText2[j]
for name in self.dataSetNameDict:
check = True
#Case insensitive by turning the strings to lower-case first
temp = name.lower()
text1 = cate1.lower()
text2 = cate2.lower()
if (temp.find(text1) == -1) or (temp.find(text2) == -1) or (text1 == text2):
#Filter out unwanted data sets for average combinations
check = False
if (text1 == "" and text2 == "" and text1 == text2):
#Keeps the case where both categories are "", so averages all data sets
check = True
if check == True:
excelAvg[name] = self.dataSetNameDict[name]
#Create a new workbook for a new set of averages, if there are data sets in the category combination
if bool(excelAvg) == True:
n = ""
n = filename + "_" + cate1 + "_" + cate2 + "_Average.xlsx"
avgWorkbook = xlsxwriter.Workbook(n)
self.avgToExcel(avgWorkbook, excelAvg)
#Close the workbook
avgWorkbook.close()
#Calculates the average magnitude, shifted phase, gain, and standard errors
def avgToExcel(self, workbook, excelAvg):
frequency = []
amplitude = []
freqlen = 0
amplen = 0
#A 2D array where each element holds a list for all the data points of that respective position
allArraysMag = defaultdict(lambda : defaultdict(list))
allArraysPhase = defaultdict(lambda : defaultdict(list))
allArraysPhaseError = defaultdict(lambda : defaultdict(list))
allArraysMagError = defaultdict(lambda : defaultdict(list))
allArraysGain = defaultdict(lambda : defaultdict(list))
allArraysGainError = defaultdict(lambda : defaultdict(list))
#Iterate through the data sets in the avgData dictionary, also unwrapping phase
for d in excelAvg:
dataSet = excelAvg[d]
audioParams = dataSet.audioParams
magResp = dataSet.magResp
phase = dataSet.phaseResp
unWrapThreshold = np.pi
phase = np.unwrap(phase, discont=unWrapThreshold, axis=0)
freq = audioParams.freq[0, :]
amp = audioParams.amp
frequency = freq
amplitude = amp
freqlen = len(freq)
amplen = len(amp)
#Calculate the gain
gain = self.calculateGain(magResp, freq, amp)
#Phase shift the phase first before averaging
phase = self.phaseShift(magResp, phase, freq, amp)
#Set the threshold/noise floor
thres = self.threshold[d]
#Adds each data point from each data set to its corresponding list
#Need the error list because the stats.sem 0.16.1 version can't handle NaNs
for i in range(len(freq)):
for j in range(len(amp)):
if magResp[i][j] >= thres:
allArraysMag[i][j].append(magResp[i][j])
allArraysPhase[i][j].append(phase[i][j])
allArraysGain[i][j].append(gain[i][j])
if np.isnan(phase[i][j]) == False:
allArraysPhaseError[i][j].append(phase[i][j])
if np.isnan(magResp[i][j]) == False:
allArraysMagError[i][j].append(magResp[i][j])
if np.isnan(gain[i][j]) == False:
allArraysGainError[i][j].append(gain[i][j])
else:
allArraysMag[i][j].append(np.NaN)
allArraysPhase[i][j].append(np.NaN)
allArraysGain[i][j].append(np.NaN)
#Hold the resulting averages of each point
resultMag = np.zeros((freqlen, amplen))
resultMag[:, :] = np.NaN
resultPhase = np.zeros((freqlen, amplen))
resultPhase[:, :] = np.NaN
resultGain = np.zeros((freqlen, amplen))
resultGain[:, :] = np.NaN
#Hold the standard errors of mean for each data point
stdErrorMag = np.zeros((freqlen, amplen))
stdErrorMag[:, :] = np.NaN
stdErrorGain = np.zeros((freqlen, amplen))
stdErrorGain[:, :] = np.NaN
stdErrorPhase = np.zeros((freqlen, amplen))
stdErrorPhase[:, :] = np.NaN
#Average each list in the 2D array, to find the average for that corresponding point
for k in range(freqlen):
for z in range(amplen):
resultMag[k][z] = np.nanmean(allArraysMag[k][z])
resultPhase[k][z] = np.nanmean(allArraysPhase[k][z])
resultGain[k][z] = np.nanmean(allArraysGain[k][z])
stdErrorMag[k][z] = stats.sem(allArraysMagError[k][z], ddof=0)
stdErrorGain[k][z] = stats.sem(allArraysGainError[k][z], ddof=0)