running-tools

A collection of tools for runners and their coaches
git clone https://git.ashermorgan.net/running-tools/
Log | Files | Refs | README

racePrediction.ts (20154B)


      1 /*
      2  * Implements various race prediction models
      3  */
      4 
      5 /*
      6  * The available race prediction models
      7  */
      8 export enum RacePredictionModels {
      9   AverageModel = 'AverageModel',
     10   PurdyPointsModel = 'PurdyPointsModel',
     11   VO2MaxModel = 'VO2MaxModel',
     12   RiegelModel = 'RiegelModel',
     13   CameronModel = 'CameronModel',
     14 };
     15 
     16 /*
     17  * The type for race prediction options
     18  */
     19 export interface RacePredictionOptions {
     20   model: RacePredictionModels,
     21   riegelExponent: number,
     22 };
     23 
     24 /*
     25  * The default race prediction options
     26  */
     27 export const defaultRacePredictionOptions = {
     28   model: RacePredictionModels.AverageModel,
     29   riegelExponent: 1.06,
     30 };
     31 
     32 /*
     33  * The type for internal variables used by the Purdy Points race prediction model
     34  */
     35 interface PurdyPointsVariables {
     36   twsec: number,
     37   a: number,
     38   b: number,
     39 };
     40 
     41 /**
     42  * Estimate the point at which a function returns a target value using Newton's Method
     43  * @param {number} initialEstimate The initial estimate
     44  * @param {number} target The target function output
     45  * @param {Function} method The function
     46  * @param {Function} derivative The function derivative
     47  * @param {number} precision The acceptable precision
     48  * @returns {number} The refined estimate
     49  */
     50 function NewtonsMethod(initialEstimate: number, target: number, method: (x: number) => number,
     51                        derivative: (x: number) => number, precision: number): number {
     52   // Initialize estimate
     53   let estimate = initialEstimate;
     54   let estimateValue;
     55 
     56   for (let i = 0; i < 500; i += 1) {
     57     // Evaluate function at estimate
     58     estimateValue = method(estimate);
     59 
     60     // Check if estimate is close enough (usually occurs way before i = 500)
     61     if (Math.abs(target - estimateValue) < precision) {
     62       break;
     63     }
     64 
     65     // Refine estimate
     66     estimate -= (estimateValue - target) / derivative(estimate);
     67   }
     68 
     69   // Return refined estimate
     70   return estimate;
     71 }
     72 
     73 /*
     74  * Methods that implement the Purdy Points race prediction model
     75  * https://www.cs.uml.edu/~phoffman/xcinfo3.html
     76  */
     77 const PurdyPointsModel = {
     78   /**
     79    * Calculate the Purdy Point variables for a distance
     80    * @param {number} d The distance in meters
     81    * @returns {PurdyPointsVariables} The Purdy Point variables
     82    */
     83   getVariables(d: number): PurdyPointsVariables {
     84     // Declare constants
     85     const c1 = 11.15895;
     86     const c2 = 4.304605;
     87     const c3 = 0.5234627;
     88     const c4 = 4.031560;
     89     const c5 = 2.316157;
     90     const r1 = 3.796158e-2;
     91     const r2 = 1.646772e-3;
     92     const r3 = 4.107670e-4;
     93     const r4 = 7.068099e-6;
     94     const r5 = 5.220990e-9;
     95 
     96     // Calculate world record velocity from running curve
     97     const v = (-c1 * Math.exp(-r1 * d))
     98               + (c2 * Math.exp(-r2 * d))
     99               + (c3 * Math.exp(-r3 * d))
    100               + (c4 * Math.exp(-r4 * d))
    101               + (c5 * Math.exp(-r5 * d));
    102 
    103     // Calculate world record time
    104     const twsec = d / v;
    105 
    106     // Calculate constants
    107     const k = 0.0654 - (0.00258 * v);
    108     const a = 85 / k;
    109     const b = 1 - (1035 / a);
    110 
    111     // Return Purdy Point variables
    112     return {
    113       twsec,
    114       a,
    115       b,
    116     };
    117   },
    118 
    119   /**
    120    * Get the Purdy Points for a race
    121    * @param {number} d The distance of the race in meters
    122    * @param {number} t The finish time of the race in seconds
    123    * @returns {number} The Purdy Points for the race
    124    */
    125   getPurdyPoints(d: number, t: number): number {
    126     // Get variables
    127     const variables = PurdyPointsModel.getVariables(d);
    128 
    129     // Calculate Purdy Points
    130     const points = variables.a * ((variables.twsec / t) - variables.b);
    131 
    132     // Return Purdy Points
    133     return points;
    134   },
    135 
    136   /**
    137    * Predict a race time using the Purdy Points Model
    138    * @param {number} d1 The distance of the input race in meters
    139    * @param {number} t1 The finish time of the input race in seconds
    140    * @param {number} d2 The distance of the output race in meters
    141    * @returns {number} The predicted time for the output race in seconds
    142    */
    143   predictTime(d1: number, t1: number, d2: number): number {
    144     // Calculate Purdy Points for distance 1
    145     const points = PurdyPointsModel.getPurdyPoints(d1, t1);
    146 
    147     // Calculate time for distance 2
    148     const variables = PurdyPointsModel.getVariables(d2);
    149     const seconds = (variables.a * variables.twsec) / (points + (variables.a * variables.b));
    150 
    151     // Return predicted time
    152     return seconds;
    153   },
    154 
    155   /**
    156    * Calculate the derivative with respect to distance of the Purdy Points curve at a specific point
    157    * @param {number} d1 The distance of the input race in meters
    158    * @param {number} t1 The finish time of the input race in seconds
    159    * @param {number} d2 The distance of the output race in meters
    160    * @return {number} The derivative with respect to distance
    161    */
    162   derivative(d1: number, t1: number, d2: number): number {
    163     const result = (85 * d2) / (((2316157 * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000
    164       + (100789 * Math.exp(-(7068099 * d2) / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767
    165       * d2) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000
    166       - (223179 * Math.exp(-(1898079 * d2) / 50000000)) / 20000) * (327 / 5000 - (129 * ((2316157
    167       * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
    168       / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
    169       + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
    170       * d2) / 50000000)) / 20000)) / 50000) * ((85 * (1 - (207 * (327 / 5000 - (129 * ((2316157
    171       * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
    172       / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
    173       + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
    174       * d2) / 50000000)) / 20000)) / 50000)) / 17)) / (327 / 5000 - (129 * ((2316157
    175       * Math.exp(-(522099 * d2) / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d2)
    176       / 1000000000000)) / 25000 + (5234627 * Math.exp(-(410767 * d2) / 1000000000)) / 10000000
    177       + (860921 * Math.exp(-(411693 * d2) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079
    178       * d2) / 50000000)) / 20000)) / 50000) + (85 * (d1 / (((2316157 * Math.exp(-(522099 * d1)
    179       / 100000000000000)) / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000
    180       + (5234627 * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693
    181       * d1) / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000) * t1)
    182       + (207 * (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000))
    183       / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627
    184       * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1)
    185       / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000))
    186       / 17 - 1)) / (327 / 5000 - (129 * ((2316157 * Math.exp(-(522099 * d1) / 100000000000000))
    187       / 1000000 + (100789 * Math.exp(-(7068099 * d1) / 1000000000000)) / 25000 + (5234627
    188       * Math.exp(-(410767 * d1) / 1000000000)) / 10000000 + (860921 * Math.exp(-(411693 * d1)
    189       / 250000000)) / 200000 - (223179 * Math.exp(-(1898079 * d1) / 50000000)) / 20000)) / 50000)));
    190     return result;
    191   },
    192 
    193   /**
    194    * Predict a race distance using the Purdy Points Model
    195    * @param {number} t1 The finish time of the input race in seconds
    196    * @param {number} d1 The distance of the input race in meters
    197    * @param {number} t2 The finish time of the output race in seconds
    198    * @returns {number} The predicted distance for the output race in meters
    199    */
    200   predictDistance(t1: number, d1: number, t2: number): number {
    201     // Initialize estimate
    202     let estimate = (d1 * t2) / t1;
    203 
    204     // Refine estimate (derivative on its own is too slow)
    205     const method = (x: number) => PurdyPointsModel.predictTime(d1, t1, x);
    206     const derivative = (x: number) => PurdyPointsModel.derivative(d1, t1, x) / 500;
    207     estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01);
    208 
    209     // Return estimate
    210     return estimate;
    211   },
    212 };
    213 
    214 /*
    215  * Methods that implement the VO2 Max race prediction model
    216  * http://run-down.com/statistics/calcs_explained.php
    217  * https://vdoto2.com/Calculator
    218  */
    219 const VO2MaxModel = {
    220   /**
    221    * Calculate the VO2 of a runner during a race
    222    * @param {number} d The race distance in meters
    223    * @param {number} t The finish time in seconds
    224    * @returns {number} The VO2
    225    */
    226   getVO2(d: number, t: number): number {
    227     const minutes = t / 60;
    228     const v = d / minutes;
    229     const result = -4.6 + (0.182258 * v) + (0.000104 * (v ** 2));
    230     return result;
    231   },
    232 
    233   /**
    234    * Calculate the percentage of VO2 max a runner is at during a race
    235    * @param {number} t The race time in seconds
    236    * @returns {number} The percentage of VO2 max
    237    */
    238   getVO2Percentage(t: number): number {
    239     const minutes = t / 60;
    240     const result = 0.8 + (0.189439 * Math.exp(-0.012778 * minutes)) + (0.298956 * Math.exp(-0.193261
    241       * minutes));
    242     return result;
    243   },
    244 
    245   /**
    246    * Calculate a runner's VO2 max from a race result
    247    * @param {number} d The race distance in meters
    248    * @param {number} t The finish time in seconds
    249    * @returns {number} The runner's VO2 max
    250    */
    251   getVO2Max(d: number, t: number): number {
    252     const result = VO2MaxModel.getVO2(d, t) / VO2MaxModel.getVO2Percentage(t);
    253     return result;
    254   },
    255 
    256   /**
    257    * Calculate the derivative with respect to time of the VO2 max curve at a specific point
    258    * @param {number} d The race distance in meters
    259    * @param {number} t The finish time in seconds
    260    * @return {number} The derivative with respect to time
    261    */
    262   VO2MaxTimeDerivative(d: number, t: number): number {
    263     const result = (-(273 * d) / (25 * (t ** 2)) - (468 * (d ** 2)) / (625 * (t ** 3))) / ((189
    264       * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5)
    265       - (((273 * d) / (25 * t) + (234 * (d ** 2)) / (625 * (t ** 2)) - 23 / 5) * (-(63
    266       * Math.exp(-(2 * t) / 9375)) / 1562500 - (57707 * Math.exp(-(193 * t) / 60000)) / 60000000))
    267       / (((189 * Math.exp(-(2 * t) / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000
    268       + 4 / 5) ** 2);
    269     return result;
    270   },
    271 
    272   /**
    273    * Predict a race time using the VO2 Max Model
    274    * @param {number} d1 The distance of the input race in meters
    275    * @param {number} t1 The finish time of the input race in seconds
    276    * @param {number} d2 The distance of the output race in meters
    277    * @returns {number} The predicted time for the output race in seconds
    278    */
    279   predictTime(d1: number, t1: number, d2: number): number {
    280     // Calculate input VO2 max
    281     const inputVO2Max = VO2MaxModel.getVO2Max(d1, t1);
    282 
    283     // Initialize estimate
    284     let estimate = (t1 * d2) / d1;
    285 
    286     // Refine estimate
    287     const method = (x: number) => VO2MaxModel.getVO2Max(d2, x);
    288     const derivative = (x: number) => VO2MaxModel.VO2MaxTimeDerivative(d2, x);
    289     estimate = NewtonsMethod(estimate, inputVO2Max, method, derivative, 0.0001);
    290 
    291     // Return estimate
    292     return estimate;
    293   },
    294 
    295   /**
    296    * Calculate the derivative with respect to distance of the VO2 max curve at a specific point
    297    * @param {number} d The race distance in meters
    298    * @param {number} t The finish time in seconds
    299    * @return {number} The derivative with respect to distance
    300    */
    301   VO2MaxDistanceDerivative(d: number, t: number): number {
    302     const result = ((468 * d) / (625 * (t ** 2)) + 273 / (25 * t)) / ((189 * Math.exp(-(2 * t)
    303       / 9375)) / 1000 + (299 * Math.exp(-(193 * t) / 60000)) / 1000 + 4 / 5);
    304     return result;
    305   },
    306 
    307   /**
    308    * Predict a race distance using the VO2 Max Model
    309    * @param {number} t1 The finish time of the input race in seconds
    310    * @param {number} d1 The distance of the input race in meters
    311    * @param {number} t2 The finish time of the output race in seconds
    312    * @returns {number} The predicted distance for the output race in meters
    313    */
    314   predictDistance(t1: number, d1: number, t2: number): number {
    315     // Calculate input VO2 max
    316     const inputVO2 = VO2MaxModel.getVO2Max(d1, t1);
    317 
    318     // Initialize estimate
    319     let estimate = (d1 * t2) / t1;
    320 
    321     // Refine estimate
    322     const method = (x: number) => VO2MaxModel.getVO2Max(x, t2);
    323     const derivative = (x: number) => VO2MaxModel.VO2MaxDistanceDerivative(x, t2);
    324     estimate = NewtonsMethod(estimate, inputVO2, method, derivative, 0.0001);
    325 
    326     // Return estimate
    327     return estimate;
    328   },
    329 };
    330 
    331 /*
    332  * Methods that implement Dave Cameron's race prediction model
    333  * https://www.cs.uml.edu/~phoffman/cammod.html
    334  */
    335 const CameronModel = {
    336   /**
    337    * Predict a race time using Dave Cameron's Model
    338    * @param {number} d1 The distance of the input race in meters
    339    * @param {number} t1 The finish time of the input race in seconds
    340    * @param {number} d2 The distance of the output race in meters
    341    * @returns {number} The predicted time for the output race in seconds
    342    */
    343   predictTime(d1: number, t1: number, d2: number): number {
    344     const a = 13.49681 - (0.000030363 * d1) + (835.7114 / (d1 ** 0.7905));
    345     const b = 13.49681 - (0.000030363 * d2) + (835.7114 / (d2 ** 0.7905));
    346     return (t1 / d1) * (a / b) * d2;
    347   },
    348 
    349   /**
    350    * Calculate the derivative with respect to distance of the Cameron curve at a specific point
    351    * @param {number} d1 The distance of the input race in meters
    352    * @param {number} t1 The finish time of the input race in seconds
    353    * @param {number} d2 The distance of the output race in meters
    354    * @return {number} The derivative with respect to distance
    355    */
    356   derivative(d1: number, t1: number, d2: number): number {
    357     const result = -(100 * (30363 * (d1 ** (3581 / 2000)) - 13496810000 * (d1 ** (1581 / 2000))
    358       - 835711400000) * t1 * (134968100 * (d2 ** (3581 / 2000)) + 14963412617 * d2)) / ((d1 ** (3581
    359       / 2000)) * (d2 ** (419 / 2000)) * ((30363 * (d2 ** (3581 / 2000)) - 13496810000 * (d2 ** (1581
    360       / 2000)) - 835711400000) ** 2));
    361     return result;
    362   },
    363 
    364   /**
    365    * Predict a race distance using Dave Cameron's Model
    366    * @param {number} t1 The finish time of the input race in seconds
    367    * @param {number} d1 The distance of the input race in meters
    368    * @param {number} t2 The finish time of the output race in seconds
    369    * @returns {number} The predicted distance for the output race in meters
    370    */
    371   predictDistance(t1: number, d1: number, t2: number): number {
    372     // Initialize estimate
    373     let estimate = (d1 * t2) / t1;
    374 
    375     // Refine estimate
    376     const method = (x: number) => CameronModel.predictTime(d1, t1, x);
    377     const derivative = (x: number) => CameronModel.derivative(d1, t1, x);
    378     estimate = NewtonsMethod(estimate, t2, method, derivative, 0.01);
    379 
    380     // Return estimate
    381     return estimate;
    382   },
    383 };
    384 
    385 /*
    386  * Methods that implement Pete Riegel's race prediction model
    387  * https://en.wikipedia.org/wiki/Peter_Riegel
    388  */
    389 const RiegelModel = {
    390   /**
    391    * Predict a race time using Pete Riegel's Model
    392    * @param {number} d1 The distance of the input race in meters
    393    * @param {number} t1 The finish time of the input race in seconds
    394    * @param {number} d2 The distance of the output race in meters
    395    * @param {number} c The value of the exponent in the equation
    396    * @returns {number} The predicted time for the output race in seconds
    397    */
    398   predictTime(d1: number, t1: number, d2: number, c: number = 1.06): number {
    399     return t1 * ((d2 / d1) ** c);
    400   },
    401 
    402   /**
    403    * Predict a race distance using Pete Riegel's Model
    404    * @param {number} t1 The finish time of the input race in seconds
    405    * @param {number} d1 The distance of the input race in meters
    406    * @param {number} t2 The finish time of the output race in seconds
    407    * @param {number} c The value of the exponent in the equation
    408    * @returns {number} The predicted distance for the output race in meters
    409    */
    410   predictDistance(t1: number, d1: number, t2: number, c: number = 1.06) {
    411     return d1 * ((t2 / t1) ** (1 / c));
    412   },
    413 };
    414 
    415 /*
    416  * Methods that average the results of different race prediction models
    417  */
    418 const AverageModel = {
    419   /**
    420    * Predict a race time by averaging the results of different models
    421    * @param {number} d1 The distance of the input race in meters
    422    * @param {number} t1 The finish time of the input race in seconds
    423    * @param {number} d2 The distance of the output race in meters
    424    * @param {number} c The value of the exponent in Pete Riegel's Model
    425    * @returns {number} The predicted time for the output race in seconds
    426    */
    427   predictTime(d1: number, t1: number, d2: number, c: number = 1.06): number {
    428     const purdy = PurdyPointsModel.predictTime(d1, t1, d2);
    429     const vo2max = VO2MaxModel.predictTime(d1, t1, d2);
    430     const cameron = CameronModel.predictTime(d1, t1, d2);
    431     const riegel = RiegelModel.predictTime(d1, t1, d2, c);
    432     return (purdy + vo2max + cameron + riegel) / 4;
    433   },
    434 
    435   /**
    436    * Predict a race distance by averaging the results of different models
    437    * @param {number} t1 The finish time of the input race in seconds
    438    * @param {number} d1 The distance of the input race in meters
    439    * @param {number} t2 The finish time of the output race in seconds
    440    * @param {number} c The value of the exponent in Pete Riegel's Model
    441    * @returns {number} The predicted distance for the output race in meters
    442    */
    443   predictDistance(t1: number, d1: number, t2: number, c: number = 1.06) {
    444     const purdy = PurdyPointsModel.predictDistance(t1, d1, t2);
    445     const vo2max = VO2MaxModel.predictDistance(t1, d1, t2);
    446     const cameron = CameronModel.predictDistance(t1, d1, t2);
    447     const riegel = RiegelModel.predictDistance(t1, d1, t2, c);
    448     return (purdy + vo2max + cameron + riegel) / 4;
    449   },
    450 };
    451 
    452 /**
    453  * Predict a race time
    454    * @param {number} d1 The distance of the input race in meters
    455    * @param {number} t1 The finish time of the input race in seconds
    456    * @param {number} d2 The distance of the output race in meters
    457    * @param {RacePredictionOptions} options The race prediction options
    458    * @param {number} The predicted finish time in seconds
    459  */
    460 export function predictTime(d1: number, t1: number, d2: number,
    461                             options: RacePredictionOptions): number {
    462   switch (options.model) {
    463     default:
    464     case RacePredictionModels.AverageModel:
    465       return AverageModel.predictTime(d1, t1, d2, options.riegelExponent);
    466     case RacePredictionModels.PurdyPointsModel:
    467       return PurdyPointsModel.predictTime(d1, t1, d2);
    468     case RacePredictionModels.VO2MaxModel:
    469       return VO2MaxModel.predictTime(d1, t1, d2);
    470     case RacePredictionModels.RiegelModel:
    471       return RiegelModel.predictTime(d1, t1, d2, options.riegelExponent);
    472     case RacePredictionModels.CameronModel:
    473       return CameronModel.predictTime(d1, t1, d2);
    474   }
    475 }
    476 
    477 /**
    478  * Predict a race distance
    479    * @param {number} t1 The finish time of the input race in seconds
    480    * @param {number} d1 The distance of the input race in meters
    481    * @param {number} t2 The finish time of the output race in seconds
    482    * @param {RacePredictionOptions} options The race prediction options
    483    * @param {number} The predicted finish distance in meters
    484  */
    485 export function predictDistance(t1: number, d1: number, t2: number,
    486                                 options: RacePredictionOptions): number {
    487   switch (options.model) {
    488     default:
    489     case RacePredictionModels.AverageModel:
    490       return AverageModel.predictDistance(t1, d1, t2, options.riegelExponent);
    491     case RacePredictionModels.PurdyPointsModel:
    492       return PurdyPointsModel.predictDistance(t1, d1, t2);
    493     case RacePredictionModels.VO2MaxModel:
    494       return VO2MaxModel.predictDistance(t1, d1, t2);
    495     case RacePredictionModels.RiegelModel:
    496       return RiegelModel.predictDistance(t1, d1, t2, options.riegelExponent);
    497     case RacePredictionModels.CameronModel:
    498       return CameronModel.predictDistance(t1, d1, t2);
    499   }
    500 }
    501 
    502 export const getPurdyPoints = PurdyPointsModel.getPurdyPoints;
    503 export const getVO2 = VO2MaxModel.getVO2;
    504 export const getVO2Percentage = VO2MaxModel.getVO2Percentage;
    505 export const getVO2Max = VO2MaxModel.getVO2Max;