Module: FitnessPredictor

Included in:
Calcpace
Defined in:
lib/calcpace/fitness_predictor.rb

Overview

Module for predicting race performances from a VO2max value

This is the inverse of Vo2maxEstimator#estimate_vo2max: instead of asking "what fitness does this race result imply?", it asks "what race result does this fitness imply?".

The Daniels & Gilbert (1979) model cannot be inverted in closed form — the %VO2max term mixes two exponentials of time with a quadratic in velocity — so the finish time is found by bisection on the time axis. VO2max decreases monotonically with time for a fixed distance, which makes the search exact to within the tolerance and guarantees the round trip:

estimate_vo2max(distance, predict_time_from_vo2max(vo2max, distance)) == vo2max

Predicted times reproduce Daniels' published VDOT table within a few seconds for the shorter races and about a minute for the marathon.

Constant Summary collapse

SUPPORTED_VO2MAX_RANGE =

Range of VO2max values the model is meaningful for. Below it the effort is slower than a walk, above it faster than any human has run — in both cases the resulting "prediction" would be arithmetic, not physiology.

(10.0..100.0)
FASTEST_PACE_SECONDS_PER_KM =

Bisection bounds, as seconds per kilometre: from 1:00/km (well beyond world record pace) to 20:00/km (slower than walking). They bracket every VO2max in SUPPORTED_VO2MAX_RANGE at any distance.

60.0
SLOWEST_PACE_SECONDS_PER_KM =
1200.0
TIME_TOLERANCE_SECONDS =

Search stops when the bracket is tighter than this many seconds or when the VO2max at the midpoint is this close to the target

0.001
VO2MAX_TOLERANCE =
1e-6
DEFAULT_RACES =

Races reported by #race_times_from_vo2max when none are given

%w[5k 10k half_marathon marathon].freeze

Instance Method Summary collapse

Instance Method Details

#predict_time_from_vo2max(vo2max, race, distance_unit: nil) ⇒ Float

Note:

estimate_vo2max can report values below 10 for very slow efforts (walking pace); those estimates are outside this predictor's supported range on purpose — a race plan built on them would be meaningless

Predicts the finish time a given VO2max is worth over a given race

Examples:

calc.predict_time_from_vo2max(50, '5k')       #=> 1196.02 (≈19:56)
calc.predict_time_from_vo2max(50, 'marathon') #=> 11439.74 (≈3:10:39)
calc.predict_time_from_vo2max(50, 6.2, distance_unit: :mi)

Parameters:

  • vo2max (Numeric)

    VO2max in ml/kg/min (must be within SUPPORTED_VO2MAX_RANGE)

  • race (Numeric, String, Symbol)

    race distance in kilometres (or in miles via distance_unit: :mi), either numeric or as a numeric string ('10', '21.0975'), or a standard race name ('5k', '10k', 'half_marathon', 'marathon', '1mile', '5mile', '10mile', '100k' — see PaceCalculator::RACE_DISTANCES)

  • distance_unit (Symbol, nil) (defaults to: nil)

    unit of a numeric race distance — :km (default) or :mi. Rejected when race is a race name: standard races already carry their own distance

Returns:

  • (Float)

    predicted finish time in seconds

Raises:



64
65
66
67
68
69
70
# File 'lib/calcpace/fitness_predictor.rb', line 64

def predict_time_from_vo2max(vo2max, race, distance_unit: nil)
  target = validated_vo2max(vo2max)
  distance_km = predicted_race_distance_km(race, distance_unit)
  check_positive(distance_km, 'Distance')

  solve_time_for_vo2max(target, distance_km)
end

#predict_time_from_vo2max_clock(vo2max, race, distance_unit: nil) ⇒ String

Predicts the finish time and returns it as a clock time string

Examples:

calc.predict_time_from_vo2max_clock(50, 'marathon') #=> '03:10:39'

Parameters:

  • vo2max (Numeric)

    VO2max in ml/kg/min (must be within SUPPORTED_VO2MAX_RANGE)

  • race (Numeric, String, Symbol)

    race distance in kilometres (or in miles via distance_unit: :mi), either numeric or as a numeric string ('10', '21.0975'), or a standard race name ('5k', '10k', 'half_marathon', 'marathon', '1mile', '5mile', '10mile', '100k' — see PaceCalculator::RACE_DISTANCES)

  • distance_unit (Symbol, nil) (defaults to: nil)

    unit of a numeric race distance — :km (default) or :mi. Rejected when race is a race name: standard races already carry their own distance

Returns:

  • (String)

    predicted finish time in HH:MM:SS format



79
80
81
# File 'lib/calcpace/fitness_predictor.rb', line 79

def predict_time_from_vo2max_clock(vo2max, race, distance_unit: nil)
  convert_to_clocktime(predict_time_from_vo2max(vo2max, race, distance_unit: distance_unit))
end

#race_times_from_vo2max(vo2max, races: nil, unit: :km) ⇒ Hash{String => Hash}

Builds a full race-time table for one VO2max — one call per dashboard

Examples:

calc.race_times_from_vo2max(50)['10k']
#=> { time: 2479.6, time_clock: '00:41:19', pace: 247.96, pace_clock: '00:04:07' }
calc.race_times_from_vo2max(50, races: %w[5k 10mile], unit: :mi)

Parameters:

  • vo2max (Numeric)

    VO2max in ml/kg/min

  • races (Array<String, Symbol>, nil) (defaults to: nil)

    race names to report (default: 5k, 10k, half marathon, marathon)

  • unit (Symbol) (defaults to: :km)

    unit of the returned paces — :km (default) or :mi

Returns:

  • (Hash{String => Hash})

    race name => { time: seconds, time_clock: 'HH:MM:SS', pace: seconds per unit, pace_clock: 'HH:MM:SS' }

Raises:



99
100
101
102
103
104
105
106
# File 'lib/calcpace/fitness_predictor.rb', line 99

def race_times_from_vo2max(vo2max, races: nil, unit: :km)
  meters = pace_unit_meters(unit)
  validated_vo2max(vo2max)

  Array(races || DEFAULT_RACES).to_h do |race|
    [normalize_race_key(race), race_time_entry(vo2max, race, meters)]
  end
end