def AutoFillConfigFile(FilePath): CodePath = stBar.GetStatusText() (FitFilePath, FITFileName) = os.path.split(FileNameCtl.GetLabel()) print 'AutoFillConfigFile called. CodePath: ' + CodePath ConfigFile = FindConfigFile(CodePath, FilePath) if ConfigFile is None: ConfigFileCtl.SetLabel('') else: ConfigFileCtl.SetLabel(ConfigFile)
def endurance_summary(FitFilePath, ConfigFile=None, OutStream=sys.stdout): (FilePath, FitFileName) = os.path.split(FitFilePath) if ConfigFile is None: ConfigFile = FindConfigFile('', FilePath) if (ConfigFile is None) or (not os.path.exists(ConfigFile)): raise IOError('Configuration file not specified or found') # # Parse the configuration file # from ConfigParser import ConfigParser config = ConfigParser() config.read(ConfigFile) print >> OutStream, 'reading config file ' + ConfigFile ThresholdPower = config.getfloat( 'power', 'ThresholdPower' ) ThresholdHR = config.getfloat( 'power', 'ThresholdHR' ) print >> OutStream, 'ThresholdPower: ', ThresholdPower print >> OutStream, 'ThresholdHR : ', ThresholdHR # power zones from "Cyclist's Training Bible", 5th ed., by Joe Friel, p51 FTP = ThresholdPower pZones = { 1 : [ 0 , 0.55*FTP ], 2 : [ 0.55*FTP, 0.75*FTP ], 3 : [ 0.75*FTP, 0.90*FTP ], 4 : [ 0.90*FTP, 1.05*FTP ], 5 : [ 1.05*FTP, 1.20*FTP ], 6 : [ 1.20*FTP, 1.50*FTP ], 7 : [ 1.50*FTP, 2.50*FTP ]} # heart-rate zones from "Cyclist's Training Bible" 5th ed. by Joe Friel, p50 FTHR = ThresholdHR hZones = { 1 : [ 0 , 0.82*FTHR ], # 1 2 : [ 0.82*FTHR, 0.89*FTHR ], # 2 3 : [ 0.89*FTHR, 0.94*FTHR ], # 3 4 : [ 0.94*FTHR, 1.00*FTHR ], # 4 5 : [ 1.00*FTHR, 1.03*FTHR ], # 5a 6 : [ 1.03*FTHR, 1.06*FTHR ], # 5b 7 : [ 1.07*FTHR, 1.15*FTHR ]} # 5c # get zone bounds for plotting p_zone_bounds = [ pZones[1][0], pZones[2][0], pZones[3][0], pZones[4][0], pZones[5][0], pZones[6][0], pZones[7][0], pZones[7][1] ] h_zone_bounds = [ 0.4*FTHR, # better plotting hZones[2][0], hZones[3][0], hZones[4][0], hZones[5][0], hZones[6][0], hZones[7][0], hZones[7][1] ] from datetime import datetime from fitparse import Activity from activity_tools import extract_activity_signals required_signals = [ 'power', 'heart_rate' ] # get the signals activity = Activity(FitFilePath) signals = extract_activity_signals(activity, resample='existing') if not all( s in signals.keys() for s in required_signals ): msg = 'required signals not in file' print >> OutStream, msg print >> OutStream, 'Signals required:' for s in required_signals: print >> OutStream, ' ' + s print >> OutStream, 'Signals contained:' for s in signals.keys(): print >> OutStream, ' ' + s raise IOError(msg) ''' #################### # Get Records of type 'lap' # types: [ 'record', 'lap', 'event', 'session', 'activity', ... ] records = activity.get_records_by_type('lap') current_record_number = 0 elapsed_time = [] timer_time = [] avg_heart_rate = [] avg_power = [] avg_cadence = [] max_heart_rate = [] balance = [] lap_timestamp = [] lap_start_time = [] FirstIter = True for record in records: # Print record number current_record_number += 1 #print (" Record #%d " % current_record_number).center(40, '-') # Get the list of valid fields on this record valid_field_names = record.get_valid_field_names() for field_name in valid_field_names: # Get the data and units for the field field_data = record.get_data(field_name) field_units = record.get_units(field_name) ## Print what we've got! #if field_units: # print >> OutStream, " * %s: %s %s" % (field_name, field_data, field_units) #else: # print >> OutStream, " * %s: %s" % (field_name, field_data) if 'timestamp' in field_name: lap_timestamp.append( field_data ) if 'start_time' in field_name: lap_start_time.append( field_data ) if 'total_elapsed_time' in field_name: elapsed_time.append( field_data ) if 'total_timer_time' in field_name: timer_time.append( field_data ) if 'avg_power' in field_name: avg_power.append( field_data ) # avg_heart_rate is in a lap record if 'avg_heart_rate' in field_name: avg_heart_rate.append(field_data) if 'max_heart_rate' in field_name: max_heart_rate.append(field_data) if 'avg_cadence' in field_name: avg_cadence.append(field_data) if 'left_right_balance' in field_name: balance.append(field_data) #print #################### ''' # # extract lap results # from fitparse import Activity from activity_tools import extract_activity_laps import numpy as np activity = Activity(FitFilePath) laps = extract_activity_laps(activity) avg_power = laps['power'] time = laps['time'] cadence = laps['cadence'] avg_heart_rate = laps['avg_hr'] max_heart_rate = laps['max_hr'] balance = laps['balance'] lap_start_time = laps['start_time'] lap_timestamp = laps['timestamp' ] timer_time = laps['total_timer_time'] elapsed_time = laps['total_elapsed_time'] IntervalThreshold = 0.0 # get all laps (0.72*FTP) from numpy import nonzero, array, arange, zeros, average, logical_and # resample power to constant-increment (1 Hz) with zeros at missing samples time_idx = signals['time'].astype('int') power_vi = signals['power'] heart_rate_vi = signals['heart_rate'] nScans = time_idx[-1]+1 time_ci = arange(nScans) power = zeros(nScans) power[time_idx] = power_vi heart_rate_ci = zeros(nScans) heart_rate_ci[time_idx] = heart_rate_vi t0 = signals['metadata']['timestamp'] print >> OutStream, 'signal timestamp: ', t0.time() # plot lap results as continuous time signals lap_avg_hr_c = zeros(nScans) lap_avg_power_c = zeros(nScans) lap_norm_power_c = zeros(nScans) # compute the 30-second, moving-average power signal. p30 = BackwardMovingAverage( power ) # # compute lap metrics # print >> OutStream, 'lap results:' nLaps = len(elapsed_time) vi_time_vector = signals['time'] lap_avg_power = zeros(nLaps) lap_norm_power = zeros(nLaps) lap_avg_hr = zeros(nLaps) lap_if = zeros(nLaps) # intensity factor lap_start_sec = zeros(nLaps) # lap start times in seconds #time = array(elapsed_time) #cadence = array(avg_cadence) #avg_hr = array(avg_heart_rate) #max_hr = array(max_heart_rate) #balance = array(balance) names1 = [ '', ' lap', ' avg', ' norm', 'avg', 'max', '' ] names2 = [ 'lap', ' time', 'power', 'power', ' HR', ' HR', ' IF' ] fmt = "%8s"+"%10s"+"%8s"*5 print >> OutStream, fmt % tuple(names1) print >> OutStream, fmt % tuple(names2) for i in range(nLaps): # count samples in this lap tBeg = (lap_start_time[i] - t0).total_seconds() tEnd = (lap_timestamp[i] - t0).total_seconds() ii = nonzero( logical_and( time_idx >= tBeg, \ time_idx < tEnd) )[0] nPts = ii.size lap_start_sec[i] = tBeg lap_avg_hr[i] = average(heart_rate_vi[ii]) lap_avg_power[i] = average(power[time_idx[ii]]) lap_norm_power[i] = average( p30[time_idx[ii]]**4 )**(0.25) lap_if[i] = lap_norm_power[i] / FTP # duration from lap metrics dur = (lap_timestamp[i] - lap_start_time[i]).total_seconds() mm = timer_time[i] // 60 ss = timer_time[i] % 60 fmt = '%8d'+'%7i:%02i'+'%8i'*4 + '%8.2f' print >> OutStream, fmt \ % ( i, mm, ss, avg_power[i], lap_norm_power[i], avg_heart_rate[i], max_heart_rate[i], lap_if[i] ) # plot lap results as continuous time signals lap_avg_hr_c [time_idx[ii]] = lap_avg_hr[i] lap_avg_power_c [time_idx[ii]] = lap_avg_power[i] lap_norm_power_c[time_idx[ii]] = lap_norm_power[i] # # ride-level results # print >> OutStream, 'ride-level results:' names1 = [ '', 'moving', ' avg', ' norm', 'avg', '', 'Pw:' ] names2 = [ 'seg', ' time', 'power', 'power', ' HR', ' IF', ' HR' ] fmt = "%8s"+"%10s"+"%8s"*5 print >> OutStream, fmt % tuple(names1) print >> OutStream, fmt % tuple(names2) # whole ride tBeg = (lap_start_time[0] - t0).total_seconds() tEnd = (lap_timestamp[-1] - t0).total_seconds() ii = nonzero( logical_and( time_idx >= tBeg, \ time_idx < tEnd) )[0] nPts = ii.size dur = nPts # sample rate 1 Hz hh = dur // 3600 mm = (dur % 3600) // 60 ss = (dur % 3600) % 60 all_avg_hr = average(heart_rate_vi[ii]) all_avg_power = average(power_vi[ii]) all_norm_power = average( p30[time_idx[ii]]**4 )**(0.25) all_max_hr = max(heart_rate_vi[ii]) all_if = all_norm_power / FTP # aerobic decoupling iiH1 = ii[0:nPts/2] h1_norm_power = average( p30[time_idx[iiH1]]**4 )**(0.25) h1_avg_hr = average(heart_rate_vi[iiH1]) h1ef = h1_norm_power / h1_avg_hr iiH2 = ii[nPts/2:] h2_norm_power = average( p30[time_idx[iiH2]]**4 )**(0.25) h2_avg_hr = average(heart_rate_vi[iiH2]) h2ef = h2_norm_power / h2_avg_hr all_pw_hr = (h1ef-h2ef)/(h1ef)*100.0 fmt = '%8s'+'%4i:%02i:%02i'+'%8i'*3 + '%8.2f' + '%8.1f' print >> OutStream, fmt \ % ( 'all', hh, mm, ss, all_avg_power, all_norm_power, all_avg_hr, all_if, all_pw_hr ) # without end laps tBeg = (lap_start_time[1] - t0).total_seconds() tEnd = (lap_timestamp[-2] - t0).total_seconds() ii = nonzero( logical_and( time_idx >= tBeg, \ time_idx < tEnd) )[0] nPts = ii.size dur = nPts # sample rate 1 Hz hh = dur // 3600 mm = (dur % 3600) // 60 ss = (dur % 3600) % 60 mid_avg_hr = average(heart_rate_vi[ii]) mid_avg_power = average(power_vi[ii]) mid_norm_power = average( p30[time_idx[ii]]**4 )**(0.25) mid_max_hr = max(heart_rate_vi[ii]) mid_if = mid_norm_power / FTP # aerobic decoupling iiH1 = ii[0:nPts/2] h1_norm_power = average( p30[time_idx[iiH1]]**4 )**(0.25) h1_avg_hr = average(heart_rate_vi[iiH1]) h1ef = h1_norm_power / h1_avg_hr iiH2 = ii[nPts/2:] h2_norm_power = average( p30[time_idx[iiH2]]**4 )**(0.25) h2_avg_hr = average(heart_rate_vi[iiH2]) h2ef = h2_norm_power / h2_avg_hr mid_pw_hr = (h1ef-h2ef)/(h1ef)*100.0 fmt = '%5i-%02i'+'%4i:%02i:%02i'+'%8i'*3 + '%8.2f' + '%8.1f' print >> OutStream, fmt \ % ( 1, nLaps-2, hh, mm, ss, mid_avg_power, mid_norm_power, mid_avg_hr, mid_if, mid_pw_hr ) print print # # time plot # import matplotlib.pyplot as plt import matplotlib.dates as md from matplotlib.dates import date2num, DateFormatter import datetime as dt base = dt.datetime(2014, 1, 1, 0, 0, 0) x = [base + dt.timedelta(seconds=t) for t in time_ci.astype('float')] x = date2num(x) # Convert to matplotlib format fig1, (ax0, ax1) = plt.subplots(nrows=2, sharex=True) ax0.plot_date( x, heart_rate_ci, 'r-', linewidth=1 ); ax0.plot_date( x, lap_avg_hr_c, 'r-', linewidth=3 ); ax0.set_yticks( h_zone_bounds, minor=False) x_laps = [ base + dt.timedelta(seconds=t) \ for t in lap_start_sec.astype('float') ] x_laps = date2num(x_laps) ax0.grid(True) ax0.set_ylabel('heart rate, BPM') ax1.plot_date( x, power, 'k-', linewidth=1 ); ax1.plot_date( x, p30, 'm-', linewidth=1); ax1.plot_date( x, lap_avg_power_c, 'b-', linewidth=3); ax1.plot_date( x, lap_norm_power_c, 'g-', linewidth=3); ax1.xaxis.set_major_formatter(DateFormatter('%H:%M:%S')) ax1.set_yticks( p_zone_bounds, minor=False) ax1.grid(True) ax1.set_ylabel('power, watts') ax1.legend(['power', 'p30', 'lap_avg_power', 'lap_norm_power'], loc='upper left'); for i in range(nLaps): ax0.axvline( x_laps[i], label=str(i+1) ) ax1.axvline( x_laps[i], label=str(i+1) ) fig1.autofmt_xdate() fig1.suptitle('Endurance Power Results', fontsize=20) fig1.tight_layout() fig1.subplots_adjust(hspace=0) # Remove horizontal space between axes fig1.canvas.set_window_title(FitFilePath) plt.show() def ClosePlots(): plt.close('all') return ClosePlots
def zone_detect(FitFilePath, ConfigFile=None, OutStream=sys.stdout): (FilePath, FitFileName) = os.path.split(FitFilePath) if ConfigFile is None: ConfigFile = FindConfigFile('', FilePath) if (ConfigFile is None) or (not os.path.exists(ConfigFile)): raise IOError('Configuration file not specified or found') # # Parse the configuration file # from ConfigParser import ConfigParser config = ConfigParser() config.read(ConfigFile) print >> OutStream, 'reading config file ' + ConfigFile ThresholdPower = config.getfloat('power', 'ThresholdPower') ThresholdHR = config.getfloat('power', 'ThresholdHR') print >> OutStream, 'ThresholdPower: ', ThresholdPower print >> OutStream, 'ThresholdHR : ', ThresholdHR from datetime import datetime from fitparse import Activity from activity_tools import extract_activity_signals required_signals = ['power'] # 'heart_rate' optional # get the signals activity = Activity(FitFilePath) signals = extract_activity_signals(activity) if not all(s in signals.keys() for s in required_signals): msg = 'required signals not in file' print >> OutStream, msg print >> OutStream, 'Signals required:' for s in required_signals: print >> OutStream, ' ' + s print >> OutStream, 'Signals contained:' for s in signals.keys(): print >> OutStream, ' ' + s raise IOError(msg) hasHR = True if 'heart_rate' in signals.keys() else False # up-sample by 5x so that zone-skipping is not needed SampleRate = 5.0 from numpy import arange, interp n = len(signals['power']) old_time = arange(n) nPts = int(n * SampleRate) # 32-bit integer new_time = arange(nPts) / SampleRate power = interp(new_time, old_time, signals['power']) if hasHR: heart_rate = interp(new_time, old_time, signals['heart_rate']) # power zones from "Cyclist's Training Bible", 5th ed., by Joe Friel, p51 FTP = ThresholdPower pZones = { 1: [0, 0.55 * FTP], 2: [0.55 * FTP, 0.75 * FTP], 3: [0.75 * FTP, 0.90 * FTP], 4: [0.90 * FTP, 1.05 * FTP], 5: [1.05 * FTP, 1.20 * FTP], 6: [1.20 * FTP, 1.50 * FTP], 7: [1.50 * FTP, 2.50 * FTP] } # heart-rate zones from "Cyclist's Training Bible" 5th ed. by Joe Friel, p50 FTHR = ThresholdHR hZones = { 1: [0, 0.82 * FTHR], # 1 2: [0.82 * FTHR, 0.89 * FTHR], # 2 3: [0.89 * FTHR, 0.94 * FTHR], # 3 4: [0.94 * FTHR, 1.00 * FTHR], # 4 5: [1.00 * FTHR, 1.03 * FTHR], # 5a 6: [1.03 * FTHR, 1.06 * FTHR], # 5b 7: [1.07 * FTHR, 1.15 * FTHR] } # 5c def LocateZone(x, zones): Z = 1 if x >= zones[2][0]: Z = 2 if x >= zones[3][0]: Z = 3 if x >= zones[4][0]: Z = 4 if x >= zones[5][0]: Z = 5 if x >= zones[6][0]: Z = 6 if x >= zones[7][0]: Z = 7 return Z # define boxcar averages used to test for upward transition out of # indicated zone. fpZ1 = ForwardBoxcarAverage(power, window=90, SampleRate=SampleRate) fpZ2 = ForwardBoxcarAverage(power, window=60, SampleRate=SampleRate) fpZ3 = ForwardBoxcarAverage(power, window=45, SampleRate=SampleRate) fpZ4 = ForwardBoxcarAverage(power, window=30, SampleRate=SampleRate) fpZ5 = ForwardBoxcarAverage(power, window=15, SampleRate=SampleRate) fpZ6 = ForwardBoxcarAverage(power, window=5, SampleRate=SampleRate) # fpZ7 not needed # assemble these into a dictionary: # so that I could test # if LocateZone( FBoxCars[CurrentZone][i], pZones ) # > CurrentZone: FBoxCars = { 1: fpZ1, 2: fpZ2, 3: fpZ3, 4: fpZ4, 5: fpZ5, 6: fpZ6 } # Z7 not needed # define boxcar averages used to test for downward transition into # indicated zone. cpZ1 = CenteredBoxcarAverage(power, window=60, SampleRate=SampleRate) cpZ2 = CenteredBoxcarAverage(power, window=45, SampleRate=SampleRate) cpZ3 = CenteredBoxcarAverage(power, window=30, SampleRate=SampleRate) cpZ4 = CenteredBoxcarAverage(power, window=15, SampleRate=SampleRate) cpZ5 = CenteredBoxcarAverage(power, window=7, SampleRate=SampleRate) cpZ6 = CenteredBoxcarAverage(power, window=3, SampleRate=SampleRate) # fpZ7 not needed # assemble these into a dictionary: # so that I could test # if LocateZone( CBoxCars[CurrentZone][i], pZones ) # > CurrentZone: CBoxCars = { 1: cpZ1, 2: cpZ2, 3: cpZ3, 4: cpZ4, 5: cpZ5, 6: cpZ6 } # Z7 not needed from numpy import array, arange, append, zeros, cumsum, average cp2 = zeros(nPts) fboxpower = zeros(nPts) cboxpower = zeros(nPts) zone = zeros(nPts) zone_mid = zeros(nPts) CurrentZone = 1 # create a phaseless, lowpass-filtered signal for downward transitions # see # https://docs.scipy.org/doc/scipy/reference/signal.html from scipy import signal poles = 4 cutoff = 0.1 # Hz Wn = cutoff / (SampleRate / 2) PadLen = int(SampleRate / cutoff) b, a = signal.butter(poles, Wn, btype='lowpass') # lpfpower = signal.filtfilt(b, a, power, padlen=PadLen) # calculate zone midpoints for plotting ZoneMidPoint = {} # empty dictionary ZoneMidPoint[1] = (pZones[1][0] + pZones[1][1]) / 2 ZoneMidPoint[2] = (pZones[2][0] + pZones[2][1]) / 2 ZoneMidPoint[3] = (pZones[3][0] + pZones[3][1]) / 2 ZoneMidPoint[4] = (pZones[4][0] + pZones[4][1]) / 2 ZoneMidPoint[5] = (pZones[5][0] + pZones[5][1]) / 2 ZoneMidPoint[6] = (pZones[6][0] + pZones[6][1]) / 2 ZoneMidPoint[7] = (pZones[7][0] + pZones[7][1]) / 2 for i, p in zip(range(nPts), power): # compute the centered 3-second power #raise RuntimeError("need to account for sample rate in cp2") sr = int(SampleRate) if i == 0: cp2[i] = power[i] elif i < 2 * sr: cp2[i] = average(power[0:i]) elif i > nPts - 2 * sr: cp2[i] = average(power[i - sr:]) else: cp2[i] = average(power[i - sr:i + sr + 1]) # upward transition cz = CurrentZone # short name if cz < 7: tz = 7 # Test Zone while tz > cz: if (LocateZone( cp2[i], pZones ) >= tz) \ & (LocateZone( FBoxCars[tz-1][i], pZones ) >= tz): CurrentZone = tz zone[i] = CurrentZone break tz -= 1 # downward transition. Avoid 2nd test if in Z1. # use centered-boxcar average to avoid getting "trapped" # in low zones. if cz > 1: tz = cz - 1 while tz >= 1: if (LocateZone( cp2[i], pZones ) <= tz) \ & (LocateZone( CBoxCars[tz][i], pZones ) <= tz) \ & (LocateZone( FBoxCars[tz][i], pZones ) <= tz): CurrentZone = tz zone[i] = CurrentZone break tz -= 1 # the filtered power comes from CurrentZone after any transition fboxpower[i] = FBoxCars[min(CurrentZone, 6)][i] cboxpower[i] = CBoxCars[max(CurrentZone - 1, 1)][i] # calculate zone midpoints for plotting zone_mid[i] = ZoneMidPoint[CurrentZone] # get zone bounds for plotting p_zone_bounds = [ pZones[1][0], pZones[2][0], pZones[3][0], pZones[4][0], pZones[5][0], pZones[6][0], pZones[7][0], pZones[7][1] ] h_zone_bounds = [ 0.4 * FTHR, # better plotting hZones[2][0], hZones[3][0], hZones[4][0], hZones[5][0], hZones[6][0], hZones[7][0], hZones[7][1] ] ############################################################ # plotting # ############################################################ # # extract lap times # from activity_tools import extract_activity_laps activity = Activity(FitFilePath) laps = extract_activity_laps(activity) lap_start_time = laps['start_time'] # datetime object lap_timestamp = laps['timestamp'] nLaps = len(lap_start_time) t0 = signals['metadata']['timestamp'] lap_start_sec = zeros(nLaps) # lap start times in seconds for i in range(nLaps): tBeg = (lap_start_time[i] - t0).total_seconds() tEnd = (lap_timestamp[i] - t0).total_seconds() lap_start_sec[i] = tBeg # time plot import matplotlib.pyplot as plt import matplotlib.dates as md from matplotlib.dates import date2num, DateFormatter import datetime as dt base = dt.datetime(2014, 1, 27, 0, 0, 0) x = [base + dt.timedelta(seconds=t) for t in new_time] x = date2num(x) # Convert to matplotlib format x_laps = [ base + dt.timedelta(seconds=t) \ for t in lap_start_sec.astype('float') ] x_laps = date2num(x_laps) if hasHR: fig1, (ax0, ax1) = plt.subplots(nrows=2, sharex=True) ax0.plot_date(x, heart_rate, 'r-', linewidth=3) ax0.set_yticks(h_zone_bounds, minor=False) ax0.grid(True) ax0.set_title('heart rate, BPM') for i in range(nLaps): ax0.axvline(x_laps[i], label=str(i + 1)) else: fig1, ax1 = plt.subplots(nrows=1, sharex=True) ax1.plot_date(x, power, 'k-', linewidth=1) ax1.plot_date(x, fboxpower, 'm-', linewidth=1) ax1.plot_date(x, cp2, 'r.', markersize=4) ax1.plot_date(x, cboxpower, 'b-', linewidth=1) ax1.plot_date(x, zone_mid, 'g-', linewidth=3) ax1.xaxis.set_major_formatter(DateFormatter('%H:%M:%S')) ax1.set_yticks(p_zone_bounds, minor=False) ax1.grid(True) ax1.set_title('power, watts') ax1.legend(['power', 'FBoxCar', 'cp2', 'CBoxCar', 'zone mid'], loc='upper left') for i in range(nLaps): ax1.axvline(x_laps[i], label=str(i + 1)) fig1.autofmt_xdate() fig1.suptitle('Power Zone Detection', fontsize=20) fig1.tight_layout() fig1.canvas.set_window_title(FitFilePath) plt.show() # better histogram plot with control of counts from numpy import histogram PowerCounts, PowerBins = histogram(power, bins=p_zone_bounds) ZoneCounts, ZoneBins = histogram(zone_mid, bins=p_zone_bounds) fig2, ax = plt.subplots() bar_width = 0.35 opacity = 0.4 #error_config = {'ecolor': '0.3'} zone_ints = arange(7) + 1 LogY = True rects1 = ax.bar(zone_ints, PowerCounts / SampleRate / 60, bar_width, alpha=opacity, color='b', log=LogY, label='raw power') rects2 = ax.bar(zone_ints + bar_width, ZoneCounts / SampleRate / 60, bar_width, alpha=opacity, color='r', log=LogY, label='detected zone') ax.set_xlabel('Zone') ax.set_ylabel('minutes') ax.set_title('Zone Detection Histogram') ax.set_xticks(zone_ints + bar_width / 2) ax.set_xticklabels(('Rec', 'End', 'Tmp', 'Thr', 'VO2', 'An', 'NM')) ax.legend() fig2.tight_layout() fig2.canvas.set_window_title(FitFilePath) plt.show() # formatted print of histogram print >> OutStream, 'Power Zone Histogram:' for i in range(7): dur = ZoneCounts[i] / SampleRate pct = dur / sum(ZoneCounts / SampleRate) * 100 hh = dur // 3600 mm = (dur % 3600) // 60 ss = (dur % 3600) % 60 print >> OutStream, ' Zone %i: %2i:%02i:%02i (%2i%%)' \ % (i+1, hh, mm, ss, pct) dur = sum(ZoneCounts) / SampleRate hh = dur // 3600 mm = (dur % 3600) // 60 ss = (dur % 3600) % 60 print >> OutStream, ' total: %2i:%02i:%02i' % (hh, mm, ss) def ClosePlots(): plt.close('all') return ClosePlots
def pwhr_transfer_function(FitFilePath, ConfigFile=None, OutStream=sys.stdout): # this needs to stay INSIDE the function or bad things happen import matplotlib.pyplot as plt (FilePath, FitFileName) = os.path.split(FitFilePath) if ConfigFile is None: ConfigFile = FindConfigFile('', FilePath) if (ConfigFile is None) or (not os.path.exists(ConfigFile)): raise IOError('Configuration file not specified or found') # # Parse the configuration file # if type(ConfigFile) != type(ConfigParser()): if (ConfigFile is None) or (not os.path.exists(ConfigFile)): raise IOError('Configuration file not specified or found') config = ConfigParser() config.read(ConfigFile) print >> OutStream, 'reading config file ' + ConfigFile else: config = ConfigFile WeightEntry = config.getfloat('user', 'weight') WeightToKg = config.getfloat('user', 'WeightToKg') weight = WeightEntry * WeightToKg age = config.getfloat('user', 'age') EndurancePower = config.getfloat('power', 'EndurancePower') ThresholdPower = config.getfloat('power', 'ThresholdPower') EnduranceHR = config.getfloat('power', 'EnduranceHR') ThresholdHR = config.getfloat('power', 'ThresholdHR') HRTimeConstant = config.getfloat('power', 'HRTimeConstant') HRDriftRate = config.getfloat('power', 'HRDriftRate') print >> OutStream, 'WeightEntry : ', WeightEntry print >> OutStream, 'WeightToKg : ', WeightToKg print >> OutStream, 'weight : ', weight print >> OutStream, 'age : ', age print >> OutStream, 'EndurancePower : ', EndurancePower print >> OutStream, 'ThresholdPower : ', ThresholdPower print >> OutStream, 'EnduranceHR : ', EnduranceHR print >> OutStream, 'ThresholdHR : ', ThresholdHR print >> OutStream, 'HRTimeConstant : ', HRTimeConstant print >> OutStream, 'HRDriftRate : ', HRDriftRate # power zones from "Cyclist's Training Bible", 5th ed., by Joe Friel, p51 FTP = ThresholdPower pZones = { 1: [0, 0.55 * FTP], 2: [0.55 * FTP, 0.75 * FTP], 3: [0.75 * FTP, 0.90 * FTP], 4: [0.90 * FTP, 1.05 * FTP], 5: [1.05 * FTP, 1.20 * FTP], 6: [1.20 * FTP, 1.50 * FTP], 7: [1.50 * FTP, 2.50 * FTP] } # heart-rate zones from "Cyclist's Training Bible" 5th ed. by Joe Friel, p50 FTHR = ThresholdHR hZones = { 1: [0, 0.82 * FTHR], # 1 2: [0.82 * FTHR, 0.89 * FTHR], # 2 3: [0.89 * FTHR, 0.94 * FTHR], # 3 4: [0.94 * FTHR, 1.00 * FTHR], # 4 5: [1.00 * FTHR, 1.03 * FTHR], # 5a 6: [1.03 * FTHR, 1.06 * FTHR], # 5b 7: [1.07 * FTHR, 1.15 * FTHR] } # 5c # get zone bounds for plotting p_zone_bounds = [ pZones[1][0], pZones[2][0], pZones[3][0], pZones[4][0], pZones[5][0], pZones[6][0], pZones[7][0], pZones[7][1] ] h_zone_bounds = [ 0.4 * FTHR, # better plotting hZones[2][0], hZones[3][0], hZones[4][0], hZones[5][0], hZones[6][0], hZones[7][0], hZones[7][1] ] from fitparse import Activity from activity_tools import extract_activity_signals required_signals = ['power', 'heart_rate'] # get the signals activity = Activity(FitFilePath) signals = extract_activity_signals(activity, resample='existing') if not all(s in signals.keys() for s in required_signals): msg = 'required signals not in file' print >> OutStream, msg print >> OutStream, 'Signals required:' for s in required_signals: print >> OutStream, ' ' + s print >> OutStream, 'Signals contained:' for s in signals.keys(): print >> OutStream, ' ' + s raise IOError(msg) # resample to constant-increment (1 Hz) with zeros at missing samples time_idx = signals['time'].astype('int') power_vi = signals['power'] heart_rate_vi = signals['heart_rate'] nScans = time_idx[-1] + 1 time_ci = np.arange(nScans) power = np.zeros(nScans) power[time_idx] = power_vi heart_rate_ci = np.zeros(nScans) heart_rate_ci[time_idx] = heart_rate_vi # compute the 30-second, moving-average power signal. p30 = BackwardMovingAverage(power) ''' # compute moving time moving_time would contain data in the gaps--repeats of the "stopped" time until it resumes: >>> time_idx array([ 0, 1, 2, 5, 6, 7, 10, 11, 12]) >>> time_ci array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) >>> moving_time array([ 0, 1, 2, 2, 2, 3, 4, 5, 5, 5, 6, 7, 8]) ''' moving_time = np.zeros(nScans).astype('int') idx = 0 for i in time_ci[0:-1]: moving_time[i] = idx if time_idx[idx + 1] == i + 1: idx += 1 moving_time[nScans - 1] = idx # Calculate running normalized power and TSS. norm_power = np.zeros(nScans) TSS = np.zeros(nScans) for i in range(1, nScans): ii = np.nonzero(time_idx <= i)[0] norm_power[i] = np.average(p30[time_idx[ii]]**4)**(0.25) TSS[i] = moving_time[i] / 36 * (norm_power[i] / FTP)**2 # # simulate the heart rate # SampleRate = 1.0 tau = HRTimeConstant # 63.0 seconds PwHRTable = np.array([ [0, 0.50 * FTHR], # Active resting HR [0.55 * FTP, 0.70 * FTHR], # Recovery [0.70 * FTP, 0.82 * FTHR], # Aerobic threshold [1.00 * FTP, FTHR], # Functional threshold [1.20 * FTP, 1.03 * FTHR], # Aerobic capacity [1.50 * FTP, 1.06 * FTHR] ]) # Max HR def heartrate_dot(HR, t): i = min(int(t * SampleRate), nScans - 1) HRp = np.interp(power[i], PwHRTable[:, 0], PwHRTable[:, 1]) HRt = HRp + HRDriftRate * TSS[i] return (HRt - HR) / tau heart_rate_sim = odeint(heartrate_dot, heart_rate_ci[0], time_ci) err = np.squeeze( heart_rate_sim ) \ - np.squeeze( heart_rate_ci ) RMSError = np.sqrt(np.average(err[time_idx]**2)) print >> OutStream, 'Average measured HR : %7i BPM' \ % np.average(heart_rate_ci[time_idx]) print >> OutStream, 'Average simulated HR : %7i BPM' \ % np.average(heart_rate_sim[time_idx]) print >> OutStream, 'RMS error : %7i BPM' % RMSError # # Estimate better values for FTHR and HRDriftRate # coef = np.polyfit(TSS[time_idx], -err[time_idx], deg=1, w=heart_rate_ci[time_idx] - 0.50 * FTHR) slope = coef[0] offset = coef[1] NewThresholdHR = offset + ThresholdHR NewHRDriftRate = slope + HRDriftRate print >> OutStream, 'Estimated ThresholdHR : %7.1f BPM' \ % NewThresholdHR print >> OutStream, 'Estimated HRDriftRate : %7.4f BPM/TSS' \ % NewHRDriftRate # -------------- debug --------------- print 'coef = ', coef print 'TSS = ', TSS[-1] hh = moving_time[-1] // 3600 mm = (moving_time[-1] % 3600) // 60 ss = (moving_time[-1] % 3600) % 60 print 'moving time: %4i:%02i:%02i' % (hh, mm, ss) print 'normalized power:', norm_power[-1], norm_power.max() CrossPlotFig = plt.figure() sc = plt.scatter(TSS[time_idx], -err[time_idx], s=5) plt.title('Simulation Error Vs TSS') plt.xlabel('TSS') plt.ylabel('BPM') plt.grid(b=True, which='major', axis='both') a = plt.axis() #plt.axis([ 0, a[1], 0, a[3] ]) y_fit = slope * TSS[time_idx] + offset plt.plot(TSS[time_idx], y_fit, 'k-') plt.show() # # extract lap results # from activity_tools import extract_activity_laps activity = Activity(FitFilePath) laps = extract_activity_laps(activity) lap_start_time = laps['start_time'] # datetime object lap_timestamp = laps['timestamp'] nLaps = len(lap_start_time) t0 = signals['metadata']['timestamp'] lap_start_sec = np.zeros(nLaps) # lap start times in seconds for i in range(nLaps): tBeg = (lap_start_time[i] - t0).total_seconds() tEnd = (lap_timestamp[i] - t0).total_seconds() lap_start_sec[i] = tBeg # # time plot # import matplotlib.dates as md from matplotlib.dates import date2num, DateFormatter import datetime as dt base = dt.datetime(2014, 1, 1, 0, 0, 0) x = [base + dt.timedelta(seconds=t) for t in time_ci.astype('float')] x = date2num(x) # Convert to matplotlib format x_laps = [ base + dt.timedelta(seconds=t) \ for t in lap_start_sec.astype('float') ] x_laps = date2num(x_laps) fig1, (ax0, ax1) = plt.subplots(nrows=2, sharex=True) ax0.plot_date(x, heart_rate_ci, 'r-', linewidth=1) ax0.plot_date(x, heart_rate_sim, 'm-', linewidth=3) ax0.set_yticks(h_zone_bounds, minor=False) ax0.grid(True) ax0.legend(['measured', 'simulated'], loc='upper left') ax0.set_title('heart rate, BPM') ax1.plot_date(x, power, 'k-', linewidth=1) ax1.plot_date(x, p30, 'b-', linewidth=3) ax1.xaxis.set_major_formatter(DateFormatter('%H:%M:%S')) ax1.set_yticks(p_zone_bounds, minor=False) ax1.grid(True) ax1.set_title('power, watts') for i in range(nLaps): ax0.axvline(x_laps[i], label=str(i + 1)) ax1.axvline(x_laps[i], label=str(i + 1)) fig1.autofmt_xdate() ax1.legend(['power', 'p30'], loc='upper left') fig1.suptitle('Pw:HR Transfer Function', fontsize=20) fig1.tight_layout() fig1.canvas.set_window_title(FitFilePath) fig1.subplots_adjust(hspace=0) # Remove horizontal space between axes plt.show() def ClosePlots(): plt.close('all') return ClosePlots
def force_analysis(FitFilePath, ConfigFile=None, OutStream=sys.stdout): verbose = False (FilePath, FitFileName) = os.path.split(FitFilePath) if ConfigFile is None: ConfigFile = FindConfigFile('', FilePath) if (ConfigFile is None) or (not os.path.exists(ConfigFile)): raise IOError('Configuration file not specified or found') # # Parse the configuration file # from ConfigParser import ConfigParser config = ConfigParser() config.read(ConfigFile) print >> OutStream, 'reading config file ' + ConfigFile CrankRadius = config.getfloat( 'power', 'CrankRadius' ) \ / 25.4 # mm -> inches print >> OutStream, 'CrankRadius: ', CrankRadius, ' inches' from datetime import datetime from fitparse import Activity from activity_tools import extract_activity_signals required_signals = ['power', 'cadence'] # get the signals activity = Activity(FitFilePath) signals = extract_activity_signals(activity) if not all(s in signals.keys() for s in required_signals): msg = 'required signals not in file' print >> OutStream, msg print >> OutStream, 'Signals required:' for s in required_signals: print >> OutStream, ' ' + s print >> OutStream, 'Signals contained:' for s in signals.keys(): print >> OutStream, ' ' + s raise IOError(msg) SampleRate = 1.0 # Hz time_signal = signals['time'] power = signals['power'] cadence = signals['cadence'] nPts = len(power) # Calculate leg force and reps from math import pi torque = np.zeros(nPts) ii = cadence.nonzero()[0] torque[ii] = power[ii] / cadence[ii] / (2*pi/60) \ * 8.8507 # N*m -> in*lb leg_force = torque / CrankRadius ''' Reference from squat: At the end of the Anatomic Adaptation phase, I could squat 160lbx4x25. With 75% of my body weight (190 lbs), the total leg force was 300 lbs, or 150 lbs per leg. This went through a depth of 16 inches. So each leg performed work of 100 reps times 150 lbs times 16 inches, or 240,000 in*lb, which is 27.117 kJ at 150 lb. ''' SquatLegForce = (160 + 0.75 * 190) / 2 # lb SquatDepth = 16.0 # inches SquatReps = 100.0 SquatWork = SquatLegForce * SquatDepth * SquatReps \ * 0.000112985416 # in*lb -> kJ MaxForce = max(SquatLegForce, max(leg_force)) # Histogram bin edges. The first bin is underflow (includes data # down to the minimum regardless of the first edge), and the last # is overflow (includes data up to maximum regardless of the last edge). force_bins = np.arange(0, 82, 2) # (0, 85, 5) cadence_bins = np.arange(26, 126, 2) # (25, 125, 5) # compute the force-work histogram # Instead of counts, we have revs and work as a function of force. # So we have to perform the histogram manually. nFBins = len(force_bins) - 1 force_width = (force_bins[1:nFBins + 1] - force_bins[0:nFBins]) * 0.95 revs = np.zeros(nFBins) force_work = np.zeros(nFBins) for i in range(nFBins): FBinLo = force_bins[i] if i > 1 else leg_force.min() FBinHi = force_bins[i + 1] if i < nFBins else leg_force.max() * 1.1 ii = np.nonzero(np.logical_and(leg_force >= FBinLo, leg_force < FBinHi))[0] # Compute the revs and work at these indices: revs[i] = sum(cadence[ii]) / 60 / SampleRate force_work[i] = sum(power[ii]) / SampleRate \ / 1000.0 # J -> kJ # compute the cadence-work histogram nCBins = len(cadence_bins) - 1 cadence_width = (cadence_bins[1:nCBins + 1] - cadence_bins[0:nCBins]) * 0.95 cadence_work = np.zeros(nCBins) for i in range(nCBins): CBinLo = cadence_bins[i] if i > 1 else leg_force.min() CBinHi = cadence_bins[i + 1] if i < nCBins else cadence.max() * 1.1 ii = np.nonzero(np.logical_and(cadence >= CBinLo, cadence < CBinHi))[0] cadence_work[i] = sum(power[ii]) / SampleRate \ / 1000.0 # J -> kJ # # Exposure Histogram # # I want the plot to display force on the X axis, but this is # the column (2nd) index, and the Y axis is the row (1st) index. # So I need to transpose work2d at some point; it would probably # be best to do so right up front since np.meshgrid() formats # the coordinates this way. work2d = np.zeros([nCBins, nFBins]) # note shape! power_grid = np.zeros([nCBins + 1, nFBins + 1]) force_grid, cadence_grid = np.meshgrid(force_bins, cadence_bins) for i in range(nFBins): FBinLo = force_bins[i] if i > 1 else leg_force.min() FBinHi = force_bins[i + 1] if i < nFBins else leg_force.max() * 1.1 ii = np.nonzero(np.logical_and(leg_force >= FBinLo, leg_force < FBinHi))[0] for j in range(nCBins): CBinLo = cadence_bins[j] if j > 1 else cadence.min() CBinHi = cadence_bins[j + 1] if j < nCBins else cadence.max() * 1.1 jj = np.nonzero( np.logical_and(cadence[ii] >= CBinLo, cadence[ii] < CBinHi))[0] work2d[j,i] = sum(power[ii[jj]]) \ / SampleRate / 1000.0 # J -> kJ TorqueNM = force_bins[i] * CrankRadius / 8.8507 power_grid[j, i] = TorqueNM * cadence_bins[j] * (2 * pi / 60) # last grid column i += 1 TorqueNM = force_bins[i] * CrankRadius / 8.8507 for j in range(nCBins + 1): power_grid[j, i] = TorqueNM * cadence_bins[j] * (2 * pi / 60) # last grid row. Leave j = nCBins for i in range(nFBins + 1): TorqueNM = force_bins[i] * CrankRadius / 8.8507 power_grid[j, i] = TorqueNM * cadence_bins[j] * (2 * pi / 60) power_lines = np.arange(50, power_grid.max(), 50) ########################################################### ### plotting ### ########################################################### # this needs to stay INSIDE the function or bad things happen import matplotlib.pyplot as plt # force & cadence time plot with grids at valid bin edges so that # underflow and overflow bins are shown accurately. import matplotlib.dates as md from matplotlib.dates import date2num, DateFormatter import datetime as dt base = dt.datetime(2014, 1, 1, 0, 0, 0) x = [base + dt.timedelta(seconds=t) for t in time_signal.astype('float')] x = date2num(x) # Convert to matplotlib format fig1, (ax0, ax1) = plt.subplots(nrows=2, sharex=True) ax0.plot_date(x, leg_force, 'r-+', linewidth=1) ax0.grid(True) ax0.legend(['leg_force'], loc='upper left') ax0.set_title('Leg Force, lb') ax0.set_yticks(force_bins[1:-1], minor=False) ax1.plot_date(x, cadence, 'g-+', linewidth=1) ax1.legend(['cadence'], loc='upper left') ax1.xaxis.set_major_formatter(DateFormatter('%H:%M:%S')) ax1.grid(True) ax1.set_title('cadence, RPM') ax1.set_yticks(cadence_bins[1:-1], minor=False) fig1.autofmt_xdate() fig1.suptitle('Force Analysis', fontsize=20) fig1.tight_layout() fig1.canvas.set_window_title(FitFilePath) plt.show() # force_work histogram plot fig2, ax2 = plt.subplots() opacity = 0.8 LogY = False rects1 = ax2.bar(force_bins[:-1], force_work, force_width, align='edge', alpha=opacity, color='r', log=LogY, label='force') ax2.set_xlabel('Pedal Force, lb') ax2.set_ylabel('work, kJ') ax2.set_title('Pedal Force Work Histogram') ax2.legend() fig2.tight_layout() fig2.canvas.set_window_title(FitFilePath) plt.show() # cadence_work histogram plot fig4, ax4 = plt.subplots() opacity = 0.8 rects4 = ax4.barh(cadence_bins[:-1], cadence_work, cadence_width, align='edge', alpha=opacity, color='g', log=False, label='cadence') ax4.set_ylabel('cadence, RPM') ax4.set_xlabel('work, kJ') ax4.set_title('Cadence-Work Histogram') ax4.legend() fig4.tight_layout() fig4.canvas.set_window_title(FitFilePath) plt.show() # create a custom segmented colormap. I want zero to be a cool color # on which I can see the power contours; I want a quick transition that # exposes low work values with a subtle cool color; then I want the # map to gradually transition through "hotter" colors to red. from matplotlib import colors as mcolors colors = dict(mcolors.BASE_COLORS, **mcolors.CSS4_COLORS) segment_data = [ # X color (0, 'darkviolet'), (0.005, 'purple'), (0.1, 'darkgreen'), (0.3, 'blue'), (0.5, 'cyan'), (0.7, 'yellow'), (1.0, 'red') ] cdict = {'red': [], 'green': [], 'blue': []} for x, cName in segment_data: rgba = mcolors.to_rgba(colors[cName]) cdict['red'].append((x, rgba[0], rgba[0])) cdict['green'].append((x, rgba[1], rgba[1])) cdict['blue'].append((x, rgba[2], rgba[2])) newcmp = mcolors.LinearSegmentedColormap('WillsCmap', segmentdata=cdict, N=256) # exposure histogram plot # clone from # https://matplotlib.org/gallery/images_contours_and_fields/pcolor_demo.html fig3, ax3 = plt.subplots() c = ax3.pcolor(force_grid, cadence_grid, work2d, cmap=newcmp, vmin=work2d.min(), vmax=work2d.max()) ax3.axis([ force_bins.min(), force_bins.max(), cadence_bins.min(), cadence_bins.max() ]) cbar = fig3.colorbar(c, ax=ax3) cbar.ax.set_ylabel('work, kJ') CS = ax3.contour(force_grid, cadence_grid, power_grid, power_lines, colors='k') ax3.clabel(CS, fontsize=9, inline=1) ax3.set_xlabel('Pedal Force, lb') ax3.set_ylabel('cadence, RPM') ax3.set_title('Pedal Exposure Histogram') fig3.tight_layout() fig3.canvas.set_window_title(FitFilePath) plt.show() def ClosePlots(): plt.close('all') return ClosePlots
def interval_laps(FitFilePath, ConfigFile=None, OutStream=sys.stdout): (FilePath, FitFileName) = os.path.split(FitFilePath) if ConfigFile is None: ConfigFile = FindConfigFile('', FilePath) if (ConfigFile is None) or (not os.path.exists(ConfigFile)): raise IOError('Configuration file not specified or found') # # Parse the configuration file # from ConfigParser import ConfigParser config = ConfigParser() config.read(ConfigFile) print >> OutStream, 'reading config file ' + ConfigFile ThresholdPower = config.getfloat('power', 'ThresholdPower') ThresholdHR = config.getfloat('power', 'ThresholdHR') print >> OutStream, 'ThresholdPower: ', ThresholdPower print >> OutStream, 'ThresholdHR : ', ThresholdHR FTP = ThresholdPower ''' # Get Records of type 'lap' # types: [ 'record', 'lap', 'event', 'session', 'activity', ... ] records = activity.get_records_by_type('lap') current_record_number = 0 elapsed_time = [] avg_heart_rate = [] avg_power = [] avg_cadence = [] max_heart_rate = [] balance = [] FirstIter = True for record in records: # Print record number current_record_number += 1 #print >> OutStream, (" Record #%d " % current_record_number).center(40, '-') # Get the list of valid fields on this record valid_field_names = record.get_valid_field_names() for field_name in valid_field_names: # Get the data and units for the field field_data = record.get_data(field_name) field_units = record.get_units(field_name) ## Print what we've got! #if field_units: # print >> OutStream, " * %s: %s %s" % (field_name, field_data, field_units) #else: # print >> OutStream, " * %s: %s" % (field_name, field_data) if 'timestamp' in field_name: if FirstIter: t0 = field_data # datetime t = t0 FirstIter = False else: t = field_data # datetime if 'total_timer_time' in field_name: elapsed_time.append( field_data ) if 'avg_power' in field_name: avg_power.append( field_data ) # avg_heart_rate is in a lap record if 'avg_heart_rate' in field_name: avg_heart_rate.append(field_data) if 'max_heart_rate' in field_name: max_heart_rate.append(field_data) if 'avg_cadence' in field_name: avg_cadence.append(field_data) if 'left_right_balance' in field_name: balance.append(field_data) from numpy import nonzero, array, arange, zeros, average, logical_and power = array(avg_power) time = array(elapsed_time) cadence = array(avg_cadence) avg_hr = array(avg_heart_rate) max_hr = array(max_heart_rate) if len(balance) == 0: balance = zeros(len(power)) else: balance = array(balance) ''' # # extract lap results # from fitparse import Activity from activity_tools import extract_activity_laps import numpy as np activity = Activity(FitFilePath) laps = extract_activity_laps(activity) power = laps['power'] time = laps['total_timer_time'] cadence = laps['cadence'] avg_hr = laps['avg_hr'] max_hr = laps['max_hr'] balance = laps['balance'] # All high-intensity intervals: >80 %FTP ii = np.nonzero(power >= 0.80 * FTP)[0] # index array if len(ii) > 0: print >> OutStream, 'processing %d all laps above %d watts...' \ % (len(ii), 0.80*FTP) names1 = ['', '', 'avg', 'avg', 'avg', 'max', 'avg'] names2 = ['lap', 'time', 'power', 'cad', 'HR', 'HR', 'bal'] print >> OutStream, "%8s" * 7 % tuple(names1) print >> OutStream, "%8s" * 7 % tuple(names2) for i in range(len(ii)): mm = time[ii[i]] // 60 ss = time[ii[i]] % 60 print >> OutStream, '%8d%5i:%02i%8d%8d%8d%8d%8.1f' \ % (ii[i], mm, ss, power[ii[i]], cadence[ii[i]], avg_hr[ii[i]], max_hr[ii[i]], balance[ii[i]] ) mm = sum(time[ii]) // 60 ss = sum(time[ii]) % 60 print >> OutStream, '%8s%5i:%02i%8d%8d%8d%8d%8.1f' \ % ("AVERAGE", mm, ss, sum( power[ii]*time[ii]) / sum(time[ii]), sum(cadence[ii]*time[ii]) / sum(time[ii]), sum( avg_hr[ii]*time[ii]) / sum(time[ii]), max(max_hr[ii]), sum(balance[ii]*time[ii]) / sum(time[ii]) ) else: print >> OutStream, 'No high-intensity laps found.' \ # Tempo intervals: 75-88 %FTP ii = np.nonzero(np.logical_and(power >= 0.75 * FTP, power < 0.88 * FTP))[0] # index array if len(ii) > 0: print >> OutStream, 'processing %d tempo laps between %d and %d watts...' \ % (len(ii), 0.75*FTP, 0.88*FTP) names1 = ['', '', 'avg', 'avg', 'avg', 'max', 'avg'] names2 = ['lap', 'time', 'power', 'cad', 'HR', 'HR', 'bal'] print >> OutStream, "%8s" * 7 % tuple(names1) print >> OutStream, "%8s" * 7 % tuple(names2) for i in range(len(ii)): mm = time[ii[i]] // 60 ss = time[ii[i]] % 60 print >> OutStream, '%8d%5i:%02i%8d%8d%8d%8d%8.1f' \ % (ii[i], mm, ss, power[ii[i]], cadence[ii[i]], avg_hr[ii[i]], max_hr[ii[i]], balance[ii[i]] ) mm = sum(time[ii]) // 60 ss = sum(time[ii]) % 60 print >> OutStream, '%8s%5i:%02i%8d%8d%8d%8d%8.1f' \ % ("AVERAGE", mm, ss, sum( power[ii]*time[ii]) / sum(time[ii]), sum(cadence[ii]*time[ii]) / sum(time[ii]), sum( avg_hr[ii]*time[ii]) / sum(time[ii]), max(max_hr[ii]), sum(balance[ii]*time[ii]) / sum(time[ii]) ) else: print >> OutStream, 'No tempo laps found.' \ # Cruise intervals: 88-105 %FTP. ii = np.nonzero(np.logical_and(power >= 0.88 * FTP, power < 1.05 * FTP))[0] # index array if len(ii) > 0: print >> OutStream, 'processing %d threshold laps between %d and %d watts...' \ % (len(ii), 0.88*FTP, 1.05*FTP) names1 = ['', '', 'avg', 'avg', 'avg', 'max', 'avg'] names2 = ['lap', 'time', 'power', 'cad', 'HR', 'HR', 'bal'] print >> OutStream, "%8s" * 7 % tuple(names1) print >> OutStream, "%8s" * 7 % tuple(names2) for i in range(len(ii)): mm = time[ii[i]] // 60 ss = time[ii[i]] % 60 print >> OutStream, '%8d%5i:%02i%8d%8d%8d%8d%8.1f' \ % (ii[i], mm, ss, power[ii[i]], cadence[ii[i]], avg_hr[ii[i]], max_hr[ii[i]], balance[ii[i]] ) mm = sum(time[ii]) // 60 ss = sum(time[ii]) % 60 print >> OutStream, '%8s%5i:%02i%8d%8d%8d%8d%8.1f' \ % ("AVERAGE", mm, ss, sum( power[ii]*time[ii]) / sum(time[ii]), sum(cadence[ii]*time[ii]) / sum(time[ii]), sum( avg_hr[ii]*time[ii]) / sum(time[ii]), max(max_hr[ii]), sum(balance[ii]*time[ii]) / sum(time[ii]) ) else: print >> OutStream, 'No threshold laps found.' \ # VO2max intervals: 105-200 %FTP. ii = np.nonzero(np.logical_and(power >= 1.05 * FTP, power < 2.00 * FTP))[0] # index array if len(ii) > 0: print >> OutStream, 'processing %d VO2max laps between %d and %d watts...' \ % (len(ii), 1.05*FTP, 2.00*FTP) names1 = ['', '', 'avg', 'avg', 'avg', 'max', 'avg'] names2 = ['lap', 'time', 'power', 'cad', 'HR', 'HR', 'bal'] print >> OutStream, "%8s" * 7 % tuple(names1) print >> OutStream, "%8s" * 7 % tuple(names2) for i in range(len(ii)): mm = time[ii[i]] // 60 ss = time[ii[i]] % 60 print >> OutStream, '%8d%5i:%02i%8d%8d%8d%8d%8.1f' \ % (ii[i], mm, ss, power[ii[i]], cadence[ii[i]], avg_hr[ii[i]], max_hr[ii[i]], balance[ii[i]] ) mm = sum(time[ii]) // 60 ss = sum(time[ii]) % 60 print >> OutStream, '%8s%5i:%02i%8d%8d%8d%8d%8.1f' \ % ("AVERAGE", mm, ss, sum( power[ii]*time[ii]) / sum(time[ii]), sum(cadence[ii]*time[ii]) / sum(time[ii]), sum( avg_hr[ii]*time[ii]) / sum(time[ii]), max(max_hr[ii]), sum(balance[ii]*time[ii]) / sum(time[ii]) ) else: print >> OutStream, 'No VO2max laps found.' \
def plot_heartrate(FitFilePath, ConfigFile=None, OutStream=sys.stdout): verbose = False (FilePath, FitFileName) = os.path.split(FitFilePath) if ConfigFile is None: ConfigFile = FindConfigFile('', FilePath) if (ConfigFile is None) or (not os.path.exists(ConfigFile)): raise IOError('Configuration file not specified or found') # # Parse the configuration file # from ConfigParser import ConfigParser config = ConfigParser() config.read(ConfigFile) print >> OutStream, 'reading config file ' + ConfigFile WeightEntry = config.getfloat('user', 'weight') WeightToKg = config.getfloat('user', 'WeightToKg') weight = WeightEntry * WeightToKg age = config.getfloat('user', 'age') sex = config.get('user', 'sex') EndurancePower = config.getfloat('power', 'EndurancePower') ThresholdPower = config.getfloat('power', 'ThresholdPower') EnduranceHR = config.getfloat('power', 'EnduranceHR') ThresholdHR = config.getfloat('power', 'ThresholdHR') HRTimeConstant = config.getfloat('power', 'HRTimeConstant') HRDriftRate = config.getfloat('power', 'HRDriftRate') print >> OutStream, 'WeightEntry : ', WeightEntry print >> OutStream, 'WeightToKg : ', WeightToKg print >> OutStream, 'weight : ', weight print >> OutStream, 'age : ', age print >> OutStream, 'sex : ', sex print >> OutStream, 'EndurancePower: ', EndurancePower print >> OutStream, 'ThresholdPower: ', ThresholdPower print >> OutStream, 'EnduranceHR : ', EnduranceHR print >> OutStream, 'ThresholdHR : ', ThresholdHR print >> OutStream, 'HRTimeConstant : ', HRTimeConstant print >> OutStream, 'HRDriftRate : ', HRDriftRate from datetime import datetime from fitparse import Activity from activity_tools import extract_activity_signals required_signals = ['heart_rate'] # get the signals activity = Activity(FitFilePath) signals = extract_activity_signals(activity) if not all(s in signals.keys() for s in required_signals): msg = 'required signals not in file' print >> OutStream, msg print >> OutStream, 'Signals required:' for s in required_signals: print >> OutStream, ' ' + s print >> OutStream, 'Signals contained:' for s in signals.keys(): print >> OutStream, ' ' + s raise IOError(msg) time_signal = signals['time'] heartrate_signal = signals['heart_rate'] # plot the heart rate import numpy as np ######################################################################## ### Compute Calories ### ######################################################################## ''' Formula widely available. One site: https://www.easycalculation.com/formulas/heart-rate-calorie-burn.html For Male, Calorie Burned = ( ( -55.0969 + (0.6309 x HR) + (0.1988 x W ) + (0.2017 x A ) ) / 4.184) x 60 x T For Female, Calorie Burned = ( ( -20.4022 + (0.4472 x HR) + (0.1263 x W ) + (0.0740 x A ) ) / 4.184) x 60 x T Where, HR = Heart Rate W = Weight in kilograms A = Age T = Exercise duration time in hours However, the formula is adapted to the user's power capacity: : At EnduranceHR, CalPerMin is set to EndurancePower (assuming efficiency of 1/4.184 so that calories burned equal kJ expended). : At ThresholdHR, CalPerMin is set to ThresholdPower. : Between EnduranceHR and ThresholdHR, CalPerMin is interpolated. : Below EnduranceHR, CalPerMin follows the formula, but it is scaled onto [EnduranceHR, EndurancePower]. : Above ThresholdHR, CalPerMin follows the formula, but it is scaled onto [ThresholdHR, ThresholdPower]. ''' hr_sig = signals['heart_rate'] t_sig = signals['time'] dt_sig = np.append(np.array([1.0]), t_sig[1:] - t_sig[0:-1]) nPts = t_sig.size calories = np.zeros(nPts) EnduranceBurn = EndurancePower * 3600 / 1e3 / 60 # Cal/min print >> OutStream, 'EnduranceBurn = %5.2f cal/min' % EnduranceBurn ThresholdBurn = ThresholdPower * 3600 / 1e3 / 60 # Cal/min print >> OutStream, 'ThresholdBurn = %5.2f cal/min' % ThresholdBurn if sex == 'male': # calibration at endurance EnduranceCoef = EnduranceBurn \ / ( -55.0969 \ + 0.6309*EnduranceHR \ + 0.1988*weight \ + 0.2017*age) \ * 4.184 # calibration at threshold ThresholdCoef = ThresholdBurn \ / ( -55.0969 \ + 0.6309*EnduranceHR \ + 0.1988*weight \ + 0.2017*age) \ * 4.184 else: # female # calibration at endurance EnduranceCoef = EnduranceBurn \ / ( -20.4022 \ + 0.4472*EnduranceHR \ + 0.1263*weight \ + 0.0740*age) \ * 4.184 # calibration at threshold ThresholdCoef = ThresholdBurn \ / ( -20.4022 \ + 0.4472*EnduranceHR \ + 0.1263*weight \ + 0.0740*age) \ * 4.184 for i, dt, HR in zip(range(nPts), dt_sig, hr_sig): # calories per minute if HR >= EnduranceHR and HR <= ThresholdHR: CalPerMin = EnduranceBurn \ + (HR-EnduranceHR) \ * (ThresholdBurn-EnduranceBurn) \ / (ThresholdHR-EnduranceHR) else: coef = EnduranceCoef if (HR < EnduranceHR) else ThresholdCoef if sex == 'male': CalPerMin = ( -55.0969 \ + 0.6309*HR \ + 0.1988*weight \ + 0.2017*age) \ / 4.184 \ * coef else: CalPerMin = ( -20.4022 \ + 0.4472*HR \ + 0.1263*weight \ + 0.0740*age) \ / 4.184 \ * coef calories[i] = dt * CalPerMin / 60 running_calories = np.cumsum(calories) print >> OutStream, 'total calories = %i' % running_calories[nPts - 1] ######################################################################## ### Zone Histogram ### ######################################################################## # heart-rate zones from "Cyclist's Training Bible" 5th ed. by Joe Friel, p50 FTHR = ThresholdHR hZones = { 1: ([0, 0.82 * FTHR], ' 1'), 2: ([0.82 * FTHR, 0.89 * FTHR], ' 2'), 3: ([0.89 * FTHR, 0.94 * FTHR], ' 3'), 4: ([0.94 * FTHR, 1.00 * FTHR], ' 4'), 5: ([1.00 * FTHR, 1.03 * FTHR], '5a'), 6: ([1.03 * FTHR, 1.06 * FTHR], '5b'), 7: ([1.07 * FTHR, 1.15 * FTHR], '5c') } h_zone_bounds = [ 0.4 * FTHR, # 1 lo hZones[2][0][0], # 2 lo hZones[3][0][0], # 3 lo hZones[4][0][0], # 4 lo hZones[5][0][0], # 5a lo hZones[6][0][0], # 5b lo hZones[7][0][0], # 5c lo hZones[7][0][1] ] # 5c hi h_zone_labels = [hZones[k][1] for k in range(1, 8)] ZoneCounts, ZoneBins = np.histogram(hr_sig, bins=h_zone_bounds) # formatted print of histogram SampleRate = 1.0 print >> OutStream, 'Heart-Rate Zone Histogram:' for i in range(7): dur = ZoneCounts[i] / SampleRate pct = dur / sum(ZoneCounts / SampleRate) * 100 hh = dur // 3600 mm = (dur % 3600) // 60 ss = (dur % 3600) % 60 print >> OutStream, ' Zone %2s: %2i:%02i:%02i (%2i%%)' \ % ( h_zone_labels[i], hh, mm, ss, pct) dur = sum(ZoneCounts) / SampleRate hh = dur // 3600 mm = (dur % 3600) // 60 ss = (dur % 3600) % 60 print >> OutStream, ' total: %2i:%02i:%02i' % (hh, mm, ss) ######################################################################## ### Power & TSS Estimation ### ######################################################################## from endurance_summary import BackwardMovingAverage # see # https://docs.scipy.org/doc/scipy/reference/signal.html from scipy import signal poles = 3 cutoff = 0.10 # Hz Wn = cutoff / (SampleRate / 2) ''' # construct and apply a differentiating, lowpass filter NumB, DenB = signal.butter(poles, Wn, btype='lowpass', output='ba', analog=True) NumF = signal.convolve( NumB, [1,0]) # add differentiator bDLP, aDLP = signal.bilinear( NumF,DenB, fs=SampleRate ) hr_dot = signal.lfilter(bDLP, aDLP, hr_sig) ''' # apply a phaseless lowpass filter, then differentiate. # for some reason, running the Butterworth analog filter # through bilinear() gives a better result. Otherwise, # set analog=False to get coefficients directly. PadLen = int(SampleRate / cutoff) # one period of cutoff NumB, DenB = signal.butter(poles, Wn, btype='lowpass', output='ba', analog=True) bLPF, aLPF = signal.bilinear(NumB, DenB, fs=SampleRate) hr_lpf = signal.filtfilt(bLPF, aLPF, hr_sig, padlen=PadLen) hr_dot = np.gradient(hr_lpf, 1 / SampleRate) FTP = ThresholdPower FTHR = ThresholdHR tau = HRTimeConstant PwHRTable = np.array([ [0, 0.50 * FTHR], # Active resting HR [0.55 * FTP, 0.70 * FTHR], # Recovery [0.70 * FTP, 0.82 * FTHR], # Aerobic threshold [1.00 * FTP, FTHR], # Functional threshold [1.20 * FTP, 1.03 * FTHR], # Aerobic capacity [1.50 * FTP, 1.06 * FTHR] ]) # Max HR # loop through the time series building running power and TSS. # Notice this is necessary because HRd[i] depends on TSS[i-1]. sPower = np.zeros(nPts) TSS = np.zeros(nPts) HRd = np.zeros(nPts) # fatigue drift HRp = np.zeros(nPts) # power target p30 = np.zeros(nPts) # 30-sec boxcar average w = int(30 * SampleRate) # window for boxcar NPower = np.zeros(nPts) # normalized power for i in range(1, nPts): HRd[i] = HRDriftRate * TSS[i - 1] HRp[i] = hr_sig[i] + tau * hr_dot[i] - HRd[i] sPower[i] = np.interp(HRp[i], PwHRTable[:, 1], PwHRTable[:, 0]) if i < w: p30[i] = np.average(sPower[:i + 1]) # include i else: p30[i] = np.average(sPower[i - w:i + 1]) # include i NPower[i] = np.average(p30[:i]**4)**(0.25) TSS[i] = t_sig[i] / 36 * (NPower[i] / FTP)**2 print >> OutStream, 'estimated NP = %6i W' % NPower[-1] print >> OutStream, 'estimated work = %6i kJ' % \ ( np.cumsum(sPower)[-1] / 1e3 / SampleRate ) print >> OutStream, 'estimated TSS = %6i TSS' % TSS[-1] if 'power' in signals.keys(): mPower = signals['power'] print >> OutStream, 'measured work = %6i kJ' % \ ( np.cumsum( mPower )[-1] / 1e3 / SampleRate ) mP30 = BackwardMovingAverage(mPower) mNPower = np.average(mP30**4)**(0.25) mTSS = t_sig[-1] / 36 * (mNPower / FTP)**2 print >> OutStream, 'measured NP = %6i W' % mNPower print >> OutStream, 'measured TSS = %6i TSS' % mTSS ########################################################### ### plotting ### ########################################################### # power zones from "Cyclist's Training Bible", 5th ed., by Joe Friel, p51 pZones = { 1: [0, 0.55 * FTP], 2: [0.55 * FTP, 0.75 * FTP], 3: [0.75 * FTP, 0.90 * FTP], 4: [0.90 * FTP, 1.05 * FTP], 5: [1.05 * FTP, 1.20 * FTP], 6: [1.20 * FTP, 1.50 * FTP], 7: [1.50 * FTP, 2.50 * FTP] } # heart-rate zones from "Cyclist's Training Bible" 5th ed. by Joe Friel, p50 hZones = { 1: [0, 0.82 * FTHR], # 1 2: [0.82 * FTHR, 0.89 * FTHR], # 2 3: [0.89 * FTHR, 0.94 * FTHR], # 3 4: [0.94 * FTHR, 1.00 * FTHR], # 4 5: [1.00 * FTHR, 1.03 * FTHR], # 5a 6: [1.03 * FTHR, 1.06 * FTHR], # 5b 7: [1.07 * FTHR, 1.15 * FTHR] } # 5c # get zone bounds for plotting p_zone_bounds = [ pZones[1][0], pZones[2][0], pZones[3][0], pZones[4][0], pZones[5][0], pZones[6][0], pZones[7][0], pZones[7][1] ] h_zone_bounds = [ 0.4 * FTHR, # better plotting hZones[2][0], hZones[3][0], hZones[4][0], hZones[5][0], hZones[6][0], hZones[7][0], hZones[7][1] ] # power simulation plot import matplotlib.pyplot as plt import matplotlib.dates as md from matplotlib.dates import date2num, DateFormatter import datetime as dt base = dt.datetime(2014, 1, 1, 0, 0, 0) x = [base + dt.timedelta(seconds=t) for t in t_sig.astype('float')] x = date2num(x) # Convert to matplotlib format fig1, (ax0, ax1) = plt.subplots(nrows=2, sharex=True) ax0.plot_date(x, hr_sig, 'r-', linewidth=2) ax0.plot_date(x, tau * hr_dot, 'b-', linewidth=2) ax0.plot_date(x, HRd, 'g-', linewidth=2) ax0.plot_date(x, HRp, 'k-', linewidth=2) ax0.set_yticks(h_zone_bounds, minor=False) ax0.grid(True) ax0.legend(['HR', 'tau*HRdot', 'HRd', 'HRp'], loc='upper left') ax0.set_title('heart rate, BPM') if 'power' in signals.keys(): mPower = signals['power'] ax1.plot_date(x, mPower, 'k-', linewidth=1) ax1.plot_date(x, sPower, 'b-', linewidth=2) ax1.legend(['measured power', 'simulated power'], loc='upper left') else: ax1.plot_date(x, sPower, 'b-', linewidth=2) ax1.legend(['simulated power'], loc='upper left') ax1.xaxis.set_major_formatter(DateFormatter('%H:%M:%S')) ax1.set_yticks(p_zone_bounds, minor=False) ax1.grid(True) ax1.set_title('power, watts') fig1.autofmt_xdate() fig1.suptitle('Pw:HR Transfer Function', fontsize=20) fig1.tight_layout() fig1.canvas.set_window_title(FitFilePath) plt.show() # plot heart rate and calories import matplotlib.dates as md from matplotlib.dates import date2num, DateFormatter import datetime as dt base = dt.datetime(2014, 1, 27, 0, 0, 0) x = [base + dt.timedelta(seconds=t) for t in t_sig] x = date2num(x) # Convert to matplotlib format fig1, ax0 = plt.subplots() ax0.plot_date(x, hr_sig, 'r-', linewidth=3) ax0.set_yticks(h_zone_bounds, minor=False) ax0.grid(True) ax0.set_title('heart rate, BPM') ax0.set_title('Heart Rate Analysis') fig1.autofmt_xdate() fig1.tight_layout() fig1.canvas.set_window_title(FitFilePath) plt.show() # plt.plot(t_sig/60, hr_sig, 'r.-') # plt.title('Heart Rate and Calories') # plt.ylabel('BPM') # plt.subplot(2, 1, 2) # plt.plot(t_sig/60, running_calories, 'b.-') # plt.xlabel('time (min)') # plt.ylabel('calories') # heart rate histogram plot fig2, ax2 = plt.subplots() bar_width = 0.80 # 0.35 opacity = 0.4 #error_config = {'ecolor': '0.3'} zone_ints = np.arange(7) + 1 LogY = False rects1 = ax2.bar(zone_ints + bar_width / 2, ZoneCounts / SampleRate / 60, bar_width, alpha=opacity, color='r', log=LogY, label='heart rate') ax2.set_xlabel('Zone') ax2.set_ylabel('minutes') ax2.set_title('Heart Rate Zone Histogram') ax2.set_xticks(zone_ints + bar_width / 2) ax2.set_xticklabels(h_zone_labels) ax2.legend() fig2.tight_layout() fig2.canvas.set_window_title(FitFilePath) plt.show() def ClosePlots(): plt.close('all') return ClosePlots
def channel_inspect_anls(FitFilePath, ConfigFile=None, OutStream=sys.stdout, ParentWin=None): (FilePath, FitFileName) = os.path.split(FitFilePath) if ConfigFile is None: ConfigFile = FindConfigFile('', FilePath) if (ConfigFile is None) or (not os.path.exists(ConfigFile)): raise IOError('Configuration file not specified or found') # # Parse the configuration file # from ConfigParser import ConfigParser config = ConfigParser() config.read(ConfigFile) print >> OutStream, 'reading config file ' + ConfigFile if config.has_section('units'): # convert list of tuples to dictionary UserUnits = dict( config.items('units') ) else: UserUnits = None # get the signals from datetime import datetime from fitparse import Activity from activity_tools import extract_activity_signals, UnitHandler activity = Activity(FitFilePath) signals = extract_activity_signals(activity, resample='existing') # convert units if UserUnits is not None: unithandler = UnitHandler(UserUnits) signals = unithandler.ConvertSignalUnits(signals) ChannelList = signals.keys() ChannelList.remove('time') ChannelList.remove('metadata') print >> OutStream, 'Signals contained:' for s in ChannelList: print >> OutStream, ' ' + s if ParentWin is None: app = wx.App() dlg = wx.MultiChoiceDialog( ParentWin, "Pick channels\nto plot", "channel inspector: " + FitFileName, ChannelList) if (dlg.ShowModal() == wx.ID_OK): selections = dlg.GetSelections() ChannelNames = [ ChannelList[x] for x in selections ] print >> OutStream, "Plotting: %s" % (ChannelNames) dlg.Destroy() # # extract lap results # from activity_tools import extract_activity_laps activity = Activity(FitFilePath) laps = extract_activity_laps(activity) lap_start_time = laps['start_time'] # datetime object lap_timestamp = laps['timestamp' ] nLaps = len(lap_start_time) t0 = signals['metadata']['timestamp'] lap_start_sec = np.zeros(nLaps) # lap start times in seconds for i in range(nLaps): tBeg = (lap_start_time[i] - t0).total_seconds() tEnd = (lap_timestamp[i] - t0).total_seconds() lap_start_sec[i] = tBeg # # time plot # nPlots = len(ChannelNames) PlotColors = { 'power' : 'm' , 'heart_rate' : 'r' , 'cadence' : 'g' , 'speed' : 'b' , 'temperature' : 'brown' } import matplotlib.pyplot as plt import matplotlib.dates as md from matplotlib.dates import date2num, DateFormatter import datetime as dt base = dt.datetime(2014, 1, 1, 0, 0, 0) x = [base + dt.timedelta(seconds=t) for t in signals['time'].astype('float')] x = date2num(x) # Convert to matplotlib format #fig1, (ax0, ax1) = plt.subplots(nrows=2, sharex=True) x_laps = [ base + dt.timedelta(seconds=t) \ for t in lap_start_sec.astype('float') ] x_laps = date2num(x_laps) axislist = [] fig = plt.figure() for i, channel in zip( range(nPlots), ChannelNames ): if PlotColors.has_key(channel): pcolor = PlotColors[channel] else: pcolor = 'k-' if i > 0: ax = plt.subplot( nPlots, 1, i+1, sharex=axislist[0] ) else: ax = plt.subplot( nPlots, 1, i+1 ) ax.plot_date( x, signals[channel], pcolor, linewidth=1, linestyle='-' ); for j in range(nLaps): ax.axvline( x_laps[j], label=str(j+1) ) ax.grid(True) if signals['metadata']['units'].has_key(channel): YLabel = channel + '\n' \ + signals['metadata']['units'][channel] else: YLabel = channel + '\n' + 'none' ax.set_ylabel(YLabel) ax.xaxis.set_major_formatter(DateFormatter('%H:%M:%S')) ax.grid(True) axislist.append(ax) fig.autofmt_xdate() fig.suptitle(FitFileName, fontsize=20) fig.tight_layout() fig.canvas.set_window_title(FitFilePath) fig.subplots_adjust(hspace=0) # Remove horizontal space between axes plt.show() def ClosePlots(): plt.close('all') return ClosePlots