Sunday, February 7, 2021

Reverse Engineering Zwift Physics - A Fun Look


Last year, I performed a simple exercise to reverse engineer a cycling ride done on Fulgaz app to understand in-game variables being employed. I described it in this post. It was nice to receive a message from Fulgaz suggesting that I'd come very close to what they actually use for their sanctioned events. 

Overall, the Fulgaz app seemed very in-tune with the physics behavior we cyclists normally expect to encounter with cycling outdoors. Zwift, on the other hand is tricky. None of the in-game physics constants and parameters are known to the public. 

Given many of us outdoor cyclists find the discrepancies between in-game Zwift physics and real world confusing, it is apt to ask whether Zwift is even based on an "earth" model.

With that fun question - which world is Zwift in? - I make an attempt to understand what makes the app work the way it does. Its far from perfect but hopefully it simulates your own thinking and you can go off and try to do something similar. 

Just don't send me any hate mail. My approach maybe far from perfect.


The modeling tool is the same one I used to model the Fulgaz performance. It takes more than 30  variables and allows breaking a course into many many small segments for steady speed analysis. 

For Zwift, I included some adjustments where the model would accept different g constants, drag co-efficient of bikes, altitude de-rates to power depending on whether a cyclist is acclimated or not, segment-by-segment rolling resistances and drag areas depending on the heading of the cyclist, direction of wind and the known characteristics of the road. 

In short, there's a lot of "handles" that I can pull in order to understand the whacky world of Zwift.


I rode one lap of Mighty Metropolitan using my real bike hooked up to Computrainer. The in-game bike chosen was the 2021 Canyon Aeroad with Zipp 202 wheelset. My weight and height were set to 64.8 kg and 173cm respectively. The bike weight was assumed to be a race-ready 6.8kg, which came off some forums on the internet. 

Fig 1 : A crazy twisty, windy course! Course details on Veloviewer

Power output was dual recorded. Primary source of power was dual-sided power pedals while Racermate maintained the load at 150W. This would also yield second by second transmission differences between the two sites of power application depending on where on the course I changed a gear or my cadence. Trainer load was held at a constant 150W. I freely chose a cadence.

Using a script for processing the GPX file, the course was broken into 60 segments in order to capture all the features of the terrain - flats, downhills and uphill sections. Specific sections that had the glass or road surfaces were marked for rolling resistance adjustments.

Model variables were tweaked as far as practical to match the model segment time to the Zwift recorded segment time and speeds which came off the raw GPX file. The strategies being :

1) Minimize the difference between average model speed and Zwift reported average speed for the course. 


2) Minimize the difference between segment model speed and Zwift reported segment speed individually for uphills, flats and downhills. 

The exercise seemed to often be a tradeoff between the above two. In principle, the two scenarios should be intertwined but the way I derived segment-segment information from the GPX data could have led to some errors. For example, the average speed in a segment was derived from the data which may have been acutely affected by outliers within that segment. 

The best solution would balance the accuracy in average course speeds with the match within each of the segments. I ran a few scenarios to check the sensitivity of the model. 


A CdA of 0.22 sq.m was modeled from frontal area from Bassett et al. (Med Sci Sports Exerc 1999; 31:1665-1676) using my height and weight and co-efficient of drag Cd from Heil was computed as : 

Cd = 4.45 x mass (kg)^-0.45

CdA factors were set to 90% on the flat and downhill sections assuming an aero position but 100% on the uphills. 

Is this CdA representative of the Zwift world? Probably. Earlier, I did some Aero testing using the Chung "regression method" with the same bike and weight settings on another course called Queen's Highway. The CdA that resulted was around 0.21 sq.m (Fig 2). The Crr values were bonkers so instead I tried the Chung "virtual elevation method" and achieved better results (Fig 3). 

In the VE method, the CdA was more like 0.0028 sq.m and Crr around 0.0032. To achieve that, I had to constrain Crr to 0.0032 and used Goal Seek to hone in on the CdA value in order to get the virtual elevation to match the expected elevation profile. Since CdA and Crr have an inverse relationship with each other, I can only find out how sensitive CdA is to a given Crr by changing the Crr value. For this exercise, I simply fixed Crr to 0.0032. 

Fig 2 : (click to view) Results of aero testing done by Chung method. Spreadsheet courtesy of Alex Simmons , Google Wattage Group

Fig 3 : (click to view) Results of the Chung virtual elevation method. Spreadsheet courtesy of Alex Simmons, Google Wattage Group.

From the above results, it appears my simulation tests using CdA between 0.22 and 0.29 sq.m and Crr of 0.003 was alright. However, I'm pretty unsure of modification factors used for the uphill, downhill and flats. Slope is hardly the reason for a rider to change his bike position, infact it must actually be speed that sets the body position. However, I've assumed speed to be low on the uphills to increase the CdA factors and high on the downhills and flats. 

The Crr of tarmac was chosen as 0.003, the same I used for Fulgaz. A Crr of 0.003 is representative of the performance of a high quality tire on a smooth road. Note that the Crr for glassy segments of 65% of tarmac is arbitrary and hypothetical. 

Other factors like acceleration due to gravity, relative humidity, altitude and air temperature in Mighty Metropolitan etc were all based on data from NYC. 


I've attached the results from the tabulation below. 

Fig 4 : (click to view) Tabulation of different case runs with the variables chosen to run the model. The first two runs are representative of earth while the rest are whackier attempts with whimsical variables.

Fig 5 : (click to view) Model performance compared to the "virtual" performance in Zwift for segmented distances of the course using the given variables in Case# 1. 


As you can see through Fig 5, the model did well to bring down the overall error in overall course performance time and speed but seemed to struggle with matching Zwift recorded speed and time data for segments. In the best "earth" scenario Case #1 (see Fig 4), the model flew downhills but rode uphills and flats slower. But trying to make segment performance improve had a tradeoff on the course average performance as shown in Case #2. 

The segment specific speed and time matching were not that great. This could also stem from the errors in speed and time calculation within the segments themselves, which in turn stems from irregularities in the original GPX file. Moreover, since the GPX file is a continuous speed run of my avatar with one segment's speed input being linked to the output from a previous segment (such as steep downhill speeds leading to a climb), the transient nature would probably differ from a purely steady state analysis from the model. 

In other whacky attempts (#3-#6), I employed a "non-earth" scenario by manipulating air density and acceleration due to gravity. The best whacky attempt (#6) provided me a near identical error in course average performance speed and time as Scenario #1 but with improvements in the segment specific matching. These results are "whacky" because a change in the acceleration due to gravity and air density should also affect the CdA values, in other words they are all dependent on each other. However, I've ignored that obvious complexity and stopped further attempts here. 


I tried to reverse engineer the physics variables and parameters set in the whacky world of Zwift. The model came close but a closer match can be achieved only with inputs of whimsical numbers. A purely steady state type cycling power model is perhaps not best to use for matching to running-segment data however overall, the model course time matched closely with Zwift course time. 

If you don't know what to make of this fun attempt, don't worry. I'm just as puzzled how the world of Zwift works. And perhaps I just said it. The world of Zwift may not even be a world on earth. We would perhaps like to assume so, but it might just be the case that we're in a parallel world as earth with similar names of cities and hypothetical physics. Perhaps those flying cars and glassy climbs where riders seem to have the ability to climb at 40kph and descend at 80kph are enough proof.

As a cyclist who is clearly in-tune with the world around him and how his bike rides in that world for over 15 years, the way Zwift overreports speeds on flats and downhills seems overly flattering. That said, I love my in-game CdA and rolling resistances and whatever other Zwifty physics constants there might be. The enjoyment of Zwifting far overrules the eccentricities of this software.


*  *  *

No comments: