Esempio n. 1
0
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
Esempio n. 2
0
fmt     = '%20s:' + '%15s'*3
print >> OutStream, fmt % tuple(names1)
OutStream.flush()

for FitFile in fit_files:

    TimerStart = time.time()     # measure execution time

    print >> OutStream, '#################################################'
    print >> OutStream, '###%s###' % FitFile.center(43)
    print >> OutStream, '#################################################'

    # get the signals
    FitFilePath = FilePath + FitFile
    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']
Esempio n. 3
0
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
Esempio n. 4
0
# Delay range in seconds.
# for finding the delay in Edge relative to Zwift:
# a positive delay means Edge looks like a delayed version of Zwift.
MinDelay = 40 * 60
MaxDelay = 50 * 60

#EdgeFilePath    = r'2017-01-02-16-12-43_edge.fit'
#ZwiftFilePath   = r'2017-01-02-16-07-51_zwift.fit'

#EdgeFilePath    = r'2017-01-07-10-20-12_edge_quarq.fit'
#ZwiftFilePath   = r'2017-01-07-10-15-57_zwift_fluid2.fit'

EdgeActivity = Activity(EdgeFilePath)
ZwiftActivity = Activity(ZwiftFilePath)

EdgeSignals = extract_activity_signals(EdgeActivity)
ZwiftSignals = extract_activity_signals(ZwiftActivity)

# save signals for faster analysis
SignalMap = {'EdgeSignals': EdgeSignals, 'ZwiftSignals': ZwiftSignals}
import pickle
SignalsFile = open('signals.pkl', 'wb')
pickle.dump(SignalMap, SignalsFile)
SignalsFile.close()

from pylab import arange, interp, array, zeros, sqrt, average
#from numpy import array, zeros, arange, interp, sqrt, average
# plot heart rate and calories
edge_hr = EdgeSignals['heart_rate']
edge_t = arange(len(edge_hr))
edge_power = EdgeSignals['power']
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
Esempio n. 6
0
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
Esempio n. 7
0
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
Esempio n. 8
0
print 'weight        : ', weight
print 'age           : ', age
print 'EndurancePower: ', EndurancePower
print 'ThresholdPower: ', ThresholdPower
print 'EnduranceHR   : ', EnduranceHR
print 'ThresholdHR   : ', ThresholdHR

from datetime import datetime
from fitparse import Activity
from activity_tools import extract_activity_signals

# typ 'S:\\will\\documents\\bike\\fitfiles\\2017-01-08-15-41-48_zwift.fit'
fitfilepath = sys.argv[1]

activity = Activity(fitfilepath)
signals = extract_activity_signals(activity)

########################################################################
###         Compute Calories                                         ###
########################################################################

from numpy import array, arange, append, zeros, cumsum, average
#from pylab import *

#   Formula widely available. One site:
#       https://www.easycalculation.com/formulas/heart-rate-calorie-burn.html

#weight  = 188.0*0.45359237  # lb->kg
#age     = 50.0

#   calibration at endurance
Esempio n. 9
0
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
Esempio n. 10
0
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