-
Notifications
You must be signed in to change notification settings - Fork 15
/
DF_Multipeakfit.py
2928 lines (2105 loc) · 112 KB
/
DF_Multipeakfit.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 Fri Nov 02 14:01:17 2018
@author: car72
Contains all necessary functions for analysis of NPoM darkfield and photoluminescence spectra
Best used in conjunction with Condense_Fit_DF or script
"""
from __future__ import division
from __future__ import print_function
from builtins import zip
from builtins import str
from builtins import range
from past.utils import old_div
if __name__ == '__main__':
print('Importing modules...')
import h5py
import numpy as np
import os
import matplotlib.pyplot as plt
from scipy.signal import butter, filtfilt
from lmfit.models import GaussianModel
import time
from random import randint
import re
import scipy.optimize as spo
from scipy.signal import savgol_filter as sgFilt
if __name__ == '__main__':
absoluteStartTime = time.time()
print('\tModules imported\n')
print('Initialising functions...')
def findH5File(rootDir, mostRecent = True, nameFormat = 'date'):
'''
Finds either oldest or most recent .h5 file in a folder whose name begins with a specified string
Default name format ('date') is yyyy-mm-dd
'''
os.chdir(rootDir)
if mostRecent == True:
n = -1
else:
n = 0
if nameFormat == 'date':
if mostRecent == True:
print('Searching for most recent instance of yyyy-mm-dd.h5 or similar...')
else:
print('Searching for oldest instance of yyyy-mm-dd.h5 or similar...')
h5File = sorted([i for i in os.listdir('.') if re.match('\d\d\d\d-[01]\d-[0123]\d', i[:10])
and (i.endswith('.h5') or i.endswith('.hdf5'))],#finds list of filenames with yyyy-mm-dd(...).h(df)5 format
key = lambda i: os.path.getmtime(i))[n]#sorts them by date and picks either oldest or newest depending on value of 'mostRecent'
else:
if mostRecent == True:
print('Searching for most recent instance of %s.h5 or similar...' % nameFormat)
else:
print('Searching for oldest instance of %s.h5 or similar...' % nameFormat)
h5File = sorted([i for i in os.listdir('.') if i.startswith(nameFormat)#finds list of filenames with (nameFormat)(...).h(df)5 format
and (i.endswith('.h5') or i.endswith('.hdf5'))],
key = lambda i: os.path.getmtime(i))[n]#sorts them by date and picks either oldest or newest depending on value of 'mostRecent'
print('\tH5 file %s found\n' % h5File)
return h5File
def removeNaNs(array):
'''
Converts NaN values to numbers via linear interpolation between adjacent finite elements.
Input = 1D array or list.
Output = copy of same array/list with no NaNs
'''
numNaNs = len([i for i in array if not np.isfinite(i)])#checks for NaN values
if numNaNs == 0:
return array#returns original array if no NaNs
newArray = np.copy(array)#so we don't change original array
for n, i in enumerate(newArray):#checks for NaNs at start of array
if np.isfinite(i):#finds index of first finite value in array
break
newArray[:n] = np.average(newArray[n:n+3])#turns any initial missing values into a flat line
for n, i in enumerate(newArray[::-1]):#checks for NaNs at end of array
if np.isfinite(i):#finds index of last finite value in array
break
if n > 0:
newArray[-n:] = np.average(newArray[-(n+4):-(n + 1)])#turns any final missing values into a flat line
nandices = np.array([n for n, i in enumerate(newArray) if not np.isfinite(i)])#locates indices of remaining NaN values
for nandex in nandices:
if np.isfinite(newArray[nandex]):#if NaN value has already been fixed on a previous iteration, moves to the next one
continue
for n, i in enumerate(newArray[nandex:]):#scans forward to look for consecutive NaNs
if np.isfinite(i):#finds length of NaN sequence
break
interpInit = np.average([i for i in newArray[nandex - 4:nandex] if np.isfinite(i)])#start point for linear interpolation; corrects for noise by averaging a few values
interpEnd = np.average([i for i in newArray[nandex + n :nandex + n + 4] if np.isfinite(i)])#interpolation end point; also de-noised
interPlump = np.linspace(interpInit, interpEnd, n + 2)#linearly interpolates between the finite values either side of the NaN sequence
newArray[nandex:nandex+n] = interPlump[1:-1]#replaces NaNs with the new data points
return newArray
def removeCosmicRays(x, y, reference = 1, factor = 15):
'''
Looks for large sharp spikes in spectrum via 1st derivative
Threshold of "large" determined by 'factor'
If correecting a referenced DF spectrum (or similar), reference = reference spectrum (1D array). Otherwise, reference= 1
Erases a small window around each spike and replaces it with a straight line via the removeNaNs function
'''
newY = np.copy(y)
cosmicRay = True#Guilty until proven innocent
iteration = 0
rayDex = 0
nSteps = 1
while cosmicRay == True and iteration < 20:
d1 = centDiff(x, newY)#takes dy/dx via central difference method
d1 *= np.sqrt(reference)#de-references the spectrum to enhance cosmic ray detection in noisy regions
d1 = abs(d1)#takes magnitude of first derivative
d1Med = np.median(d1)#finds median gradient -> dy/dx should be larger than this for a cosmic ray
if old_div(max(d1),d1Med) > factor:#if the maximum dy/dx value is more than a certain mutliple of the median, a cosmic ray exists
oldRayDex = rayDex
rayDex = d1.argmax() - 1#cosmic ray spike happens just before largest |dy/dx| value
if abs(rayDex - oldRayDex) < 5:#if a cosmic ray still exists near where the old one was 'removed':
nSteps += 1#the erasure window is iteratively widened
else:#otherwise, just clean up to one data point either side
nSteps = 1
iteration += 1
for i in np.linspace(0 - nSteps, nSteps, 2*nSteps + 1):#for a window centred around the spike
newY[rayDex + int(i)] = np.nan #erase the data points
newY = removeNaNs(newY)#linearly interpolate between data points adjacent to the spike
else:#if no 'large' spikes exist in the spectrum
cosmicRay = False #no cosmic rays left to fix
return newY
def truncateSpectrum(x, y, startWl = 450, finishWl = 900, xOnly = False):
'''
Truncates xy data spectrum within a specified wavelength range. Useful for removing high and low-end noise or analysing certain spectral regions.
x and y must be 1D arrays (or lists) of identical length
Default range is 450-900 nm (good for lab 3)
If y == None, (specifically, if type(y) == type(None)) or xOnly == True, function returns truncated x array only
'''
x = np.array(x)
if type(y) == type(None) or xOnly == True:
xOnly = True
y = np.zeros(len(x))
y = np.array(y)
reverse = False
if x[0] > x[-1]:#if x is in descending order, x and y are reversed
reverse = True
x = x[::-1]
y = y[::-1]
if x[0] > startWl:#if truncation window extends below spectral range:
xStart = np.arange(x[0], startWl - 2, x[0] - x[1])[1:][::-1]
yStart = np.array([np.average(y[:5])] * len(xStart))
x = np.concatenate((xStart, x))
y = np.concatenate((yStart, y))#Adds buffer to start of x and y to ensure the truncated length is still defined by startWl and finishWl
if x[-1] < finishWl:#if truncation window extends above spectral range:
xFin = np.arange(x[-1], finishWl + 2, x[1] - x[0])[1:]
yFin = np.array([np.average(y[-5:])] * len(xFin))
x = np.concatenate((x, xFin))
y = np.concatenate((y, yFin))#Adds buffer to end of x and y to ensure the truncated length is still defined by startWl and finishWl
startIndex = (abs(x - startWl)).argmin()#finds index corresponding to startWl
finishIndex = (abs(x - finishWl)).argmin()#index corresponding to finishWl
xTrunc = np.array(x[startIndex:finishIndex])#truncates x using these indices
yTrunc = np.array(y[startIndex:finishIndex])#truncates y using these indices
if reverse == True:#if the spectrum had to be reversed earlier, this flips it back.
xTrunc = xTrunc[::-1]
yTrunc = yTrunc[::-1]
if xTrunc.size <= 10 and x.size <= 100:#sometimes fails for very short arrays; this extra bit works better in those cases
if startWl > finishWl:
wl1 = finishWl
wl2 = startWl
startWl = wl1
finishWl = wl2
xTrunc, yTrunc = np.transpose(np.array([[i, y[n]] for n, i in enumerate(x) if startWl < i < finishWl]))
if xOnly == True:
return xTrunc
return np.array([xTrunc, yTrunc])
def retrieveData(directory, summaryNameFormat = 'summary', first = 0, last = 0, attrsOnly = False):
'''
Retrieves darkfield data and metadata from summary file
Use 'first' and 'last' to truncate dataset if necessary. Setting last = 0 -> last = (end of dataset). Useful if initial spectra failed or if someone switched the lights on in the morning
'''
summaryFile = findH5File(directory, nameFormat = summaryNameFormat)#looks for most recent file titled 'summary(...).h(df)5 in current directory
if attrsOnly == False:
print('Retrieving data...')
else:
print('Retrieving sample attributes...')
with h5py.File(summaryFile, 'a') as f:#opens summary file
mainDatasetName = sorted([scan for scan in list(f['particleScanSummaries/'].keys())],
key = lambda scan: len(f['particleScanSummaries/'][scan]['spectra']))[-1]#finds largest datset. Useful if you had to stop and start your particle tracking before leaving it overnight
mainDataset = f['particleScanSummaries/'][mainDatasetName]['spectra']#opens dataset object
summaryAttrs = {key : mainDataset.attrs[key] for key in list(mainDataset.attrs.keys())}#creates python dictionary from dataset attributes/metadata
if attrsOnly == True:#If you only want the metadata to update your main output file
print('\tInfo retrieved from %s' % mainDatasetName)
print('\t\t%s spectra in total\n' % len(mainDataset))
return summaryAttrs
if last == 0:
last = len(mainDataset)#last = 0 -> last = (end of dataset)
spectra = mainDataset[()][first:last]#truncates dataset, if specified
wavelengths = summaryAttrs['wavelengths'][()]#x axis
print('\t%s spectra retrieved from %s\n' % (len(spectra), mainDatasetName))
print('Removing cosmic ray events...')
prepStart = time.time()
wavelengths = removeNaNs(wavelengths)#what it says on the tin
reference = summaryAttrs['reference']#for use in cosmic ray removal
for n, spectrum in enumerate(spectra):
try:
newSpectrum = removeCosmicRays(wavelengths, spectrum, reference = reference)#attempts to remove cosmic rays from spectrum
if False in np.where(newSpectrum == newSpectrum[0], True, False):#if removeCosmicRays and removeNaNs have worked properly
spectra[n] = newSpectrum#replaces spectrum with cleaned up version
else:
print('Cosmic ray removal failed for spectrum %s' % n)
except Exception as e:
print('Cosmic ray removal failed for spectrum %s because %s' % (n, e))
pass
prepEnd = time.time()
prepTime = prepEnd - prepStart
print('\tAll cosmic rays removed in %.2f seconds\n' % (prepTime))
print('Cleaning up NaN values...')
prepStart = time.time()
spectra = np.array([removeNaNs(spectrum) for spectrum in spectra])#Extra NaN removal in case removeCosmicRays failed
prepEnd = time.time()
prepTime = prepEnd - prepStart#time elapsed
print('\tAll spectra cleared of NaNs in %.2f seconds\n' % (prepTime))
return wavelengths, spectra, summaryAttrs
def retrievePlData(directory, summaryNameFormat = 'summary', first = 0, last = 0):
'''
Retrieves photolumineasence data and metadata from summary file
Use 'first' and 'last' to truncate dataset if necessary. Setting last = 0 -> last = (end of dataset). Useful if initial spectra failed or if someone switched the lights on in the morning
'''
summaryFile = findH5File(directory, nameFormat = summaryNameFormat) #looks for most recent file titled 'summary(...).h(df)5 in current directory
print('Retrieving PL data...')
with h5py.File(summaryFile, 'a') as f:#Opens summary file
gPlName = sorted([scan for scan in list(f['particleScanSummaries/'].keys())],
key = lambda scan: len(f['particleScanSummaries/'][scan]['spectra']))[-1]#finds largest datset. Useful if you had to stop and start your particle tracking before leaving it overnight
reference = f['particleScanSummaries/%s/spectra' % gPlName].attrs['reference'][()]#gets reference from DF spectra metadata
gPl = f['NPoM PL Spectra/%s' % gPlName]#opens dataset object
if last == 0:
last = len(list(gPl.keys()))#last = 0 -> last = (end of dataset)
dPlNames = sorted(list(gPl.keys()), key = lambda dPlName: int(dPlName.split(' ')[-1]))[first:last]#creates list of PL spectrum names within specified bounds
print('\t%s PL spectra retrieved from %s\n' % (len(dPlNames), gPlName))
print('Removing cosmic ray events...')
prepStart = time.time()
xPl = gPl[dPlNames[0]].attrs['wavelengths']#x axis
xPl = removeNaNs(xPl)#what it says on the tin
reference = truncateSpectrum(xPl, reference, startWl = xPl[0], finishWl = xPl[-1])[1]
reference = np.append(reference, reference[-1])#for processing post-PL DF
plData = np.array([gPl[dPlName][()] for dPlName in dPlNames])#collects all PL spectra of interest
dfAfter = np.array([gPl[dPlName].attrs['DF After'][()] for dPlName in dPlNames])#collects corresponding DF spectra
areas = np.array([gPl[dPlName].attrs['Total Area'] for dPlName in dPlNames])#corresponding integrated PL intensities
bgScales = np.array([gPl[dPlName].attrs['Background Scale Factor'] for dPlName in dPlNames])#corresponding scaling factors for PL background subtraction
for n, plSpectrum in enumerate(plData):
try:
plSpectrum = removeCosmicRays(xPl, plSpectrum, reference = plSpectrum)#attempts to remove cosmic rays from PL spectrum
if False in np.where(plSpectrum == plSpectrum[0], True, False):#if removeCosmicRays and removeNaNs have worked properly
plData[n] = plSpectrum#replaces PL spectrum with cleaned up version
else:
print('Cosmic ray removal failed for PL spectrum spectrum %s' % n)
except:
pass
try:
dfAfterSpec = removeCosmicRays(xPl, dfAfter[n], reference = reference)#attempts to remove cosmic rays from DF spectrum
if False in np.where(dfAfterSpec == dfAfterSpec[0], True, False):#if removeCosmicRays and removeNaNs have worked properly
dfAfter[n] = dfAfterSpec#replaces DF spectrum with cleaned up version
else:
print('Cosmic ray removal failed for post-PL DF spectrum spectrum %s' % n)
except:
pass
prepEnd = time.time()
prepTime = prepEnd - prepStart#time elapsed
print('\tAll cosmic rays removed in %.2f seconds\n' % (prepTime))
print('Cleaning up NaN values...')
prepStart = time.time()
plData = np.array([removeNaNs(plSpec) for plSpec in plData])#Extra NaN removal in case removeCosmicRays failed
dfAfter = np.array([removeNaNs(dfSpectrum) for dfSpectrum in dfAfter])#Extra NaN removal in case removeCosmicRays failed
prepEnd = time.time()
prepTime = prepEnd - prepStart#time elapsed
print('\tAll spectra cleared of NaNs in %.2f seconds\n' % (prepTime))
return xPl, plData, dfAfter, areas, bgScales
def determineVLims(zData, threshold = 1e-4):
'''
Calculates appropriate intensity limits for 2D plot based on frequency distribution of intensities.
'''
zFlat = zData.flatten()
frequencies, bins = np.histogram(zFlat, bins = 100, density = False)
freqThresh = frequencies.max()*threshold
binCentres = np.array([np.average([bins[n], bins[n + 1]]) for n in range(len(bins) - 1)])
#plt.plot(binCentres, frequencies)
#plt.plot(binCentres, [freqThresh]*len(binCentres))
#plt.show()
binsThreshed = binCentres[np.nonzero(np.where((frequencies > freqThresh), frequencies, 0))]
#plt.plot(binsThreshed, frequencies)
#plt.show()
vMin = binsThreshed[0]
vMax = binsThreshed[-1]
return vMin, vMax
def plotStackedMap(x, yData, imgName = 'Stack', plotTitle = 'Stack', closeFigures = False, init = False, vThresh = 1e-4, xLims = (450, 900)):
'''
Plots stack of xy data.
x = 1d array
y = list/array of 1d arrays. Must all be the same length as x.
Stacks will be saved as [imgName].png in 'Stacks'
If init == False, image will be saved in current directory
'''
if init == True:
print('Plotting %s...' % imgName)
stackStartTime = time.time()
elif init == False:
if 'Stacks' not in os.listdir('.'):
os.mkdir('Stacks')
#try:
xStack = x # Wavelength range
yStack = list(range(len(yData))) # Number of spectra
zStack = np.vstack(yData) # Spectral data
#vmin, vmax = determineVLims(zStack, threshold = vThresh)
fig = plt.figure(figsize = (9, 7))
plt.pcolormesh(xStack, yStack, zStack, cmap = 'inferno', vmin = 0, vmax = 6)
plt.xlim(xLims)
plt.xlabel('Wavelength (nm)', fontsize = 14)
plt.ylabel('Spectrum #', fontsize = 14)
cbar = plt.colorbar()
cbar.set_ticks([])
cbar.set_label('Intensity (a.u.)', fontsize = 14)
plt.ylim(min(yStack), max(yStack))
plt.yticks(fontsize = 14)
plt.xticks(fontsize = 14)
plt.title(plotTitle)
if not imgName.endswith('.png'):
imgName = '%s.png' % imgName
if init == True:
imgPath = imgName
elif init == False:
imgPath = 'Stacks/%s' % (imgName)
fig.savefig(imgPath, bbox_inches = 'tight')
if closeFigures == True:
plt.close('all')
if init == True:
stackEndTime = time.time()
timeElapsed = stackEndTime - stackStartTime
print('\tInitial stack plotted in %s seconds\n' % timeElapsed)
def plotInitStack(x, yData, imgName = 'Initial Stack', closeFigures = True, vThresh = 2e-4):
'''Quickly plots stack of all DF spectra before doing the full analysis. Useful for quickly assessing the dataset quality'''
yDataTrunc = np.array([truncateSpectrum(x, spectrum)[1] for spectrum in yData])#truncate to NPoM range
xStack = truncateSpectrum(x, yData[0])[0]#x axis
transIndex = abs(xStack - 533).argmin()
yDataTrunc = np.array([old_div(spectrum, spectrum[transIndex]) for spectrum in yDataTrunc])#normalise to ~transverse mode
plotStackedMap(xStack, yDataTrunc, imgName = imgName, plotTitle = imgName, closeFigures = closeFigures, init = True, vThresh = vThresh)
def plotInitPlStack(xPl, plData, imgName = 'Initial PL Stack', closeFigures = True, vThresh = 5e-5):
'''Same as above, but for PL data'''
yDataTrunc = np.array([truncateSpectrum(xPl, plSpectrum, startWl = 580)[1] for plSpectrum in plData])# truncate to remove laser leak
xStack = truncateSpectrum(xPl, plData[0], startWl = 580)[0] # x axis
yDataTrunc = np.array([old_div(plSpectrum, plSpectrum[0]) for plSpectrum in yDataTrunc])# normalise to 580 nm value
plotStackedMap(xStack, yDataTrunc, imgName = imgName, plotTitle = imgName, closeFigures = closeFigures, vThresh = vThresh, init = True, xLims = (580, 900))
def createOutputFile(filename):
'''Auto-increments new filename if file exists'''
print('Creating output file...')
outputFile = filename
if not (filename.endswith('.h5') or filename.endswith('.hdf5')):
outputFile = '%s.h5' % filename
if outputFile in os.listdir('.'):
print('\t%s already exists' % outputFile)
n = 0
outputFile = '%s_%s.h5' % (filename, n)
while outputFile in os.listdir('.'):
print('\t%s already exists' % outputFile)
n += 1
outputFile = '%s_%s.h5' % (filename, n)
print('\tOutput file %s created\n' % outputFile)
return outputFile
def butterLowpassFiltFilt(data, cutoff = 1500, fs = 60000, order=5):
'''
Decent smoothing function for DF spectra
Increase cutoff/decrease fs for more wibbles
'''
nyq = 0.5 * fs
normalCutoff = old_div(cutoff, nyq)
b, a = butter(order, normalCutoff, btype='low', analog=False)
yFiltered = filtfilt(b, a, data)
return yFiltered
def printEnd():
'''Some Doge approval for when you finish'''
print('%s%s%sv gud' % ('\t' * randint(0, 12), '\n' * randint(0, 5), ' ' * randint(0, 4)))
print('%s%ssuch python' % ('\n' * randint(0, 5), ' ' * randint(0, 55)))
print('%s%smany spectra' % ('\n' * randint(0, 5), ' ' * randint(10, 55)))
print('%s%smuch fitting' % ('\n' * randint(0, 5), ' ' * randint(8, 55)))
print('%s%swow' % ('\n' * randint(2, 5), ' ' * randint(5, 55)))
print('\n' * randint(0, 7))
def detectMinima(array, threshold = 0):
'''
detectMinima(array) -> mIndices
Finds the turning points within a 1D array and returns the indices of the minima.
'''
mIndices = []
if (len(array) < 3):
return mIndices
neutral, rising, falling = list(range(3))
def getState(a, b):
if a < b: return rising
if a > b: return falling
return neutral
ps = getState(array[0], array[1])
begin = 1
for i in range(2, len(array)):
s = getState(array[i - 1], array[i])
if s != neutral:
if ps != neutral and ps != s:
if s != falling:
mIndices.append(old_div((begin + i - 1), 2))
begin = i
ps = s
return np.array(mIndices)
def testIfNpom(x, y, lower = 0.05, upper = 2.5, NpomThreshold = 1.5):
'''Filters out spectra that are obviously not from NPoMs'''
isNpom = False #Guilty until proven innocent
'''To be accepted as an NPoM, you must first pass four trials'''
x = np.array(x)
y = np.array(y)
try:
[xTrunc, yTrunc] = truncateSpectrum(x, y)
[xUpper, yUpper] = truncateSpectrum(x, y, startWl = 900, finishWl = x.max())
yTrunc -= yTrunc.min()
except Exception as e:
print('NPoM test failed because %s' % e)
return False
'''Trial the first: do you have a reasonable signal?'''
YuNoNpom = 'Signal too low'
if np.sum(yTrunc) > lower and y.min() > -0.1:
#If sum of all intensities lies outside a given range, it's probably not an NPoM
#Can adjust range to suit system
YuNoNpom = 'CM region too weak'
'''Trial the second: do you slant in the correct direction?'''
firstHalf = yTrunc[:int(old_div(len(yTrunc),3))]
secondHalf = yTrunc[int(old_div(len(yTrunc),3)):]
if np.sum(firstHalf) < np.sum(secondHalf) * NpomThreshold:
#NPoM spectra generally have greater total signal at longer wavelengths due to coupled mode
YuNoNpom = 'Just Noise'
'''Trial the third: are you more than just noise?'''
if np.sum(yTrunc)*3 > old_div(np.sum(yUpper), NpomThreshold):
#If the sum of the noise after 900 nm is greater than that of the spectrum itself, it's probably crap
YuNoNpom = 'Too few peaks detected'
'''Trial the fourth: do you have more than one maximum?'''
ySmooth = butterLowpassFiltFilt(y)
minima = detectMinima(-ySmooth)
if len(minima) > 1:
#NPoM spectra usually have more than one distinct peak, separated by a minimum
isNpom = True
YuNoNpom = 'N/A'
return isNpom, YuNoNpom
def testIfDouble(x, y, doublesThreshold = 2, lowerLimit = 600, plot = False, raiseExceptions = True):
isDouble = False
isNpom = True
xy = truncateSpectrum(x, y)
xTrunc = xy[0]
yTrunc = xy[1]
ySmooth = butterLowpassFiltFilt(yTrunc)
mIndices = detectMinima(ySmooth)
maxdices = detectMinima(-ySmooth)
if len(mIndices) == 0 or len(maxdices) == 0:
isNpom = False
isDouble = 'N/A'
return isNpom, isDouble
xMins = xTrunc[mIndices]
yMins = yTrunc[mIndices]
xMaxs = xTrunc[maxdices]
yMaxs = yTrunc[maxdices]
maxs = [[xMax, yMaxs[n]] for n, xMax in enumerate(xMaxs)]
if len(yMaxs) == 0:
isNpom = False
isDouble = 'N/A'
else:
try:
yMax = max(yMaxs)
except Exception as e:
if raiseExceptions == True:
pass
else:
print(e)
return False
maxsSortedY = sorted(maxs, key = lambda maximum: maximum[1])
yMax = maxsSortedY[-1][1]
xMax = maxsSortedY[-1][0]
try:
yMax2 = maxsSortedY[-2][1]
if yMax2 > old_div(yMax, doublesThreshold):
isDouble = True
except:
isDouble = False
if xMax < lowerLimit:
isNpom = False
isDouble = 'N/A'
if plot == True or plot == 'all' or plot == 'double test':
if isDouble == True:
title = 'Double Peak'
elif isDouble == False:
title = 'Single Peak'
elif isDouble == 'N/A':
title = 'No Peak'
plt.figure(figsize = (8, 6))
plt.plot(x, y, 'purple', lw = 0.3, label = 'Raw')
plt.xlabel('Wavelength (nm)', fontsize = 14)
plt.ylabel('Intensity', fontsize = 14)
plt.plot(xTrunc, ySmooth, 'g', label = 'Smoothed')
plt.plot(xMins, yMins, 'ko', label = 'Minima')
plt.plot(xMaxs, yMaxs, 'go', label = 'Maxima')
plt.legend(loc = 0, ncol = 3, fontsize = 10)
plt.ylim(0, ySmooth.max()*1.23)
plt.xlim(450, 900)
plt.title(title, fontsize = 16)
plt.show()
return isNpom, isDouble
def centDiff(x, y):
'''Numerically calculates dy/dx using central difference method'''
x1 = np.concatenate((x[:2][::-1], x))
x2 = np.concatenate((x, x[-2:][::-1]))
dx = x2 - x1
dx = dx[1:-1]
y1 = np.concatenate((y[:2][::-1], y))
y2 = np.concatenate((y, y[-2:][::-1]))
dy = y2 - y1
dy = dy[1:-1]
if 0 in dx:
dx = removeNaNs(np.where(dx == 0, dx, np.nan))
d = (old_div(dy,dx))
d /= 2
return d
def normToTrans(x, y, transNorm = 1, troughNorm = 0.61, transInit = 533):
xy = truncateSpectrum(x, y, finishWl = 600)#Truncate data from 450 to 600 nm
xTrunc = xy[0]
yTrunc = xy[1]
ySmooth = butterLowpassFiltFilt(yTrunc)#Smooth data
#try:
mIndices = detectMinima(ySmooth)#Find minima
if len(mIndices) > 0:
yMins = ySmooth[mIndices]
xMins = xTrunc[mIndices]
mins = np.array(list(zip(*[xMins, yMins])))#Corresponding (x, y) values
d1 = centDiff(xTrunc, ySmooth)
d2 = centDiff(xTrunc, d1)
d2Mindices = detectMinima(d2)
trandex = abs(xTrunc[d2Mindices] - transInit).argmin()#Closest minimum in second derivative to 533 nm is likely the transverse mode
transWl = xTrunc[d2Mindices][trandex]
trandex = abs(xTrunc - transWl).argmin()
initMins = [minimum for minimum in mins if minimum[0] < transWl]#Minima occuring before transverse mode
if len(initMins) == 0:
d2Maxdices = detectMinima(-d2)
yMins = ySmooth[d2Maxdices]
xMins = xTrunc[d2Maxdices]
mins = np.array(list(zip(*[xMins, yMins])))
initMins = [minimum for minimum in mins if minimum[0] < transWl]
initMinWls = np.array(list(zip(*mins))[0])
initMinHeights = np.array(list(zip(*mins))[1])
initMindex = abs(initMinWls - transInit).argmin()
initMinWl = initMinWls[initMindex]
a0 = initMinHeights[initMindex]
t0 = ySmooth[trandex]
tInit = ySmooth[abs(xTrunc - transInit).argmin()]
if old_div(tInit,ySmooth[trandex]) > 2:
t0 = tInit
transWl = transInit
aN = troughNorm
tN = transNorm
if a0 < t0:
yNorm = y - a0
yNorm /= (t0 - a0)
yNorm *= (tN - aN)
yNorm += aN
else:
yNorm = y - ySmooth.min()
yNorm /= t0
else:
yNorm = y - ySmooth.min()
trandex = abs(xTrunc - transInit).argmin()
transWl = xTrunc[trandex]
t0 = ySmooth[trandex]
yNorm /= t0
initMinWl = 'N/A'
return yNorm, initMinWl, t0, transWl
def testIfWeirdPeak(x, y, factor = 1.4, transWl = 533, upperLimit = 670, plot = False, debug = False):
'''
Probes NPoM spectrum for presence of 'weird' sharp peak at ~600 nm
Truncates spectrum from 450 to 670 nm, smoothes and finds maxima
If maximum is greater than transverse mode intensity by a certain factor and at a longer wavelength, spectrum has the weird peak.
'''
xy = truncateSpectrum(x, y, finishWl = upperLimit)
xTrunc = xy[0]
yTrunc = xy[1]
yTruncSmooth = butterLowpassFiltFilt(yTrunc)
transHeight = yTruncSmooth[abs(xTrunc - transWl).argmin()]
yMaxs = detectMinima(-yTruncSmooth)
if len(yMaxs) == 0:
return False
peakHeight = yTruncSmooth[yMaxs].max()
peakWl = xTrunc[yTruncSmooth.argmax()]
if peakHeight >= transHeight * factor and peakWl > transWl:
weird = True
else:
weird = False
if plot == 'all' or plot == True:
if weird == True:
color = 'k'
elif weird == False:
color = 'b'
plt.figure()
plt.plot(xTrunc, yTrunc, color = color)
plt.plot(peakWl, peakHeight, 'ro')
plt.plot(transWl, transHeight, 'go')
plt.xlabel('Wavelength (nm)')
plt.ylabel('Scattered Intensity')
plt.title('Weird peak = %s' % weird)
if debug == True:
return weird, peakHeight, peakWl
else:
return weird
def getFWHM(x, y, fwhmFactor = 1.1, smooth = False, peakpos = 0):
'''Estimates FWHM of largest peak in a given dataset'''
'''Also returns xy coords of peak'''
if smooth == True:
y = butterLowpassFiltFilt(y)
maxdices = detectMinima(-y)
if len(maxdices) == 0:
if peakpos != 0:
maxdices = np.array([abs(x - peakpos).argmin()])
else:
return None, None, None
yMax = y[maxdices].max()
halfMax = old_div(yMax,2)
maxdex = maxdices[y[maxdices].argmax()]
xMax = x[maxdex]
halfDex1 = abs(y[:maxdex][::-1] - halfMax).argmin()
halfDex2 = abs(y[maxdex:] - halfMax).argmin()
xHalf1 = x[:maxdex][::-1][halfDex1]
xHalf2 = x[maxdex:][halfDex2]
hwhm1 = abs(xMax - xHalf1)
hwhm2 = abs(xMax - xHalf2)
hwhms = [hwhm1, hwhm2]
hwhmMax, hwhmMin = max(hwhms), min(hwhms)
if hwhmMax > hwhmMin*fwhmFactor:
fwhm = hwhmMin * 2
else:
fwhm = sum(hwhms)
return fwhm, xMax, yMax
def gaussian(x, height, center, fwhm, offset = 0):
'''Gaussian as a function of height, centre, fwhm and offset'''
a = height
b = center
c = fwhm
N = 4*np.log(2)*(x - b)**2
D = c**2
F = -(old_div(N, D))
E = np.exp(F)
y = a*E
y += offset
return y
def gaussArea(height, fwhm):
h = height
c = fwhm
area = h*np.sqrt(old_div((np.pi*c**2),(4*np.log(2))))
return area
def findMainPeaks(x, y, fwhmFactor = 1.1, plot = False, midpoint = 680, weirdPeak = True):
peakFindMetadata = {}
xy = truncateSpectrum(x, y, finishWl = 987)
xTrunc = xy[0]
yTrunc = xy[1]
ySmooth = butterLowpassFiltFilt(yTrunc)
mIndices = detectMinima(ySmooth)
xMins = xTrunc[mIndices]
yMins = ySmooth[mIndices]
mins = [[xMin, yMins[n]] for n, xMin in enumerate(xMins)]
minsSorted = sorted(mins, key = lambda minimum: abs(minimum[0] - midpoint))
try:
if abs(minsSorted[0][1] - minsSorted[1][1]) > ySmooth.max() * 0.6:
midMin = sorted(minsSorted[:2], key = lambda minimum: minimum[1])[0][0]
else:
midMin = xMins[abs(xMins - midpoint).argmin()]
except:
midMin = xMins[abs(xMins - midpoint).argmin()]
initMin = xMins[0]
if initMin == midMin:
initMin = 450
xy1 = truncateSpectrum(xTrunc, ySmooth, startWl = initMin, finishWl = midMin)
xy2 = truncateSpectrum(xTrunc, ySmooth, startWl = midMin, finishWl = 987)
if weirdPeak == True:
x1 = xy1[0]
y1 = xy1[1]
fwhm, xMax, yMax = getFWHM(x1, y1, fwhmFactor = fwhmFactor)
peakFindMetadata['Weird peak FWHM'] = fwhm
peakFindMetadata['Weird peak intensity'] = yMax
peakFindMetadata['Weird peak wavelength'] = xMax
weirdGauss = [gaussian(i, yMax, xMax, fwhm) for i in x]
else:
peakFindMetadata['Weird peak FWHM'] = 'N/A'
peakFindMetadata['Weird peak intensity'] = 'N/A'
peakFindMetadata['Weird peak wavelength'] = 'N/A'
weirdGauss = 'N/A'
x2 = xy2[0]
y2 = xy2[1]
fwhm, xMax, yMax = getFWHM(x2, y2, fwhmFactor = fwhmFactor)
peakFindMetadata['Coupled mode FWHM'] = fwhm
peakFindMetadata['Coupled mode intensity'] = yMax
peakFindMetadata['Coupled mode wavelength'] = xMax
cmGauss = [gaussian(i, yMax, xMax, fwhm) for i in x]
if plot == True or plot == 'all':
weirdHeight = peakFindMetadata['Weird peak intensity']
weirdWl = peakFindMetadata['Weird peak wavelength']
weirdFwhm = peakFindMetadata['Weird peak FWHM']
cmHeight = peakFindMetadata['Coupled mode intensity']
cmWl = peakFindMetadata['Coupled mode wavelength']
cmFwhm = peakFindMetadata['Coupled mode FWHM']
weirdFwhmHorizX = np.linspace(weirdWl - old_div(weirdFwhm,2), weirdWl + old_div(weirdFwhm,2), 2)
weirdFwhmHorizY = np.array([old_div(weirdHeight,2)] * 2)
cmFwhmHorizX = np.linspace(cmWl - old_div(cmFwhm,2), cmWl + old_div(cmFwhm,2), 2)
cmFwhmHorizY = np.array([old_div(cmHeight,2)] * 2)
plt.plot(x, y, 'purple', lw = 0.3, label = 'Raw')
plt.xlabel('Wavelength (nm)', fontsize = 14)