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). ParseFitDistance_r3
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.
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
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.
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
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!
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