running-tools

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

commit 31633310b07de3b22b973e2db91c17c407c92031
parent 7c2b54eed8e0cc3ea978f9b6cdf0a36019374ffa
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Fri, 27 Aug 2021 19:36:06 -0700

Add Purdy Point and VO2 Max race prediction models

Diffstat:
Msrc/utils/races.js | 174++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/views/RaceCalculator.vue | 2+-
Mtests/unit/utils/races.spec.js | 63++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtests/unit/views/RaceCalculator.spec.js | 5++---
4 files changed, 201 insertions(+), 43 deletions(-)

diff --git a/src/utils/races.js b/src/utils/races.js @@ -1,46 +1,164 @@ /** - * Predict a race time using Pete Riegel's Formula - * https://en.wikipedia.org/wiki/Peter_Riegel - * @param {Number} d1 The distance of the input race (in meters) - * @param {Number} t1 The finish time of the input race (in seconds) - * @param {Number} d2 The distance of the output race (in meters) - * @param {Number} c The value of the exponent in the equation - * @returns {Number} The predicted time for the output race (in seconds) + * Predict a race time using the Purdy Points Model + * https://www.cs.uml.edu/~phoffman/xcinfo3.html + * @param {Number} d1 The distance of the input race in meters + * @param {Number} t1 The finish time of the input race in seconds + * @param {Number} d2 The distance of the output race in meters + * @returns {Number} The predicted time for the output race in seconds */ -function RiegelFormula(d1, t1, d2, c = 1.06) { - return t1 * ((d2 / d1) ** c); +function PurdyPointsModel(d1, t1, d2) { + // Declare constants + const c1 = 11.15895; + const c2 = 4.304605; + const c3 = 0.5234627; + const c4 = 4.031560; + const c5 = 2.316157; + const r1 = 3.796158e-2; + const r2 = 1.646772e-3; + const r3 = 4.107670e-4; + const r4 = 7.068099e-6; + const r5 = 5.220990e-9; + + // Calculate world record velocity from running curve + const v1 = (-c1 * Math.exp(-r1 * d1)) + + (c2 * Math.exp(-r2 * d1)) + + (c3 * Math.exp(-r3 * d1)) + + (c4 * Math.exp(-r4 * d1)) + + (c5 * Math.exp(-r5 * d1)); + const v2 = (-c1 * Math.exp(-r1 * d2)) + + (c2 * Math.exp(-r2 * d2)) + + (c3 * Math.exp(-r3 * d2)) + + (c4 * Math.exp(-r4 * d2)) + + (c5 * Math.exp(-r5 * d2)); + + // Calculate world record time + const twsec1 = d1 / v1; + const twsec2 = d2 / v2; + + // Calculate constants + const k1 = 0.0654 - (0.00258 * v1); + const k2 = 0.0654 - (0.00258 * v2); + const a1 = 85 / k1; + const a2 = 85 / k2; + const b1 = 1 - (1035 / a1); + const b2 = 1 - (1035 / a2); + + // Calculate Purdy Points for distance 1 + const points = a1 * ((twsec1 / t1) - b1); + + // Calculate time for distance 2 + const seconds = (a2 * twsec2) / (points + (a2 * b2)); + + // Return predicted time + return seconds; +} + +/** + * Calculate a runner's VO2 max from their performance in a race + * @param {Number} d The race distance in meters + * @param {Number} t The finish time in minutes + * @returns {Number} The runner's VO2 max + */ +function VO2Max(d, t) { + const v = d / t; + const result = (-4.6 + (0.182 * v) + (0.000104 * (v ** 2))) + / (0.8 + (0.189 * Math.exp(-0.0128 * t)) + (0.299 * Math.exp(-0.193 * t))); + return result; +} + +/** + * Calculate the derivative with respect to time of the VO2 max curve at a specific point + * @param {Number} d The race distance in meters + * @param {Number} t The finish time in minutes + * @return {Number} The derivative + */ +function VO2MaxDerivative(d, t) { + const result = ((-575000 * (t ** 2)) + (22750 * d * t) + (13 * (d ** 2))) / (125 + * (t ** 2) * (189 * Math.exp((-8 * t) / 625) + (299 * Math.exp((-193 * t) / 1000) + 800))); + return result; } /** - * Predict a race time using Dave Cameron's Formula + * Predict a race time using the VO2 Max Model + * http://run-down.com/statistics/calcs_explained.php + * @param {Number} d1 The distance of the input race in meters + * @param {Number} t1 The finish time of the input race in seconds + * @param {Number} d2 The distance of the output race in meters + * @param {Number} iterations The maximum number of times to refine the prediction + * @returns {Number} The predicted time for the output race in seconds + */ +function VO2MaxModel(d1, t1, d2, iterations = 500) { + // Calculate input VO2 max + const inputVO2 = VO2Max(d1, t1 / 60); + + // Initialize estimate + let estimate = (t1 * d2) / (d1 * 60); + let estimateVO2; + + for (let i = 0; i < iterations; i += 1) { + // Get estimate's VO2 max + estimateVO2 = VO2Max(d2, estimate); + + // Check if estimate is close enough + if (Math.abs(inputVO2 - estimateVO2) < 0.0001) { + break; + } + + // Refine estimate + estimate += (estimateVO2 - inputVO2) / VO2MaxDerivative(d2, estimate); + } + + // Return estimate + return estimate * 60; +} + +/** + * Predict a race time using Dave Cameron's Model * https://www.cs.uml.edu/~phoffman/cammod.html - * @param {Number} d1 The distance of the input race (in meters) - * @param {Number} t1 The finish time of the input race (in seconds) - * @param {Number} d2 The distance of the output race (in meters) - * @returns {Number} The predicted time for the output race (in seconds) + * @param {Number} d1 The distance of the input race in meters + * @param {Number} t1 The finish time of the input race in seconds + * @param {Number} d2 The distance of the output race in meters + * @returns {Number} The predicted time for the output race in seconds */ -function CameronFormula(d1, t1, d2) { +function CameronModel(d1, t1, d2) { const a = 13.49681 - (0.000030363 * d1) + (835.7114 / (d1 ** 0.7905)); const b = 13.49681 - (0.000030363 * d2) + (835.7114 / (d2 ** 0.7905)); return (t1 / d1) * (a / b) * d2; } /** - * Predict a race time by averaging the results of different formulas - * @param {Number} d1 The distance of the input race (in meters) - * @param {Number} t1 The finish time of the input race (in seconds) - * @param {Number} d2 The distance of the output race (in meters) - * @param {Number} c The value of the exponent in Pete Riegel's Formula - * @returns {Number} The predicted time for the output race (in seconds) + * Predict a race time using Pete Riegel's Model + * https://en.wikipedia.org/wiki/Peter_Riegel + * @param {Number} d1 The distance of the input race in meters + * @param {Number} t1 The finish time of the input race in seconds + * @param {Number} d2 The distance of the output race in meters + * @param {Number} c The value of the exponent in the equation + * @returns {Number} The predicted time for the output race in seconds + */ +function RiegelModel(d1, t1, d2, c = 1.06) { + return t1 * ((d2 / d1) ** c); +} + +/** + * Predict a race time by averaging the results of different models + * @param {Number} d1 The distance of the input race in meters + * @param {Number} t1 The finish time of the input race in seconds + * @param {Number} d2 The distance of the output race in meters + * @param {Number} c The value of the exponent in Pete Riegel's Model + * @returns {Number} The predicted time for the output race in seconds */ -function AverageFormula(d1, t1, d2, c = 1.06) { - const riegel = RiegelFormula(d1, t1, d2, c); - const cameron = CameronFormula(d1, t1, d2); - return (riegel + cameron) / 2; +function AverageModel(d1, t1, d2, c = 1.06) { + const purdy = PurdyPointsModel(d1, t1, d2); + const vo2max = VO2MaxModel(d1, t1, d2); + const cameron = CameronModel(d1, t1, d2); + const riegel = RiegelModel(d1, t1, d2, c); + return (purdy + vo2max + cameron + riegel) / 4; } export default { - RiegelFormula, - CameronFormula, - AverageFormula, + PurdyPointsModel, + VO2MaxModel, + CameronModel, + RiegelModel, + AverageModel, }; diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -102,7 +102,7 @@ export default { unitUtils.DISTANCE_UNITS.meters); // Get prediction - const time = raceUtils.AverageFormula(d1, this.inputTime, d2); + const time = raceUtils.AverageModel(d1, this.inputTime, d2); // Return result return { diff --git a/tests/unit/utils/races.spec.js b/tests/unit/utils/races.spec.js @@ -2,26 +2,67 @@ import { expect } from 'chai'; import raceUtils from '@/utils/races'; describe('utils/races.js', () => { - describe('RiegelFormula method', () => { + describe('PurdyPointsModel method', () => { it('Predictions should be approximately correct', () => { - const result = raceUtils.RiegelFormula(400, 60, 800); - expect(result).to.be.closeTo(125, 0.5); + const result = raceUtils.PurdyPointsModel(5000, 1200, 10000); + expect(result).to.be.closeTo(2490, 1); + }); + + it('Should predict identical times for itentical distances', () => { + const result = raceUtils.PurdyPointsModel(5000, 1200, 5000); + expect(result).to.be.closeTo(1200, 0.001); + }); + }); + + describe('VO2MaxModel method', () => { + it('Predictions should be approximately correct', () => { + const result = raceUtils.VO2MaxModel(5000, 1200, 10000); + expect(result).to.be.closeTo(2488, 1); + }); + + it('Should predict identical times for itentical distances', () => { + const result = raceUtils.VO2MaxModel(5000, 1200, 5000); + expect(result).to.be.closeTo(1200, 0.001); + }); + }); + + describe('CameronModel method', () => { + it('Predictions should be approximately correct', () => { + const result = raceUtils.CameronModel(5000, 1200, 10000); + expect(result).to.be.closeTo(2500, 1); + }); + + it('Should predict identical times for itentical distances', () => { + const result = raceUtils.CameronModel(5000, 1200, 5000); + expect(result).to.be.closeTo(1200, 0.001); }); }); - describe('CameronFormula method', () => { + describe('RiegelModel method', () => { it('Predictions should be approximately correct', () => { - const result = raceUtils.CameronFormula(800, 115, 1500); - expect(result).to.be.closeTo(238.48, 0.5); + const result = raceUtils.RiegelModel(5000, 1200, 10000); + expect(result).to.be.closeTo(2502, 1); + }); + + it('Should predict identical times for itentical distances', () => { + const result = raceUtils.RiegelModel(5000, 1200, 5000); + expect(result).to.be.closeTo(1200, 0.001); }); }); - describe('AverageFormula method', () => { + describe('AverageModel method', () => { it('Predictions should be correct', () => { - const result = raceUtils.AverageFormula(5000, 20 * 60, 10000); - const riegel = raceUtils.RiegelFormula(5000, 20 * 60, 10000); - const cameron = raceUtils.CameronFormula(5000, 20 * 60, 10000); - expect(result).to.equal((riegel + cameron) / 2); + const result = raceUtils.AverageModel(5000, 1200, 10000); + const riegel = raceUtils.RiegelModel(5000, 1200, 10000); + const cameron = raceUtils.CameronModel(5000, 1200, 10000); + const purdyPoints = raceUtils.PurdyPointsModel(5000, 1200, 10000); + const vo2Max = raceUtils.VO2MaxModel(5000, 1200, 10000); + expect(result).to.equal((riegel + cameron + purdyPoints + vo2Max) / 4); + }); + + it('Should predict identical times for itentical distances', () => { + const result = raceUtils.AverageModel(5000, 1200, 5000); + expect(result).to.be.closeTo(1200, 0.001); }); }); }); diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js @@ -24,12 +24,11 @@ describe('views/RaceCalculator.vue', () => { }); // Assert result is correct - const riegel = raceUtils.RiegelFormula(5000, 20 * 60, 10000); - const cameron = raceUtils.CameronFormula(5000, 20 * 60, 10000); + const prediction = raceUtils.AverageModel(5000, 1200, 10000); expect(result).to.deep.equal({ distanceValue: 10, distanceUnit: 'kilometers', - time: (riegel + cameron) / 2, + time: prediction, }); }); });