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;