-
Notifications
You must be signed in to change notification settings - Fork 0
/
ojfresult.py
5616 lines (4692 loc) · 214 KB
/
ojfresult.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 Wed Aug 8 09:03:47 2012
@author: dave
"""
from __future__ import division
import math
import os
import sys
#import timeit
import logging
import pickle
import numpy as np
import scipy.io
#from scipy.interpolate import UnivariateSpline
#import scipy.integrate as integrate
#import scipy.interpolate as sp_int
#import scipy.constants as spc
import scipy.interpolate as interpolate
import scipy as sp
from scipy.cluster import vq
#from matplotlib.figure import Figure
#from matplotlib.backends.backend_agg import FigureCanvasAgg as FigCanvas
#import matplotlib.font_manager as mpl_font
#from matplotlib.ticker import FormatStrFormatter
import matplotlib as mpl
#from matplotlib import tight_layout as tight_layout
#from matplotlib.ticker import FormatStrFormatter
import pylab as plt
import cv2
import pandas as pd
import progressbar as progbar
import wafo
import plotting
import misc
from staircase import StairCase
def calc_sample_rate(time, rel_error=1e-4):
"""
the sample rate should be constant throughout the measurement serie
define the maximum allowable relative error on the local sample rate
rel_error = 1e-4 # 0.0001 = 0.01%
"""
deltas = np.diff(time)
# the sample rate should be constant throughout the measurement serie
# define the maximum allowable relative error on the local sample rate
if not (deltas.max() - deltas.min())/deltas.max() < rel_error:
print 'Sample rate not constant, max, min values:',
print '%1.6f, %1.6f' % (1/deltas.max(), 1/deltas.min())
raise AssertionError
return 1/deltas.mean()
class CalibrationData:
"""
This should be the final and correct calibration data
"""
# ---------------------------------------------------------------------
# definition of the calibration files for February
calpath = 'data/calibration/'
ycp = os.path.join(calpath, 'YawLaserCalibration/runs_050_051.yawcal-pol10')
caldict_dspace_02 = {}
caldict_dspace_02['Yaw Laser'] = ycp
# do not calibrate tower strain in February, results are not reliable
#tfacp = calpath + 'TowerStrainCal/towercal-pol1_fa'
#tsscp = calpath + 'TowerStrainCal/towercal-pol1_ss'
#caldict_dspace_02['Tower Strain For-Aft'] = tfacp
#caldict_dspace_02['Tower Strain Side-Side'] = tsscp
caldict_blade_02 = {}
bcp = os.path.join(calpath, 'BladeStrainCal/')
caldict_blade_02[0] = bcp + '0214_run_172_ch1_0214_run_173_ch1.pol1'
caldict_blade_02[1] = bcp + '0214_run_172_ch2_0214_run_173_ch2.pol1'
caldict_blade_02[2] = bcp + '0214_run_172_ch3_0214_run_173_ch3.pol1'
caldict_blade_02[3] = bcp + '0214_run_172_ch4_0214_run_173_ch4.pol1'
# ---------------------------------------------------------------------
# ---------------------------------------------------------------------
tw_cal = 'opt'
# definition of the calibration files for April
ycp04 = os.path.join(calpath,
'YawLaserCalibration-04/runs_289_295.yawcal-pol10')
# for the tower calibration, the yaw misalignment is already taken
# into account in the calibration polynomial, no need to include the
# yaw angle in the calibration. We always measure in the FA,SS dirs
# if that needs to be converted, than do sin/cos psi to have the
# components aligned with the wind
tfacp = calpath + 'TowerStrainCal-04/'
tfacp += 'towercal_249_250_251_yawcorrect_fa-cal_pol1_%s' % tw_cal
tsscp = calpath + 'TowerStrainCal-04/'
tsscp += 'towercal_249_250_251_yawcorrect_ss-cal_pol1_%s' % tw_cal
caldict_dspace_04 = {}
caldict_dspace_04['Yaw Laser'] = ycp04
caldict_dspace_04['Tower Strain For-Aft'] = tfacp
caldict_dspace_04['Tower Strain Side-Side'] = tsscp
# and to convert to yaw coordinate frame of reference
target_fa = calpath + 'TowerStrainCalYaw/psi_fa_max_%s' % tw_cal
caldict_dspace_04['psi_fa_max'] = target_fa
target_ss = calpath + 'TowerStrainCalYaw/psi_ss_0_%s' % tw_cal
caldict_dspace_04['psi_ss_0'] = target_ss
caldict_blade_04 = {}
caldict_blade_04[0] = bcp + '0412_run_357_ch1_0412_run_358_ch1.pol1'
caldict_blade_04[1] = bcp + '0412_run_357_ch2_0412_run_358_ch2.pol1'
caldict_blade_04[2] = bcp + '0412_run_356_ch3_0412_run_358_ch3.pol1'
caldict_blade_04[3] = bcp + '0412_run_356_ch4_0412_run_358_ch4.pol1'
# ---------------------------------------------------------------------
class BladeStrainFile:
def __init__(self, strainfile, Fs=512, verbose=False, debug=False,
norm_strain=False, silent=False, checkpulse=True):
"""
Paramters
---------
strainfile : str
string containing the full path+filename to the blade strain file
Fs : int, default=None
Sampling frequency of the blade strain log file. Only relevant
if the source is a streamed file. In case the source is a
triggered file, the sample rate is given in the file header.
verbose : boolean, default=False
norm_strain : boolean, default=False
silent : boolean, default=False
checkpulse : boolean, default=True
For the triggered file type (April and later) we can find the
starting of the pulse signal and cut off everything before that
point. This is helpful when syncing with dSpace.
"""
# class properties
self.__name__ = 'bladestrainfile'
self.verbose = verbose
self.silent = silent
self.strainfile = strainfile
self.Fs = Fs
self.sample_rate = Fs
self.norm_strain = norm_strain
self.debug = debug
if strainfile:
if not self.silent:
print '\n'+'*'*80
print 'LOADING BLADE FILE'
self.time, self.data, self.labels = self.read(strainfile)
if self.filetype == 'streaming':
self.time, self.data = self._tune_stream(self.time, self.data)
elif self.filetype == 'triggered':
self.time, self.data = self._tune_trig(self.time, self.data,
checkpulse=checkpulse)
# split strainfile into respath and resfile
#self.respath
self.resfile = self.strainfile.split('/')[-1]
self.cnames = ['blade2_root', 'blade2_30pc',
'blade1_root', 'blade1_30pc', 'blade_rpm_pulse']
def _add_missing_point(self, time, data):
"""
Insert missing data point
=========================
Insert a missing data point in between index k and k+1.
.. math::
t_{new} = \\frac{ t_k + t_{k+1} }{2}
Why is there actually a missing time step? The time step counter
missed a step or did the V-strain link failed to transmit/receive one
time step? Communication error is more likely in my opinion.
Conclusion: add the missing time step, obtain values by interpolation.
Note that maybe many more time steps are missing. We don't know for
sure if it is only one.
Parameters
----------
time : ndarray(n)
1D array holding the time stamps
data : ndarray(n)
1D array holding the data points
Returns
-------
time_new
data_new
missing
"""
# see if there are some sweeps missing. This would destroy the
# continuous sample time series
deltas = time[1:] - time[:-1]
# we are still in int mode, so all deltas need to be == 1
d_check = np.abs(deltas-1).__gt__(0)
if np.any(d_check):
# find out how many missing points we have
nr_missing = len(deltas[d_check])
# ff = deltas[d_check.argsort()]
# print ff[-nr_missing:]
# the arguments that would sort d_check: containing False if ok,
# True if delta > 1. The True values will be at the end of the
# sort array
tmp_i = d_check.argsort()
# # reverse the arrays, so the True values are upfront
# tmp_i = tmp_i[::-1]
# d_check = d_check[::-1]
sel_i = tmp_i[-nr_missing:]
if self.verbose:
print 'nr of missing data points: %i' % nr_missing
# print 'deltas[sel_i], sel_i:', deltas[sel_i], sel_i
# only do this for the first point, otherwise the indeces will
# become a mess
k = sel_i[0]
# we need to add one additional point at in between k and k+1
time_new = time[:k+1].copy()
data_new = data[:k+1,:].copy()
# interpolate the new value between k and k+1
x = np.array([time[k], time[k+1]])
y = np.array([data[k,:], data[k+1,:]])
# in case there are many missing values, exclude start and end!
# x_new = (time[k] + time[k+1])/2.
x_new = np.arange(time[k]+1, time[k+1], 1)
y_new = sp.interpolate.griddata(x, y, x_new)
# add the missing point
time_new = np.append(time_new, x_new)
data_new = np.append(data_new, y_new, axis=0)
# and the rest of the data
time_new = np.append( time_new, time[k+1:].copy() )
data_new = np.append( data_new, data[k+1:,:].copy(), axis=0 )
if self.verbose:
print 'added x_new.shape', x_new.shape,
print 'added y_new.shape', y_new.shape
print 'data_new.shape',data_new.shape,'data.shape',data.shape
print ' (k-1) (k) (k+1)'
print ' time:',time[k-1],time[k],time[k+1]
print 'time_new:',time_new[k], time_new[k+1], time_new[k+2]
else:
if self.verbose:
print 'no missing data points'
return time, data, False
return time_new, data_new, True
def _tune_trig(self, time, data, checkpulse=True):
"""
Some tuning for the triggered filetype
"""
# make sure we have a continues series and check the sample rate
# Fs_dummy = calc_sample_rate(time)
deltas = time[1:] - time[:-1]
rel_error = 5e-3 # 0.0050 = 0.50%
assert (deltas.max() - deltas.min())/deltas.max() < rel_error
# compare sample rates
Fs_ratio = (1/deltas.mean())/self.Fs
if not (Fs_ratio > 0.9999 and Fs_ratio < 1.0001):
msg = 'Sample rate from file header does not match actual data.'
raise ValueError, msg
# set zero load to zero strain instead of 2048
# DO NOT CHANGE THIS. UNLESS YOU REDO ALL THE CALIBRATION AGAIN
data[:,0:4] = (data[:,0:4] - 2048.)
# convert to some other scale
if self.norm_strain:
data[:,0:4] = 100.*data[:,0:4]/2048.
#data[:,0:4] = 100.*data[:,0:4]/data[:,0:4].max()
if checkpulse:
# normalise the pulse signal
data[:,4] = 2.*data[:,4]/data[:,4].max()
# and find the location of the first pulse
self.i_first = (data[:,4] - 0.9).__ge__(0).argmax()
# and cut off everything before that
time = time[self.i_first:]
time = time - time[0] + 0.002
data = data[self.i_first:,:]
if not self.silent:
print '========> calibrated data.shape:', data.shape
return time, data
def _tune_stream(self, time, data):
"""
Cleanup some stuff if the source is a streaming file
Data calibration is dependent on the date and is specific for a certain
dataset and even channel
First 10 datapoints are always ignored due to some startup peaks.
"""
# always ignore the first 10 points, they contain initial noise
time, data = time[10:], data[10:,:]
# add values for each missing point
deltas = time[1:] - time[:-1]
# we are still in int mode, so all deltas need to be == 1
d_check = np.abs(deltas-1).__gt__(0)
if np.any(d_check):
# find out how many missing points we have
nr_missing = len(deltas[d_check])
if not self.silent:
print '=======> missing points in blade.time: %i' % nr_missing
missing = True
while missing:
time, data, missing = self._add_missing_point(time, data)
# make sure we have a continues series, Fs will be 1, still in
# counting time steps mode here
Fs_dummy = calc_sample_rate(time)
# set zero load to zero strain instead of 2048
# DO NOT CHANGE THIS. UNLESS YOU REDO ALL THE CALIBRATION AGAIN
data[:,0:4] = (data[:,0:4] - 2048.)
# convert time signal from binary to seconds
time *= (1./self.Fs)
if not self.silent:
print '=======> calibrated data.shape:', data.shape
return time, data
def read(self, strainfile):
"""
Read the blade strain file which is saved from a streaming session.
File header for a triggered session (used in April):
Node,1259
Opening Tag,65535
Trigger ID,48
# Sweeps,65500
Trigger #,1
Num Channels,5
Channel Mask,47
Clock Freq (Hz),1024
Time (ms),Channel 1,Channel 2,Channel 3,Channel 4,Channel 6,
Note that time is given in seconds, starting at zero.
File header for a saved stream session:
Port, 4
Node, 1259
Time, 02/09/2012 15:29:18
Channel Mask, 15
Acquisition Attempt, Channel 1, Channel 2, Channel 3, Channel 4,
Note that time is given as an acquisition attempt number, not a time
stamp.
"""
# read the file header and determine which kind it is: a streamed
# or triggered log file
try:
f = open(strainfile, 'r')
except IOError:
strainfile += '.csv'
f = open(strainfile, 'r')
# stuff for reading the new node commander software
channel_info, data_start = False, False
for i, line in enumerate(f):
#print line.replace('\n', '')
# determine streaming properties
# ------------------------------
# read the line containing the start Time (for stream version)
#if line.startswith('Time,'):
#starttime = line.replace('\n', '').replace('Time, ', '')
#pass
# reading stuff for the new Node Commander triggered file type
# --------------------------------------
if channel_info and not line.startswith('Channel'):
self.Fs = int(line.split(';')[2].replace(' Hz', ''))
self.sample_rate = self.Fs
channel_info = False
# FIXME: each channel has an entry in this section, but we
# assume all channels have the same stuff
if self.debug:
print 'sample rate: %i' % self.Fs
elif data_start:
header_lines = i+1
# unicode array for the labels
labels = np.ndarray((4), dtype='<U15')
sep = ';'
# skip the first label, that's time, and last entry is empty
labels[:] = line.replace('\n', '').split(sep)[1:-1]
if self.debug:
print 'header lines: %i' % header_lines
print 'labels: ', labels
# stop reading, switch to faster np.loadtxt method for data
break
# --------------------------------------
# original stream file on WinXP
elif line.startswith('Acquisition'):
self.filetype = 'streaming'
header_lines = i+1
# unicode array for the labels
labels = np.ndarray((4), dtype='<U15')
sep = ','
# skip the first label, that's time, and last entry is empty
labels[:] = line.replace('\n', '').split(sep)[1:-1]
# stop reading, switch to faster np.loadtxt method for data
break
# with the new software Node Commander, the header looks different
elif line.startswith('Sweeps;Channel'):
self.filetype = 'streaming'
header_lines = i+1
sep = ';'
# unicode array for the labels
labels = np.ndarray((4), dtype='<U15')
# skip the first label, that's time, and last entry is empty
labels[:] = line.replace('\n', '').split(sep)[1:-1]
# stop reading, switch to faster np.loadtxt method for data
break
# determine triggered session properties
# --------------------------------------
# triggered type with the new Node Commander software
elif line.startswith('FILE_INFO'):
self.filetype = 'triggered-v2'
# with the new Node Commander, we also have the sample rate
elif line.startswith('CHANNEL_INFO'):
channel_info = True
elif line.startswith('DATA_START'):
data_start = True
elif line.startswith('Clock Freq'):
self.Fs = int(line.split(',')[1])
self.sample_rate = self.Fs
self.filetype = 'triggered'
elif line.startswith('Num Channels'):
sep = ','
num_chan = int(line.split(sep)[1])
# unicode array for the labels
labels = np.ndarray((num_chan), dtype='<U15')
elif line.startswith('Time (ms),'):
header_lines = i
# read the labels and ignore the last bogus entry
# skip also the first label, that's time
labels[:] = line.replace('\n', '').split(sep)[1:-1]
# stop reading, switch to faster np.loadtxt method for data
break
f.close()
# check: if the sample rate has not been given and we are talking
# about a streamed file, raise an error.
if not self.Fs:
msg = 'Blade strain file has no sample rate set.'
raise UserWarning, msg
# read the data part
#data = np.loadtxt(strainfile, delimiter=',', skiprows=6)
# genfromtxt is faster, but skip the last line since in some cases
# it might not be complete (especially for the triggered type)
data = np.genfromtxt(strainfile, skip_header=header_lines,
delimiter=sep, dtype=np.float32, skip_footer=1)
if not self.silent:
print strainfile
print 'loaded strain blade file, shape:', data.shape
# seperate the time signal from the data array
time = data[20:,0]
data = data[20:,1::]
return time, data, labels
def plot_channel(self, **kwargs):
"""
Plot a set of channels. Figure name is the same as the blade result
file except that '_chX' is added. Figure is seized dynamically based
on the number of plots.
Arguments
---------
channel : int or str, default=0
Index of the channel or string of the short channel name
figpath : str, default=None
Full path of the to be saved figure
"""
channel = kwargs.get('channel', 0)
figpath = kwargs.get('figpath', None)
# in case we have not an integer but a string: convert to index
if type(channel).__name__ == 'int':
channel = [channel]
figfile = self.resfile + '_ch'
for k in channel:
figfile += '_' + str(k)
plot = plotting.A4Tuned()
plot.plot_simple(figpath+figfile, self.time, self.data, self.labels,
channels=channel, grandtitle=figfile)
def psd_plots(self, figpath, channels, **kwargs):
"""
Power Spectral Density Analysis
===============================
Do for all blade strain channels a PSD analysis, put besides
the raw signal, and indicate the highest peak.
"""
figfile = kwargs.get('figfile', self.resfile) + '_eigenfreqs'
# take a few different number of samples for the NFFT
nnfts = kwargs.get('nnfts', [16384, 8192, 4096, 2048])
channel_names = kwargs.get('channel_names', None)
fn_max = kwargs.get('fn_max', 100)
saveresults = kwargs.get('saveresults', False)
pa4 = plotting.A4Tuned()
grandtitle = figfile.replace('_','\_')
pa4.setup(figpath+figfile, nr_plots=len(channels)*2,
grandtitle=grandtitle)
eigenfreqs = pa4.psd_eigenfreq(self.time, self.data, channels,
self.sample_rate, channel_names=channel_names,
nnfts=nnfts, fn_max=fn_max, saveresults=saveresults)
pa4.save_fig()
return eigenfreqs
class OJFLogFile:
def __init__(self, ojffile=None, silent=False):
"""
"""
self.silent = silent
# class properties
self.__name__ = 'ojflogfile'
self.ojffile = ojffile
self.cnames = ['RPM_fan', 'temperature', 'static_p', 'delta_p',
'wind_speed']
if ojffile:
self.time, self.data, self.labels = self.read(ojffile)
self.cnames = [str(k).replace(' ', '_') for k in self.labels]
# sample rate is fixed
self.sample_rate = 4
def _calibrate(self):
"""
Data calibration is dependent on the date and is specific for a certain
dataset and even channel
"""
pass
def _timestr2float(self, s):
"""
Convert the time stamp string HH:MM:SS to a float in seconds where
0 is 00:00:00 and 86400 is 23:59:59
"""
s = s.split(':')
# make sure that we have 3 elements back!
assert len(s) == 3
return (int(s[0])*60*60) + (int(s[1])*60) + (int(s[2]))
def _correct_accuracy(self, time):
"""
There are 4 log entries per second, however, the time stamps accuracy
is only 1 second. Add 1/4 s to the corresponding entries
"""
# figure out how many out of 4 time stamps are there in the beginning
# of the file
nr_ini = len(time[(time-time[0]).__eq__(0)])
# this answer should always be lower than 4! There are only 4
if nr_ini < 4:
for k in range(0,nr_ini):
time[k] += (3-k)*0.25
elif nr_ini == 4:
# switch ini back to zero, first second has a full set of 4
# measurement points
nr_ini = 0
else:
raise ValueError, 'OJF time series has unknown sample rate'
# the first data point is only 1 second, the other ones are 4 per s
time[nr_ini+1::4] += 0.25
time[nr_ini+2::4] += 0.50
time[nr_ini+3::4] += 0.75
return time
def read(self, ojffile):
"""
"""
# define the labels
labels = np.ndarray((5), dtype='<U15')
# labels[0] = u'time'
labels[0] = u'RPM fan'
labels[1] = u'temperature'
labels[2] = u'static_p'
labels[3] = u'delta_p'
labels[4] = u'wind speed'
# and the inverse for easy lookup in dictionary
self.labels_ch = dict()
for index, item in enumerate(labels):
self.labels_ch[item] = index
self.ojffile = ojffile
# in April the data did not hold a time column...
# determine ojf log file type
try:
f = open(ojffile, 'r')
# names have not consistantly saved with .log, anticipate troubles
except IOError:
f = open(ojffile + '.log', 'r')
ojffile += '.log'
# For the february session
if f.readline().find(':') > 0:
f.close()
# reading the file as a recarray
names=('time', 'rpm', 'temp', 'static_p', 'delta_p', 'windspeed')
formats=('S8', 'f8', 'f8', 'f8', 'f8', 'f8')
self.data_or = np.loadtxt(ojffile,
dtype={'names': names, 'formats': formats})
# read as a simple array, convert the time stampts (HH:MM:SS) to a
# float between 0 and 86400 (=24*60*60)
data = np.loadtxt(ojffile, converters = {0: self._timestr2float})
# split-up time, labels and actual data
time = self._correct_accuracy(data[:,0])
# ditch the time from the data signal
data = data[:,1::]
# and for the April session there is no time stamp HH:MM:SS
else:
f.close()
# reading the file as a recarray
names = ('rpm', 'temp', 'static_p', 'delta_p', 'windspeed')
formats = ( 'f8', 'f8', 'f8', 'f8', 'f8')
self.data_or = np.loadtxt(ojffile,
dtype={'names': names, 'formats': formats})
data = np.loadtxt(ojffile)
# we don't know the sampling rate for sure, but it is probably
# 4 Hz
time = np.arange(0,len(data),0.25)
if not self.silent:
print '\n'+'*'*80
print 'LOADING OJF FILE'
print ojffile
print 'ojffile.shape:', data.shape
return time, data, labels
class DspaceMatFile:
def __init__(self, matfile=None, silent=False):
"""
Load a dSPACE generated matfile
===============================
Load the measurement data aggregated by dSPACE and saved as a mat file.
If the keyword matfile is a valid filename of a mat file, the latter
will be loaded.
keywords
--------
matfile : str, defaul=None
file name of a valid dSPACE generated mat file.
Members
-------
time : ndarray(n)
array containing the time signal
data : ndarray(n,m)
array containing all m channels of measurement data
labels : ndarray(m)
array with the channel labels
"""
# class properties
self.__name__ = 'dspacematfile'
self.silent = silent
# declare the channel changes
self.ch_dict = dict()
self.ch_dict['"Model Root"/"Duty_Cycle"/"Value"'] = 'Duty Cycle'
self.ch_dict['"Model Root"/"Rotorspeed_Estimator"/"Rotor_freq_RPM"']\
= 'RPM Estimator v1'
# make it twice for easy RPM channel selection when mixing results
# from April
self.ch_dict['"Model Root"/"Rotorspeed_Estimator"/"Rotor_freq_RPM"']\
= 'RPM'
self.ch_dict['"Model Root"/"Rotorspeed_Estimator"/"Rotor_freq_RPM1"'] \
= 'RPM Estimator v2'
self.ch_dict['"Model Root"/"Rotorspeed_Estimator"/"Trigger"'] \
= 'HS trigger'
self.ch_dict['"Model Root"/"Sensors"/"50mA_v"/"Out1"'] = 'Current'
self.ch_dict['"Model Root"/"Sensors"/"Current_Filter"']='Current Filter'
self.ch_dict['"Model Root"/"Sensors"/"FA_gain"/"Out1"'] \
= 'Tower Strain For-Aft'
self.ch_dict['"Model Root"/"Sensors"/"Power"'] = 'Power'
self.ch_dict['"Model Root"/"PWM_GEN"/"PWM_GEN_Out"'] = 'Power2'
self.ch_dict['"Model Root"/"Sensors"/"SS_gain"/"Out1"'] \
= 'Tower Strain Side-Side'
self.ch_dict['"Model Root"/"Sensors"/"TWRFA1"'] \
= 'Tower Strain For-Aft filtered'
self.ch_dict['"Model Root"/"Sensors"/"TWRSS1"'] \
= 'Tower Strain Side-Side filtered'
self.ch_dict['"Model Root"/"Sensors"/"Voltage_Filter"'] \
= 'Voltage filtered'
# at least in april, accZ is actually SS
self.ch_dict['"Model Root"/"Sensors"/"accX"'] = 'Tower Top acc Z'
self.ch_dict['"Model Root"/"Sensors"/"accY"'] = 'Tower Top acc Y (FA)'
self.ch_dict['"Model Root"/"Sensors"/"accZ"'] = 'Tower Top acc X (SS)'
# RPM pulse is for April the wireless strain sensor pulse
self.ch_dict['"Model Root"/"Sensors"/"pulse1"'] = 'RPM Pulse'
self.ch_dict['"Model Root"/"Sensors"/"Dummy"'] = 'Yaw Laser'
# for February 14, Dummy is renamed to Voltage_LP9
self.ch_dict['"Model Root"/"Sensors"/"Voltage_LP9"/"Out1"']='Yaw Laser'
# changes for April
self.ch_dict['"Model Root"/"Sensors"/"Rotor_Speed_RPM"'] = 'RPM'
self.ch_dict['"Model Root"/"Sensors"/"Rotor_Pos_deg"'] = 'Azimuth'
self.ch_dict['"Model Root"/"Sensors"/"sound"'] = 'Sound'
self.ch_dict['"Model Root"/"Sensors"/"sound_gain"/"Out1"']='Sound_gain'
self.ch_dict['"Model Root"/"Tigger_Signal"/"Trigger"'] = 'HS trigger'
self.ch_dict['"Model Root"/"Tigger_Signal"/"Trigger_signal"/"Dummy"']\
= 'HS trigger start-end'
self.cnames = {}
self.cnames['"Model Root"/"Duty_Cycle"/"Value"'] = 'duty_cycle'
self.cnames['"Model Root"/"Rotorspeed_Estimator"/"Rotor_freq_RPM"']\
= 'rpm_est_v1'
# make it twice for easy RPM channel selection when mixing results
# from April
self.cnames['"Model Root"/"Rotorspeed_Estimator"/"Rotor_freq_RPM"']\
= 'rpm'
self.cnames['"Model Root"/"Rotorspeed_Estimator"/"Rotor_freq_RPM1"'] \
= 'rpm_est_v2'
# or is this the blade strain trigger?
self.cnames['"Model Root"/"Rotorspeed_Estimator"/"Trigger"'] \
= 'hs_trigger'
self.cnames['"Model Root"/"Sensors"/"50mA_v"/"Out1"'] = 'current'
self.cnames['"Model Root"/"Sensors"/"Current_Filter"']='current_filt'
self.cnames['"Model Root"/"Sensors"/"FA_gain"/"Out1"'] \
= 'tower_strain_fa'
self.cnames['"Model Root"/"Sensors"/"Power"'] = 'power'
self.cnames['"Model Root"/"PWM_GEN"/"PWM_GEN_Out"'] = 'power2'
self.cnames['"Model Root"/"Sensors"/"SS_gain"/"Out1"'] \
= 'tower_strain_ss'
self.cnames['"Model Root"/"Sensors"/"TWRFA1"'] \
= 'tower_strain_fa_filt'
self.cnames['"Model Root"/"Sensors"/"TWRSS1"'] \
= 'tower_strain_ss_filt'
self.cnames['"Model Root"/"Sensors"/"Voltage_Filter"'] \
= 'voltage_filt'
# at least in april, accZ is actually SS
self.cnames['"Model Root"/"Sensors"/"accX"'] = 'towertop_acc_z'
self.cnames['"Model Root"/"Sensors"/"accY"'] = 'towertop_acc_fa'
self.cnames['"Model Root"/"Sensors"/"accZ"'] = 'towertop_acc_ss'
# RPM pulse is for April the wireless strain sensor pulse
self.cnames['"Model Root"/"Sensors"/"pulse1"'] = 'rpm_pulse'
self.cnames['"Model Root"/"Sensors"/"Dummy"'] = 'yaw_angle'
# for February 14, Dummy is renamed to Voltage_LP9
self.cnames['"Model Root"/"Sensors"/"Voltage_LP9"/"Out1"']='yaw_angle'
# changes for April
self.cnames['"Model Root"/"Sensors"/"Rotor_Speed_RPM"'] = 'rpm'
self.cnames['"Model Root"/"Sensors"/"Rotor_Pos_deg"'] = 'rotor_azimuth'
self.cnames['"Model Root"/"Sensors"/"sound"'] = 'sound'
self.cnames['"Model Root"/"Sensors"/"sound_gain"/"Out1"']='sound_gain'
self.cnames['"Model Root"/"Tigger_Signal"/"Trigger"'] = 'hs_trigger'
self.cnames['"Model Root"/"Tigger_Signal"/"Trigger_signal"/"Dummy"']\
= 'hs_trigger_start_end'
self.rpm_spike_removed = False
# they are named withouth quotes for the sweep files
keys = self.ch_dict.keys()
for key in keys:
self.ch_dict[key.replace('"', '')] = self.ch_dict[key]
self.cnames[key.replace('"', '')] = self.cnames[key]
self.matfile = matfile
if matfile:
# file name only, excluding the path
self.resfile = matfile.split('/')[-1]
self.time, self.data, self.labels = self.read(matfile)
def remove_rpm_spike(self, plot=False):
"""
For some mistirious reason, the RPM signal can hold extreme big spikes.
Remove only the spiked region and assume that ends after 0.6 seconds
"""
irpm = self.labels_ch['RPM']
rpm = self.data[:,irpm]
# normalise by dividing by the time per sample
# or multiply with sample rate
diff_rpm = np.abs(np.diff(self.data[:,irpm]))*self.sample_rate
if diff_rpm.max() > 1000:
# find the insane high peak, and the next normal peak. Ignore
# anything in between.
# just make n as large as the array
ids = wafo.misc.findpeaks(rpm, n=len(rpm), min_h=2, min_p=0)
# sort, otherwise ordered according to significant wave height
ids.sort()
# seems the peak is often negative. So teak the peak before and
# after the abs max value and replace values with interpolation
imax = np.abs(rpm).argmax()
# the first value bigger than the peak index is the end peak
iiend = ids.__ge__(imax).argmax()
iend = ids[iiend]
istart = ids[iiend-1]
# do some interactive plots to check
if plot:
plt.figure()
plt.plot(self.time, rpm, 'b', label='rpm')
plt.plot(self.time[1:], diff_rpm, 'r', label='diff')
# plot all the RPM peaks
plt.plot(self.time[ids], rpm[ids], 'k+')
# plot the start end ending point of interpolation replacement
plt.plot(self.time[[istart,iend]], rpm[[istart,iend]], 'ms')
# data checks: time between peaks should not exceed 1 second
# and the normalised difference between the two peaks should not
# be too large
tdelta = self.time[istart] - self.time[iend]
ratio = np.abs((rpm[iend] - rpm[istart])/rpm[iend])
if tdelta > 1.5:
msg = 'remove peak failed: time between peaks %1.3f' % tdelta
logging.warn(msg)
elif ratio > 0.2:
msg = 'remove peak failed: delta ratio peaks %1.3f' % ratio
logging.warn(msg)
# if tests when ok, save the results
else:
replace = np.linspace(rpm[istart], rpm[iend], iend-istart)
self.data[istart:iend,irpm] = replace
logging.warn('replaced peak in RPM signal')
self.rpm_spike_removed = True
if plot:
# and plot the corrected value
plt.plot(self.time[istart:iend], replace, 'y')
def rpm_from_pulse(self, pulse_treshold=0.2, h=0.2, plot=False):
"""
Derive the RPM from the pulse signal. However, note that this approach
only works well if all pulses are of approximately the same height,
and the mean level should'nt change.
This function is quite similar to the RPM Estimater v1. There seems
some averaging going on for the latter? The estimator is based on
crossing a certain treshold maybe while this algorithm selects the
signal peak!
"""
ch = self.labels_ch['RPM Pulse']
# use wafo to find the turning points: min/max positions on the waves
wafo_sig = np.transpose(np.array([self.time, self.data[:,ch]]))
sig_ts = wafo.objects.mat2timeseries(wafo_sig)
self.sig_tp = sig_ts.turning_points(wavetype=None, h=h)
# Select only the max peaks, not the min peaks. Make sure the initial
# point has a value higher than pulse_treshold
if self.sig_tp.data[0] > pulse_treshold: start = 0
elif self.sig_tp.data[1] > pulse_treshold: start = 1
elif self.sig_tp.data[2] > pulse_treshold: start = 2
else:
msg = 'Can\'t find a peak value above treshold for first 3 points'
raise UserWarning, msg
end = len(self.sig_tp.data)
# select only the signal peaks. Each peak seems to be accompinied by
# a low peak, so skip every other peak found by wafo
data_peaks = self.sig_tp.data[start:end:2]
time_peaks = self.sig_tp.args[start:end:2]
# and make sure we only have selected peaks above pulse_treshold
if not data_peaks.__ge__(pulse_treshold).all():
msg = 'selected peak range is not exclusively above threshold'
raise ValueError, msg
# convert to rpm, based on pulses per revolution
# time per peak = sec/120deg = 3*sec/rev = 3*min/60*rev
# carefull: in February we had 3 peaks per revolution,
# somewhere in april they where made black though
if self.campaign == 'February':
pulses_per_rev = 3
elif self.campaign == 'April':
pulses_per_rev = 1
else:
msg='campaign has to be February or April, not %s' % self.campaign
raise ValueError, msg
self.data_rpm = 60.0/((time_peaks[1:]-time_peaks[:-1])*pulses_per_rev)
self.time_rpm = time_peaks[1:]
if plot:
plt.figure()
plt.plot(self.time, self.data[:,ch])
plt.plot(time_peaks, data_peaks, 'g+')
plt.title('original signal and selected peaks')
plt.legend()
plt.figure()
plt.plot(self.time_rpm, self.data_rpm, 'r', label='peak derived')
# overplot the RPM version1 signal
try:
ch_rpm_v1 = self.labels_ch['RPM']
plt.plot(self.time, self.data[:,ch_rpm_v1], label='RPM est v1')
except KeyError:
pass
try:
ch_rpm_v1 = self.labels_ch['RPM Estimator v2']
plt.plot(self.time, self.data[:,ch_rpm_v1], label='RPM est v2')
except KeyError:
pass
plt.title('comparing rpm estimater with peak derived rpm')
plt.legend()
plt.figure()
plt.title('RPM Pulse')
plt.subplot(211)
plt.plot(self.time, self.data[:,ch], label='data or')
plt.plot(self.sig_tp.args, self.sig_tp.data, '+', label='wafo tp')
plt.legend()
plt.subplot(212)
plt.title('PSD from original data')
Fs = self.sample_rate
Pxx, freqs = plt.psd(self.data[:,ch], NFFT=4*8192, Fs=Fs)