Skip to content

Latest commit

 

History

History
205 lines (181 loc) · 22.1 KB

README.md

File metadata and controls

205 lines (181 loc) · 22.1 KB

Critical Power on the fly

I was puzzled by a publication that claimed: it is possible to Measure your FTP in a Minute in BikeRadar. Further Googling revealed that baronbiosys.com developed an Xert-app that claims this:

The method uses sophisticated techniques and pattern recognition to determine your FTP. Whereas in the past you either needed to test using a 20 minute FTP protocol for example, or examine many months’ worth of data to get a realistic FTP value, this method enables you to determine your FTP on that day or even at that moment. cf Baronbiosys

After installing the Xert app on my Garmin (830), I have experienced, until free trial time was expended, that it does its estimates of FTP remarkably well. DcRainmaker reviewed it and is quite positive about its performance and accuracy and concluded:

Whereas here it is the fact that I am getting FTP feedback in real-time that is so unique. I can go out for a ride and start to see these values formulate as I am giving hard efforts. No waiting hours, or even minutes later. I am not aware of any other platform or app that does that. cf DCRainmaker

I decided to develop C++ code for an indoor training application that is running on an Arduino nRF52840 Express board and that comes as close as possible to the functionality of the Xert app (for Garmin Connect). It became an integral part of a larger project: AIRFLOW. The following is an explanation of the science and math behind its fundamentals.

It was clear to me that the Xert app is based on depletion of the so-called Anaerobic Work Capacity (AWC) or Functional Reserve Capacity (FRC). To keep it simple, assume that a cyclist has a given amount of this finite work capacity (an energy reserve) stored internally at the beginning of a ride. This amount of energy is known in mathematical terms as W' (pronounce W prime), it is energy and consequently measured in joules. While you are riding at a low intensity, W' remains at its full level since it is not expended, and you are able to continue riding at this intensity for a long time. But if you push harder, you will start using this energy. The limit at which you will start expending this energy reserve is known as Critical Power (CP). If you push on the pedals harder than CP, W' will decrease. As soon as your produced power (watts) get lower than CP, W' will “regenerate”, and the energy reserve will increase again. When you ride long enough below CP, W' will approach the 100% level again. However, when you work hard and long enough above CP, W' will be depleted completely and you will be exhausted at the very moment (a.k.a. Tlim)! The variations in W' are expressed as “W' Balance” in the Dr. Skiba algorithm (2). Another parameter of the algorithm is Tau that defines the speed at which W' is regenerating when the power is below CP.

CP is defined as the highest exercise intensity that can be maintained for prolonged periods of time, typically for 45 to 60 min. Functional Threshold Power (FTP) is more well-known in recreational cycling and has been defined as the highest average power output that can be maintained for 60 min (1). Given the great similarity in definition, we assume for non-elite cycling the absence of a significant difference between CP and FTP values!

It is all about algorithms

The algorithms that had to be implemented are the original Dr. Skiba algorithm (2) and an optimization (approximation) of the Integral Skiba algorithm by Dave Waterworth (3). Aart Goossens published code (in Python) and explanatory information on Github (4) that helped a great deal to understand and implement the different algorithms in an Arduino setting. The following information is paraphrased from his original work, to give the reader some insight in the mathematical background of the algorithms, see his work at: Aart Goosens @ Github

Integral Skiba algorithm

The integral Skiba algorithm is the best-known algorithm to calculate W' balance and has been scientifically validated (5). The equations for the algorithm are:
W Prime equations
Where W'bal(t) is equal to W'bal at time t, W' is the amount of available energy above CP (Critical Power), t the time for which W'bal is calculated, u the iterator of the summation, W'exp(u) amount of energy above CP that is used at time u (expended), e the Euler number and ƮW' (pronounced Tau) a time constant that describes the recovery speed. The numbers 546, -0.01 and 316 are determined experimentally in Skiba's original article and do not change between individuals. DCP is the difference between CP and the average power of the intervals in which the power was below CP. DCP can be calculated dynamically (the average until time t) or calculated once for the entire workout and used as a static value. Skiba recommends using a static value for DCP. P(t) is the power produced at time t.

Dave Waterworth optimization of integral Skiba algorithm

Mathematician Dave Waterworth (3) helped the core developer of Golden Cheetah, Mark Liversedge, to develop an optimization of the Skiba algorithm (6). This reformulation approximates the Skiba algorithm so results can vary a little in extreme cases only, especially when (Tau) is very small compared with the sample time. The W'bal integral part of the equations of Skiba is rewritten by Waterworth to:
Waterworth equations
Where S(t) is a running sum at time t after the start, other symbols conform the previous equations. TauW') and W'exp(t) are calculated with the original equations presented by Skiba. The integral Skiba algorithm is quite expensive to compute, even on fast computers since the summation must be repeated for every time t again. The big advantage of the Waterworth optimization is that now W' balance can be calculated at real time: during the ride and not only afterwards! In addition it is very helpful when one wants to determine the Critical Power on the fly during HIIT workouts or strenuous workouts when W' balance becomes negative and has been depleted!

The Power-Duration Relationship

Power Duration
Mathematically, the Power-Duration relationship is described as a hyperbolic function. The 4 different points on the curve represent points in Time (Tlim) when corresponding maximum sustainable Power above CP is reached and exhaustion occurs. When exercise tolerance is considered, the power-asymptote is known as CP (Watts). The curvature constant is known as W' (i.e., W prime), it is measured in units of work done (Joules). Notice that the 4 greyed areas, representing W', are different in form but about equal in size. This hyperbolic power-duration relationship can be transformed into a linear relationship if work done is plotted against time, such that the slope of the line equals CP and the intercept equals W'. It should be emphasised that the power-duration relationship describes exercise tolerance but does not explain it. Nevertheless, the physiological responses to exercise performed below and above CP may provide important insights into the fatigue process. CP was originally defined as the external power output that could be sustained “indefinitely” or for a very long time without fatigue. This definition should be considered theoretical, however, since no exercise can ever be undertaken indefinitely. It is now understood that CP separates power outputs for which exercise tolerance is predictably limited (exercise power > CP). The actual time to intolerance (Tlim) for exercise performed above CP is defined, and therefore closely predicted, by the equation:

Tlim = W′/(P-CP)

This equation highlights that the time to intolerance above CP is a function of the proximity of the power output (P) being sustained to CP and the size of W'. When P is considerably above CP, the constant amount of work represented by the W' parameter will be utilized rapidly and Tlim will be short. Should P be closer to CP, then W' would be ‘used’ more slowly and Tlim would be longer. A crucial consideration here is that W' is assumed to be constant for all P above CP. This ‘two parameter’ power-time or power-duration model therefore implies that absolute exercise performance depends on simply the value of CP (in Watts) and the value of W' (in Joules). Both CP and W' parameters can vary considerably among individuals as a function of health/disease, age, fitness, and training.

Code

// ------------ W' Balance calculation -------------------
// Global variables related to Cycling Power and W-Prime
uint16_t TAWC_Mode = 1;                   // Track Anaerobic Capacity Depletion Mode == TRUE -> APPLY and SHOW
uint16_t CP60 = 160;                      // Your (estimate of) Critical Power, more or less the same as FTP
uint16_t eCP = CP60;                      // Algorithmic estimate of Critical Power during intense workout
uint16_t w_prime_usr = 7500;              // Your (estimate of) W-prime or a base value
uint16_t ew_prime_mod = w_prime_usr;      // First order estimate of W-prime modified during intense workout
uint16_t ew_prime_test = w_prime_usr;     // 20-min-test algorithmic estimate (20 minute @ 5% above eCP) of W-prime for a given eCP! 
long int w_prime_balance = 0;             // Can be negative !!!
bool IsShowWprimeValuesDominant = false;  // Boolean that determines to show W Prime data on Oled or not
//-------------------------------------------------------   
// ------------------------   W'Balance Functions  -----------------------------------
uint16_t CalculateAveragePowerBelowCP(uint16_t iPower, uint16_t iCP);
void CalculateAveragePowerAboveCP(uint16_t iPower, uint16_t &iavPwr, unsigned long &iCpACp);
double tau_w_prime_balance(uint16_t iPower, uint16_t iCP);
void w_prime_balance_waterworth(uint16_t iPower, uint16_t iCP, uint16_t iw_prime);
void ConstrainW_PrimeValue(uint16_t &iCP, uint16_t &iw_prime);
uint16_t GetCPfromTwoParameterAlgorithm(uint16_t iav_Power, unsigned long iT_lim, uint16_t iw_prime);
uint16_t GetWPrimefromTwoParameterAlgorithm(uint16_t iav_Power, double iT_lim, uint16_t iCP);
// ------------------------   W'Balance Functions  ------------------------------------
uint16_t CalculateAveragePowerBelowCP(uint16_t iPower, uint16_t iCP){
  // calculate avg_power_below_cp real time using a running sum and counter
  static unsigned long int CountPowerBelowCP = 0;
  static unsigned long int SumPowerBelowCP = 0;
    if (iPower < iCP) { 
      SumPowerBelowCP += (unsigned long int)iPower;
      CountPowerBelowCP++;
      }
  return uint16_t(SumPowerBelowCP/CountPowerBelowCP); // average power below CP
}   // end calculate avg_power_below_cp

void CalculateAveragePowerAboveCP(uint16_t iPower, uint16_t &iavPwr, unsigned long int &iCpACp){
  // calculate avg_power_above_cp real time using a running sum and counter
  // returning the values by C++ reference!
  static unsigned long int SumPowerAboveCP = 0;
      SumPowerAboveCP += (unsigned long int)iPower;
      iCpACp++;
      iavPwr = uint16_t(SumPowerAboveCP/iCpACp); // average power above CP
}   // end calculate avg_power_above_cp

double tau_w_prime_balance(uint16_t iPower, uint16_t iCP){  
    uint16_t avg_power_below_cp = CalculateAveragePowerBelowCP(iPower, iCP);
    double delta_cp = double(iCP - avg_power_below_cp);
    return (double(546.00) * exp(-0.01 * delta_cp) + double(316.00));
}   // end Tau W Prime Balance

void w_prime_balance_waterworth(uint16_t iPower, uint16_t iCP, uint16_t iw_prime) {
    // Most power meters measure power, torque a.o. in a high frequency (20-60 Hz) but 
    // transmit (BLE) datasets to a monitoring device in much lower frequency: 1-4 times per second.
    int power_above_cp = 0; // Power > CP
    static double T_lim = 0; // Time (duration) while Power is above CP, the summed value of every sample time value P > CP
    double w_prime_expended = 0.0; // Expended energy in Joules
    double ExpTerm1 = 0.0, ExpTerm2 = 0.0;
    static double TimeSpent = 0.0; // Total Time spent in the workout, the summed value of every sample time value
    static double running_sum = 0.0;
    static unsigned long int CountPowerAboveCP = 0; // Count the Power readings above CP 
    static uint16_t avPower = 0; // Average power above CP
    const long int NextLevelStep = 1000; // Stepsize of the next level of w-prime modification --> 1000 Joules step
    static long int NextUpdateLevel = 0; // The next level at which to update eCP, e_w_prime_mod and ew_prime_test
    // Quarq Dfour Zero Spider power meter sends between 2 and 1.2 power readings per second, dependent of POWER level !!!
    // We assume that the sample frequency (number of samples per second) is VARIABLE !!!
    // Determine the individual sample time in seconds, it may/will vary during the workout !!! 
    static unsigned long PrevReadingTime = 0;
    double SampleTime  = double(millis()-PrevReadingTime)/1000; // Time or duration since the previous sample, convert from millis to seconds
    PrevReadingTime = millis(); // Update for the next sample
    double tau = tau_w_prime_balance(iPower, iCP); // Determine the value for tau
    TimeSpent += SampleTime ; // The summed value of all sample time values during the workout
    power_above_cp = (iPower - iCP);
#ifdef DEBUGAIR
    Serial.printf("Time:%6.1f ST: %4.2f tau: %f ", TimeSpent, SampleTime , tau);
#endif
    // w_prime is energy and measured in Joules = Watt*second
    // Determine the expended energy above CP since the previous measurement (--> i.e. during sample time)
    w_prime_expended = double(max(0, power_above_cp))*SampleTime; // Determine (Watts_above_CP) * (its duration in seconds) = expended energy in Joules!
    // Calculate some terms of the equation
    ExpTerm1 = exp(TimeSpent/tau); // Exponential term1
    ExpTerm2 = exp(-TimeSpent/tau); // Exponential term2
#ifdef DEBUGAIR
    Serial.printf("W prime expended: %3.0f exp-term1: %f exp-term2: %f ", w_prime_expended , ExpTerm1, ExpTerm2);
#endif
    running_sum = running_sum + (w_prime_expended*ExpTerm1); // Determine the running sum
#ifdef DEBUGAIR
    Serial.printf("Running Sum: %f ", running_sum);
#endif    
    w_prime_balance = (long int)( (double)iw_prime - (running_sum*ExpTerm2) ) ; // Determine w prime balance and cast from double to int
#ifdef DEBUGAIR
    Serial.printf(" w_prime_balance: %d ", w_prime_balance);
#endif
    //--------------- extra --------------------------------------------------------------------------------------
    // Workout starts at a certain W'= ##,### Joules and CP = ### watts, set by the user; the algorithm increases CP and W' stepwise
    // to more realistic values every time when W'balance is depleted to a certain level; -> 2-Parameter Algorithm updates CP and W'
    if (power_above_cp > 0) { 
      CalculateAveragePowerAboveCP(iPower, avPower, CountPowerAboveCP); // Average power above CP is to be calculated for future use
      T_lim += SampleTime ; // Time to exhaustion: the accurate sum of every second spent above CP, calculated for future use
    }
#ifdef DEBUGAIR
    Serial.printf(" [%d]\n", CountPowerAboveCP);
#endif
    // When working above CP, the moment comes that we need to update eCP and ew_prime !!
    if ( (w_prime_balance < NextUpdateLevel) && (w_prime_expended > 0) ) { // W' balance is further depleted --> test for an update moment
       NextUpdateLevel -= NextLevelStep; // Move down another level of depletion, update eCP, ew_prime_mod and ew_prime_test
       eCP = GetCPfromTwoParameterAlgorithm(avPower, T_lim, iw_prime); // Estimate a new eCP value
       ew_prime_mod = w_prime_usr - NextUpdateLevel; // Adjust ew_prime_modified to the next level of depletion to be checked
       ew_prime_test = GetWPrimefromTwoParameterAlgorithm(uint16_t(eCP*1.045), double(1200), eCP); // 20-Min-test estimate for W-Prime
#ifdef DEBUGAIR
       Serial.printf("Update of eCP - ew_prime %5d - avPower: %3d - T-lim:%6.1f --> eCP: %3d ", ew_prime_mod, avPower, T_lim, eCP);
       Serial.printf("--> Test estimate of W-Prime: %d \n", ew_prime_test );
#endif
    }
    //-----------------extra -------------------------------------------------------------------------------
} // end

// Check and Set starting value of w_prime to realistic numbers!!
void ConstrainW_PrimeValue(uint16_t &iCP, uint16_t &iw_prime) {
    if (iCP < 100) { iCP = 100; } // Update to lowest level that we allow for
    // First determine the "minimal" value for W_Prime according to a 20-min-test estimate, given the iCP value! 
    uint16_t w_prime_estimate = GetWPrimefromTwoParameterAlgorithm(uint16_t(iCP*1.045), double(1200), iCP); 
    if (iw_prime < w_prime_estimate) { iw_prime = w_prime_estimate; } // Update iw_prime to a realistic level
    return;
} // end

uint16_t GetCPfromTwoParameterAlgorithm(uint16_t iav_Power, double iT_lim, uint16_t iw_prime) {
     uint16_t WprimeDivTlim = uint16_t( double(iw_prime)/iT_lim ); // type cast for correct calculations
     if (iav_Power > WprimeDivTlim){ // test for out of scope
      return (iav_Power - WprimeDivTlim); // Solve 2-parameter algorithm to estimate CP
      } else {
      return eCP; // Something went wrong do'nt allow an update of CP
     }
} // end

uint16_t GetWPrimefromTwoParameterAlgorithm(uint16_t iav_Power, double iT_lim, uint16_t iCP) {
     if (iav_Power > iCP){ // test for out of scope
      return (iav_Power-iCP)*((uint16_t)iT_lim); // Solve 2-parameter algorithm to estimate new W-Prime
      } else {
      return w_prime_usr; // Something went wrong don't allow an update of w_prime
     }
} // end

Airflow Device and Companion app

Airflow app
The code has been integrated in a larger project called Airflow. Setting or changing your basal CP and W Prime are an integral part of an AIRFLOW Companion app! The AIRFLOW smart device continuously tunes the requested airflow velocity of the cooling fan(s) for a stable Heat Balance during all phases of an indoor cycling workout, warm-up, intensity intervals, intermittent recovery and at cooldown. The cyclist has no on-the-way interference and can fully concentrate on the demands of the stationay trainer workout, always facing the ideal airstream that will cool him/her appropriately. In addition the cyclists gets (as a bonus) insight in the development of his/her Critical Power when training intensity is intense and long enough! The applied Arduino nRF52480 Express CPU is so powerfull that Real Time calculation of CP and W Prime can be accomplished aside all the calculations for determining the Heat Balance terms and setting the fans to the appropriate blowing capacity.

Example Critical Power on the fly

A cyclist (properties: CP = 140 watt and W Prime = 7.2 kJ) is doing an intense workout with the duration and intensity as shown in the figure. W Prime is depleted several times during the first interval block. New values for CP and W Prime will be estimated by the algorithm during workout time only when appropriate. Run the video to see 100% depletion of W Prime and the estimated values as they were calculated in Real Time and presented on an OLED screen... Notice how the horizontal bar shrinks when power is above CP at 175 and 195 watts. The bar is proportional with W' Balance and in addition what is "left in the tank" is shown as a percentage! Readings are shown in an accellerated sequence and do not conform indicated time duration of the workout! Workout

WPrimeBalance_02.mp4

References

  1. Hunter A, Coggan A. Training and Racing with a Power Meter. Boulder (CO): Velo Press; 2010. p. 5-20, 41-65 p. 9
  2. Skiba, P. F., Chidnok, W., Vanhatalo, A., & Jones, A. M. (2012). Modelling the expenditure and reconstitution of work capacity above critical power. Medicine and science in sports and exercise, 44(8), 1526-1532.
  3. http://markliversedge.blogspot.nl/2014/10/wbal-optimisation-by-mathematician.html
  4. https://github.com/AartGoossens/publications/blob/master/w_balance_algorithms/article.ipynb
  5. Skiba, P. F., Jackman, S., Clarke, D., Vanhatalo, A., & Jones, A. M. (2014). Effect of work and recovery durations on W' reconstitution during intermittent exercise. Medicine and science in sports and exercise, 46(7), 1433-1440.
  6. https://github.com/GoldenCheetah/GoldenCheetah/blob/master/src/Metrics/WPrime.cpp