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 saddle_endurance_anls(FitFilePath, ConfigFile=None, OutStream=sys.stdout): (FilePath, FitFileName) = os.path.split(FitFilePath) # no config file needed from datetime import datetime from fitparse import Activity from activity_tools import extract_activity_signals import numpy as np 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 cadence = signals['cadence'] elapsed_time = signals['time'] nPts = len(cadence) cad_fwd_min_3_8 = FwdRunningMinimum(cadence, wBeg=3, wEnd=8) cad_fwd_min_1_8 = FwdRunningMinimum(cadence, wBeg=1, wEnd=8) STANDING = 0 SEATED = 1 state = STANDING CThr = 40.0 seated_state = np.zeros(nPts) for i in range(nPts): if state == STANDING: if cadence[i] >= CThr and cad_fwd_min_1_8[i] >= CThr: state = SEATED else: # state==SEATED: if cadence[i] < CThr and cad_fwd_min_3_8[i] < CThr: state = STANDING seated_state[i] = state # # Determine seated segment durations # iiUp = np.nonzero(seated_state[1:] - seated_state[0:-1] == 1)[0] iiDn = np.nonzero(seated_state[1:] - seated_state[0:-1] == -1)[0] if iiUp[-1] < iiDn[-1]: seg_durtns = np.zeros(len(iiDn)) seg_starts = np.zeros(len(iiDn)).astype('int') seg_stops = np.zeros(len(iiDn)).astype('int') else: seg_durtns = np.zeros(len(iiDn) + 1) seg_starts = np.zeros(len(iiDn) + 1).astype('int') seg_stops = np.zeros(len(iiDn) + 1).astype('int') if iiDn[0] < iiUp[0]: # begins seated seg_durtns[0] = iiDn[0] seg_starts[0] = 0 seg_stops[0] = iiDn[0] if iiUp[-1] > iiDn[-1]: # Ends seated seg_durtns[1:-1] = iiDn[1:] - iiUp[0:-1] seg_starts[1:-1] = iiUp[0:-1] seg_stops[1:-1] = iiDn[1:] seg_durtns[-1] = nPts - iiUp[-1] seg_starts[-1] = iiUp[-1] seg_stops[-1] = nPts else: # ends standing seg_durtns[1:] = iiDn[1:] - iiUp seg_starts[1:] = iiUp seg_stops[1:] = iiDn[1:] elif iiUp[0] < iiDn[0]: # begins standing if iiUp[-1] > iiDn[-1]: # ends seated seg_durtns[0:-1] = iiDn - iiUp[0:-1] seg_starts[0:-1] = iiUp[0:-1] seg_stops[0:-1] = iiDn seg_durtns[-1] = nPts - iiUp[-1] seg_starts[-1] = iiUp[-1] seg_stops[-1] = nPts else: # ends standing seg_durtns = iiDn - iiUp seg_starts = iiUp seg_stops = iiDn else: raise RuntimeError("shouldn't be able to reach this code.") # # Formatted print of results for all segments # # overall results dur = nPts / SampleRate hh = dur // 3600 mm = (dur % 3600) // 60 ss = (dur % 3600) % 60 print >> OutStream, 'total time : %2i:%02i:%02i' % (hh, mm, ss) iSeat = np.nonzero(seated_state == 1)[0] pct = len(iSeat) / float(nPts) * 100.0 dur = len(iSeat) / SampleRate hh = dur // 3600 mm = (dur % 3600) // 60 ss = (dur % 3600) % 60 print >> OutStream, 'seated time : %2i:%02i:%02i (%2i%%))' % (hh, mm, ss, pct) iStnd = np.nonzero(seated_state == 0)[0] pct = len(iStnd) / float(nPts) * 100.0 dur = len(iStnd) / SampleRate hh = dur // 3600 mm = (dur % 3600) // 60 ss = (dur % 3600) % 60 print >> OutStream, 'standing time : %2i:%02i:%02i (%2i%%))' % (hh, mm, ss, pct) # segment results nSeg = len(seg_durtns) print >> OutStream, 'standing segments:' names = ['segment', 'start', 'stop', 'duration'] fmt = "%12s" + "%10s" * 3 print >> OutStream, fmt % tuple(names) for i in range(nSeg): Beg = elapsed_time[seg_starts[i]] hhBeg = Beg // 3600 mmBeg = (Beg % 3600) // 60 ssBeg = (Beg % 3600) % 60 End = elapsed_time[seg_stops[i]] hhEnd = End // 3600 mmEnd = (End % 3600) // 60 ssEnd = (End % 3600) % 60 dur = seg_durtns[i] / SampleRate hhDur = dur // 3600 mmDur = (dur % 3600) // 60 ssDur = (dur % 3600) % 60 DurPlus = '' if hhDur == 0 else '+%ih' % (hhDur) fmt = '%12d' + '%4i:%02i:%02i' + '%4i:%02i:%02i' + '%7i:%02i' + '%s' print >> OutStream, fmt \ % (i, hhBeg, mmBeg, ssBeg, hhEnd, mmEnd, ssEnd, mmDur, ssDur, DurPlus) # # best hour saddle endurance # ''' Find the longest segments that together total one hour, and compute the average duration to serve as a metric for the ride. ''' # time limit: 15 minutes less than end for short rides TimeLimit = min(3600.0, (nPts / SampleRate - 15 * 60)) # list of indices for longest durations def GetSegment(i): return seg_durtns[i] indx = range(nSeg) indx.sort(reverse=True, key=GetSegment) # compute average and print print >> OutStream, 'best-hour segments:' names = ['segment', 'start', 'stop', 'duration'] fmt = "%12s" + "%10s" * 3 print >> OutStream, fmt % tuple(names) TotalTime = 0.0 i = 0 while TotalTime < TimeLimit and i < nSeg: Beg = elapsed_time[seg_starts[indx[i]]] hhBeg = Beg // 3600 mmBeg = (Beg % 3600) // 60 ssBeg = (Beg % 3600) % 60 End = elapsed_time[seg_stops[indx[i]]] hhEnd = End // 3600 mmEnd = (End % 3600) // 60 ssEnd = (End % 3600) % 60 dur = seg_durtns[indx[i]] / SampleRate hhDur = dur // 3600 mmDur = (dur % 3600) // 60 ssDur = (dur % 3600) % 60 DurPlus = '' if hhDur == 0 else '+%ih' % (hhDur) fmt = '%12d' + '%4i:%02i:%02i' + '%4i:%02i:%02i' + '%7i:%02i' + '%s' print >> OutStream, fmt \ % (indx[i], hhBeg, mmBeg, ssBeg, hhEnd, mmEnd, ssEnd, mmDur, ssDur, DurPlus) TotalTime += dur i += 1 BestAve = TotalTime / float(i) hh = BestAve // 3600 mm = (BestAve % 3600) // 60 ss = (BestAve % 3600) % 60 DurPlus = '' if hh == 0 else '+%ih' % (hh) print >> OutStream, ' BEST ONE-HOUR AVERAGE: %7i:%02i%s' \ % (mm, ss, DurPlus) ############################################################ # 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 = 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.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 elapsed_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) fig, (ax0, ax1, ax2) = plt.subplots(nrows=3, sharex=True) ax0.plot_date(x, signals['power'], 'b-', linewidth=1) ax0.grid(True) ax0.set_ylabel('power, W') ax0.set_title('Saddle Endurance') ax1.plot_date(x, signals['cadence'], 'g-', linewidth=1) ax1.plot_date(x, cad_fwd_min_3_8, 'm-', linewidth=1) ax1.plot_date(x, cad_fwd_min_1_8, 'brown', linestyle='-', linewidth=1) ax1.grid(True) ax1.set_ylabel('cadence, RPM') ax1.legend(['cadence', 'cad_fwd_min_3_8', 'cad_fwd_min_1_8'], loc='upper left') ax2.plot_date(x, seated_state, 'r-', linewidth=3) ax2.xaxis.set_major_formatter(DateFormatter('%H:%M:%S')) ax2.grid(True) ax2.set_ylabel('seated') ax2.set_yticks([0, 1]) ax2.set_yticklabels(('standing', 'seated')) for i in range(nLaps): ax0.axvline(x_laps[i], label=str(i + 1)) ax1.axvline(x_laps[i], label=str(i + 1)) ax2.axvline(x_laps[i], label=str(i + 1)) fig.canvas.set_window_title(FitFilePath) fig.tight_layout() fig.subplots_adjust(hspace=0) # Remove horizontal space between axes 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 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