Code for Podcast 287 Distance Calculation Question

As @Nate_Pearson mentioned in the latest podcast (287) in response to the question at 1:23:40 (regarding how TrainerRoad estimates distance) he said he’d be interested in having this as a microservice on AWS or Azure.

To help get this done here’s python code for calculating the speed from a fit file using a basic physics model (the code details all the assumptions). http://www.robertoneil.com/TrainerRoad/ParseFitDistance_r3.html

I haven’t made a microservice before, but for anyone who wants to take the basic physics model and port it into a microservice it seems you just need to accept a file file, and have the ability to also take in CdA values and the combined rider bicycle mass which will allow for customization.

I previously went through the math for the physics in this thread: [request] Stop sending speed data to strava and here’s a python notebook that shows the formula and different ways to calculate the results: http://www.robertoneil.com/TrainerRoad/Example_ParseFit2.html

Oh and for anyone who wants to weigh in on whether or not Nate’s idea of doing this is a good or bad idea - please do that back on the previous thread. Let’s keep this just for folks who want to work on coding this.

Thanks.

[edit - realized how to post code]

#fitdecode from https://pypi.org/project/fitdecode/
import fitdecode

#for online use point this to the location of the fit file to analyze
fit_file = 'rob0-2020-12-04-glassy-95960008.fit'

g = 9.81           #gravity in m/s^2
m = 79.4 + 1       #rider + bike mass in kg with 1kg more simulating the wheel's rotational intertia
Crr = 0.005        #approximate rolling resistance
CdA = 0.324        #approximate CdA in m^2 - hands on hoods elbows bent - can be varied
Rho = 1.225        #air density sea level STP
dt = 1             #time step from the fit file; will be updated below
speed_total = 0    #add up all the speeds, later divide by total steps to get average
Vi = 0             #initialize the starting speed at 0  
count = 0          #used to average the speed

time_prev = None   #last fit message time, for edge cases when not just 1 second intervals
total_time = 0     #length of the ride in seconds

with fitdecode.FitReader(fit_file) as fit:
    for frame in fit:
        if isinstance(frame, fitdecode.FitDataMessage) and frame.has_field('power'):
            time_current = frame.get_field('timestamp').value
            if time_prev:
                dt = (time_current-time_prev).seconds
                total_time += dt
            
            p=(frame.get_field('power').value)
            Vf = ((-dt*(CdA*Rho*Vi**3-2*p+2*Crr*Vi*g*m)+Vi**2*m)/m)**.5
            speed_total += Vf
            count += 1
            Vi = Vf
            time_prev = time_current

v = speed_total* 2.23694/count #convert from m/s to mph and average        
t = total_time/3600

print ("Average Speed: {:.2f} mph - Time: {:.2f} hours - Distance: {:.2f} miles".format(v,t,(v*t)))

Average Speed: 20.48 mph - Time: 1.33 hours - Distance: 27.30 miles

12 Likes

OK since no one seemed to be interested in doing the 2nd half of the project (taking the previous code I wrote and making a microservice), I decided to read how to make a microservice using AWS lambda.

This uses an S3 bucket that you upload the fit file to which then triggers the lambda function to calculate the distance traveled (and average speed as well, which you get anyway as part of the calculation).

The result is then saved with the same name as the .fit file, but with a .txt extension in a 2nd S3 bucket for downloading.

You could also choose to have this directed somewhere else, not sure exactly how things integrate on the backend at TR - but this is just a working proof of concept.

So… @Nate_Pearson there you have it.

import boto3
import uuid
from urllib.parse import unquote_plus
#fitdecode from https://pypi.org/project/fitdecode/
import fitdecode

s3_client = boto3.client('s3')

def lambda_handler(event, context):
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = unquote_plus(record['s3']['object']['key'])
        tmpkey = key.replace('/', '')
        fit_path = '/tmp/{}{}'.format(uuid.uuid4(), tmpkey)
        s3_client.download_file(bucket, key, fit_path)
        get_distance(fit_path,key)
        
def get_distance(fit_path,key):
    g = 9.81           #gravity in m/s^2
    m = 79.4 + 1       #rider + bike mass in kg with 1kg more simulating the wheel's rotational intertia
    Crr = 0.005        #approximate rolling resistance
    CdA = 0.324        #approximate CdA in m^2 - hands on hoods elbows bent - can be varied
    Rho = 1.225        #air density sea level STP
    dt = 1             #time step from the fit file; will be updated below
    speed_total = 0    #add up all the speeds, later divide by total steps to get average
    Vi = 0             #initialize the starting speed at 0  
    count = 0          #used to average the speed
    
    time_prev = None   #last fit message time, for edge cases when not just 1 second intervals
    total_time = 0     #length of the ride in seconds

    with fitdecode.FitReader(fit_path) as fit:
        for frame in fit:
            if isinstance(frame, fitdecode.FitDataMessage) and frame.has_field('power'):
                time_current = frame.get_field('timestamp').value
                if time_prev:
                    dt = (time_current-time_prev).seconds
                    total_time += dt
                
                p=(frame.get_field('power').value)
                Vf = ((-dt*(CdA*Rho*Vi**3-2*p+2*Crr*Vi*g*m)+Vi**2*m)/m)**.5
                speed_total += Vf
                count += 1
                Vi = Vf
                time_prev = time_current

    v = speed_total* 2.23694/count #convert from m/s to mph and average        
    t = total_time/3600

    #create a string with the results
    string = 'Average Speed: {:.2f} mph - Time: {:.2f} hours - Distance: {:.2f} miles'.format(v,t,(v*t))

    #write the data to a file
    file_name = '{}.txt'.format(key)
    lambda_path = '/tmp/{}'.format(file_name)
    s3_path = file_name
    
    with open(lambda_path, 'w+') as file:
        file.write(string)
        file.close()
    
    #move the file to the tr-fit-results bucket
    s3 = boto3.resource('s3')
    s3.meta.client.upload_file(lambda_path, 'tr-fit-results', s3_path, ExtraArgs={'ACL': 'public-read'})

    #print ('done -> {}'.format(string))  #for logging results
12 Likes

Super cool! Do you want to put this in a GitHub repo and put an open-source MIT license on it?

We have a “Creative Day” coming up and I’ve reached out to the devs to see if anyone wants to hook this up.

9 Likes

Will do.

5 Likes

This is awesome! After hearing this episode of the podcast, I thought this sounded like a fun project to try, too.
I started coding up something similar but got stuck on the calculation.
Thanks for sharing the code and breaking down that calculation! Very cool!

Here it is on github: https://github.com/robertoneil/Fit-Parse-Distance

You can always reach out if you have any questions.

No problem. If you want to dig into how the calculation works make sure to checkout the longer python notebook where I approached it several different ways:

This does it fairly explicitly, but has issues when the bike is moving very close to or at 0 mph - the notebook shows other methods as well:

#This calculation is more explicit - however care must be taken when the speed is near zero
speed2 = []
Vi = 0 
for p in tcx_watts:
    Wroll = Crr*m*g*Vi           #For each time step calculate Watts lost to rolling resistance
    Wair = 1/2*CdA*Rho*Vi**3     #And the wind resistance
    Wacc = p - Wroll - Wair      #If this is positive then the bike accelerates, negative is slows down
    if Vi:                       #This is to avoid a divide by 0 error if Vi = 0
        F = Wacc/Vi              #Calculate the force 
    else:
        F = Wacc/1
    a = F/m                      #Newton's second law - we get the acceleration
    Vf = Vi + a*dt               #This calculates how much velocity changes for this time step
    speed2.append(Vf * 2.23694)  #Record the speed for this time step
    Vi = Vf                      #Update the current speed to be used in the next time step
1 Like