**More details available on our FAQ.**

## Modeling Cyclist Power
With the Tour de France nearing its end, and with some controversy about the power developed by some racers, I thought it would be timely to share some work I did in a recent project called Anal The first thing to understand is that the relationship between power and speed can be derived from the laws of physics. The speed of a cyclist depends primarily on: - His power
- The slope of the road
- The speed and direction of wind
Let us first assume that we have no wind and that the road is flat. There is one force that a cyclist must fight when riding: air resistance. That force is proportional to the square of the speed, and the power is proportional to the cube of the speed:
where P is the power in Watts, S the speed in meter per second (m/s), and Cx a coefficient for air penetration. The following function computes the power as function of the speed. All the code used for this blog entry is available as a notebook. def get_
For the sake of simplicity we will assume a coefficient Cx of 0.25. With that, here is how power increases with speed. Power in Watts as function of speed in km/h. No wind, no slope. A cyclist delivering 400 watts will ride at about 42 km/h (26 mph). We have used a very simplistic model In reality, there is a second force that the rider must fight when cycling: the friction on the road. That friction is proportional to the weight W of the cyclist with his bike and all its equipment. The power equation then become:
where P is the power in Watts, S the speed in meter per second (m/s), Cx a coefficient for air penetration, W the cyclist weight, f the road friction coefficient, and G the gravity constant (G = 9.81) We will use f = 1%. In reality a rough road will have a greater friction coefficient, say 1.5%, and a very neat road a smaller one, say 0.5%. Our Python function for power becomes: def get_
Power in Watts as function of speed in km/h, with and without road friction. No wind, no slope. We see that the power with friction (Power_f) is higher than the power without it. A cyclist developing 400 Watts will now ride at about 38 km/h (About 24 mph) Let us now look at the slope of the road. Anyone with cycling experience knows that it is way harder to climb than to descend. This is because the slope adds yet another force that the cyclist must fight: gravity. If A is the angle that the road has with an imaginary horizontal line then the power equation becomes:
where P is the power in Watts, S the speed in meter per second (m/s), Cx a coefficient for air penetration, W the cyclist weight, f the road friction coefficient, A the angle of the road with a horizontal line, and G the gravity constant (G = 9.81).
Positive angle correspond to climbing roads, while negative angles correspond to descent. Given the angles are rather small, we can approximate the sine of the angle to be equal to its tangent. In plain English it means that we can replace sine(A) by A expressed in slope percentage. A 1% percent slope means that the road gains 1 meter in altitude every 10 meters. Our power equation becomes:
where P is the power in Watts, S the speed in meter per second (m/s), Cx a coefficient for air penetration, W the cyclist weight, f the road friction coefficient, A the slope percentage of the road, and G the gravity constant (G = 9.81) .
def get_ Let us draw the effect of slope on power. Power in Watts as function of speed in km/h, with and without road friction. No wind, slopes ranging from -6% to 6%. We see that for positive slopes, power increases rapidly, which is a well known fact for anyone with cycling experience. For negative slope, power is negative for moderate speeds! Given the cyclist is not a reversible machine it means that the region where power is negative has no physical reality. For instance, with a negative slope of -6%, the first speed at which power becomes positive is about 44 km/h (about 27 mph). It means that a cyclist going downhill on that road will ride at 44 km/h if he does not pedal at all. We will see later how to compute speed from power, which will make this kind of reasoning easier. Let us now look at the effect of wind. It is also well known to all cyclists that headwind slows you down significantly while tailwind makes your ride effortless. This is because air resistance is proportional to the square of the speed at which we move relative to the air. If there is headwind, then we move at a given speed, and the air moves at wind speed in the opposite direction. We therefore need to add our speed and the headwind speed to compute air resistance. Similarly, if we have tailwind, air is moving in the same direction as us, hence our speed relative to air is the difference between our speed, and wind speed. When wind direction is not aligned with ours then we must project wind along our direction, see the picture below. What matters is the the part of the wind that is aligned with the cyclist direction, i.e. the road direction. In order to compute the relative wind, one needs to multiply the wind speed by the cosine of the angle between the wind direction and the direction that the cyclist follows ( in the above picture). From now on we will assume that we know the relative wind speed. Positive relative wind speed means headwind, and negative relative wind speed means tailwind. The power equation becomes:
where P is the power in Watts, S the speed in meter per second (m/s), Cx a coefficient for air penetration, W the cyclist weight, f the road friction coefficient, A the slope percentage of the road, H headwind speed, and G the gravity constant (G = 9.81) . There is a little caveat here. If tailwind speed is higher than the cyclist speed, then there is no air resistance left. To the contrary even, air is pushing the cyclist from the back. It means that in this case, the air resistance term in our power equation becomes negative:
We assumed that the air penetration coefficient Cx would be the same for tailwind, but that may not be right. Our Python code tests for that case. It also requires one extra argument for headwind speed: def get_
Let us plot this for various headwind speeds. Power in Watts as function of speed in km/h, with and without road friction. No slope, headwind ranging from -30 km/h to 30 km/h. We see that wind makes a huge difference. In particular, we see that strong tailwind yields a negative power. As before, it means that these conditions are not physically possible. For instance, with tailwind at 30 km/h (roughly 20 mph), then the first speed at which the power becomes positive is around 10 km/h. It means that the cyclist would move at 10 km/h without pedaling. This shows how strong wind influence can be. Maybe it is a little too strong compared to reality. Let me explain why. When we speak of wind speed, we usually mean the wind speed as documented in weather reports and weather forecasts. According to my colleague Lloyd Treinish, IBM's Chief Scientist for Environmental modeling, weather forecasts usually report wind speed at a 10m high over the ground. Why is that relevant? Because wind speed decreases as you get closer to the ground. To experiment it, simply lay down on a day with great wind. You will immediately feel much less wind on your face when you lie on the ground than when you stand up. Back to our cyclist model, what we need is wind speed at a cyclist height, say 1 meter. We therefore need to compute the air speed at 1 meter given air speed at 10 meter, the latter being provided by weather reports. According to Lloyd, the simplest conversion for the horizontal wind speed, H, would be
where z is the desired height and z0 is the height where you have the wind measurement or forecast. The constant, a, is dependent on the atmospheric conditions. On stable conditions, a is ~0.143. This is a very crude approximation but it is the simplest one we could find. In our case, z0 = 10 meters, and z is 1 meter. Therefore,
Replacing a by 0.143 yields:
Our Python code becomes: def get_
Let us plot our power equation when we scale wind speed accordingly: Power in Watts as function of speed in km/h, with and without road friction. No slope, headwind ranging from -30 km/h to 30 km/h measured at 10m high. We see that wind still has a strong effect, although a smaller one compared to the previous one. We are almost done. A last tweak makes sense when the cyclist rides at high elevation. In my project, cyclists had to ride up to an elevation of 3320 meters at Wolf Creek Pass. It is well known that air pressure decreases with elevation. This in turn decreases air resistance. More precisely, air resistance is proportional to air pressure. If p(e) is air pressure at elevation e, than air resistance at elevation e is p(e)/p(0) times the resistance at sea level. The latter is what we have been using so far. Let us see if this elevation effect is significant or not. According to http
Let us plot this Air pressure as a function of elevation in meters. At Wolf Creek Pass, air pressure is about two third of what it is at sea level. This is significant. In order to simplify a bit our computations we used a linear approximation of the above relation:
This approximation is quite good as shown by a plot of it against the above formula. Our Python code becomes. def get_ Let us plot it Power as a function of speed. No wind, no slope, elevation ranging from 0 to 4000 meters. We see that the power required to ride decreases significantly with elevation. Does it mean that riding at high altitude is easier? Certainly not, as the lower air pressure negatively impacts the physical condition of the cyclist. It is more difficult to sustain a given power at high altitude than at sea level. I don't think that the lower air resistance offsets the difficulty to sustain power. We are done with our modeling. In order to use it for predicting how fast a cyclist will ride, we need to solve it for speed given power. The formula we derived so far is that power is a polynomial of degree 3 in the speed. We therefore need to find roots of this polynomial to compute speed from power. This is where Python and its scientific libraries shine. I simply had to use the built in root finding function of the NumPy package. import numpy as np def get_
Let us look at this code in detail. After scaling air resistance according to the elevation, we set the polynomial to be solved. For that we provide the list of the coefficients of the polynomial and pass it to the NumPy roots function: roots = np.roots([Cx, 2*Cx*headwind, Cx*headwind**2 + W*G* roots = np.r We then look for positive speeds as these are the only physically possible ones: roots = roots[roots>0] speed = np.min(roots) This concludes our modeling discussion. We can now use the get_power function to study the influence of road conditions on our cyclist speed. We will assume that our cyclist is either a professional, able to deliver 400 Watts on a regular basis, or a trained cyclo tourist able to deliver 200 Watts, or an occasional cyclist able to deliver 100 Watts.
Let us start with slope. Speed as a function of slope percentage, with power ranging from 100 Watts to 400 Watts. No wind. We clearly see that speed drops as slope increases. The drop is way more important for the occasional cyclist than for the professional. We also see that the difference in power is not that significant with steep descent. Another way to look at this is to look at the time needed to ride a given distance. For this we simply plot the inverse of the speed. Remember that speed is distance divided by time, therefore time is distance divided by speed. Time to ride a distance as a function of slope, with power ranging from 100 Watts to 400 Watts. No wind. This is striking: there is no difference between our three cyclist when going downhill! The difference shows when climbing however, where the time is roughly proportional to the power used. This is a clear lesson for all cyclists: it is way more useful to use high power when climbing than when descending. Let us now look at the effect of wind. Speed as a function of headwind in km/h at 10 m high, with power ranging from 100 Watts to 400 Watts. No slope. We see that speed decreases almost linearly with headwind but we cannot say much more. Let us look at time with the hope it could be more revealing. Time to ride a distance as a function of headwind in km/h at 10 m high, with power ranging from 100 Watts to 400 Watts. No slope. Strong tailwind levels out the difference in power. The power differences play a greater role with strong headwind. The situation is a bit similar than with slopes: easy conditions remove the difference between the cyclists. These differences play a much greater role in adversary conditions. How can one use that model to predict how a given cyclist will ride on a given route? The first thing to do is to calibrate it, i.e. get values for the air penetration coefficient and the road friction coefficient. One way to determine the former coefficient is to have a power meter on the cycle, and to have your cyclist ride at a constant speed on a flat road on a clear day without wind. You can then derive Cx from the measured speed and power with:
where P is the measured power and S the measured speed. We already discussed that a friction coefficient of 1% is probably right on average. Given a calibrated model one can derive time to ride given power, road slope, elevation, and wind. For the - Power. We created a predictive model of how Dave Haase, our cyclist, was delivering power over time. This model was derived from actual race data.
- Road slope. We used RAAM gps data.
- Wind speed. We leverages weather forecasts provided by The
Weat .her Comp an y
All data shaping, machine learning and reporting was done in Python. I may blog about this in the future. Let me conclude with some plotting code. There may be more efficient ways to produce the graphics used in this blog entry, but I went for code simplicity as I had little time to implement all of this. I leveraged two very convenient Python packages: Pandas and Matplotlib. We first need to import them. I used a Jupyter notebook for my code. The first thing we do in that notebook is to import all relevant packages and declare inline display: import numpy as np %matplotlib inline
Then display is created via a temporary pandas DataFrame. For instance, let us show how we created the last plot above. def plot Let me comment on the last argument for this function. We want to display wind speed as reported in weather forecasts, i.e. wind speed in km/h at 10 meters high. We then need to transform this into wind speed at 1 meter high in m/s. According to the above discussion, the scaling factor is
which is very close to 0.2.
Part of the above was based on Jacques Fine's nice write up of the physical laws governing a cyclist (in French). I expanded it to cover the case where tailwind is faster than cyclist speed. I also expanded it to cope with elevation effect. |