calcpace 1.18.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +354 -1
- data/README.md +319 -43
- data/calcpace.gemspec +9 -5
- data/lib/calcpace/age_grading.rb +15 -22
- data/lib/calcpace/cameron_predictor.rb +73 -27
- data/lib/calcpace/checker.rb +101 -21
- data/lib/calcpace/converter.rb +19 -14
- data/lib/calcpace/data/environmental_factors.yml +88 -8
- data/lib/calcpace/data/friend_2015_vo2max_percentiles.yml +49 -0
- data/lib/calcpace/data/mldr_2025_road.yml +21 -0
- data/lib/calcpace/data/mldr_2025_road_open_standards.yml +46 -0
- data/lib/calcpace/environmental_adjuster.rb +65 -31
- data/lib/calcpace/grade_adjusted_pace.rb +114 -0
- data/lib/calcpace/humidity.rb +137 -0
- data/lib/calcpace/personalized_predictor.rb +220 -0
- data/lib/calcpace/race_predictor.rb +7 -2
- data/lib/calcpace/race_splits.rb +7 -5
- data/lib/calcpace/track_calculator.rb +183 -10
- data/lib/calcpace/training_zones.rb +42 -11
- data/lib/calcpace/version.rb +1 -1
- data/lib/calcpace/vo2max_estimator.rb +29 -4
- data/lib/calcpace/vo2max_norms.rb +121 -0
- data/lib/calcpace.rb +7 -0
- metadata +16 -9
- data/lib/calcpace/data/wma_2023_open_standards.yml +0 -40
- data/lib/calcpace/data/wma_2023_road.yml +0 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d60f32c2e712a9f2c4ab9a1a8724ac9b932838acab9bbe1db333f557d0cf1797
|
|
4
|
+
data.tar.gz: 6bd621feb8849df93ab197bbe77795da67878ddfa46ee493acd8e2ffe53a651e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6b76162fc0e1e575c1c1fc8d06c782cf603775c374c1a75dc5bacab50e2958a99c18ba47e429b65bc154f72d12d127e7c0fae99f9d59cd4dc2d3a54541fe335
|
|
7
|
+
data.tar.gz: 260a67baea8e765b473261c0c24bfc12af6672404713f92bec266c070c5c0ae5beaa7da4ef9b3714d9286f868158fddca5c74e9bb0da182d78bd393947191fdb
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,358 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.0.0] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
A major release. Four physiological models gave unrealistic numbers and now
|
|
13
|
+
give different ones (Cameron prediction, altitude, race splits, heat), the
|
|
14
|
+
marathon pace band and age grading move to their proper sources, input
|
|
15
|
+
validation is stricter, and there are new personalized predictions,
|
|
16
|
+
humidity, grade-adjusted pace and VO2max norms. Method names, signatures and
|
|
17
|
+
return shapes are unchanged except where listed under Breaking; the
|
|
18
|
+
`environmental_factors.yml` keys are unchanged.
|
|
19
|
+
|
|
20
|
+
### Breaking
|
|
21
|
+
- **Invalid clocks raise, everywhere.** Every method that takes a time or pace
|
|
22
|
+
string now validates it: `convert_to_seconds` itself, and so `checked_*`,
|
|
23
|
+
`race_time`/`race_pace` (and `_clock`), `race_splits` (`target_time:`),
|
|
24
|
+
`convert_pace`, `pace_km_to_mi`, `pace_mi_to_km`,
|
|
25
|
+
`predict_time`/`predict_pace` (and `_clock`), `equivalent_performance`,
|
|
26
|
+
`predict_time_adjusted`, every Cameron method, `riegel_exponent`,
|
|
27
|
+
`predict_time_personal`, `predict_marathon_from_training`,
|
|
28
|
+
`estimate_vo2max`, `estimate_detailed_vo2max`, `training_paces_from_race`,
|
|
29
|
+
`age_grade`, `stride_length`, `cadence_for_stride` and `grade_adjusted_pace`
|
|
30
|
+
(and `_clock`). Before, `check_time` accepted any two digits per field
|
|
31
|
+
(`'05:99'`, `'1:60:00'`), and the methods that did not call it turned such
|
|
32
|
+
strings — or garbage like `'abc'` — into the wrong number of seconds,
|
|
33
|
+
`convert_to_seconds` returning `0` for anything it could not split and
|
|
34
|
+
dropping the sign of `'-0:40'`.
|
|
35
|
+
- Accepted — every clock the gem writes (`Checker::CLOCK_FORMAT`):
|
|
36
|
+
`H:MM:SS` with any number of hour digits (`'400:00:00'`); `M:SS` with any
|
|
37
|
+
number of minute digits, counting past the hour (`'75:00'`, `'123:45'`); the
|
|
38
|
+
`'D HH:MM:SS'` day prefix of `convert_to_clocktime` (`'1 03:46:40'`); and a
|
|
39
|
+
leading `-` (`track_splits` writes `'-0:40'` for a backwards split;
|
|
40
|
+
`convert_to_seconds` returns `-40`). A few clocks the gem never writes
|
|
41
|
+
parse too, such as `'-1 03:46:40'`, `'0:00:00'` or `'000000000:00'`.
|
|
42
|
+
- Rejected with `Calcpace::InvalidTimeFormatError`: seconds that are not two
|
|
43
|
+
digits below 60; minutes that are not two digits below 60 when hours are
|
|
44
|
+
given (`'1:60:00'`, `'1:5:00'`); after a day prefix, hours that are not two
|
|
45
|
+
digits below 24 (`'1 3:46:40'`, `'1 24:00:00'`); a `+` sign, blanks,
|
|
46
|
+
surrounding whitespace, non-ASCII digits, more than three fields, strings
|
|
47
|
+
in an invalid or non-ASCII-compatible encoding (broken UTF-8, UTF-16,
|
|
48
|
+
UTF-32), and anything that is not a String.
|
|
49
|
+
- A negative clock parses, but every method that needs a positive time or
|
|
50
|
+
pace rejects it with `Calcpace::NonPositiveInputError`, as it does a
|
|
51
|
+
clock too large for a Float (see Fixed). Numeric (seconds) inputs are
|
|
52
|
+
unaffected.
|
|
53
|
+
- **Cameron predictions are limited to 100 km.** `predict_time_cameron`,
|
|
54
|
+
`predict_time_cameron_clock`, `predict_pace_cameron`,
|
|
55
|
+
`predict_pace_cameron_clock` and `predict_time_cameron_adjusted` raise
|
|
56
|
+
`ArgumentError` when either distance exceeds `CAMERON_MAX_DISTANCE_KM`
|
|
57
|
+
(100 km, which keeps the standard `'100k'` race usable). Cameron's model is
|
|
58
|
+
fitted from 800 m to the marathon and its `f(d)` crosses zero near 445 km:
|
|
59
|
+
without the limit a 500 km target got a negative time.
|
|
60
|
+
- **Removed `CameronPredictor::CAMERON_A`, `CAMERON_B` and `CAMERON_C`.** They
|
|
61
|
+
described the wrong formula (see Changed). The model's constants are now
|
|
62
|
+
`CAMERON_CONSTANT`, `CAMERON_LINEAR_COEFFICIENT`, `CAMERON_POWER_COEFFICIENT`
|
|
63
|
+
and `CAMERON_POWER_EXPONENT`.
|
|
64
|
+
- **`AgeGrading::WMA_DATA` lost its track keys and changed its age range.**
|
|
65
|
+
Each sex now holds only the road distances the gem grades — `"5000"`,
|
|
66
|
+
`"10000"`, `"21097"` and `"42195"`; code that read `WMA_DATA["M"]["1500"]`
|
|
67
|
+
(or `"3000"`) gets `nil` / `KeyError`. Its factor tables run from age 18 to
|
|
68
|
+
100 (they were 30–110). `table_version` is now
|
|
69
|
+
`"MLDR_2025_ROAD_ONE_YEAR_FACTORS_V1"` (was
|
|
70
|
+
`"WMA_2023_ONE_YEAR_FACTORS_V1"`), and the data files were renamed to
|
|
71
|
+
`lib/calcpace/data/mldr_2025_road.yml` and
|
|
72
|
+
`lib/calcpace/data/mldr_2025_road_open_standards.yml`. `DATA_PATH`,
|
|
73
|
+
`OPEN_STANDARDS_DATA_PATH`, `WMA_DATA`, `OPEN_STANDARDS_DATA` and
|
|
74
|
+
`TABLE_VERSION` keep their names.
|
|
75
|
+
|
|
76
|
+
### Changed (numbers)
|
|
77
|
+
- **Cameron prediction uses Dave Cameron's actual model.** The previous
|
|
78
|
+
constants (`a + b·e^(−d/c)` with a = 0.000495, b = 0.000985, c = 1.4485)
|
|
79
|
+
were not Cameron's formula and were far more optimistic than Riegel for the
|
|
80
|
+
marathon, while the real model is more conservative. The Cameron methods now
|
|
81
|
+
use his velocity-ratio function with distances in metres, as in his own
|
|
82
|
+
metric version (t-and-f mailing list, 20 Jun 2001) and the had2know.org
|
|
83
|
+
calculator: `f(d) = 13.49681 − 0.000030363·d + 835.7114 / d^0.7905`,
|
|
84
|
+
`T2 = T1 · (D2/D1) · f(D1)/f(D2)`. Distances are still passed in km or as
|
|
85
|
+
race names.
|
|
86
|
+
- **Altitude no longer jumps at 914 m and no longer stops at 2438 m.** The
|
|
87
|
+
penalty starts at 300 m (new `300: 0.0` point) and ramps linearly to the
|
|
88
|
+
first NCAA point (914.4 m → 1.41%) instead of jumping from 0% at 914 m to
|
|
89
|
+
1.41% at 915 m. The NCAA points up to 2438.4 m (5.90%) are unchanged. Above
|
|
90
|
+
it, where everything used to be capped at 5.90%, three points come from a
|
|
91
|
+
quadratic fit to the NCAA table (`p = 0.3647·x² + 1.9482·x`,
|
|
92
|
+
`x = km − 0.3`): 3000 m → 7.92%, 3500 m → 9.97%, 4000 m → 12.2% (capped
|
|
93
|
+
there). The redundant `0: 0.0` point is gone.
|
|
94
|
+
- **Negative and positive race splits are ±1% per half instead of ±4%.** A
|
|
95
|
+
3:00:00 marathon with `strategy: :negative` used to go through halfway in
|
|
96
|
+
1:33:36 (a 7-minute negative split); it now splits 1:30:54 + 1:29:06.
|
|
97
|
+
- **Heat: base curve and duration factor fitted jointly to marathon data, and
|
|
98
|
+
capped at 35 °C.** The 60-minute base was 2.8 / 4.3 / 6.5% at
|
|
99
|
+
20 / 25 / 30 °C, flat from 30 °C up (35 °C and 40 °C read like 30 °C), and
|
|
100
|
+
the duration factor was 1.0× (60 min) → 3.0× (3 h) → 4.5× (4 h), its 3 h
|
|
101
|
+
point justified by Ely 2007 percentages that the paper's abstract does not
|
|
102
|
+
contain. Both are now fitted to El Helou et al. (2012, PLoS One
|
|
103
|
+
7(5):e37407, Table S3; 1.79 M finishers of six majors, 2001–2010):
|
|
104
|
+
1. speed loss at 15, 20 and 25 °C, straight line between the table's points
|
|
105
|
+
(each group's optimum −10 … +20 °C);
|
|
106
|
+
2. time penalty against 15 °C, where the gem's curve is zero:
|
|
107
|
+
`P = ((1 − loss15) / (1 − lossT) − 1) × 100`;
|
|
108
|
+
3. **base shape**: P25/P20 is 2.70–2.97 in every group, so `P ∝ (T − 15)^p`
|
|
109
|
+
with `p = log2(P25/P20)`; the sex-weighted mean (each sex half the
|
|
110
|
+
weight) is 1.497 → **1.5**. Base = `4.3 · ((T − 15)/10)^1.5`, keeping the
|
|
111
|
+
25 °C / 60-minute anchor of 4.3%, stored every 2.5 °C from 15 to 40 °C
|
|
112
|
+
(linear interpolation stays within 0.08 points of the curve): 0, 0.54,
|
|
113
|
+
1.52, 2.79, 4.3, 6.01, 7.9, 9.95, 12.16, then 12.16 and 12.16 at 37.5
|
|
114
|
+
and 40 °C. **Capped at 35 °C**: 35–40 °C and hotter all read like 35 °C,
|
|
115
|
+
because above 25 °C the curve is an extrapolation (El Helou's hottest
|
|
116
|
+
race was 25.2 °C) and the uncapped law (14.51 at 37.5 °C, 17.0 at 40 °C)
|
|
117
|
+
gave 47.77% for 4 h at 40 °C;
|
|
118
|
+
4. ratio = P ÷ base(T); finish time = 42195 m ÷ the group's speed at its
|
|
119
|
+
optimum;
|
|
120
|
+
5. **duration factor**: weighted least squares over the 15 ratios (men P1 at
|
|
121
|
+
25 °C is beyond the table), each sex half the weight, 0.5× (≤30 min) and
|
|
122
|
+
1.0× (60 min) kept, 180 and 240 min free, flat after 240: 1.761 / 2.814 →
|
|
123
|
+
**1.76× at 3 h, 2.81× at 4 h** (1.38× at 2 h on the straight line). A
|
|
124
|
+
free 150-min point cut the weighted residual by 1%; a free 210-min point
|
|
125
|
+
made the curve non-monotonic (2.97 > 2.73 at 240). Neither was kept.
|
|
126
|
+
|
|
127
|
+
| Group | Finish | loss@15 | loss@20 | loss@25 | P20 | P25 | P25/P20 | ratio @20 | ratio @25 |
|
|
128
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
129
|
+
| men P1 | 2:41 | 1.88 | 3.93 | n/a | 2.14 | n/a | n/a | 1.41 | n/a |
|
|
130
|
+
| women P1 | 3:06 | 0.79 | 3.13 | 7.27 | 2.42 | 6.99 | 2.89 | 1.59 | 1.63 |
|
|
131
|
+
| men Q1 | 3:31 | 2.86 | 7.00 | 13.58 | 4.46 | 12.41 | 2.78 | 2.93 | 2.89 |
|
|
132
|
+
| men median | 3:57 | 3.18 | 7.93 | 15.63 | 5.17 | 14.76 | 2.86 | 3.40 | 3.43 |
|
|
133
|
+
| women Q1 | 4:00 | 1.86 | 4.73 | 9.26 | 3.02 | 8.16 | 2.70 | 1.99 | 1.90 |
|
|
134
|
+
| women median | 4:25 | 2.09 | 5.30 | 10.40 | 3.39 | 9.27 | 2.73 | 2.23 | 2.16 |
|
|
135
|
+
| men Q3 | 4:28 | 2.92 | 7.91 | 16.38 | 5.42 | 16.10 | 2.97 | 3.57 | 3.74 |
|
|
136
|
+
| women Q3 | 4:54 | 2.03 | 5.37 | 10.80 | 3.54 | 9.83 | 2.78 | 2.33 | 2.29 |
|
|
137
|
+
|
|
138
|
+
Losses and P in %. With the new base each group's ratio is almost the same
|
|
139
|
+
at 20 and 25 °C, so `base(T) × duration_factor(seconds)` fits; the weighted
|
|
140
|
+
residual in penalty points drops from 18.3 (linear base, 1.24×/2.18×) to
|
|
141
|
+
8.8, and what remains is mostly sex (see Known limitations). Ely et al.
|
|
142
|
+
(2007) stays a qualitative source (top men 1.7 / 2.5 / 3.3 / 4.5% off the
|
|
143
|
+
course record across WBGT 5–10 … 20–25 °C, i.e. +2.8 points); the model
|
|
144
|
+
gives a 2:10 effort 4.03% at 22.5 °C against 0% at 7.5 °C, a little above
|
|
145
|
+
that for elite runners. The duration points live in the new
|
|
146
|
+
`EnvironmentalAdjuster::HEAT_DURATION_FACTORS`; `duration_factor(time_seconds)`
|
|
147
|
+
is unchanged.
|
|
148
|
+
|
|
149
|
+
| Heat penalty (%), 1.18.1 → 2.0.0 | 20 min | 60 min | 120 min | 180 min | 240 min | 300 min |
|
|
150
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
151
|
+
| 20 °C | 1.4 → 0.76 | 2.8 → 1.52 | 5.6 → 2.1 | 8.4 → 2.68 | 12.6 → 4.27 | 12.6 → 4.27 |
|
|
152
|
+
| 25 °C | 2.15 → 2.15 | 4.3 → 4.3 | 8.6 → 5.93 | 12.9 → 7.57 | 19.35 → 12.08 | 19.35 → 12.08 |
|
|
153
|
+
| 30 °C | 3.25 → 3.95 | 6.5 → 7.9 | 13.0 → 10.9 | 19.5 → 13.9 | 29.25 → 22.2 | 29.25 → 22.2 |
|
|
154
|
+
| 35 °C | 3.25 → 6.08 | 6.5 → 12.16 | 13.0 → 16.78 | 19.5 → 21.4 | 29.25 → 34.17 | 29.25 → 34.17 |
|
|
155
|
+
| 40 °C | 3.25 → 6.08 | 6.5 → 12.16 | 13.0 → 16.78 | 19.5 → 21.4 | 29.25 → 34.17 | 29.25 → 34.17 |
|
|
156
|
+
|
|
157
|
+
- **The marathon pace band ends at the runner's predicted marathon pace.**
|
|
158
|
+
Daniels' M pace is the predicted marathon race pace, but
|
|
159
|
+
`training_paces(50)[:marathon]` ran from 4:50 to 4:25/km (75–84% VO2max)
|
|
160
|
+
while the VDOT marathon prediction for VO2max 50 is 3:10:39, 4:31/km. The
|
|
161
|
+
fast end now comes from `predict_time_from_vo2max(vo2max, 'marathon')`
|
|
162
|
+
(0.800–0.849 of VO2max across VO2max 10–100; 0.805 at 30, 0.830 at 70); the
|
|
163
|
+
slow end stays at 75%. Beyond VO2max 10–100 the race-pace fraction of the
|
|
164
|
+
nearest bound is used, so `training_paces` still accepts any positive
|
|
165
|
+
VO2max. `TRAINING_INTENSITIES` stays all-numeric
|
|
166
|
+
(`marathon: { low: 0.75, high: 0.84 }`, the nominal upper bound); the new
|
|
167
|
+
`PREDICTED_RACE_PACE_ZONES` (`%i[marathon]`) names the zones whose fast end
|
|
168
|
+
is the predicted race pace. The marathon and threshold bands no longer
|
|
169
|
+
overlap below VO2max ~69.5 (the threshold band starts at 0.83): M pace is
|
|
170
|
+
slower than T pace, as in Daniels. At VO2max 70 they touch (3:24).
|
|
171
|
+
|
|
172
|
+
| VO2max | M band before | M band after | Predicted marathon pace |
|
|
173
|
+
| --- | --- | --- | --- |
|
|
174
|
+
| 30 | 7:15–6:38/km | 7:15–6:52/km | 6:52/km |
|
|
175
|
+
| 40 | 5:47–5:17/km | 5:47–5:27/km | 5:27/km |
|
|
176
|
+
| 50 | 4:50–4:25/km | 4:50–4:31/km | 4:31/km |
|
|
177
|
+
| 60 | 4:11–3:49/km | 4:11–3:52/km | 3:52/km |
|
|
178
|
+
| 70 | 3:41–3:22/km | 3:41–3:24/km | 3:24/km |
|
|
179
|
+
|
|
180
|
+
- **Age grading uses the 2025 road tables.** Age factors and open standards
|
|
181
|
+
come from Alan Jones' 2025 road age-grading tables, approved on 2025-01-10
|
|
182
|
+
by the USATF Masters Long Distance Running Council
|
|
183
|
+
([source spreadsheets](https://github.com/AlanLyttonJones/Age-Grade-Tables/tree/4aac6737cb9f216c90a0a610355667cd3d921c61/2025%20Files):
|
|
184
|
+
`MaleRoadStd2025.xlsx`, `FemaleRoadStd2025.xlsx`). The previous data,
|
|
185
|
+
despite the `wma_2023_road.yml` name, came from the WMA 2023 **track and
|
|
186
|
+
field** tables: track open standards (5000 m 12:35 / 14:06, 10 000 m
|
|
187
|
+
26:11 / 29:01), an outdated women's marathon standard (2:14:04) and no
|
|
188
|
+
factors under age 30, so road age grades were off by about 1–3%. New open
|
|
189
|
+
standards — men: 5K 12:49, 10K 26:24, half 57:31, marathon 2:00:35; women:
|
|
190
|
+
5K 13:54, 10K 28:46, half 1:02:52, marathon 2:09:56. There is one factor
|
|
191
|
+
per year of age from 18 to 100: runners under 30 get the table's real
|
|
192
|
+
factors instead of 1.0 (a male 18-year-old at 5K: 0.9995; a female
|
|
193
|
+
30-year-old at 5K: 0.9959), and ages 101 and over use the age-100 factor.
|
|
194
|
+
Category labels are unchanged.
|
|
195
|
+
|
|
196
|
+
| Case | 1.18.1 (WMA 2023 track) | 2.0.0 (2025 road) | Official 2025 |
|
|
197
|
+
| --- | --- | --- | --- |
|
|
198
|
+
| Male 40, marathon 3:30:00 | 58.8% (factor 0.9804) | 58.7% (0.9783) | 58.7% |
|
|
199
|
+
| Female 50, marathon 4:00:00 | 62.7% (0.8915) | 60.2% (0.8998) | 60.2% |
|
|
200
|
+
| Female 30, 5K 25:00 | 56.4% (1.0) | 55.8% (0.9959) | 55.8% |
|
|
201
|
+
| Male 60, half 1:50:00 | 63.3% (0.8264) | 64.7% (0.8082) | 64.7% |
|
|
202
|
+
| Male 55, 10K 45:00 | 69.0% (0.8438) | 68.9% (0.8511) | 68.9% |
|
|
203
|
+
|
|
204
|
+
"Official" is age standard / time from the spreadsheets' `AgeStdSec` sheet,
|
|
205
|
+
at one decimal.
|
|
206
|
+
|
|
207
|
+
Summary of the other models:
|
|
208
|
+
|
|
209
|
+
| Case | 1.18.1 | 2.0.0 |
|
|
210
|
+
| --- | --- | --- |
|
|
211
|
+
| Cameron 10K 42:00 → marathon | 02:57:34 | 03:16:46 (Riegel 03:13:12) |
|
|
212
|
+
| Cameron 5K 20:00 → marathon | 02:59:25 | 03:15:11 (Riegel 03:11:49) |
|
|
213
|
+
| Cameron 5K 20:00 → 10K | 00:42:26 | 00:41:39 |
|
|
214
|
+
| Cameron 7.79 km 26:59 → half marathon | 01:13:44 | 01:17:26 |
|
|
215
|
+
| Altitude 500 m | 0.0% | 0.46% |
|
|
216
|
+
| Altitude 760 m (São Paulo) | 0.0% | 1.06% |
|
|
217
|
+
| Altitude 914 m / 915 m | 0.0% / 1.41% | 1.41% / 1.41% |
|
|
218
|
+
| Altitude 2800 m | 5.9% | 7.2% |
|
|
219
|
+
| Altitude 3600 m | 5.9% | 10.42% |
|
|
220
|
+
| Heat 20 °C, 60 min | 2.8% | 1.52% |
|
|
221
|
+
| Heat 30 °C, 60 min | 6.5% | 7.9% |
|
|
222
|
+
| Heat 35 °C / 40 °C, 60 min | 6.5% | 12.16% |
|
|
223
|
+
| Heat 25 °C, 2 h / 3 h / 4 h | 8.6% / 12.9% / 19.35% | 5.93% / 7.57% / 12.08% |
|
|
224
|
+
| Heat 30 °C, 4 h | 29.25% | 22.2% |
|
|
225
|
+
| Heat 35 °C / 40 °C, 4 h | 29.25% | 34.17% |
|
|
226
|
+
| Marathon band, VO2max 50 | 4:50–4:25/km | 4:50–4:31/km |
|
|
227
|
+
| Splits marathon 3:00:00 `:negative` (halves) | 1:33:36 + 1:26:24 | 1:30:54 + 1:29:06 |
|
|
228
|
+
| Splits marathon 3:00:00 `:positive` (halves) | 1:26:24 + 1:33:36 | 1:29:06 + 1:30:54 |
|
|
229
|
+
|
|
230
|
+
### Added
|
|
231
|
+
- **Humidity in the heat penalty.** `calculate_penalty` — and therefore
|
|
232
|
+
`adjust_time`, `normalize_time`, `predict_time_adjusted` and
|
|
233
|
+
`predict_time_cameron_adjusted`, which forward their options — accepts
|
|
234
|
+
`humidity:` (relative humidity, 0–100 %) or `dew_point:` (in
|
|
235
|
+
`temperature_unit`). 30 °C dry and 30 °C at 80% (typical of coastal Brazil)
|
|
236
|
+
used to get the same penalty. The temperature is replaced by an effective
|
|
237
|
+
temperature: the air temperature that, at `REFERENCE_HUMIDITY` (50%), has
|
|
238
|
+
the same simplified WBGT (Australian Bureau of Meteorology:
|
|
239
|
+
`WBGT = 0.567·Ta + 0.393·e + 3.94`, `e` = vapour pressure in hPa) as the
|
|
240
|
+
real temperature and humidity, solved exactly by bisection. The heat curve
|
|
241
|
+
is read at the unrounded effective temperature, so `humidity: 50` gives
|
|
242
|
+
exactly the temperature-only numbers: 50% is the humidity at which that
|
|
243
|
+
WBGT equals the air temperature between 20 °C and 35 °C (51–56%), i.e. how
|
|
244
|
+
the temperature-only points already read. `factors` gains
|
|
245
|
+
`:effective_temperature_celsius` (rounded to 2 decimals) when humidity or
|
|
246
|
+
dew point is given. `ArgumentError` for humidity outside 0–100, NaN,
|
|
247
|
+
Complex or non-numeric; a dew point above the temperature, below −100 °C or
|
|
248
|
+
not a finite real number; both keywords together; or either without a
|
|
249
|
+
finite temperature.
|
|
250
|
+
|
|
251
|
+
| 30 °C at | Effective temperature | 60 min | 4 h |
|
|
252
|
+
| --- | --- | --- | --- |
|
|
253
|
+
| 30% RH | 26.69 °C | 5.46% | 15.34% |
|
|
254
|
+
| 50% RH (= no humidity) | 30.0 °C | 7.9% | 22.2% |
|
|
255
|
+
| 70% RH | 33.07 °C | 10.46% | 29.39% |
|
|
256
|
+
| 90% RH | 35.94 °C (capped at 35 °C) | 12.16% | 34.17% |
|
|
257
|
+
|
|
258
|
+
- **Marathon from training volume.**
|
|
259
|
+
`predict_marathon_from_training(weekly_distance:, training_pace:, unit: :km)`
|
|
260
|
+
predicts a marathon from the mean weekly distance and mean training pace of
|
|
261
|
+
the 8 weeks before the race, with Tanda (2011), *Journal of Human Sport and
|
|
262
|
+
Exercise* 6(3):511–520: `Pm = 17.1 + 140.0 · exp(−0.0053 · K) + 0.55 · P`.
|
|
263
|
+
Returns `:time`, `:time_clock`, `:pace`, `:pace_clock` (in `unit`, `:km` or
|
|
264
|
+
`:mi`), `:within_validated_range` and `:out_of_range`, which lists any of
|
|
265
|
+
`:weekly_distance` (sample: 40.4–110.7 km/week), `:training_pace`
|
|
266
|
+
(253.3–330.6 s/km) and `:marathon_time` (167–216 min) that fall outside the
|
|
267
|
+
paper's sample. Out of range is a flag, not an error.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
calc.predict_marathon_from_training(weekly_distance: 60, training_pace: '05:00')[:time_clock] # => "03:19:41"
|
|
271
|
+
```
|
|
272
|
+
- **Personal Riegel exponent.** `riegel_exponent(race1, time1, race2, time2)`
|
|
273
|
+
fits `ln(t2/t1) / ln(d2/d1)` to two performances, and
|
|
274
|
+
`predict_time_personal(race1, time1, race2, time2, to_race)` predicts with
|
|
275
|
+
it. A target between the two races is interpolated along the curve through
|
|
276
|
+
both, with the raw exponent (never clamped, independent of argument order);
|
|
277
|
+
a target outside the pair is extrapolated from the closer performance in
|
|
278
|
+
log-distance, with the exponent clamped to 1.01–1.20. Returns `:time`,
|
|
279
|
+
`:time_clock`, `:exponent`, `:raw_exponent` and `:clamped`; an exponent
|
|
280
|
+
that needed clamping usually means one of the races was not all-out.
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
calc.predict_time_personal('10k', '00:45:00', 'half_marathon', '01:42:00', 'marathon')[:time_clock] # => "03:38:03"
|
|
284
|
+
```
|
|
285
|
+
- **Grade-adjusted pace** from the energy cost of running on gradients of
|
|
286
|
+
Minetti et al. (2002), J Appl Physiol 93:1039–1046:
|
|
287
|
+
`grade_adjustment_factor(grade)` (Cr(i)/Cr(0), grade as a fraction, clamped
|
|
288
|
+
to the measured ±0.45), `grade_adjusted_pace(pace, grade, unit: :km)` and
|
|
289
|
+
`grade_adjusted_pace_clock(pace, grade, unit: :km, compact: false)`.
|
|
290
|
+
- **`track_grade_adjusted_splits(points, split_km = 1.0, compact: false)`**:
|
|
291
|
+
the `track_splits` splits with a `:gap` pace per split, computed segment by
|
|
292
|
+
segment from `:ele`. Grades are measured over segments of at least 100 m of
|
|
293
|
+
horizontal distance so GPS elevation noise does not become fake climbing;
|
|
294
|
+
stretches without `:ele` (or with a non-finite one), and stretches with
|
|
295
|
+
elevation too short to grade, are flat. `track_splits` output is unchanged.
|
|
296
|
+
- **VO2max norms by age and sex** from the FRIEND registry (Kaminsky, Arena &
|
|
297
|
+
Myers, Mayo Clin Proc 2015;90(11):1515–1523, Table 3: treadmill, measured
|
|
298
|
+
VO2max), stored in `lib/calcpace/data/friend_2015_vo2max_percentiles.yml`:
|
|
299
|
+
- `vo2max_label(value, age: nil, sex: nil)` — optional keywords; with both,
|
|
300
|
+
the label comes from the percentile among the same sex and age decade
|
|
301
|
+
(≥95th Elite, ≥90th Excellent, ≥75th Very Good, ≥50th Good, ≥25th Fair,
|
|
302
|
+
else Beginner). Without them the fixed thresholds and labels are
|
|
303
|
+
unchanged.
|
|
304
|
+
- `vo2max_percentile(value, age:, sex:)` — linearly interpolated percentile,
|
|
305
|
+
bounded to the table's 5–95. Ages 18–19 use the 20–29 row and 80+ the
|
|
306
|
+
70–79 row; under 18 is rejected.
|
|
307
|
+
- New public constants (removed ones are under Breaking):
|
|
308
|
+
- `CameronPredictor`: `CAMERON_CONSTANT`, `CAMERON_LINEAR_COEFFICIENT`,
|
|
309
|
+
`CAMERON_POWER_COEFFICIENT`, `CAMERON_POWER_EXPONENT`,
|
|
310
|
+
`CAMERON_MAX_DISTANCE_KM`.
|
|
311
|
+
- `Checker::CLOCK_FORMAT` — the clock grammar every time string is read with.
|
|
312
|
+
- `EnvironmentalAdjuster`: `HEAT_DURATION_FACTORS`, `REFERENCE_HUMIDITY`, and
|
|
313
|
+
the `EnvironmentalAdjuster::Humidity` module with `REFERENCE_HUMIDITY`,
|
|
314
|
+
`WBGT_TEMPERATURE_COEFFICIENT`, `WBGT_VAPOUR_PRESSURE_COEFFICIENT`,
|
|
315
|
+
`MIN_DEW_POINT_CELSIUS`, `BRACKET_CELSIUS` and `BISECTION_STEPS`.
|
|
316
|
+
- `GradeAdjustedPace`: `MINETTI_RUNNING_COEFFICIENTS`, `MINETTI_GRADE_RANGE`.
|
|
317
|
+
- `PersonalizedPredictor`: `TANDA_INTERCEPT`, `TANDA_VOLUME_AMPLITUDE`,
|
|
318
|
+
`TANDA_VOLUME_DECAY`, `TANDA_PACE_SLOPE`, `TANDA_MARATHON_KM`,
|
|
319
|
+
`TANDA_WEEKLY_DISTANCE_RANGE_KM`, `TANDA_TRAINING_PACE_RANGE_SECONDS_PER_KM`,
|
|
320
|
+
`TANDA_MARATHON_TIME_RANGE_SECONDS`, `PERSONAL_EXPONENT_RANGE`.
|
|
321
|
+
- `TrackCalculator`: `GRADE_SEGMENT_MIN_KM`, `GRADE_SEGMENT_TOLERANCE_KM`.
|
|
322
|
+
- `TrainingZones::PREDICTED_RACE_PACE_ZONES`.
|
|
323
|
+
- `Vo2maxNorms`: `NORMS_DATA_PATH`, `VO2MAX_NORMS`, `VO2MAX_NORMS_VERSION`,
|
|
324
|
+
`VO2MAX_NORM_PERCENTILES`, `VO2MAX_PERCENTILE_LABELS`.
|
|
325
|
+
|
|
326
|
+
### Fixed
|
|
327
|
+
- `check_positive` let `Float::INFINITY` through, so every method guarded by it
|
|
328
|
+
accepted an infinite distance or time: an infinite weekly distance became a
|
|
329
|
+
finite (and fast) marathon prediction, an infinite pace a `FloatDomainError`
|
|
330
|
+
far from the input. Infinity now raises `Calcpace::NonPositiveInputError`
|
|
331
|
+
("must be a finite positive number"), like zero, negatives and NaN already
|
|
332
|
+
did, and so does an Integer too large for a Float (e.g. a clock with
|
|
333
|
+
hundreds of hour digits), which used to become `Infinity` or a
|
|
334
|
+
`FloatDomainError` inside the formulas.
|
|
335
|
+
- `Calcpace::VERSION` is defined after `require 'calcpace'`; before, it only
|
|
336
|
+
existed once the gemspec had been loaded.
|
|
337
|
+
- The `vo2max_label` docstring now documents the error it actually raises for
|
|
338
|
+
a non-positive value (`Calcpace::NonPositiveInputError`, not
|
|
339
|
+
`ArgumentError`). Behaviour is unchanged.
|
|
340
|
+
|
|
341
|
+
### Known limitations
|
|
342
|
+
- **The heat model has no sex term.** One curve serves everyone, and El Helou
|
|
343
|
+
et al. (2012) Table S3 shows men slowing more than women in the heat: at
|
|
344
|
+
25 °C the model reads men's slower groups low (men's median 11.9% vs 14.76%
|
|
345
|
+
observed, Q3 12.08% vs 16.10%) and women's high (women's median 12.08% vs
|
|
346
|
+
9.27%, Q1 12.08% vs 8.16%).
|
|
347
|
+
- **Heat above ~25 °C is extrapolated.** The marathon data behind the curve
|
|
348
|
+
stops at 25.2 °C; the 35 °C cap is a deliberate choice, not a measurement.
|
|
349
|
+
The 25 °C / 60-minute anchor (4.3%) and the 30- and 60-minute duration
|
|
350
|
+
factors have no direct published source.
|
|
351
|
+
- **The size of the humidity adjustment rests on the WBGT index, not on race
|
|
352
|
+
data.** The marathon outcome studies (El Helou 2012, Vihma 2010) found no
|
|
353
|
+
humidity effect independent of temperature.
|
|
354
|
+
- **Tanda's equation is validated only inside its sample** (22 experienced
|
|
355
|
+
runners, 21 of them men): outside it the prediction is still returned and
|
|
356
|
+
`out_of_range` says what fell outside.
|
|
357
|
+
- **Grade-adjusted pace is a metabolic model.** It does not see the muscular
|
|
358
|
+
cost of long descents or technical terrain, and the factor is applied to
|
|
359
|
+
horizontal (Haversine) distance without the √(1 + grade²) slope-length
|
|
360
|
+
correction (0.5% at 10%).
|
|
361
|
+
|
|
10
362
|
## [1.18.1] - 2026-09-06
|
|
11
363
|
|
|
12
364
|
### Fixed
|
|
@@ -649,7 +1001,8 @@ predictors are untouched.
|
|
|
649
1001
|
|
|
650
1002
|
See git history for changes in earlier versions.
|
|
651
1003
|
|
|
652
|
-
[Unreleased]: https://github.com/0jonjo/calcpace/compare/
|
|
1004
|
+
[Unreleased]: https://github.com/0jonjo/calcpace/compare/v2.0.0...HEAD
|
|
1005
|
+
[2.0.0]: https://github.com/0jonjo/calcpace/compare/v1.18.1...v2.0.0
|
|
653
1006
|
[1.18.1]: https://github.com/0jonjo/calcpace/compare/v1.18.0...v1.18.1
|
|
654
1007
|
[1.18.0]: https://github.com/0jonjo/calcpace/compare/v1.17.0...v1.18.0
|
|
655
1008
|
[1.17.0]: https://github.com/0jonjo/calcpace/compare/v1.16.0...v1.17.0
|